diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index f8a782270..fefaa6850 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Label PR based on changed files - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 + uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7 with: repo-token: ${{ secrets.GITHUB_TOKEN }} configuration-path: .github/labeler.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5678bd034..b96e4615b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: fetch-depth: 0 - name: Detect changed components - uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2 + uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4 id: filter with: filters: | @@ -71,7 +71,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 @@ -114,7 +114,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 64ebf94e0..717aa9f45 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -38,7 +38,7 @@ jobs: fetch-depth: 0 - name: Set up pnpm - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 @@ -84,7 +84,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Set up pnpm - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 diff --git a/.github/workflows/e2e-attachments.yml b/.github/workflows/e2e-attachments.yml index b01441b09..12fae07f6 100644 --- a/.github/workflows/e2e-attachments.yml +++ b/.github/workflows/e2e-attachments.yml @@ -76,7 +76,7 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 diff --git a/.github/workflows/formulus-android.yml b/.github/workflows/formulus-android.yml index 821f317c8..52220c511 100644 --- a/.github/workflows/formulus-android.yml +++ b/.github/workflows/formulus-android.yml @@ -44,7 +44,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Setup pnpm - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d # v4.1 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: version: 11.22.0 @@ -70,7 +70,7 @@ jobs: run: pnpm run build:copy - name: Upload formplayer assets artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: formplayer-assets path: formulus/android/app/src/main/assets/formplayer_dist @@ -89,13 +89,13 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Download formplayer assets artifact - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: formplayer-assets path: formulus/android/app/src/main/assets/formplayer_dist - name: Setup pnpm - uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d # v4.1 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 with: version: 11.22.0 @@ -126,7 +126,7 @@ jobs: # vendor-notifee-core.mjs is a no-op when third_party/notifee is already at the pinned commit. - name: Cache vendored Notifee - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: formulus/third_party/notifee key: notifee-${{ hashFiles('formulus/scripts/vendor-notifee-core.mjs') }} @@ -153,7 +153,7 @@ jobs: # Gradle dependency/wrapper caching is handled by gradle/actions/setup-gradle below. - name: Setup Gradle - uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2 + uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 with: # Persist the Gradle build cache across CI runs (wrapper + deps + task outputs). # Leave the daemon on so assembleRelease + bundleRelease share one JVM. @@ -227,7 +227,7 @@ jobs: -PreactNativeArchitectures=${{ env.FORMULUS_ABIS_RELEASE }} - name: Upload APK artifact - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: formulus-android-apk-${{ github.event_name }}-${{ github.run_id }} path: | @@ -235,7 +235,7 @@ jobs: - name: Upload AAB artifact if: github.event_name != 'pull_request' - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: formulus-android-aab-${{ github.event_name }}-${{ github.run_id }} path: | @@ -243,7 +243,7 @@ jobs: - name: Upload universal APK + AAB to GitHub Release if: github.event_name == 'release' - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 with: files: | formulus/android/app/build/outputs/apk/**/**/*-universal-*.apk diff --git a/.github/workflows/ode-desktop.yml b/.github/workflows/ode-desktop.yml index 74546b842..9620a542e 100644 --- a/.github/workflows/ode-desktop.yml +++ b/.github/workflows/ode-desktop.yml @@ -48,7 +48,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 @@ -120,7 +120,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 @@ -183,13 +183,13 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Download embedded formplayer - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: desktop-formplayer-dist path: desktop/public/formplayer_dist - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 @@ -344,13 +344,13 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Download embedded formplayer - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: desktop-formplayer-dist path: desktop/public/formplayer_dist - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 @@ -459,7 +459,7 @@ jobs: fi - name: Attach installers to Release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 with: tag_name: ${{ github.event.release.tag_name }} files: desktop/dist-release/* diff --git a/.github/workflows/sbom-release.yml b/.github/workflows/sbom-release.yml index f1619eaf2..3208400c7 100644 --- a/.github/workflows/sbom-release.yml +++ b/.github/workflows/sbom-release.yml @@ -27,7 +27,7 @@ jobs: - name: Setup pnpm # scripts/sbom/generate-sboms.mjs shells out to `pnpm list`; needs pnpm on PATH. - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 @@ -63,7 +63,7 @@ jobs: - name: Upload SBOM artifact (manual runs) if: github.event_name == 'workflow_dispatch' - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: cyclonedx-sbom path: sbom-dist/*.cdx.json @@ -71,7 +71,7 @@ jobs: - name: Upload SBOMs to GitHub Release if: github.event_name == 'release' - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 with: tag_name: ${{ github.event.release.tag_name }} files: sbom-dist/*.cdx.json diff --git a/.github/workflows/synkronus-cli-docker.yml b/.github/workflows/synkronus-cli-docker.yml index 0a5b3ff21..9326eb829 100644 --- a/.github/workflows/synkronus-cli-docker.yml +++ b/.github/workflows/synkronus-cli-docker.yml @@ -71,7 +71,7 @@ jobs: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="${LD_FLAGS}" -o "${ROOT}/docker-dist/synk-linux-arm64" ./cmd/synkronus - name: Upload docker build context artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: synkronus-cli-docker-dist path: docker-dist/ @@ -91,7 +91,7 @@ jobs: uses: actions/checkout@v4 - name: Download docker build context - uses: actions/download-artifact@v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: synkronus-cli-docker-dist path: docker-dist/ @@ -105,7 +105,7 @@ jobs: - name: Log in to Github Container Registry if: github.event_name != 'pull_request' - uses: redhat-actions/podman-login@v1 + uses: redhat-actions/podman-login@50c2d9a331bb67c8fdab99b86455fad05e2e3252 # v2 with: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -122,7 +122,7 @@ jobs: - name: Build image with Buildah id: build-image - uses: redhat-actions/buildah-build@v2 + uses: redhat-actions/buildah-build@719e3c40d8af9790c23eca13f7daa339f2867034 # v3 with: image: ${{ env.IMAGE_NAME }} tags: ${{ steps.meta.outputs.tag-names }} @@ -135,7 +135,7 @@ jobs: - name: Push image to registry id: push if: github.event_name != 'pull_request' - uses: redhat-actions/push-to-registry@v2 + uses: redhat-actions/push-to-registry@94ade333c38ecc0e60e94785125d9a52ca423b37 # v3 with: image: ${{ steps.build-image.outputs.image }} tags: ${{ steps.build-image.outputs.tags }} @@ -155,7 +155,7 @@ jobs: uses: actions/checkout@v4 - name: Download docker build context - uses: actions/download-artifact@v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: synkronus-cli-docker-dist path: docker-dist/ @@ -169,7 +169,7 @@ jobs: - name: Log in to Github Container Registry if: github.event_name != 'pull_request' - uses: redhat-actions/podman-login@v1 + uses: redhat-actions/podman-login@50c2d9a331bb67c8fdab99b86455fad05e2e3252 # v2 with: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -186,7 +186,7 @@ jobs: - name: Build image with Buildah id: build-image - uses: redhat-actions/buildah-build@v2 + uses: redhat-actions/buildah-build@719e3c40d8af9790c23eca13f7daa339f2867034 # v3 with: image: ${{ env.IMAGE_NAME }} tags: ${{ steps.meta.outputs.tag-names }} @@ -199,7 +199,7 @@ jobs: - name: Push image to registry id: push if: github.event_name != 'pull_request' - uses: redhat-actions/push-to-registry@v2 + uses: redhat-actions/push-to-registry@94ade333c38ecc0e60e94785125d9a52ca423b37 # v3 with: image: ${{ steps.build-image.outputs.image }} tags: ${{ steps.build-image.outputs.tags }} @@ -217,7 +217,7 @@ jobs: attestations: write steps: - name: Log in to Github Container Registry (docker) - uses: docker/login-action@v3 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -289,7 +289,7 @@ jobs: echo "digest=${DIGEST}" >> "${GITHUB_OUTPUT}" - name: Generate artifact attestation - uses: actions/attest-build-provenance@v1 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.manifest-digest.outputs.digest }} diff --git a/.github/workflows/synkronus-cli.yml b/.github/workflows/synkronus-cli.yml index 5ba04fb87..cab62c3f8 100644 --- a/.github/workflows/synkronus-cli.yml +++ b/.github/workflows/synkronus-cli.yml @@ -103,7 +103,7 @@ jobs: ls -R dist - name: Upload assets to GitHub Release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 with: files: | synkronus-cli/dist/synkronus-cli-* diff --git a/.github/workflows/synkronus-docker.yml b/.github/workflows/synkronus-docker.yml index 5681fe50e..8226b965b 100644 --- a/.github/workflows/synkronus-docker.yml +++ b/.github/workflows/synkronus-docker.yml @@ -69,7 +69,7 @@ jobs: cache-dependency-path: synkronus/go.sum - name: Setup pnpm - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6 with: version: 11.22.0 @@ -115,7 +115,7 @@ jobs: cp -a static "${ROOT}/docker-dist/" - name: Upload docker build context artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: synkronus-docker-dist path: docker-dist/ @@ -135,7 +135,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Download docker build context - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: synkronus-docker-dist path: docker-dist/ @@ -151,7 +151,7 @@ jobs: - name: Log in to Github Container Registry if: github.event_name != 'pull_request' - uses: redhat-actions/podman-login@4934294ad0449894bcd1e9f191899d7292469603 # v1 + uses: redhat-actions/podman-login@50c2d9a331bb67c8fdab99b86455fad05e2e3252 # v2 with: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -168,7 +168,7 @@ jobs: - name: Build image with Buildah id: build-image - uses: redhat-actions/buildah-build@7a95fa7ee0f02d552a32753e7414641a04307056 # v2 + uses: redhat-actions/buildah-build@719e3c40d8af9790c23eca13f7daa339f2867034 # v3 with: image: ${{ env.IMAGE_NAME }} tags: ${{ steps.meta.outputs.tag-names }} @@ -181,7 +181,7 @@ jobs: - name: Push image to registry id: push if: github.event_name != 'pull_request' - uses: redhat-actions/push-to-registry@5ed88d269cf581ea9ef6dd6806d01562096bee9c # v2 + uses: redhat-actions/push-to-registry@94ade333c38ecc0e60e94785125d9a52ca423b37 # v3 with: image: ${{ steps.build-image.outputs.image }} tags: ${{ steps.build-image.outputs.tags }} @@ -201,7 +201,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Download docker build context - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: synkronus-docker-dist path: docker-dist/ @@ -217,7 +217,7 @@ jobs: - name: Log in to Github Container Registry if: github.event_name != 'pull_request' - uses: redhat-actions/podman-login@4934294ad0449894bcd1e9f191899d7292469603 # v1 + uses: redhat-actions/podman-login@50c2d9a331bb67c8fdab99b86455fad05e2e3252 # v2 with: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -234,7 +234,7 @@ jobs: - name: Build image with Buildah id: build-image - uses: redhat-actions/buildah-build@7a95fa7ee0f02d552a32753e7414641a04307056 # v2 + uses: redhat-actions/buildah-build@719e3c40d8af9790c23eca13f7daa339f2867034 # v3 with: image: ${{ env.IMAGE_NAME }} tags: ${{ steps.meta.outputs.tag-names }} @@ -247,7 +247,7 @@ jobs: - name: Push image to registry id: push if: github.event_name != 'pull_request' - uses: redhat-actions/push-to-registry@5ed88d269cf581ea9ef6dd6806d01562096bee9c # v2 + uses: redhat-actions/push-to-registry@94ade333c38ecc0e60e94785125d9a52ca423b37 # v3 with: image: ${{ steps.build-image.outputs.image }} tags: ${{ steps.build-image.outputs.tags }} @@ -265,7 +265,7 @@ jobs: attestations: write steps: - name: Log in to Github Container Registry (docker) - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -337,7 +337,7 @@ jobs: echo "digest=${DIGEST}" >> "${GITHUB_OUTPUT}" - name: Generate artifact attestation - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.manifest-digest.outputs.digest }} diff --git a/AGENTS.md b/AGENTS.md index 546d3fbb4..f153bc9d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,7 +137,7 @@ git push origin v1.1.1-alpha.3 - **Custom app bridge (v1.1.0+):** `persistObservation` (headless write), `sync`, `getConnectivityStatus`, `getCurrentDataRevisionCount`, and `openFormplayer` options `skipFinalize` / `skipDraftSelection` — contract in [`FormulusInterfaceDefinition.ts`](formulus/src/webview/FormulusInterfaceDefinition.ts); run `pnpm run sync-interface` in formplayer after changes. - **Sub-observations:** Each nested Formplayer session validates its own schema; `skipFinalize` only skips the Finalize page. Custom validators are per-session — see [Custom Extensions — nested sessions](https://opendataensemble.org/docs/guides/custom-extensions#nested-sessions-and-custom-validators) (docs site). - **Shared UI tokens:** Install **tokens** before **components** / **formplayer** where the docs require it (see package READMEs and formplayer AGENTS). -- **i18n (two layers):** ODE-owned locales (`en`/`pt`/`fr`) for Formulus + Formplayer chrome via Settings → Language; form-owned copy via optional `translations` on `ui.json` elements (preprocessed at form init). See [FORM_LOCALIZATION_GUIDE.md](FORM_LOCALIZATION_GUIDE.md) (monorepo) and [form translations guide](https://opendataensemble.org/docs/guides/form-translations) (published). +- **i18n (two layers):** ODE-owned locales (`en`/`pt`/`fr`/`sw`) for Formulus + Formplayer chrome via Settings → Language; form-owned copy via optional `translations` on `ui.json` elements (preprocessed at form init). See [FORM_LOCALIZATION_GUIDE.md](FORM_LOCALIZATION_GUIDE.md) (monorepo) and [form translations guide](https://opendataensemble.org/docs/guides/form-translations) (published). --- diff --git a/Dockerfile b/Dockerfile index 02b64dce7..4374915a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,7 @@ WORKDIR /app/synkronus-portal RUN pnpm run build # Stage 2: Build the Go application (Synkronus) with embedded portal -FROM golang:1.26.0-alpine AS synkronus-builder +FROM golang:1.27.1-alpine AS synkronus-builder RUN apk add --no-cache git @@ -61,7 +61,7 @@ RUN echo "Building Synkronus with version: ${SYNKRONUS_VERSION}" && \ go build -a -ldflags="-w -s -X github.com/opendataensemble/synkronus/pkg/version.version=${SYNKRONUS_VERSION}" -o synkronus ./cmd/synkronus # Stage 3: Minimal runtime image — single Go server (API + portal) -FROM alpine:3.23 +FROM alpine:3.24 RUN apk --no-cache add ca-certificates tzdata wget diff --git a/Dockerfile.cli.slim b/Dockerfile.cli.slim index 48a1c0fa1..93bfd28dc 100644 --- a/Dockerfile.cli.slim +++ b/Dockerfile.cli.slim @@ -1,7 +1,7 @@ # CI/runtime-only image: expects a pre-built synk binary in the build context. # Build context layout: ./synk (static binary) -FROM alpine:3.23 +FROM alpine:3.24 RUN apk --no-cache add ca-certificates diff --git a/Dockerfile.slim b/Dockerfile.slim index 781427a45..ee6d18490 100644 --- a/Dockerfile.slim +++ b/Dockerfile.slim @@ -2,7 +2,7 @@ # Build context layout: ./synkronus (binary), ./openapi/, ./static/ # Portal assets are embedded in the binary at compile time. -FROM alpine:3.23 +FROM alpine:3.24 RUN apk --no-cache add ca-certificates tzdata wget diff --git a/FORM_LOCALIZATION_GUIDE.md b/FORM_LOCALIZATION_GUIDE.md index bbd7f9921..d0dd62042 100644 --- a/FORM_LOCALIZATION_GUIDE.md +++ b/FORM_LOCALIZATION_GUIDE.md @@ -12,7 +12,7 @@ How **user-defined forms** (JSON Schema + `ui.json` in custom app bundles) suppo | Layer | Who owns it | Where strings live | When it applies | |-------|-------------|-------------------|-----------------| -| **ODE chrome** | ODE platform | `formulus-formplayer/src/locales/{en,pt,fr}.json`, Formulus i18n catalogs | Next/Back, validation errors, sub-obs “+ Add …” template, loading text | +| **ODE chrome** | ODE platform | `formulus-formplayer/src/locales/{en,pt,fr,sw}.json`, Formulus i18n catalogs | Next/Back, validation errors, sub-obs “+ Add …” template, loading text | | **Form copy** | Form / app author | `ui.json` base fields + optional `translations` blocks | Field labels, descriptions, SwipeLayout headers, custom widget copy | Form authors **do not** edit ODE locale JSON. They embed translations in each form’s `ui.json`. ODE developers extend chrome strings when adding new platform UI. @@ -21,7 +21,7 @@ Form authors **do not** edit ODE locale JSON. They embed translations in each fo ## How form locale is chosen -1. User setting in Formulus **Settings → Language** (`auto`, `en`, `pt`, `fr`) +1. User setting in Formulus **Settings → Language** (`auto`, `en`, `pt`, `fr`, `sw`) 2. **Auto** → device language; if unsupported, bundle `defaultLocale` from `app.config.json` 3. Fallback `en` @@ -240,7 +240,7 @@ When changing merge rules or label resolution, update **`applyFormUiTranslations ## Author checklist - [ ] Every visible field has `Control.label` in `ui.json` (not only `schema.title`) -- [ ] Base `label` set; `translations` added for other ODE UI locales you ship (`en`, `pt`, `fr`) +- [ ] Base `label` set; `translations` added for other ODE UI locales you ship (`en`, `pt`, `fr`, `sw`) - [ ] SwipeLayout headers/buttons translated where needed - [ ] Sub-obs columns use `key` only; labels live on linked child forms - [ ] Custom question type strings in `options`, not in renderer JS diff --git a/README.md b/README.md index d671cf1e6..1d1866856 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Open Data Ensemble (ODE) 🎼 +# Open Data Ensemble (ODE) [![CI](https://github.com/OpenDataEnsemble/ode/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/OpenDataEnsemble/ode/actions/workflows/ci.yml) @@ -17,86 +17,106 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) -[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#-contributing) +[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contributing) -Welcome to the **Open Data Ensemble** - a comprehensive platform for mobile data collection and synchronization! +**Open Data Ensemble (ODE)** is an open-source platform for mobile data collection and synchronization. ## About ODE -ODE is a monorepo containing all the core components in the ODE universe - the essential members of our *ensemble*, if you will. We're building a modern, open-source solution for field data collection that works seamlessly across mobile devices and web platforms. +This monorepo contains all the core components of ODE, the members of our *ensemble*. Together they provide an offline-first solution for field data collection that works across mobile devices, the desktop and the web. + +## Formulus at a Glance + +Formulus is the mobile app that field teams use to collect data. It works fully offline, syncs with your Synkronus server when a connection is available, and can hold several projects on one device. + +

+ Home tab in Formulus showing a custom app + Forms list in Formulus + Observations list in Formulus + Sync screen in Formulus +

+

+ Sync in progress in Formulus + Profiles screen in Formulus + Settings screen in Formulus + Help and support screen in Formulus +

+ +Formulus is available on [F-Droid](https://f-droid.org/packages/org.opendataensemble.formulus/), the [App Store](https://apps.apple.com/app/formulus/id6798318215) and [Google Play](https://play.google.com/store/apps/details?id=org.opendataensemble.formulus), and as an APK from [GitHub Releases](https://github.com/OpenDataEnsemble/ode/releases/latest). ## Architecture This repository houses the core components plus the public documentation site: ### **formulus** -A React Native project containing the code for Android and iOS apps. This is your mobile data collection companion, designed for field work and offline-first data gathering. +A React Native project containing the Android and iOS apps. Formulus is the mobile data collection client, designed for field work and offline-first data gathering. ### **formulus-formplayer** -A React web application responsible for rendering JSON forms and communicating with the Formulus mobile app. It provides the dynamic form interface that powers your data collection workflows, including built-in controls such as **`format: sub-observation`** (embedded child rows). Author-facing documentation: [Custom extensions](https://opendataensemble.org/docs/guides/custom-extensions) on opendataensemble.org. +A React web application responsible for rendering JSON forms and communicating with the Formulus mobile app. It provides the form interface used during data collection, including built-in controls such as **`format: sub-observation`** (embedded child rows). Author-facing documentation: [Custom extensions](https://opendataensemble.org/docs/guides/custom-extensions) on opendataensemble.org. ### **synkronus** -The server component written in Go. This handles data synchronization, storage, and provides the backbone for all data operations across the platform. +The server component, written in Go. It handles authentication, data synchronization and storage for the whole platform. ### **synkronus-cli** -A command-line utility to interact with the Synkronus server. Use it to manage custom app data, handle user administration, export data to Parquet format, and perform various administrative tasks. +A command-line utility to interact with the Synkronus server. Use it to manage custom app data, handle user administration, export data to Parquet format, and perform other administrative tasks. -You can install the CLI by running this command: +Install the CLI with one of these commands: -#### Powershell +#### PowerShell ```powershell irm https://raw.githubusercontent.com/OpenDataEnsemble/ode/main/scripts/install-synkronus-cli.ps1 | iex ``` -#### Mac OS / zsh +#### macOS / Linux ```sh curl -fsSL https://raw.githubusercontent.com/OpenDataEnsemble/ode/main/scripts/install-synkronus-cli.sh | bash ``` - -**License note:** this component is currently **GPL-2.0-or-later** (see `synkronus-cli/LICENSE`) while the QR PNG stack depends on GPL-classified libraries; we plan to swap in a stdlib-only renderer and return the CLI to **MIT**. See `synkronus-cli/FOLLOWUP-custom-qrcode-writer.md` and `THIRD_PARTY_NOTICES.md`. +**License note:** this component is currently **GPL-2.0-or-later** (see [synkronus-cli/LICENSE](synkronus-cli/LICENSE)) while the QR PNG stack depends on GPL-classified libraries. We plan to swap in a stdlib-only renderer and return the CLI to **MIT**. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). ### **synkronus-portal** -A web-based version of the the **synkronus-cli** +A web-based admin interface for Synkronus. It covers the same tasks as **synkronus-cli** in the browser. + +### **desktop** +ODE Desktop, a Tauri app for managing collected data and for building and testing forms and custom apps. ### **docs** -The public documentation site (Docusaurus), published at [opendataensemble.org](https://opendataensemble.org/). It is an independent npm project inside this monorepo — see [docs/README.md](docs/README.md). +The public documentation site (Docusaurus), published at [opendataensemble.org](https://opendataensemble.org/). It is an independent npm project inside this monorepo. See [docs/README.md](docs/README.md). -## We're Young & Fresh! 🌱🌱🌱 +## A Young and Welcoming Project -ODE is a **young and vibrant open-source project**, and we're incredibly welcoming to contributors of all experience levels and interests! Whether you're passionate about: +ODE is a young open-source project, and we welcome contributors of all experience levels and interests, including: - **Software Development** (React Native, React, Go, TypeScript) - **Documentation** (helping others understand and use ODE) - **Implementation** (deploying, testing, real-world usage) - **Community Building** (fostering collaboration and growth) -- **UI/UX Design** (making data collection delightful) +- **UI/UX Design** (making data collection easier to use) - **Data Science** (improving data workflows and analytics) -...we'd love to have you join our ensemble! - ## Contributing -For the full contribution workflow — setup, commit rules, and PR checks — read [CONTRIBUTING.md](CONTRIBUTING.md). +For the full contribution workflow (setup, commit rules, and PR checks), read [CONTRIBUTING.md](CONTRIBUTING.md). -We believe that diverse perspectives and varied skill sets make our project stronger. Don't worry if you're new to open source or if you think your skills might not be "technical enough" - there's a place for everyone here. +Different perspectives and skills make the project stronger. If you are new to open source, or think your skills are not "technical enough", there is still a place for you here. -**Getting Started:** -- Browse our issues to find something that interests you -- Join our discussions to share ideas +**Getting started:** +- Browse the [issues](https://github.com/OpenDataEnsemble/ode/issues) to find something that interests you +- Join the [discussions](https://github.com/OpenDataEnsemble/ode/discussions) to share ideas - Improve documentation where you see gaps - Test the platform and report your experience - Share how you're using ODE in your work -## CI/CD Pipeline -This monorepo uses GitHub Actions for CI/CD. For details (trigger conditions, tags, and workflows), see `.github/CICD.md`. +## CI/CD Pipeline + +This monorepo uses GitHub Actions for CI/CD. For details (trigger conditions, tags, and workflows), see [.github/CICD.md](.github/CICD.md). - Synkronus Docker build & publish: `.github/workflows/synkronus-docker.yml` - Formulus Android build (includes Formplayer asset build): `.github/workflows/formulus-android.yml` - Documentation site: `.github/workflows/docs.yml` (validates PRs; deploys from `dev`) - Synkronus deployment docs: `synkronus/DOCKER.md`, `synkronus/DEPLOYMENT.md` -## Code Quality: Linting & Formatting +## Code Quality: Linting & Formatting ODE enforces consistent formatting and linting for the frontend projects both **locally** and in **CI**. @@ -132,10 +152,10 @@ CI will **fail** if: The repository root is [MIT](./LICENSE). **synkronus-cli** is **GPL-2.0-or-later** until the QR dependency cleanup above is done. Third-party and mixed-component details: [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md). -## Get Involved 📬 - -Ready to join the ensemble? We're excited to meet you and see what unique perspective you'll bring to ODE! +## Get Involved ---- +Questions, ideas or feedback are welcome: -*Building the future of open data collection, one contribution at a time.* +- Community forum: [forum.opendataensemble.org](https://forum.opendataensemble.org) +- GitHub: [issues](https://github.com/OpenDataEnsemble/ode/issues) and [discussions](https://github.com/OpenDataEnsemble/ode/discussions) +- Email: [hello@opendataensemble.org](mailto:hello@opendataensemble.org) diff --git a/desktop/src/App.css b/desktop/src/App.css index 219c43c23..a319d881f 100644 --- a/desktop/src/App.css +++ b/desktop/src/App.css @@ -3094,14 +3094,59 @@ input:not([type]) { grid-template-rows: auto minmax(0, 1fr); } + /* Keep navigation compact on smaller desktop windows so page content remains usable. */ .sidebar { position: sticky; top: 0; z-index: 4; + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + grid-template-areas: + 'brand mode status' + 'nav nav nav'; + align-items: center; + gap: 0.65rem 1rem; + padding: 0.75rem 1rem; border-right: 0; border-bottom: 1px solid #2d3449; } + .brand { + grid-area: brand; + min-width: 0; + } + + .brand p { + display: none; + } + + .mode-switch { + grid-area: mode; + } + + .nav { + grid-area: nav; + flex-direction: row; + gap: 0.35rem; + overflow-x: auto; + overscroll-behavior-inline: contain; + padding-bottom: 0.15rem; + } + + .nav-link { + flex: 0 0 auto; + white-space: nowrap; + } + + .sidebar-footer { + grid-area: status; + margin-top: 0; + } + + .sidebar-footer > span { + display: none; + } + .cards { grid-template-columns: 1fr; } diff --git a/desktop/src/lib/uiLocale.ts b/desktop/src/lib/uiLocale.ts index 6cf31b636..08b2704a7 100644 --- a/desktop/src/lib/uiLocale.ts +++ b/desktop/src/lib/uiLocale.ts @@ -1,7 +1,7 @@ /** Desktop UI locale — mirrors Formulus Settings key and resolution rules. */ const STORAGE_KEY = '@ode/uiLocale'; -const ODE_UI_LOCALES = ['en', 'pt', 'fr'] as const; +const ODE_UI_LOCALES = ['en', 'pt', 'fr', 'sw'] as const; export type DesktopUiLocale = (typeof ODE_UI_LOCALES)[number]; export type UiLocalePreference = 'auto' | DesktopUiLocale; @@ -32,11 +32,9 @@ export function getDesktopLocalePreference(): UiLocalePreference { const stored = localStorage.getItem(STORAGE_KEY); if ( stored === 'auto' || - stored === 'en' || - stored === 'pt' || - stored === 'fr' + (stored != null && (ODE_UI_LOCALES as readonly string[]).includes(stored)) ) { - return stored; + return stored as UiLocalePreference; } } catch { // ignore diff --git a/desktop/src/pages/FormPreviewPage.tsx b/desktop/src/pages/FormPreviewPage.tsx index df2cbca02..fcf24fdcf 100644 --- a/desktop/src/pages/FormPreviewPage.tsx +++ b/desktop/src/pages/FormPreviewPage.tsx @@ -735,6 +735,7 @@ export function FormPreviewPage() { + +
+
+
+
+
+ +

Your ODE journey

+
How have you taken part in ODE so far?

Select all that apply.

+
+ + + + + + + + + + + + + +
+

Links to pull requests, issues, posts or events are welcome.

+ +

Council

+
Which council are you applying to? (required) +
+ + + + +
+

What interests you about this area, and why do you want a more active role?

+
+
+
+
+
+
+ +

Availability

+
How much time could you usually give? +
+ + + + + +
+
How would you prefer to take part?

Select all that apply.

+
+ + + + + + +
+

Optional. It helps us plan and won't count against you.

+ +

Working with others

+
+
+
+
+ +
+ +
+

Your answers stay on this device until you send them. They are read only by ODE council members.

+
+ + + + + + + diff --git a/fastlane/metadata/android/en-US/changelogs/108.txt b/fastlane/metadata/android/en-US/changelogs/108.txt new file mode 100644 index 000000000..0859002ab --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/108.txt @@ -0,0 +1,7 @@ +New in Formulus 1.4.0: + +• Multiple profiles: keep several projects on one device, each with its own custom app and Synkronus server, and switch between them inside the app +• Photos and files are now chosen with the Android system picker +• Tables adapt to small screens +• Safer app bundle updates, with a clear message when storage is low +• Fixes for the camera, sign-in and switching servers diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png index 5a0b366fb..f7a3a3ccb 100644 Binary files a/fastlane/metadata/android/en-US/images/featureGraphic.png and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index ce1f057ce..41e3f002c 100644 Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/01_home.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/01_home.png new file mode 100644 index 000000000..e01486961 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/01_home.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/01_settings.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/01_settings.jpg deleted file mode 100644 index 565e17dd8..000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/01_settings.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/02_forms.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_forms.png new file mode 100644 index 000000000..53f6f8356 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_forms.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/02_observations.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_observations.jpg deleted file mode 100644 index b50b77a6a..000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/02_observations.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/03_observations.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/03_observations.png new file mode 100644 index 000000000..fb42ac966 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/03_observations.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/03_sync.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/03_sync.jpg deleted file mode 100644 index 3d1eced0e..000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/03_sync.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/04_about.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/04_about.jpg deleted file mode 100644 index 31b8dcb8d..000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/04_about.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/04_sync.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/04_sync.png new file mode 100644 index 000000000..a7ff1024e Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/04_sync.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/05_demo1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/05_demo1.jpg deleted file mode 100644 index 917afd8b6..000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/05_demo1.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/05_sync_progress.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/05_sync_progress.png new file mode 100644 index 000000000..3bba4a5a8 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/05_sync_progress.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/06_demo2.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/06_demo2.jpg deleted file mode 100644 index 34a2897ed..000000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/06_demo2.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/06_profiles.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/06_profiles.png new file mode 100644 index 000000000..9377811d5 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/06_profiles.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/07_settings.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/07_settings.png new file mode 100644 index 000000000..dfa609ac9 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/07_settings.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/08_help_support.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/08_help_support.png new file mode 100644 index 000000000..987fad6e9 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/08_help_support.png differ diff --git a/formulus-formplayer/AGENTS.md b/formulus-formplayer/AGENTS.md index aa163c3aa..7079ef645 100644 --- a/formulus-formplayer/AGENTS.md +++ b/formulus-formplayer/AGENTS.md @@ -67,10 +67,10 @@ When a Control becomes not visible (`visible: false` from JSON Forms rules), For Two **independent** layers: -| Layer | Owner | Mechanism | -| ---------- | ----------- | ----------------------------------------------------------------------------------------- | -| ODE chrome | ODE | `src/locales/{en,pt,fr}.json`, `createOdeI18n()`, JsonForms `i18n` prop, `useOdeT()` hook | -| Form copy | Form author | Optional `translations` on `ui.json`; `applyFormUiTranslations()` at init in `App.tsx` | +| Layer | Owner | Mechanism | +| ---------- | ----------- | -------------------------------------------------------------------------------------------- | +| ODE chrome | ODE | `src/locales/{en,pt,fr,sw}.json`, `createOdeI18n()`, JsonForms `i18n` prop, `useOdeT()` hook | +| Form copy | Form author | Optional `translations` on `ui.json`; `applyFormUiTranslations()` at init in `App.tsx` | - Host passes `params.locale` (reserved bridge key, not observation data). - Custom question types receive localized `options` from `ui.json` via `CustomQuestionTypeAdapter`. diff --git a/formulus-formplayer/README.md b/formulus-formplayer/README.md index 01089b790..2ef85148c 100644 --- a/formulus-formplayer/README.md +++ b/formulus-formplayer/README.md @@ -33,7 +33,7 @@ Use `pnpm run build:copy` to build the project and copy the bundle into the Form ## Internationalization -- **ODE chrome** — `src/locales/{en,pt,fr}.json`, wired via `createOdeI18n()` and JsonForms `i18n`. Host sets `params.locale`. +- **ODE chrome** — `src/locales/{en,pt,fr,sw}.json`, wired via `createOdeI18n()` and JsonForms `i18n`. Host sets `params.locale`. - **Form copy** — optional `translations` objects on `ui.json`; merged once at init by `applyFormUiTranslations()`. See [form translations](https://opendataensemble.org/docs/guides/form-translations). ## Javascript interface diff --git a/formulus-formplayer/src/i18n/createOdeI18n.ts b/formulus-formplayer/src/i18n/createOdeI18n.ts index 45fefe578..f9453d4e0 100644 --- a/formulus-formplayer/src/i18n/createOdeI18n.ts +++ b/formulus-formplayer/src/i18n/createOdeI18n.ts @@ -3,6 +3,7 @@ import type { JsonFormsI18nState, Translator } from '@jsonforms/core'; import en from '../locales/en.json'; import pt from '../locales/pt.json'; import fr from '../locales/fr.json'; +import sw from '../locales/sw.json'; import type { OdeUiLocale } from './localeUtils'; type Catalog = Record; @@ -11,6 +12,7 @@ const CATALOGS: Record = { en: en as Catalog, pt: pt as Catalog, fr: fr as Catalog, + sw: sw as Catalog, }; /** Simple {{key}} interpolation for catalog strings. */ diff --git a/formulus-formplayer/src/i18n/localeUtils.test.ts b/formulus-formplayer/src/i18n/localeUtils.test.ts new file mode 100644 index 000000000..2a2d6e390 --- /dev/null +++ b/formulus-formplayer/src/i18n/localeUtils.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; +import { matchOdeCatalogLocale, resolveFormplayerLocale } from './localeUtils'; +import { odeT } from './createOdeI18n'; +import en from '../locales/en.json'; +import sw from '../locales/sw.json'; + +describe('Formplayer locale resolution', () => { + it('matches explicit sw and regional tags to the Swahili catalog', () => { + expect(matchOdeCatalogLocale('sw')).toBe('sw'); + expect(matchOdeCatalogLocale('sw-KE')).toBe('sw'); + expect(matchOdeCatalogLocale('sw-TZ')).toBe('sw'); + expect(resolveFormplayerLocale('sw-KE')).toBe('sw'); + expect(resolveFormplayerLocale('sw-TZ')).toBe('sw'); + }); + + it('Swahili catalog matches English keys and is not an English fallback', () => { + const enKeys = Object.keys(en).sort(); + const swKeys = Object.keys(sw).sort(); + expect(swKeys).toEqual(enKeys); + expect(odeT('sw', 'nav.nextScreen')).toBe('Skrini inayofuata'); + expect(odeT('sw', 'nav.nextScreen')).not.toBe(en['nav.nextScreen']); + }); +}); diff --git a/formulus-formplayer/src/i18n/localeUtils.ts b/formulus-formplayer/src/i18n/localeUtils.ts index ab4fc6c4d..ecfbdb8fa 100644 --- a/formulus-formplayer/src/i18n/localeUtils.ts +++ b/formulus-formplayer/src/i18n/localeUtils.ts @@ -2,7 +2,7 @@ * Locale utilities (mirrored from formulus/src/lib/locale.ts for Formplayer). */ -export const ODE_UI_LOCALES = ['en', 'pt', 'fr'] as const; +export const ODE_UI_LOCALES = ['en', 'pt', 'fr', 'sw'] as const; export type OdeUiLocale = (typeof ODE_UI_LOCALES)[number]; export function normalizeLocaleTag(tag: string): string { diff --git a/formulus-formplayer/src/locales/en.json b/formulus-formplayer/src/locales/en.json index 2e59a5b3f..16f75b448 100644 --- a/formulus-formplayer/src/locales/en.json +++ b/formulus-formplayer/src/locales/en.json @@ -42,6 +42,7 @@ "finalize.no": "No", "finalize.none": "None", "finalize.empty": "Empty", + "finalize.value.notApplicable": "Not applicable", "finalize.value.photoCaptured": "Photo captured", "finalize.value.photoWithName": "Photo: {{name}}", "finalize.value.captured": "Captured", diff --git a/formulus-formplayer/src/locales/fr.json b/formulus-formplayer/src/locales/fr.json index 1aac836c4..417232af6 100644 --- a/formulus-formplayer/src/locales/fr.json +++ b/formulus-formplayer/src/locales/fr.json @@ -42,6 +42,7 @@ "finalize.no": "Non", "finalize.none": "Aucun", "finalize.empty": "Vide", + "finalize.value.notApplicable": "Non applicable", "finalize.value.photoCaptured": "Photo capturée", "finalize.value.photoWithName": "Photo : {{name}}", "finalize.value.captured": "Capturée", diff --git a/formulus-formplayer/src/locales/pt.json b/formulus-formplayer/src/locales/pt.json index 079de5440..34dc0a816 100644 --- a/formulus-formplayer/src/locales/pt.json +++ b/formulus-formplayer/src/locales/pt.json @@ -42,6 +42,7 @@ "finalize.no": "Não", "finalize.none": "Nenhum", "finalize.empty": "Vazio", + "finalize.value.notApplicable": "Não aplicável", "finalize.value.photoCaptured": "Fotografia capturada", "finalize.value.photoWithName": "Fotografia: {{name}}", "finalize.value.captured": "Capturada", diff --git a/formulus-formplayer/src/locales/sw.json b/formulus-formplayer/src/locales/sw.json new file mode 100644 index 000000000..969405b27 --- /dev/null +++ b/formulus-formplayer/src/locales/sw.json @@ -0,0 +1,117 @@ +{ + "form.loading": "Inapakia fomu...", + "form.waitingForData": "Inasubiri data kutoka Formulus...", + "form.submitSuccess": "Fomu imewasilishwa!", + "form.errorLoading": "Hitilafu ya kupakia fomu", + "form.errorInitializing": "Hitilafu ya kuanzisha fomu", + "form.errorProcessing": "Hitilafu ya kuchakata data ya fomu", + "form.schemaMissing": "Muundo wa fomu haupo. Uonyeshaji wa fomu unaweza kushindikana au kuwa haujakamilika.", + "form.submitFailed": "Imeshindwa kuwasilisha fomu. Tafadhali jaribu tena.", + "form.submitMissingInit": "Haiwezi kuwasilisha fomu kwa sababu data ya kuanzisha haipo.", + "form.submitValidatorErrors": "Haiwezi kuwasilisha fomu hadi hitilafu za uthibitishaji maalum zitatuliwe.", + "form.missingFormId": "Kitambulisho cha fomu hakipo. Haiwezi kuanzisha fomu.", + "form.noNativeHost": "Haiwezi kuwasiliana na programu mwenyeji. Formplayer inaweza kuwa inaendeshwa katika kivinjari pekee.", + "form.initTimeout": "Imeshindwa kuanzisha fomu: Hakuna data iliyopokelewa kutoka kwa programu mwenyeji. Tafadhali jaribu tena.", + "nav.previous": "Iliyotangulia", + "nav.next": "Ifuatayo", + "nav.done": "Maliza", + "nav.finalize": "Kamilisha", + "nav.previousScreen": "Skrini iliyotangulia", + "nav.nextScreen": "Skrini inayofuata", + "draft.title": "Endelea na rasimu au anza mpya", + "draft.resume": "Endelea", + "draft.startNew": "Anza fomu mpya", + "draft.delete": "Futa rasimu", + "draft.cancel": "Ghairi", + "draft.savedAt": "Imehifadhiwa {{date}}", + "draft.savedAtLine": "Rasimu imehifadhiwa {{date}}", + "draft.recentDrafts": "Rasimu za hivi karibuni ({{count}})", + "draft.none": "Hakuna rasimu za hivi karibuni za fomu hii.", + "draft.formLabel": "Fomu: {{formType}}", + "draft.newFormSection": "Fomu mpya", + "draft.deleteConfirm": "Una uhakika unataka kufuta rasimu hii? Kitendo hiki hakiwezi kutenduliwa.", + "finalize.title": "Kamilisha", + "finalize.summary": "MUHTASARI WA FOMU", + "finalize.notProvided": "Haijatolewa", + "finalize.allValid": "Uthibitishaji wote umefaulu! Kagua majibu yako kisha uwasilishe.", + "finalize.fixErrors": "Tafadhali rekebisha hitilafu zifuatazo kabla ya kukamilisha:", + "finalize.reviewHint": "Kagua data yote uliyoweka hapa chini. Bofya sehemu yoyote ili kuihariri.", + "finalize.editField": "Hariri sehemu", + "finalize.yes": "Ndiyo", + "finalize.no": "Hapana", + "finalize.none": "Hakuna", + "finalize.empty": "Tupu", + "finalize.value.notApplicable": "Haitumiki", + "finalize.value.photoCaptured": "Picha imenaswa", + "finalize.value.photoWithName": "Picha: {{name}}", + "finalize.value.captured": "Imenaswa", + "finalize.value.qrScanned": "Msimbo wa QR umechanganuliwa", + "finalize.value.qrWithData": "Msimbo wa QR: {{data}}", + "finalize.value.signatureCaptured": "Saini imenaswa", + "finalize.value.signatureProvided": "Saini imetolewa", + "finalize.value.fileSelected": "Faili imechaguliwa", + "finalize.value.fileWithName": "Faili: {{name}}", + "finalize.value.audioRecorded": "Sauti imerekodiwa", + "finalize.value.audioWithName": "Sauti: {{name}}{{duration}}", + "finalize.value.gpsCaptured": "Mahali pa GPS yamenaswa", + "finalize.value.location": "Mahali: {{coords}}", + "finalize.value.videoCaptured": "Video imenaswa", + "finalize.value.videoWithName": "Video: {{name}}", + "validation.missingRequiredFields": "Sehemu zinazohitajika hazipo", + "validation.missingRequiredField": "Sehemu inayohitajika haipo", + "validation.missingRequiredFieldsNamed": "Sehemu inayohitajika haipo: {{names}}", + "validation.fieldsNeedAttention": "Sehemu {{count}} zinahitaji uangalizi: {{summary}} Gusa Maliza ili kukagua.", + "validation.oneFieldAttention": "Sehemu 1 inahitaji uangalizi", + "validation.fieldsAttention": "Sehemu {{count}} zinahitaji uangalizi", + "validation.moreFields": "(+{{count}} zaidi)", + "validation.tapDoneToReview": "Gusa Maliza ili kukagua.", + "validation.draftOnReturn": "Baadhi ya sehemu zinazohitajika hazipo. Mabadiliko yoyote ambayo hayajahifadhiwa yatapatikana kama rasimu utakaporudi.", + "validation.stayHere": "Baki hapa", + "validation.goBack": "Rudi nyuma", + "subObservation.add": "+ Ongeza rekodi", + "subObservation.addItem": "+ Ongeza {{itemLabel}}", + "subObservation.none": "Hakuna rekodi", + "subObservation.adding": "Inaongeza…", + "subObservation.addingItem": "Inaongeza {{itemLabel}}…", + "media.photo": "Picha", + "media.photoTap": "Gusa ili kunasa picha", + "media.audio": "Sauti", + "media.video": "Video", + "media.gps": "Mahali pa GPS", + "media.qrCode": "Msimbo wa QR", + "media.approximateDate": "Tarehe takriban", + "media.takePhoto": "Piga picha", + "media.openingCamera": "Inafungua kamera...", + "media.retakePhoto": "Piga picha tena", + "media.deletePhoto": "Futa picha", + "media.captureHint": "Nasa picha iliyo wazi.", + "media.capturedPhotoAlt": "Picha iliyonaswa", + "media.invalidFilename": "Jina la faili la picha kutoka kamera si sahihi.", + "media.cameraError": "Hitilafu ya kamera imetokea", + "media.unknownCameraError": "Hitilafu ya kamera isiyojulikana", + "media.captureFailed": "Imeshindwa kunasa picha. Tafadhali jaribu tena.", + "media.recording": "Inarekodi...", + "dynamicEnum.selectOption": "Chagua chaguo...", + "dynamicEnum.loading": "Inapakia chaguo...", + "dynamicEnum.retry": "Jaribu tena", + "dynamicEnum.noOptions": "Hakuna chaguo zinazopatikana", + "dynamicEnum.configMissing": "Usanidi wa x-dynamicEnum haupo", + "dynamicEnum.queryRequired": "x-dynamicEnum: swali linahitajika", + "dynamicEnum.functionNotFound": "Kazi \"{{functionName}}\" haijapatikana. Zinazopatikana: {{available}}.", + "dynamicEnum.loadFailed": "Imeshindwa kupakia chaguo zinazobadilika", + "cqt.errorTitle": "Hitilafu ya aina maalum ya swali", + "cqt.errorBody": "Aina maalum ya swali \"{{format}}\" ilikutana na hitilafu na haikuweza kuonyeshwa.", + "cqt.errorDetails": "Maelezo ya hitilafu (bofya kupanua)", + "cqt.errorContinue": "Fomu itaendelea kufanya kazi, lakini sehemu hii haiwezi kuhaririwa.", + "cqt.unknownError": "Hitilafu isiyojulikana", + "error.required": "Sehemu hii inahitajika", + "error.is a required property": "Sehemu hii inahitajika", + "error.must have required property": "Sehemu hii inahitajika", + "error.minLength": "Lazima iwe angalau herufi {{limit}}", + "error.maxLength": "Lazima iwe herufi {{limit}} au chini", + "error.minimum": "Lazima iwe angalau {{limit}}", + "error.maximum": "Lazima iwe {{limit}} au chini", + "error.pattern": "Muundo si sahihi", + "error.type": "Thamani si sahihi", + "error.Validation error": "Hitilafu ya uthibitishaji" +} diff --git a/formulus/AGENTS.md b/formulus/AGENTS.md index 8d72fc723..f09b44498 100644 --- a/formulus/AGENTS.md +++ b/formulus/AGENTS.md @@ -68,7 +68,7 @@ Server-side timeouts that used to kill long transfers live in Synkronus (`ReadHe ## UI language (i18n) -- **Settings → Language** (`SettingsScreen`): Auto / `en` / `pt` / `fr`; stored in AsyncStorage (`@ode/uiLocale`) via [`LocaleSettingsService`](src/services/LocaleSettingsService.ts). +- **Settings → Language** (`SettingsScreen`): Auto / `en` / `pt` / `fr` / `sw`; stored in AsyncStorage (`@ode/uiLocale`) via [`LocaleSettingsService`](src/services/LocaleSettingsService.ts). - Resolution: [`src/lib/locale.ts`](src/lib/locale.ts) — preference → device → `app.config.json` `defaultLocale` → `en`. - Shell strings: `react-i18next` + [`src/locales/`](src/locales/). - Formplayer receives resolved locale as `params.locale` from [`FormplayerModal`](src/components/FormplayerModal.tsx). diff --git a/formulus/android/app/build.gradle b/formulus/android/app/build.gradle index dbff82cdd..a497deac0 100644 --- a/formulus/android/app/build.gradle +++ b/formulus/android/app/build.gradle @@ -198,7 +198,7 @@ dependencies { // Explicitly include AndroidX dependencies to avoid conflicts implementation 'androidx.core:core:1.9.0' - implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'androidx.appcompat:appcompat:1.8.0' // Fix for react-native-push-notification duplicate class conflicts configurations.all { @@ -206,7 +206,7 @@ dependencies { exclude group: 'androidx.legacy', module: 'legacy-support-v4' resolutionStrategy { force 'androidx.core:core:1.9.0' - force 'androidx.appcompat:appcompat:1.7.1' + force 'androidx.appcompat:appcompat:1.8.0' } } } diff --git a/formulus/android/app/src/main/assets/webview/placeholder_app.html b/formulus/android/app/src/main/assets/webview/placeholder_app.html index a96d89fac..16a417b40 100644 --- a/formulus/android/app/src/main/assets/webview/placeholder_app.html +++ b/formulus/android/app/src/main/assets/webview/placeholder_app.html @@ -2,6 +2,7 @@ Custom App Placeholder +