Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .github/workflows/ios_modernization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,92 @@ jobs:
exit 1
;;
esac

release-device-compile:
name: Unsigned Release Device Compile
runs-on: macos-15
timeout-minutes: 90
steps:
- name: Checkout exact revision
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Prove clean checkout
shell: bash
run: |
set -euo pipefail
test -z "$(git status --porcelain=v1 --untracked-files=normal)"
test "$(git rev-parse HEAD)" = "${GITHUB_SHA}"

- name: Initialize private device-build paths
shell: bash
run: |
set -euo pipefail
test -n "${RUNNER_TEMP:-}"
test -n "${GITHUB_ENV:-}"
test ! -L "${RUNNER_TEMP}"
test ! -L "${GITHUB_ENV}"
device_parent="$(mktemp -d "${RUNNER_TEMP}/sora-ios-device-compile.XXXXXX")"
chmod 700 "${device_parent}"
test "$(stat -f '%Lp' "${device_parent}")" = 700
{
printf 'SORA_DEVICE_COMPILE_PARENT=%s\n' "${device_parent}"
printf 'SORA_DEVICE_COMPILE_DERIVED_DATA=%s\n' "${device_parent}/DerivedData"
printf 'SORA_DEVICE_COMPILE_LOG=%s\n' "${device_parent}/build.log"
} >> "${GITHUB_ENV}"

# Simulator XCTest cannot compile the iPhoneOS slice. This is a compile
# check only; protected signing and retained-device admission stay separate.
- name: Compile unsigned Release app and device test targets
shell: bash
run: |
set -euo pipefail
if ! /usr/bin/xcodebuild \
-project SoraPassport.xcodeproj \
-scheme SoraPassport \
-configuration Release \
-destination 'generic/platform=iOS' \
-derivedDataPath "${SORA_DEVICE_COMPILE_DERIVED_DATA}" \
build-for-testing \
SORA_IOS_MIGRATION_EVIDENCE_BUILD_MODE=sora-ios-migration-observed-only-build-v1 \
SORA_IOS_MIGRATION_EVIDENCE_BUILD_ACTION=build-for-testing \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
ENABLE_TESTABILITY=YES > "${SORA_DEVICE_COMPILE_LOG}" 2>&1; then
tail -n 120 "${SORA_DEVICE_COMPILE_LOG}"
exit 1
fi
app="${SORA_DEVICE_COMPILE_DERIVED_DATA}/Build/Products/Release-iphoneos/SoraPassport.app"
test -f "${app}/SoraPassport"
test -f "${app}/Info.plist"
test "$(/usr/bin/lipo -archs "${app}/SoraPassport")" = arm64
test "$(/usr/bin/plutil -extract CFBundleIdentifier raw -o - "${app}/Info.plist")" = co.jp.soramitsu.sora
test ! -e "${app}/_CodeSignature"
printf 'Unsigned Release iPhoneOS arm64 build: OK\n'

- name: Archive device-build log
if: always()
uses: actions/upload-artifact@v4
with:
name: SoraPassport-Device-Compile-${{ github.sha }}
path: ${{ env.SORA_DEVICE_COMPILE_LOG }}
if-no-files-found: error
retention-days: 14

- name: Remove private device-build products
if: always()
shell: bash
run: |
set -euo pipefail
case "${SORA_DEVICE_COMPILE_PARENT:-}/" in
"${RUNNER_TEMP}/sora-ios-device-compile."*/)
test -d "${SORA_DEVICE_COMPILE_PARENT}"
test ! -L "${SORA_DEVICE_COMPILE_PARENT}"
find "${SORA_DEVICE_COMPILE_PARENT}" -depth -delete
;;
*)
printf 'Refusing unsafe device-build cleanup path\n' >&2
exit 1
;;
esac
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# iOS localization fallback qualification — 2026-09-25

The release candidate keeps all 33 bundled language catalogs. For generated
R.swift lookups, it now checks whether the selected locale contains a usable
value for the requested key. If the key is absent or its `.strings` value is
empty, lookup continues through the language fallback and then the English
development catalog. Existing translations and deliberately empty plural
formats in `.stringsdict` remain available. `WalletUX.text` also skips an empty
selected value before trying English. No catalog content or language selection
has been changed.

The legacy `L10n` path used by transfer and QR scanning now checks the selected
locale, its base language, and English for a nonempty key value. Eleven active
error keys are absent even from the shipped English catalog, so their existing
English comments are explicit last-resort text. The active transfer error title
uses the existing generated `common.error.general.title` key. An audit found
88 legacy keys absent from the English catalog; historical keys without active
callsites remain outside this bounded fix and can still resolve to empty text.

An audit of the source `Localizable.strings` catalogs found 1,111 nonempty
English keys, 5,060 missing key/locale pairs across the 32 non-English catalogs,
and three additional empty values in `egy-Egyp`. The fallback makes those
entries display English when an English value exists; it does not provide
translations. Product and language-owner approval of English text in those
locales is still pending.

The four focused Release simulator XCTest cases in `WalletUXTests.swift` passed
with no failures. They cover selected translations, missing and empty values,
formatted strings, selected plurals, an intentionally empty plural format, and
active legacy error messages. The vendored R.swift package passed 23 tests,
the generated source parsed for arm64 iOS Simulator, and the iOS migration
Release source gate passed (94 migration, 8 internal-TestFlight, 17
Release-package, 15 Taira-admission, 10 vendored-binary, 10 signing-identity,
and 20 production-promotion tests; 13 lints plus shell/Swift parse). These
checks are non-authorizing simulator and source evidence, not language-owner
approval or a production signing qualification.
18 changes: 12 additions & 6 deletions Fixtures/Modernization/ios-migration-qualification-README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ Required identity and aggregate fields:
exporter, which continues to export only independently verified database and
settings artifacts for support. Existing-target rollback and absent-target
withdrawal are both exercised by weakening the actual published inode to `.none`
inside the existing 202-method source suite.
inside the existing 229-method source suite.
- A positive `retainedReleaseSnapshotCount`, the reviewed
`retainedReleaseSnapshotManifestSha256`, both checked-in Core Data model SHA-256 values, the
exact executed `WalletModernizationTests`, `WalletRecoveryCapabilityGateTests`, and
`WalletRecoveryExporterTests` method counts, zero failure counts, and a reviewed
`testResultBundleSha256` covering all four suite inventories (202 + 11 + 12 + 3 = 228).
`testResultBundleSha256` covering all four suite inventories (229 + 11 + 12 + 3 = 255).
- True parity for account count, selected wallet, preferences, Keychain identity and accessibility,
legacy dual-read retention, existing SORA2 identity/signatures, and zero lost accounts.
- Missing-store qualification must separately retain and exercise raw selected-account settings,
Expand Down Expand Up @@ -263,9 +263,15 @@ with six success cohorts fails admission. All affected sources are bound by
be recollected and independently reviewed for the current candidate.

`SoraPassportMigrationEvidence.xcscheme` is the dedicated Release/physical-device evidence
scheme. Its exact test inventory is 202 `WalletModernizationTests`, 11
scheme. Its exact test inventory is 229 `WalletModernizationTests`, 11
`WalletRecoveryCapabilityGateTests`, 12 `WalletRecoveryExporterTests`, and three
`WalletMigrationRetainedDeviceEvidenceTests`. The last three tests bind their schema-v3 attachments
`WalletMigrationRetainedDeviceEvidenceTests`. The ordinary wallet regression suite additionally
uses 19 `WalletUXTests` instead of the three retained-device tests, for 271 tests
(229 + 11 + 12 + 19). The regressions also cover canonical interrupted and failed startup journals, marker-preserving retry, latest privacy-safe diagnostics, and mixed framework and wallet Keychain
attributes, full recovery from an earlier generic startup error, multi-account
identity and signing preservation, rejected recovery evidence, marker-generation
changes, pre-account wallets, and the recovery screen retry action.
The last three retained-device tests bind their schema-v3 attachments
to the installed production bundle identifier, the exact production IPA, the canonical projection
receipt and projector source, the raw installed-clone tree, equal production/installed canonical
projections, the installed executable, the run
Expand Down Expand Up @@ -546,7 +552,7 @@ the repository with this fixed layout:
- `application/SoraPassport.app`, the exact archive-derived installable clone;
- `application/canonical-projection-receipt-v2.json`, the canonical observed projection receipt;
- `application/installable-clone-receipt-v1.json`, the protected non-authorizing clone receipt;
- `tests/Migration.xcresult`, containing the exact 228 passing test identifiers and three
- `tests/Migration.xcresult`, containing the exact 255 passing test identifiers and three
test-associated reserved JSON attachments;
- `snapshots/index.json` and `snapshots/data/<snapshotId>/{source,migrated}`, containing the
retained Core Data/settings bundles.
Expand Down Expand Up @@ -694,7 +700,7 @@ Keychain aggregate must exactly match successful/failing source counts and the r
identity/accessibility assertions, with no credential rewrite or raw values. The device aggregate
must exactly match Core Data and interruption counts plus reinstall/upgrade, rollback, low-storage,
recovery-export, and process-death/restart assertions. The ZIP summary must exactly match all four
declared suite counts (202 + 11 + 12 + 3 = 228) and zero failure, unexpected-failure, skipped, and
declared suite counts (229 + 11 + 12 + 3 = 255) and zero failure, unexpected-failure, skipped, and
expected-failure counters. Independent byte reproduction proves that these public aggregates are
the collector's derivation from the pinned raw namespace; producer and reviewer signatures remain
necessary authentication and do not replace review of the restricted scenario material.
Expand Down
153 changes: 153 additions & 0 deletions Fixtures/Modernization/ios-wallet-opening-localization-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Wallet-opening localization handoff

The release candidate added nine user-visible wallet-opening and connection-recovery
messages. As of source revision `8af8112b`, all nine keys are present in `en` and
`ja`. For 30 other checked-in catalogs, `Try again` reuses the existing,
checked-in `common.retry` translation for the same retry button action. Catalan
uses a retry phrase derived from its existing `common.error.retry` value. The
remaining eight keys are missing in each of the 31 other catalogs: 248 missing
locale/key pairs.
The app falls back to English for those messages. This is an inventory for
translation review, not approval of the fallback or of machine-generated
translations.

## Exact keys

1. `Opening wallet`
2. `Preparing wallet services…`
3. `Try again`
4. `Change node`
5. `Check wallet again`
6. `Wallet verification needs attention. Check your saved wallet again.`
7. `Wallet services are not ready. Try again or choose another node.`
8. `Node settings are still loading. Try again.`
9. `Network unavailable. Retrying connection…`

## Reused translation provenance

`Try again` is the button that invokes `retry()` in
`PinSetupWireframe.swift`. Thirty non-English/Japanese values were copied
byte-for-byte from `common.retry` in the same catalog, a tracked generic retry
action label.
The English source wording differs (`Try again` versus
`Retry`), but both label the same immediate retry action. This reuse does not
claim a new translation review, and a language reviewer should still inspect
each value in the wallet-opening screen. The existing `common.retry` value in
`ca` reads `Reintentar`, which appears to be Spanish; that value was not reused.
The Catalan value `Torna-ho a provar` removes the existing polite prefix
`Siusplau` from `common.error.retry = "Siusplau torna-ho a provar"`, whose
English source is `Please try again`. This preserves a checked-in Catalan
retry phrase; language review is still needed for the wallet-opening context.
The storage-upgrade retry, retained-wallet recovery retry, and PIN-unavailable
alert also use this existing `Try again` lookup for the same retry action.
Their surrounding safety messages remain English and need separate review.

No other key has an equivalently direct, complete match. The existing
`switch.node` is the closest label to `Change node`, but most catalogs still
contain the English placeholder `Switch node`, and the action here opens node
settings; it was not copied. Android's `switch_node` catalogs have the same
placeholder issue and no exact text for the other new wallet-opening messages.

## Observed fallback for missing keys

`WalletUX.text` looks in the selected locale's `Localizable.strings`, then in
`en`, and finally returns the English phrase passed by the caller. The nine
new wallet-opening call sites pass English phrases as keys. A macOS Foundation
probe using the same lookup sequence against the checked-in `.lproj` folders
produces these values at the current candidate:

| Selected locale | Opening wallet | Try again | Node settings are still loading. Try again. |
| --- | --- | --- | --- |
| `fr` | Opening wallet | Recommencer | Node settings are still loading. Try again. |
| `ja` | ウォレットを開いています | 再試行 | ノード設定を読み込んでいます。再試行してください。 |
| `ca` | Opening wallet | Torna-ho a provar | Node settings are still loading. Try again. |
| unknown `xx` | Opening wallet | Try again | Node settings are still loading. Try again. |

Reproduce from the repository root:

```sh
/usr/bin/swift -e 'import Foundation; let root = URL(fileURLWithPath: CommandLine.arguments[1]); func text(_ key: String, _ selected: String) -> String { for locale in [selected, "en"] { let path = root.appendingPathComponent("\(locale).lproj").path; guard let bundle = Bundle(path: path) else { continue }; let result = bundle.localizedString(forKey: key, value: key, table: "Localizable"); if result != key { return result } }; return key }; for locale in ["fr", "ja", "ca", "xx"] { print("\(locale): \(text("Opening wallet", locale)) | \(text("Try again", locale)) | \(text("Node settings are still loading. Try again.", locale))") }' "$PWD/SoraPassport/SoraLocalizable"
```

This probes the source resources and Foundation lookup, not a rendered iPhone
screen. Missing keys display the English phrase in this path; that behavior does
not approve the 248 missing translations for release. The separate wallet
recovery screen also contains English-only safety guidance and needs its own
localization review.

## Catalogs needing reviewed translations

`akk`, `ar`, `az`, `ca`, `cs`, `de-DE`, `de`, `egy-Egyp`, `es`, `fa`,
`fi-FI`, `fr`, `he`, `hi-IN`, `hu`, `id`, `it-IT`, `it`, `ms-MY`, `nl`,
`nn-NO`, `no`, `pl`, `pt`, `ru`, `sl`, `sr`, `tr`, `vi`, `zh-Hans`,
`zh-Hant-TW`.

## Source check

Run from the repository root. The command exits nonzero until each checked-in
catalog defines all nine keys. It checks coverage, not translation quality.

```sh
python3 - <<'PY'
from pathlib import Path
import re

root = Path('SoraPassport/SoraLocalizable')
english = (root / 'en.lproj/Localizable.strings').read_text()
keys = (
'Opening wallet',
'Preparing wallet services…',
'Try again',
'Change node',
'Check wallet again',
'Wallet verification needs attention. Check your saved wallet again.',
'Wallet services are not ready. Try again or choose another node.',
'Node settings are still loading. Try again.',
'Network unavailable. Retrying connection…',
)
def contains(text, key):
return re.search(r'^\s*"' + re.escape(key) + r'"\s*=', text, re.M) is not None

assert all(contains(english, key) for key in keys)
missing = {
catalog.parent.name.removesuffix('.lproj'): [
key for key in keys if not contains(catalog.read_text(), key)
]
for catalog in sorted(root.glob('*.lproj/Localizable.strings'))
}
missing = {locale: keys for locale, keys in missing.items() if keys}
for locale, keys in missing.items():
print(f'{locale}: {len(keys)} missing')
print(f'{sum(map(len, missing.values()))} missing locale/key pairs')
raise SystemExit(bool(missing))
PY
```

The current expected result is `248 missing locale/key pairs`. To verify the
reused values have not diverged from their checked-in source, run:

```sh
python3 - <<'PY'
from pathlib import Path
import re

root = Path('SoraPassport/SoraLocalizable')
def value(text, key):
pattern = r'^\s*"' + re.escape(key) + r'"\s*=\s*"((?:\\.|[^"\\])*)"\s*;'
match = re.search(pattern, text, re.M)
assert match, key
return match.group(1)

for catalog in sorted(root.glob('*.lproj/Localizable.strings')):
if catalog.parent.name in {'en.lproj', 'ja.lproj', 'ca.lproj'}:
continue
text = catalog.read_text()
assert value(text, 'Try again') == value(text, 'common.retry'), catalog
print('30 existing retry translations reused byte-for-byte')
PY
```

After translation review, rerun the source check and the Release simulator
tests. Review the rendered messages on an iPhone for truncation and direction,
including the retry and recovery actions. The separate `WalletRecoveryViewController`
also contains English-only safety guidance and needs its own localization review.
16 changes: 1 addition & 15 deletions SoraPassport/Common/Configs/ApplicationConfigs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -298,21 +298,7 @@ extension ApplicationConfig: ApplicationConfigProtocol {
}

var defaultChainNodes: Set<ChainNodeModel> {
#if F_RELEASE
return [
ChainNodeModel(url: URL(string: "wss://mof2.sora.org")!, name: "Sora", apikey: nil),
]

#elseif F_STAGING || F_TEST
return [
ChainNodeModel(url: URL(string: "wss://mof2.sora.org")!, name: "Sora", apikey: nil),
]
#else
return [
ChainNodeModel(url: URL(string: "wss://mof2.sora.org")!, name: "Sora", apikey: nil),
]

#endif
Set(SoraNodeConnectionPolicy.bundledMainnetNodes)
}

var polkaswapIndexerURL: URL {
Expand Down
Loading
Loading