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)
[](https://github.com/OpenDataEnsemble/ode/actions/workflows/ci.yml)
@@ -17,86 +17,106 @@
[](./LICENSE)
-[](#-contributing)
+[](#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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+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() {
+