The localized store listings for Google Play and the Apple App Store are
managed with fastlane and live as plain .txt
files, one folder per language. Because this repo is public, the marketing /
ASO copy is kept in the private prop submodule under
prop/store_metadata/ (not in this repo); the fastlane lanes read it from there
via metadata_path:
prop/store_metadata/android/<locale>/ # Google Play (supply)
title.txt
short_description.txt
full_description.txt
changelogs/default.txt
prop/store_metadata/ios/<locale>/ # App Store (deliver)
name.txt
subtitle.txt
description.txt
keywords.txt
promotional_text.txt
release_notes.txt
marketing_url.txt
support_url.txt
Locales currently maintained: en, de, fr, es, it, pl
(Play uses en-US, de-DE, fr-FR, es-ES, it-IT, pl-PL; the App Store uses
en-US, de-DE, fr-FR, es-ES, it, pl).
To add a language, create the matching locale folder (in the prop submodule)
with the same files.
Just edit the .txt files and keep an eye on the store length limits:
| Field | Google Play | App Store |
|---|---|---|
| title / name | 30 | 30 |
| subtitle | – | 30 |
| short description | 80 | – |
| keywords | – | 100 |
| promotional text | – | 170 |
| full description / description | 4000 | 4000 |
Screenshots are generated by the golden test test/screenshot_test.dart into
./screenshots/ (named <scene>-<deviceType>-<WxH>.png). It carries the
screenshots tag, so a normal flutter test skips it (see dart_test.yaml) —
--run-skipped is what opts back in. To regenerate them:
flutter test --run-skipped --update-goldens test/screenshot_test.dartThey are not localized, so they are kept once in ./screenshots/ (this repo,
public) and are not duplicated into the fastlane folders. Before an upload,
scripts/prepare_store_screenshots.sh maps them into the per-language layout
fastlane expects (and the metadata upload then includes them):
- Google Play:
android→phoneScreenshots,androidTablet→tenInchScreenshots, for every locale. These stage under the metadata path, i.e.prop/store_metadata/android/<locale>/images/. - App Store:
iPhone(6.5") andiPad(12.9") images per locale, staged intoios/fastlane/screenshots/;deliverdetects the device class from the image resolution.
The generated screenshot folders are .gitignored (in this repo and in
prop). The "upload" workflow runs this prepare step automatically, so the
screenshots stay sourced from the test output rather than committed copies.
Use the Store Metadata workflow (.github/workflows/metadata.yml),
triggered manually (workflow_dispatch):
- download – pulls the current live listing from the store into
prop/store_metadata/, commits & pushes it to theproprepo, and bumps the submodule pointer here. Use this to refresh from whatever is live in the stores. - upload – pushes the
.txtfiles and the screenshots to the store; it never uploads the binary. On Android it also skips the per-release "what's new" (those are handled by the build workflow), on iOS it updates the current editable version's metadata without bumping the version or submitting for review.
Pick the platform (android, ios, or both) and the action when you run it.
These already exist for the build pipeline and are reused here:
- Android:
SERVICE_ACCOUNT_JSON(Google Play service account JSON) - iOS:
APPSTORE_API_KEY(App Store Connect key id),APPSTORE_API_ISSUER_ID,APPSTORE_API_KEY_FILE_BASE64(base64 of the.p8key) - Both:
PAT_TOKEN— used to check out the privatepropsubmodule and, on download, to push the synced listing text back toOpenBikeControl/prop. It must have write access to that repo.
The prop submodule must be checked out (git submodule update --init prop),
since the metadata lives there.
bundle install
# Before an upload, stage the screenshots into the fastlane folders:
scripts/prepare_store_screenshots.sh
# Android (needs android/fastlane/service-account.json)
cd android && bundle exec fastlane download_metadata # or upload_metadata
# iOS (needs the App Store Connect API key env vars / .p8)
cd ios && bundle exec fastlane download_metadata # or upload_metadataA local download writes into prop/store_metadata/ — commit & push that from
within the prop submodule, then bump the pointer here.
Note: keywords and promotional text are not exposed on the public store pages, so a
downloadcannot refresh those two fields — they are maintained by hand inprop/store_metadata/.