<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Manuel</title>
    <description>The latest articles on DEV Community by Manuel (@n0loman).</description>
    <link>https://dev.to/n0loman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1125205%2F7201e310-b419-4afe-a746-d5a6fcee7e8c.jpeg</url>
      <title>DEV Community: Manuel</title>
      <link>https://dev.to/n0loman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/n0loman"/>
    <language>en</language>
    <item>
      <title>Why your fastlane snapshot screenshots are all in English (and how to check every language)</title>
      <dc:creator>Manuel</dc:creator>
      <pubDate>Thu, 08 Oct 2026 08:17:47 +0000</pubDate>
      <link>https://dev.to/n0loman/why-your-fastlane-snapshot-screenshots-are-all-in-english-and-how-to-check-every-language-3o6j</link>
      <guid>https://dev.to/n0loman/why-your-fastlane-snapshot-screenshots-are-all-in-english-and-how-to-check-every-language-3o6j</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://shotfleet.com/guides/fastlane-snapshot-screenshots-all-english" rel="noopener noreferrer"&gt;shotfleet.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You set &lt;code&gt;languages(["en-US", "de-DE", "ja"])&lt;/code&gt; in your Snapfile, ran &lt;code&gt;fastlane snapshot&lt;/code&gt;, and opened the folders. German and Japanese are there, and every image is in English.&lt;/p&gt;

&lt;p&gt;I build a tool in this area (shotfleet, which comes up at the end), so read this with that in mind. The five causes below need nothing from me.&lt;/p&gt;

&lt;h2&gt;
  
  
  How snapshot is supposed to set the language
&lt;/h2&gt;

&lt;p&gt;fastlane's docs say the language reaches your UI tests "via a temporary file which is written by snapshot before running the tests". Your test target includes &lt;code&gt;SnapshotHelper.swift&lt;/code&gt;, and the docs tell you to call &lt;code&gt;setupSnapshot(app)&lt;/code&gt; before &lt;code&gt;app.launch()&lt;/code&gt;. That helper reads the file and passes the language to your app as launch arguments. There is also an option, &lt;code&gt;localize_simulator&lt;/code&gt;, that sets the Simulator's own system language.&lt;/p&gt;

&lt;p&gt;So there are several places where the chain can break. The GitHub issues are old but show the pattern. In #8895 the Simulator language "stays en-US" after an Xcode update. In #12917 the reporter got English from &lt;code&gt;fastlane snapshot&lt;/code&gt; while a manual Xcode run with the generated language file switched correctly. In #11457 the reporter found that &lt;code&gt;localize_simulator true&lt;/code&gt; worked but changed the status bar because the simulator was erased. I did not reproduce any of these; they are the documented symptom, from 2017 and 2018. All three are closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things to rule out, in order
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;setupSnapshot(app)&lt;/code&gt; runs after &lt;code&gt;app.launch()&lt;/code&gt;, or never.&lt;/strong&gt; The docs say to call it before launch, in &lt;code&gt;setUp()&lt;/code&gt;. If your test launches the app first, the language never reaches it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You ran the test from Xcode.&lt;/strong&gt; The docs say running tests in Xcode does not produce snapshots; only the &lt;code&gt;fastlane snapshot&lt;/code&gt; command creates the language file and the folders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The Simulator system language.&lt;/strong&gt; Try &lt;code&gt;localize_simulator(true)&lt;/code&gt; in your Snapfile. Per issue #11457 the cost is a status bar that looks different, because the simulator gets erased.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The app is not translated into that language.&lt;/strong&gt; If &lt;code&gt;de&lt;/code&gt; is in your Snapfile but your build has no German strings, iOS shows your base language and no setting will help. List what your build really ships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;path/to/MyApp.app | &lt;span class="nb"&gt;grep &lt;/span&gt;lproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that list with your Snapfile. The app's base language may not be listed: with a String Catalog that has no explicit English entries, my test build had &lt;code&gt;de.lproj&lt;/code&gt; and &lt;code&gt;ja.lproj&lt;/code&gt; and no &lt;code&gt;en.lproj&lt;/code&gt;. A language that is in the Snapfile but not in the app can only ever produce your base language. I checked this on a small test app: with &lt;code&gt;de.lproj&lt;/code&gt; deleted from the build, launching with German arguments showed English, and Japanese on the same build still showed Japanese.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The app remembers something.&lt;/strong&gt; If your app stores its own language choice or onboarding state, a reused install can show the old language. snapshot has &lt;code&gt;reinstall_app&lt;/code&gt; and &lt;code&gt;erase_simulator&lt;/code&gt; for this. On a small test app that saves its own language, launching with German arguments still gave English, and uninstalling and installing the app again gave German (I did that by hand, not with snapshot's option). This and the previous cause are not documented cases from the issue threads, so test them on your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then: look at every image
&lt;/h2&gt;

&lt;p&gt;With 18 languages and 4 screens that is 72 images. Opening them by hand works until the day you skip one. A faster way is to compare each screenshot with the text your app really contains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then: check the folder you already have
&lt;/h2&gt;

&lt;p&gt;shotfleet's &lt;code&gt;check&lt;/code&gt; command does that, and it is free: no licence, no account. It does not capture anything and does not touch your fastlane folders. It runs on a Mac and reads each image with the text recognition built into macOS. Give it a simulator build of your app, so it knows your real strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcodebuild &lt;span class="nt"&gt;-scheme&lt;/span&gt; MyApp &lt;span class="nt"&gt;-sdk&lt;/span&gt; iphonesimulator &lt;span class="nt"&gt;-configuration&lt;/span&gt; Debug &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-derivedDataPath&lt;/span&gt; build build

shotfleet check fastlane/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--app&lt;/span&gt; build/Build/Products/Debug-iphonesimulator/MyApp.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an Android folder, pass the APK instead: &lt;code&gt;--app path/to/app-release.apk&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It reads each image and tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong language:&lt;/strong&gt; the &lt;code&gt;de-DE&lt;/code&gt; folder shows English strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copies:&lt;/strong&gt; the same image in two languages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing:&lt;/strong&gt; a language that did not produce every screenshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store rules:&lt;/strong&gt; sizes, counts and transparency that App Store Connect or Play would reject.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A listing in a language the app does not have:&lt;/strong&gt; for example Traditional Chinese screenshots when the app has no Traditional Chinese.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The report (&lt;code&gt;check.json&lt;/code&gt; and an &lt;code&gt;index.html&lt;/code&gt; grid with a row per language and a column per screen, problems outlined in red) goes to &lt;code&gt;./shotfleet-check&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the committed fastlane screenshots of my own workout app (252 iOS images) it found one real problem: Traditional Chinese listings with Chinese captions over an app that was still in English. When I planted mistakes in a copy of a clean set (a German screen in English, a Korean screen in Japanese, two copied images), each one was reported. On 220 screenshots of another of my apps (20 languages) a check took 57 to 86 seconds once macOS text recognition was warm. The first check after a restart or a macOS update is slower, because macOS compiles its text-recognition model; that can take minutes, and once took about 40 minutes.&lt;/p&gt;

&lt;p&gt;Pass &lt;code&gt;--app&lt;/code&gt;. Without it &lt;code&gt;check&lt;/code&gt; falls back to language detection, which cannot judge short screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not tell you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;check&lt;/code&gt; says "macOS text recognition didn't answer in time" (or "isn't answering"), macOS is usually still compiling its text-reading model, which can take minutes on a busy Mac the first time (once about 40 minutes on a quiet one). &lt;code&gt;check&lt;/code&gt; doesn't fail over it: it finishes and marks the screens it couldn't read as "not verified". Run it again later, or install Tesseract (&lt;code&gt;brew install tesseract tesseract-lang&lt;/code&gt;) and &lt;code&gt;check&lt;/code&gt; uses it automatically. No restart needed.&lt;/li&gt;
&lt;li&gt;It says the language is wrong. It does not say why. Go back to the five causes.&lt;/li&gt;
&lt;li&gt;macOS text recognition reads 33 languages on macOS 27 (older versions read fewer). For other scripts (Greek, Hebrew, most Indic scripts) &lt;code&gt;check&lt;/code&gt; still catches other-language screens, copies and missing screens, and says which screens it could not read. With Tesseract installed, &lt;code&gt;check&lt;/code&gt; uses it for those scripts by itself: in my test, 84 of 85 languages could then be read, against 66 without it.&lt;/li&gt;
&lt;li&gt;Copies are only reported on screens with enough text. Two near-empty screens that are identical can pass.&lt;/li&gt;
&lt;li&gt;It is only as good as your simulator build matching the build that made the screenshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When not to use shotfleet
&lt;/h2&gt;

&lt;p&gt;If your screenshots are already right in every language, you do not need it. If you have only two or three languages, looking at the images yourself is fine. If you already have a maintained UI-test suite and snapshot works for you, keep snapshot; it is free and &lt;code&gt;check&lt;/code&gt; can sit next to it. If you would rather capture with Maestro, &lt;a href="https://shotfleet.com/guides/maestro-one-flow-every-language" rel="noopener noreferrer"&gt;one Maestro flow in every language&lt;/a&gt; shows how, without shotfleet. The tool only runs on an Apple silicon Mac (the installer refuses Intel), and capturing with &lt;code&gt;shotfleet run&lt;/code&gt; is a separate step with its own requirements (Xcode, the Android SDK for Android, Maestro 2.x, Java 17+, and a Maestro flow you write). It does not make frames or captions and does not upload to the stores. I also have no data yet on how often modern fastlane versions hit the problem in this article; the issue threads are old.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;check&lt;/code&gt; finds wrong-language images and you would rather not maintain per-language UI tests at all, shotfleet also captures: one English Maestro flow, every language your app ships, on iOS and Android. &lt;code&gt;run&lt;/code&gt; needs Maestro, Java and a flow; on iOS, your own XCUITest or &lt;code&gt;SnapshotHelper&lt;/code&gt; tests work instead (new in 0.1.6). &lt;code&gt;run&lt;/code&gt; is free for up to 2 languages. A licence unlocks every language and &lt;code&gt;export&lt;/code&gt;: $29 for the first 100 buyers, then $49 once, with a year of updates and a 14-day refund. Details at &lt;a href="https://shotfleet.com/#price" rel="noopener noreferrer"&gt;shotfleet.com/#price&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The free part is the &lt;code&gt;check&lt;/code&gt; command above. Install it, point it at your &lt;code&gt;fastlane/&lt;/code&gt; folder and read the grid.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I made shotfleet and it is paid software. The &lt;code&gt;check&lt;/code&gt; command and 2 languages per &lt;code&gt;run&lt;/code&gt; are free; the full version is $29 for the first 100 buyers, then $49 once. The five causes above need nothing from me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>fastlane</category>
      <category>localization</category>
      <category>testing</category>
    </item>
    <item>
      <title>Run one Maestro flow in every language: iOS launch arguments, Android per-app locales, and the traps</title>
      <dc:creator>Manuel</dc:creator>
      <pubDate>Wed, 07 Oct 2026 12:54:42 +0000</pubDate>
      <link>https://dev.to/n0loman/run-one-maestro-flow-in-every-language-ios-launch-arguments-android-per-app-locales-and-the-traps-2mk2</link>
      <guid>https://dev.to/n0loman/run-one-maestro-flow-in-every-language-ios-launch-arguments-android-per-app-locales-and-the-traps-2mk2</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://shotfleet.com/guides/maestro-one-flow-every-language" rel="noopener noreferrer"&gt;shotfleet.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You wrote a Maestro flow for your app and now want the same screens in German, Japanese and Arabic, for screenshots or for a smoke test. The docs are short on this, and the failure mode is quiet: every run goes green and the images are in English.&lt;/p&gt;

&lt;p&gt;I build a tool around this problem (shotfleet, which comes up in the last three sections). Everything before that is plain Maestro and &lt;code&gt;adb&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the docs say
&lt;/h2&gt;

&lt;p&gt;Maestro's locale page says there is "no place within a Flow itself" (a &lt;code&gt;launchApp&lt;/code&gt; or &lt;code&gt;config.yaml&lt;/code&gt;) to define the locale. The documented route is the &lt;code&gt;--device-locale&lt;/code&gt; flag on &lt;code&gt;maestro start-device&lt;/code&gt; (and &lt;code&gt;maestro cloud&lt;/code&gt;), not on &lt;code&gt;maestro test&lt;/code&gt;, combined with tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro start-device &lt;span class="nt"&gt;--platform&lt;/span&gt; android &lt;span class="nt"&gt;--device-locale&lt;/span&gt; fr_FR
maestro &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--include-tags&lt;/span&gt; french .maestro/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works, and it changes the whole device. For N languages you start N devices in turn, or restart one. It also means the language lives in your shell script and tags, not in the flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A per-app alternative: no device change
&lt;/h2&gt;

&lt;p&gt;You can also set the language for your app only, and keep the device as it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS: launch arguments
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;launchApp&lt;/code&gt; takes an &lt;code&gt;arguments&lt;/code&gt; map of key-value pairs that are passed to the app as launch arguments. iOS reads &lt;code&gt;AppleLanguages&lt;/code&gt; and &lt;code&gt;AppleLocale&lt;/code&gt; from them, so one flow can take the language as a variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# flow.yaml  (no env: default in the header, see trap 1)&lt;/span&gt;
&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.myapp&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AppleLanguages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(${APP_LANG})"&lt;/span&gt;
      &lt;span class="na"&gt;AppleLocale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${APP_LOCALE}"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;onboarding_next&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;takeScreenshot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shots/${APP_LANG}/02_onboarding&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran this on Maestro 2.0.10 against an iOS 26.4 simulator. The &lt;code&gt;tapOn&lt;/code&gt; before the screenshot matters: when I put a &lt;code&gt;takeScreenshot&lt;/code&gt; straight after &lt;code&gt;launchApp&lt;/code&gt;, the image showed the iOS home screen, not the app, in 4 of 4 runs (with and without &lt;code&gt;clearState&lt;/code&gt;; the Mac was under heavy load at the time). Putting &lt;code&gt;extendedWaitUntil&lt;/code&gt; with &lt;code&gt;visible: id: next&lt;/code&gt; first gave the app. Wait for something on screen before the first screenshot.&lt;/p&gt;

&lt;p&gt;Everything here was run on Maestro 2.0.10 only. Newer versions can differ: where &lt;code&gt;takeScreenshot&lt;/code&gt; writes is one thing to check on yours.&lt;/p&gt;

&lt;p&gt;Run it once per language, with the variables after &lt;code&gt;test&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;pair &lt;span class="k"&gt;in &lt;/span&gt;en:en_US de:de_DE ja:ja_JP&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;maestro &lt;span class="nt"&gt;--device&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UDID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;APP_LANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;%%&lt;/span&gt;:&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;APP_LOCALE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;##*&lt;/span&gt;:&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; flow.yaml
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Android: per-app locales (Android 13, API 33 and up)
&lt;/h3&gt;

&lt;p&gt;Android 13 added per-app language preferences. From the outside you set one with a shell command, before the flow starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb &lt;span class="nt"&gt;-s&lt;/span&gt; emulator-5554 shell pm clear com.example.myapp
adb &lt;span class="nt"&gt;-s&lt;/span&gt; emulator-5554 shell cmd locale set-app-locales com.example.myapp &lt;span class="nt"&gt;--locales&lt;/span&gt; de
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the same flow without &lt;code&gt;clearState&lt;/code&gt;, because of trap 3. To keep one file for both platforms, put the iOS &lt;code&gt;launchApp&lt;/code&gt; in a &lt;code&gt;runFlow&lt;/code&gt; with &lt;code&gt;when: platform: iOS&lt;/code&gt; and the Android one in &lt;code&gt;when: platform: Android&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The selectors
&lt;/h3&gt;

&lt;p&gt;A flow that says &lt;code&gt;tapOn: "Next"&lt;/code&gt; fails in German. Two ways out: tap by &lt;code&gt;id:&lt;/code&gt; (accessibility identifier on iOS, resource id on Android), which does not change with the language, or write the translated text per language. If your app has ids on everything you tap, you are done and need nothing else in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four traps that give you English by mistake
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. A default in the flow header beats the command line.&lt;/strong&gt; One developer documented this with Maestro 2.10, and I reproduced it on 2.0.10 on iOS: the flow had &lt;code&gt;env: { LOCALE: en }&lt;/code&gt; as a default, and every "Portuguese" and "Spanish" run still came out in English. He fixed it by removing the default from the header, and he caught it only by looking at the images (&lt;a href="https://github.com/RogerioDoCarmo/morse_app/pull/216" rel="noopener noreferrer"&gt;PR #216&lt;/a&gt;). Maestro's parameters page says that constants defined in a subflow override same-name parameters from the parent. In my run the file even landed in the &lt;code&gt;en&lt;/code&gt; folder although I passed &lt;code&gt;-e APP_LANG=de&lt;/code&gt;. Do not put a default for the language in the header. Pass the language only with &lt;code&gt;-e&lt;/code&gt;, and look at the first image of each run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;-e&lt;/code&gt; goes after &lt;code&gt;test&lt;/code&gt;.&lt;/strong&gt; In the same PR the iOS script put &lt;code&gt;-e&lt;/code&gt; before &lt;code&gt;test&lt;/code&gt;, and it failed while looking like it passed. On Maestro 2.0.10 I got &lt;code&gt;Unknown options: '-e', 'APP_LANG=de'&lt;/code&gt;, the usage text and exit code 2, so it only looks like a pass if your script ignores the exit code. The order is &lt;code&gt;maestro --device X test -e KEY=VALUE flow.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. On Android, clearing state clears the language.&lt;/strong&gt; &lt;code&gt;clearState&lt;/code&gt; wipes the app's data, and the per-app language goes with it. Clear first with &lt;code&gt;pm clear&lt;/code&gt;, set the language, then launch without &lt;code&gt;clearState&lt;/code&gt;. Otherwise you get the system language back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Parallel runs on Android.&lt;/strong&gt; Two separate &lt;code&gt;maestro test&lt;/code&gt; processes on two devices on one Mac were unreliable for me on Maestro 2.0.10. With two emulators and two &lt;code&gt;maestro test&lt;/code&gt; processes started together, one of the two failed in both attempts (a &lt;code&gt;TimeoutException&lt;/code&gt;, or an exception on Maestro's own log folder), while each passed alone. A Maestro process listened on TCP 7001 while it ran, which is my guess for the shared resource; I did not prove it. Run one device at a time, or use one run that shards across devices (&lt;code&gt;--shard-split&lt;/code&gt;, which I did not test), and check the images of both devices.&lt;/p&gt;

&lt;p&gt;Two smaller ones. System permission dialogs are drawn by the OS in the device language, which stays English while your app is German (I measured this on iOS 26.4 with Maps launched in German; I did not test Android), so write those taps in English. &lt;code&gt;launchApp&lt;/code&gt; also takes a &lt;code&gt;permissions&lt;/code&gt; setting (see its docs page); I did not test it against a permission dialog. And long languages push buttons off the screen: use &lt;code&gt;scrollUntilVisible&lt;/code&gt; before the tap, with &lt;code&gt;direction: DOWN&lt;/code&gt; (it happened to me: &lt;a href="https://shotfleet.com/#catch-h" rel="noopener noreferrer"&gt;a Next button nobody could reach&lt;/a&gt; in four languages).&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the images, not the exit code
&lt;/h2&gt;

&lt;p&gt;All four traps pass. The only way to catch them is to look at the output, or to have something look for you: &lt;code&gt;shotfleet check shots/ --app path/to/MyApp.app&lt;/code&gt; reads each image back and reports wrong-language and duplicate images. It is free (no licence, no account) and works on any folder of screenshots, fastlane's too (&lt;a href="https://shotfleet.com/guides/fastlane-snapshot-screenshots-all-english" rel="noopener noreferrer"&gt;how to check a fastlane snapshot folder&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where shotfleet fits
&lt;/h2&gt;

&lt;p&gt;Doing all of the above by hand for 18 languages on two platforms is a lot of scripting, and that is the work shotfleet does. It reads the translations compiled into your build and rewrites every text selector in your English flow for each language, so &lt;code&gt;tapOn: "Next"&lt;/code&gt; becomes &lt;code&gt;Weiter&lt;/code&gt; in German, or &lt;code&gt;(Nächste|Weiter)&lt;/code&gt; if your app has two translations for it. It runs the languages in parallel on simulators and emulators and checks each image. On my own app, 18 languages by 4 screens (72 screenshots) took 9 minutes on iOS (four simulators in parallel) and 15 on Android, on one Mac. That is one app and Maestro 2.0.10, so treat it as a data point. &lt;code&gt;run&lt;/code&gt; is free for 2 languages; a licence unlocks every language and &lt;code&gt;export&lt;/code&gt;: $29 for the first 100 buyers, then $49 once, with a year of updates and a 14-day refund.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use shotfleet
&lt;/h2&gt;

&lt;p&gt;If your app has ids on everything you tap, three or four languages, and one platform, the loop above is enough. If you test on real devices or in a device cloud, shotfleet does not help; it runs simulators and emulators on your Mac. It needs an Apple silicon Mac, Maestro 2.x and Java 17+, and Android needs API 33+ emulators. It makes no frames or captions and does not upload to the stores. For Flutter and React Native it works from your translation files. I have run that end to end on one real Flutter app (wger) and one real Expo app (the obytes template), on iOS and Android, and not yet on any other.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I made shotfleet and it is paid software. The &lt;code&gt;check&lt;/code&gt; command and 2 languages per &lt;code&gt;run&lt;/code&gt; are free; the full version is $29 for the first 100 buyers, then $49 once. Everything in the guide above works without it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
