Hardcoded strings make localization look complete when it is not. Your Android strings.xml files and iOS String Catalog can have full target-language coverage while a validation error, Compose screen, accessibility label, or SDK wrapper still displays English. Translators cannot fix text they never receive. An app localization audit must find user-facing literals before translation, turn them into stable catalog entries, and stop new ones from entering the codebase. The workflow covers inventory, extraction, suppression review, pseudo-locale testing, and CI enforcement across both platforms.
Define what the audit must find
Searching for every quoted value produces noise. Mobile repositories contain URLs, analytics event names, database keys, protocol fields, test fixtures, log messages, and user-facing copy. Only some belong in a translation catalog.
Start with a classification rule that reviewers can apply consistently:
| Literal type | Default action | Example |
|---|---|---|
| Visible interface copy | Extract | Button title, empty state, dialog message |
| Spoken or accessible copy | Extract | Screen-reader label, speech prompt |
| User-facing failure text | Extract or map to a localized error key | Validation message, offline notice |
| Protocol or storage value | Keep stable and language-neutral | JSON field, deep-link route, cache key |
| Developer-only diagnostics | Keep out of the catalog | Debug log, assertion detail |
| Test fixture | Keep unless it represents approved visible copy | Mock API field, snapshot input |
The classification matters because changing a protocol value for translation can break behavior, while leaving an accessibility label in code can ship a mixed-language screen. Record the intended audience of every ambiguous literal instead of deciding by file extension.
Map the rendering boundaries before running tools. Include XML layouts, Compose and SwiftUI views, UIKit or Android view code, notifications, widgets, authentication and payment adapters, accessibility properties, validation branches, embedded web entry points, and strings supplied by third-party SDKs. The live i18nAgent Android localization guide covers strings.xml, Compose lookups, default resources, and pseudo-locales. Its iOS localization guide covers .strings, String Catalogs, and SwiftUI lookup. Use those platform setups as the destination for extracted text, not as proof that the inventory is complete.
Establish a baseline before changing code
Run the audit on a clean commit and save machine-readable findings. A baseline lets you split the work into two tracks. Existing findings become a migration backlog. New findings can fail CI immediately.
Each finding should contain:
- Platform and target
- File and line
- Literal text or a safe fingerprint
- Rendering surface
- Classification
- Owner
- Planned catalog key or suppression reason
Do not suppress the whole rule because a mature app has hundreds of findings. Broad suppression hides new defects. Keep a reviewed baseline and make the count decrease. A pull request may remove findings or leave the baseline unchanged, but it must not add an unexplained user-facing literal.
Exclude generated code, vendored source, build output, snapshots, and test fixtures at the scanner boundary. Exclusion paths belong in version control so local runs and CI inspect the same source set. If a generated component renders user-facing text, fix the generator or its input rather than editing generated output.
Run Android lint, then inspect what it misses
Android lint provides a concrete starting point. The official Android lint guide shows how to run lint from Android Studio or Gradle, configure issue severity, and add it to CI. The HardcodedText issue can be promoted to an error in lint.xml:
<lint>
<issue id="HardcodedText" severity="error" />
</lint>
Run the release variant because build types and product flavors can include different resources and source sets:
./gradlew lintRelease
Android's HardcodedText rule reference explains that text embedded in layout attributes cannot be translated by adding resource variants. It also documents the IDE quick fix that extracts the literal into a resource lookup. A Stack Overflow author preparing an Android app for multiple languages asked how to find every hard-coded string before moving it into resources. That question captures the retrofit problem: the team needs an inventory before it can divide extraction work.
Treat lint as one evidence source, not an exhaustive inventory. Search code paths that construct visible text at runtime. Review calls that set titles, toast text, snackbar messages, dialog copy, content descriptions, validation errors, and Compose Text values. Search patterns are language and framework specific, so keep them narrow enough for a human to review.
For example, a repository audit might flag direct literals passed to common rendering APIs:
Text("...")
Toast.makeText(..., "...", ...)
setContentDescription("...")
setError("...")
AlertDialog.Builder(...).setMessage("...")
Do not replace each match mechanically. Text(order.id) may display data rather than copy. Text("HTTP") may be a product term that stays unchanged in every locale. The scanner finds candidates; the classification rule decides what enters the catalog.
A 2026 FirebaseUI Android issue reported hard-coded Compose legal labels preventing localization. The report is specific to one beta release. It shows how a library can use resources in most screens while two visible literals remain English. Audit dependency-owned screens that your app presents, especially authentication, consent, support, and checkout flows. If the library does not expose localized resources or an override, record the version and decide whether to patch, replace, or block the locale launch.
Use Xcode extraction and review unsupported paths
Create or update the app's String Catalog, then build the targets that render user-facing interfaces. Apple's String Catalog guide documents catalog-based localization and source string discovery through supported APIs. Check every app target, extension, widget, package, and test host that owns visible resources. A catalog attached only to the main target cannot account for copy rendered by another executable.
Review the generated catalog diff rather than accepting it blindly. Confirm that newly discovered entries are real interface copy, assign translator comments where context is ambiguous, and remove accidental catalog entries for machine values. Then inspect paths automatic discovery may not understand, including custom wrappers, computed strings, runtime format construction, C or C++ boundaries, accessibility assignments, and third-party SDK configuration.
Targeted searches should follow the app's actual architecture. Common candidates include direct values supplied to SwiftUI Text, UIKit labels, alerts, toolbar items, accessibility labels, local notifications, and error presentation. Dynamic interpolation needs extra care. Extract the complete message with structured variables so translators can reorder the sentence. Do not split one sentence into several keys or concatenate translated fragments.
Keep the source text and key stable enough for review. A semantic key such as checkout.payment_failed.retry communicates the surface and action better than a numeric ID. Add a comment that explains where the message appears, what each variable means, and any length or safety constraint. A translator can then distinguish the retry action from a payment-failure status without tracing the mobile code.
Audit runtime and third-party boundaries
Static analysis sees source files. It does not prove that every production path resolves through the catalog. Build a short runtime inventory around high-risk journeys:
- First launch and onboarding
- Sign-in, account recovery, and biometric fallback
- Purchase, subscription, and payment failures
- Permission and consent flows
- Offline, timeout, and server-error states
- Settings, language changes, and account deletion
- Notifications, widgets, extensions, and system entry points
For each journey, identify which component owns the visible text. App-owned copy should resolve from the app catalog. Server responses should carry stable error codes that the client maps to localized messages. SDK-owned copy needs a documented locale input, supported-language list, and fallback. Operating-system copy should remain platform owned.
Test failure branches deliberately. Happy paths often use polished resource-backed components, while rare errors contain literals added during incident fixes. Disable the network, expire a session, reject a payment fixture, deny a permission, and force an unsupported locale. Capture the screen and link each unexpected source-language phrase back to a file, service response, or dependency.
Extract strings without damaging behavior
Move one coherent surface at a time. For each literal:
- Confirm that a user can see, hear, or receive it.
- Identify the rendering target and owning component.
- Choose a semantic key and complete message boundary.
- Preserve variables as typed arguments or placeholders.
- Add translator context and screenshots where useful.
- Replace the literal with the platform lookup.
- Run unit, UI, and resource validation tests.
- Mark the baseline finding resolved.
Do not reuse a key merely because two English strings match. "Open" can describe a file state or a button action, and another language may translate those meanings differently. Reuse only when the product meaning, grammar, and lifecycle are the same.
Preserve language-neutral values. An analytics event named checkout_failed should not change when the interface switches to German. A server error code should remain stable while its client-rendered explanation changes. Keep display strings at the edge of the system so localization does not leak into storage, routing, or business logic.
Make suppressions reviewable
Some literals should remain in source. A useful suppression states why the value is not translatable and limits the exception to one location. "Protocol token required by API" is reviewable. "False positive" is not.
Store suppressions beside the finding or in a narrow configuration entry with an owner and optional expiry date. Reject file-wide and directory-wide exclusions unless the path contains only generated or vendored code. Review suppressions when a component becomes user-facing, a dependency changes, or a test fixture moves into production code.
Track three counts in CI: active migration findings, approved suppressions, and new findings. The first should fall, the second should remain explainable, and the third must be zero. Together, the counts expose an expanding blind spot without pretending to measure translation quality.
Verify with pseudo-locales and CI
Static extraction proves that code references a catalog. A pseudo-locale run proves that rendered paths use it. Android's guide cited above describes accented and right-to-left pseudo-locales. Exercise the high-risk journeys and look for unchanged source text among transformed strings. An unchanged brand name may be correct. An unchanged validation sentence is a likely miss.
Use a release-oriented verification sequence:
- Run Android lint for every shipped variant.
- Build every iOS target that owns interface resources and inspect catalog changes.
- Run targeted literal searches with repository exclusions applied.
- Compare findings with the committed baseline and suppression register.
- Launch pseudo-locales and exercise failure branches.
- Run one real target locale through sign-in, purchase, settings, and account recovery.
- Verify accessibility labels and system-adjacent surfaces.
- Save reports and screenshots as CI artifacts.
When CI fails, print the rule, file, line, classification hint, and remediation command. Do not emit only a total. Engineers need enough information to decide whether to extract, suppress, or reclassify the value.
A scanner update may create many new findings at once. Treat that as a baseline migration, not permission to disable the gate. Review the new set, commit accepted suppressions, create extraction tasks, and establish the new baseline in one explicit change. The next pull request returns to the no-new-findings rule.
Start with one conversion path
Choose the app's sign-in or checkout flow and audit it from initial screen through every failure branch. Run Android lint or Xcode extraction, review direct rendering calls, inspect SDK-owned copy, extract the confirmed user-facing literals, and rerun the flow under a pseudo-locale. Commit the resulting baseline and make CI reject one newly introduced hard-coded UI string. Once that path stays clean, apply the same inventory to the next release-critical journey.
Once strings live in catalogs, the next leak is assembling them in code; see how to stop concatenating strings in app localization.
