Skip to content

Localize the drop-in screens (string resources, English + Italian) - #3

Open
smashkins wants to merge 1 commit into
aivars:mainfrom
smashkins:localization
Open

smashkins wants to merge 1 commit into
aivars:mainfrom
smashkins:localization

Conversation

@smashkins

Copy link
Copy Markdown

Closes #2.

The Android counterpart of aivars/feedbackthread-swift#5. The SDK's Compose UI — the board, the feedback form, My requests and the 0.5.0 conversation screens — now reads its copy from the library's string resources: English as the default, plus a complete Italian translation. The screens follow the host app's locale, per-app language included, and fall back to English for any other language, so apps that only ship English see no change.

What's in the change

  • res/values/strings.xml + res/values-it/strings.xml: new; the module had no res/ before. They hold 86 strings and 3 plurals, each with a translator comment. Every name is prefixed feedbackthread_. A library's resources are merged into the host app, and the app's own resource of the same name wins. An unprefixed done would quietly pick up whatever the app means by done. That is the Android version of the bundle-misrouting problem on iOS, and a test enforces the prefix.
  • FeedbackThreadText (new, internal): a small value type that is either a string resource with its arguments or verbatim text. Much of the copy is produced outside composition: catch blocks in launched coroutines, the status label function, enum titles. That code now carries a reference, and composition resolves it. Copy that is always SDK-authored travels as a plain @StringRes Int.
  • Status labels: feedbackThreadRequestLabel() maps each known stage to its resource. An unrecognised status keeps the existing sensiblyCapitalized() passthrough, verbatim.
  • Board filters: each filter now has its own whole empty-state sentence. The old code interpolated a lowercased status into "No … requests", which no other language can inflect. This is the same call the Swift PR made.
  • Kind titles: FeedbackThreadFeedbackKind.title still returns its English String. The form uses a new internal localizedTitle beside it.
  • Error copy: see the judgement calls below.
  • Docs: a "Localization" section in the README, a ## Unreleased entry in the CHANGELOG, and the README sentence calling conversation localization "a release check" updated.

Guarantees

  • Wire values are untouched. feedbackThreadRequestStage() is byte-identical, and its match strings ("Submitted", "In review", "In progress", "Ready to release", …) were left alone even where a label reads the same. Separating them was done by hand, as CONTRIBUTING asks. The same holds for serialized names, URL paths, headers, and the persisted voter-identity key. The other wire values the screens compare against, the "private"/"public" audience and the "developer" author role, are untouched too. mapsKnownStatusesToTheirPublicBoardStage and bucketsEveryStageIntoExactlyOneMyRequestsSection are unchanged and green as the regression net.
  • No public API change, checked at the Kotlin level. I ran the binary-compatibility-validator apiDump on main and on this branch, and the two dumps are identical. (Kotlin internal compiles to JVM-public, so a javap diff would not prove this.)
  • The English copy is unchanged byte for byte, with two deliberate exceptions that the CHANGELOG lists:
    • "No requests in review" / "No requests in progress" replace "No in review requests" / "No in progress requests".
    • The vote button's screen-reader description is now a plural: "Vote, 1 vote" instead of "Vote, 1 votes".
  • No new dependencies. The tests are plain JVM JUnit, with no Robolectric.

Judgement calls

  • Exception messages stay English. FeedbackThreadException.message is public API that integrators read and log. The Swift SDK localized its errorDescription, but on Android the transport has no Context to resolve resources from. So the throw sites that write end-user copy tag the exception internally: unreachable, unreadable response, unreadable conversation settings, and the HTTP-status fallback with its code. The drop-in screens show the tag's localized text. The message stays exactly what it was. No constructor or signature changed: the tag is an internal @Transient property on the sealed base class.
  • Shown verbatim, on purpose:
    • error messages the API writes in its envelope;
    • InvalidConfiguration messages, which address the integrator wiring the SDK up, as CONTRIBUTING says;
    • any exception that isn't the SDK's own.
  • FeedbackThreadConversationState.error is a public String, so it keeps its English values. The two messages FeedbackThreadConversations writes there are now internal constants. FeedbackThreadConversationHost recognises exactly those two and shows them localized, and shows any other value as written. A typed error field on the state would be cleaner, but it would mean a public API addition, so I left that to you.
  • build.gradle.kts gets one small change: testOptions.unitTests.all { it.inputs.dir("src/main/res") }. The localization tests read src/main/res directly. Without declaring it as an input, an edit that only touches a translation leaves testDebugUnitTest UP-TO-DATE locally, a stale green. CI starts clean, so CI isn't affected. Happy to drop it if you'd rather not touch the build file.
  • Not documented: overriding strings by name. A side effect of resources is that a host app can override any SDK string, or add a language, by defining the same names in its own res/. I left that out of the README so the resource names don't become a de facto public API. Your call whether to promise it.

Tests

42 tests on main → 66 on this branch, all green.

  • FeedbackThreadLocalizationTest finds every values-*/strings.xml on its own, so adding a language needs no test change. For every language it checks:

    • the prefix;
    • that each translation has exactly the English key set, and no value is empty;
    • positional placeholders, identical per key and per plural quantity;
    • the plural quantities;
    • apostrophe escaping;
    • that every generated R.string / R.plurals entry exists in every file.
  • Status label tests, rewritten. They assert the resource each status maps to, and that its English value is exactly the pre-change label, so the English demonstrably did not move. They also assert exact Italian ("In attesa di revisione", "Rifiutata"), and that an unknown status stays a verbatim passthrough.

  • Client tests, for:

    • unreadable response,
    • the HTTP fallback,
    • an API-written message,
    • an IOException in submit,
    • InvalidConfiguration.

    Each asserts both that the exception message is unchanged English and that the screen's display text is the expected resource or verbatim text.

  • Display-mapping and conversation-error tests, for FeedbackThreadText and the conversation-error mapping.

Italian translation notes

Wherever the copy exists in both SDKs, the Italian is the Swift SDK's catalog wording verbatim, so the two platforms read the same. For example: Richieste di funzionalità, In attesa di revisione, Pianificata / Completata / Rifiutata, ✓ Rilasciata nella versione 2.4.0, Le mie richieste, plural section headers Rilasciate / Chiuse.

Status labels stay feminine because they qualify richiesta. Copy that exists only on Android is translated fresh: the conversation screens, the board's empty state, the form's confirmations. The Swift conversation views aren't localized yet, so they had no wording to reuse. Happy to adjust any of it.

Verification

  • ./gradlew :feedbackthread:testDebugUnitTest :feedbackthread:assembleDebug: green.
  • :feedbackthread:lintDebug: no localization findings (MissingTranslation, StringFormat*, plurals).
  • example/ builds against the branch, and aapt2 dump of its APK shows the SDK strings merged with both the default and the it values.
  • Checked by hand on a Pixel 8a (Android 15), in English and in Italian: the example app ran against a local mock API, with the per-app language set to en-US and then it-IT, through the board, the filters and their empty states, a request's detail, My requests, the form and its confirmation, and the load-failed state. The Italian screens show no leftover English apart from server content and the example's own tab labels. TalkBack reads the vote button as "Vote, 1 vote" / "Vota, 1 voto". The longest new string, "✓ Rilasciata nella versione 2.4.0", fits at the default font size.

Not in this change

  • The badge row on a request (status, then the shipped badge) doesn't wrap. The Italian shipped badge is the longest string in it and still fits at the default font size, but a longer version string or a larger font size could clip it. That's the existing layout, so I left it alone here.
  • requests(), setVote(), myRequests(), myUpdates() and acknowledgeUpdates() don't wrap an IOException the way submit() does. So an offline board shows the platform's own message, e.g. "Unable to resolve host …", which is English and not SDK-authored. Wrapping them in InvalidResponse would let that case read in the user's language too, but it changes which exception type public functions throw, so it belongs in its own PR. I'm happy to send one.

Adding a language from here is one new values-xx/strings.xml.

Every end-user string of the board, the feedback form, My requests and
the conversation screens now resolves from the library's string
resources: values/strings.xml (English, the default) and a complete
values-it/strings.xml. The screens follow the host app's locale,
per-app language included, and fall back to English.

- Every resource name is prefixed feedbackthread_: library resources
  merge into the host app, where an unprefixed name would be overridden
  by one of the app's own.
- FeedbackThreadText (internal) carries copy produced outside
  composition - catch blocks, the status label, enum titles - as a
  resource reference or verbatim text, resolved in composition.
- The board's empty filter states are one whole sentence per filter
  instead of a lowercased status interpolated into "No ... requests",
  and the vote button's description is a proper plural ("Vote, 1 vote").

Deliberately not localized:

- Wire strings. feedbackThreadRequestStage() and its status match
  strings, serialized names, URL paths, headers and the persisted voter
  key are untouched, byte for byte.
- Public API. FeedbackThreadFeedbackKind.title stays an English String
  beside a new internal localizedTitle. Exception messages stay English
  for integrators; the end-user copy the SDK writes (unreachable,
  unreadable response or settings, the HTTP-status fallback) is tagged
  internally and the screens show it localized. The two values
  FeedbackThreadConversations writes to FeedbackThreadConversationState
  .error become internal constants the host shows localized. A
  binary-compatibility-validator dump is identical before and after.
- InvalidConfiguration messages, messages the API writes, and statuses
  the SDK doesn't recognize, which stay verbatim.

Tests: a localization suite that discovers every values-*/strings.xml
and checks prefix, completeness, placeholders, plurals and apostrophes;
status labels asserted against the resources (English unchanged, exact
Italian); error tagging and its display mapping; the conversation error
mapping. The wire-mapping and section-bucketing tests are unchanged.
The unit test task declares src/main/res as an input, so a
translation-only edit doesn't leave it up to date.
@coderabbitai

coderabbitai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 141e60c3-2484-41e8-9919-8ffc6d8d24a4
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Localization support: the drop-in screens are hardcoded English

1 participant