Your Appium ice-rink suite's `-ios predicate string` matches in the English build but not the French one — why?
answer
- comparisons are literal by default
- square brackets after the operator
- case and accents are two different fixes
- translated copy is not a locator
basics
~20 sA predicate that compares label is comparing translated, accented copy with a case- and diacritic-sensitive operator. In Appium's iOS strategy, append [cd] to CONTAINS or BEGINSWITH, or match a stable name attribute instead of visible text.
solid answer
~40 sComparisons in an `-ios predicate string` are literal, so on Apple platforms `label CONTAINS 'Public session'` stops matching the moment the ice-rink app renders `Séance publique` — and it already stops matching `public session`. Two fixes, in order of preference. First, stop comparing copy at all: match the identifying `name` the app sets on the control, which no translator edits, as in `name BEGINSWITH 'rink.session.'`. Second, where you must compare copy, relax it with the bracket modifiers — `[c]` for case, `[d]` for diacritics, `[cd]` for both — written straight after the operator: `label CONTAINS[cd] 'seance'`. Choose the operator deliberately too: `BEGINSWITH` and `CONTAINS` for substrings, `LIKE` for `*`/`?` globs, `MATCHES` for an ICU regex that must match the **whole** attribute, and `IN` to accept any of a listed set of translations.
code
json · 4 lines{
"using": "-ios predicate string",
"value": "type == 'XCUIElementTypeStaticText' AND label CONTAINS[cd] 'seance publique'"
}go deeper
Know that text comparison in an iOS predicate is exact by default, and that [c], [d] and [cd] go immediately after the operator to relax case and accents.
Explain which operator suits which failure: BEGINSWITH and CONTAINS for substrings, LIKE for globs, MATCHES for a whole-string regex, IN for a known set of translations.
Diagnose it as a locator-source problem, not a syntax problem: show that comparing rendered copy is the defect and that identifiers are the repair, with modifiers as the interim patch.
Decide who owns locator stability. Argue for identifiers as an app-side contract and set the rule for when a suite may match visible text at all across a multi-locale ice-rink release.
## Why a locale change breaks a predicate Comparisons inside an `-ios predicate string` are literal. `label CONTAINS 'Public session'`, sent to Appium's XCUITest driver on an Apple device, asks for exactly that run of characters, in exactly that case, with exactly those letters. Ship the ice-rink session app in French and the label becomes `Séance publique`: different words, different accents, different case. The predicate is still syntactically valid and still evaluated — it simply matches nothing, and surfaces as a no-such-element failure rather than as a hint that the copy moved. Three separable causes hide behind that one symptom, and they need different fixes: - **Case** — `public` versus `Public`. This also bites when a designer switches a button to title case, with no translation involved. - **Diacritics** — `Seance` versus `Séance`. This bites the first time a locale outside English is added. - **Translation** — `Public session` versus `Séance publique`. No comparison operator repairs this, because the substring is genuinely absent. ## The three bracket modifiers The predicate language relaxes a text comparison with a modifier in square brackets, written immediately after the operator and before the literal: 1. `[c]` — case-insensitive. `label CONTAINS[c] 'public session'` matches `Public Session`. 2. `[d]` — diacritic-insensitive. `label BEGINSWITH[d] 'Seance'` matches `Séance`. 3. `[cd]` — both, which is the sane default whenever you compare copy at all: `label CONTAINS[cd] 'seance publique'`. They attach to the text operators — `==`, `BEGINSWITH`, `CONTAINS`, `LIKE` and `MATCHES` — and they cost nothing to add. They fix cause one and cause two. They do not fix cause three. ## Picking the operator for copy that moves | Operator | Use it when | Ice-rink example | |---|---|---| | `BEGINSWITH` | a stable prefix survives the copy change | `label BEGINSWITH[cd] 'freestyle'` | | `CONTAINS` | one stable word sits somewhere in the string | `label CONTAINS[cd] 'seance'` | | `LIKE` | the shape is stable but the middle varies | `label LIKE[cd] '*session*18:30*'` | | `MATCHES` | the untranslatable part is a pattern | `label MATCHES '.*[0-2][0-9]:[0-5][0-9].*'` | | `IN` | you know every translation and want them all | `label IN {'Public session','Séance publique'}` | Two traps live in this table. `MATCHES` must match the **entire** attribute, so `label MATCHES '18:30'` fails against `Book 18:30 public session` — you need `.*18:30.*`. And `LIKE` takes glob wildcards, `*` and `?`, not regular-expression syntax; writing `.*` inside a `LIKE` literal looks for a literal dot followed by anything. ## The durable fix: compare something a translator cannot edit The modifiers are a patch. The repair is to stop comparing display copy: - Compare the identifying `name` attribute the app sets on the control — `type == 'XCUIElementTypeButton' AND name == 'rink.session.book'` — which is the same string in every locale, **provided the app actually sets an identifier on that control**. Where it does not, `name` is no more locale-stable than `label`, so this fix is a contract with the app team, not a trick. - Narrow with `type` first. `type == 'XCUIElementTypeCell'` costs nothing and immediately removes the labels and buttons that happen to share a word. - Where copy is unavoidable, prefer the part of it that does not translate: a session time, a rink number, a price format. ## When you genuinely must match several locales A suite that runs the same case against several language builds has two honest options. Either drive the locator from the same resource strings the app renders, so the expected text and the rendered text come from one source, or enumerate the translations with `IN` and accept that the list is maintenance. Both beat the third option — a `MATCHES` pattern that has been loosened until it matches half the screen, which converts a clear failure into a silent mis-click on the wrong ice-rink session. ## The Android side has no equivalent None of this transfers. Appium's UiAutomator2 driver on Android has no predicate strategy; its `-android uiautomator` selectors are a different language with different matchers, so a locale-tolerant iOS predicate has no Android twin to keep in sync. Write the two per platform and let a shared helper choose between them, rather than pretending one string serves both.
- Why does `label MATCHES '18:30'` fail against the label "Book 18:30 public session"?`MATCHES` requires the pattern to match the whole attribute, not a substring. The fix is to anchor around it — `label MATCHES '.*18:30.*'` — or to use `CONTAINS`, which is a substring test by definition and cheaper to read. This trips people who arrive from XPath, where `contains()` and regex helpers behave differently.
- Would switching from `label` to `name` guarantee the ice-rink locator survives translation?Only where the app actually sets an identifier on that control. `name` is stable when the developer set it deliberately, which makes this an agreement with the app team rather than a locator trick. Where no identifier exists, ask for one; until then, treat the predicate as a temporary measure and keep the `[cd]` modifiers on it.
saying these in an interview costs you the question
- Believes predicate comparisons are case-insensitive by default.
- Puts the modifier before the operator or after the literal.
- Uses MATCHES for a substring without anchoring the pattern.
- Writes regex syntax inside a LIKE literal instead of glob wildcards.
- Claims [cd] fixes a translated string, not just case and accents.
- Assumes the same predicate can be reused on the Android run.