From 4e01534d76309510de9225066e4e8a7366c5af8a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 20 Feb 2025 12:27:33 +0200 Subject: [PATCH 1/5] refactor(i18n): move list of locales out of options --- src/routes/api/options.ts | 34 ++-------------------------------- src/services/i18n.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 33cd5fa8c..8be441b61 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -5,7 +5,7 @@ import log from "../../services/log.js"; import searchService from "../../services/search/services/search.js"; import ValidationError from "../../errors/validation_error.js"; import type { Request } from "express"; -import { changeLanguage } from "../../services/i18n.js"; +import { changeLanguage, getLocales } from "../../services/i18n.js"; import { listSyntaxHighlightingThemes } from "../../services/code_block_theme.js"; import type { OptionNames } from "../../services/options_interface.js"; @@ -154,37 +154,7 @@ function getSyntaxHighlightingThemes() { } function getSupportedLocales() { - // TODO: Currently hardcoded, needs to read the list of available languages. - return [ - { - id: "en", - name: "English" - }, - { - id: "de", - name: "Deutsch" - }, - { - id: "es", - name: "Español" - }, - { - id: "fr", - name: "Français" - }, - { - id: "cn", - name: "简体中文" - }, - { - id: "tw", - name: "繁體中文" - }, - { - id: "ro", - name: "Română" - } - ]; + return getLocales(); } function isAllowed(name: string) { diff --git a/src/services/i18n.ts b/src/services/i18n.ts index 4f659cb30..0c0330209 100644 --- a/src/services/i18n.ts +++ b/src/services/i18n.ts @@ -20,6 +20,40 @@ export async function initializeTranslations() { }); } +export function getLocales() { + // TODO: Currently hardcoded, needs to read the list of available languages. + return [ + { + id: "en", + name: "English" + }, + { + id: "de", + name: "Deutsch" + }, + { + id: "es", + name: "Español" + }, + { + id: "fr", + name: "Français" + }, + { + id: "cn", + name: "简体中文" + }, + { + id: "tw", + name: "繁體中文" + }, + { + id: "ro", + name: "Română" + } + ]; +} + function getCurrentLanguage() { let language; if (sql_init.isDbInitialized()) { From 34b4e6d069b714e3ec2fdad818cbd55e04909974 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 20 Feb 2025 12:39:56 +0200 Subject: [PATCH 2/5] feat(test): ensure frontend translations are valid JSON --- src/services/i18n.spec.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/services/i18n.spec.ts diff --git a/src/services/i18n.spec.ts b/src/services/i18n.spec.ts new file mode 100644 index 000000000..03b4fad62 --- /dev/null +++ b/src/services/i18n.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import * as i18n from "./i18n.js"; +import path from "path"; +import fs from "fs"; + +describe("i18n", () => { + it("frontend translations are valid JSON", () => { + const translationDir = "src/public/translations"; + const locales = i18n.getLocales(); + + for (const locale of locales) { + const translationPath = path.join(translationDir, locale.id, "translation.json"); + const translationFile = fs.readFileSync(translationPath, { encoding: "utf-8" }); + expect(() => { + JSON.parse(translationFile); + }, `JSON error while parsing locale '${locale.id}' at "${translationPath}"`).not.toThrow(); + } + }); +}); From c255af67c9ccd28c791ef3cce7b506b73a7a390f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 20 Feb 2025 12:40:11 +0200 Subject: [PATCH 3/5] fix(i18n): Chinese translations missing due to invalid JSON --- src/public/translations/cn/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/translations/cn/translation.json b/src/public/translations/cn/translation.json index 7deb0d59c..296a46136 100644 --- a/src/public/translations/cn/translation.json +++ b/src/public/translations/cn/translation.json @@ -988,7 +988,7 @@ "web_view": { "web_view": "网页视图", "embed_websites": "网页视图类型的笔记允许您将网站嵌入到 Trilium 中。", - "create_label": "首先,请创建一个带有您要嵌入的 URL 地址的标签,例如 #webViewSrc=\"https://www.bing.com\"", + "create_label": "首先,请创建一个带有您要嵌入的 URL 地址的标签,例如 #webViewSrc=\"https://www.bing.com\"" }, "backend_log": { "refresh": "刷新" From f6b6b2e7402efca153d4b61aca5666c70438f68f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 20 Feb 2025 12:42:42 +0200 Subject: [PATCH 4/5] feat(test): ensure backend translations are valid JSON --- src/services/i18n.spec.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/services/i18n.spec.ts b/src/services/i18n.spec.ts index 03b4fad62..03d711ae0 100644 --- a/src/services/i18n.spec.ts +++ b/src/services/i18n.spec.ts @@ -3,17 +3,24 @@ import * as i18n from "./i18n.js"; import path from "path"; import fs from "fs"; +function checkTranslations(translationDir: string, translationFileName: string) { + const locales = i18n.getLocales(); + + for (const locale of locales) { + const translationPath = path.join(translationDir, locale.id, translationFileName); + const translationFile = fs.readFileSync(translationPath, { encoding: "utf-8" }); + expect(() => { + JSON.parse(translationFile); + }, `JSON error while parsing locale '${locale.id}' at "${translationPath}"`).not.toThrow(); + } +} + describe("i18n", () => { it("frontend translations are valid JSON", () => { - const translationDir = "src/public/translations"; - const locales = i18n.getLocales(); + checkTranslations("src/public/translations", "translation.json"); + }); - for (const locale of locales) { - const translationPath = path.join(translationDir, locale.id, "translation.json"); - const translationFile = fs.readFileSync(translationPath, { encoding: "utf-8" }); - expect(() => { - JSON.parse(translationFile); - }, `JSON error while parsing locale '${locale.id}' at "${translationPath}"`).not.toThrow(); - } + it("backend translations are valid JSON", () => { + checkTranslations("translations", "server.json"); }); }); From ca1d5207d8746331f29a6f70dbdc53302a1b7dcf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 20 Feb 2025 17:02:34 +0200 Subject: [PATCH 5/5] fix(build): try using ARM runner for building docker --- .github/workflows/main-docker.yml | 73 ++++++++++++++++--------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index e3f2393b9..0c1be531a 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout the repository uses: actions/checkout@v4 - + - name: Set IMAGE_NAME to lowercase run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV - name: Set TEST_TAG to lowercase @@ -47,16 +47,16 @@ jobs: with: node-version: 20 cache: "npm" - + - name: Install npm dependencies run: npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps - + - name: Run the TypeScript build run: npx tsc - + - name: Create server-package.json run: cat package.json | grep -v electron > server-package.json @@ -69,12 +69,12 @@ jobs: tags: ${{ env.TEST_TAG }} cache-from: type=gha cache-to: type=gha,mode=max - + - name: Validate container run output run: | CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host -e TRILIUM_PORT=8082 --volume ./integration-tests/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }}) echo "Container ID: $CONTAINER_ID" - + - name: Wait for the healthchecks to pass uses: stringbean/docker-healthcheck-action@v3 with: @@ -82,7 +82,7 @@ jobs: wait-time: 50 require-status: running require-healthy: true - + - name: Run Playwright tests run: TRILIUM_DOCKER=1 npx playwright test - uses: actions/upload-artifact@v4 @@ -100,7 +100,20 @@ jobs: build: name: Build Docker images - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - dockerfile: Dockerfile.alpine + platform: linux/amd64 + image: ubuntu-latest + - dockerfile: Dockerfile + platform: linux/arm64 + image: ubuntu-24.04-arm + - dockerfile: Dockerfile + platform: linux/arm/v7 + image: ubuntu-24.04-arm + runs-on: ${{ matrix.image }} needs: - test_docker permissions: @@ -108,16 +121,6 @@ jobs: packages: write attestations: write id-token: write - strategy: - fail-fast: false - matrix: - include: - - dockerfile: Dockerfile.alpine - platform: linux/amd64 - - dockerfile: Dockerfile - platform: linux/arm64 - - dockerfile: Dockerfile - platform: linux/arm/v7 steps: - name: Prepare run: | @@ -144,13 +147,13 @@ jobs: type=sha flavor: | latest=false - + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Set up node & dependencies uses: actions/setup-node@v4 @@ -169,14 +172,14 @@ jobs: registry: ${{ env.GHCR_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - + - name: Login to DockerHub uses: docker/login-action@v3 with: registry: ${{ env.DOCKERHUB_REGISTRY }} username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - + - name: Build and push by digest id: build uses: docker/build-push-action@v6 @@ -186,13 +189,13 @@ jobs: platforms: ${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} outputs: type=image,name=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - + - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - + - name: Upload digest uses: actions/upload-artifact@v4 with: @@ -220,7 +223,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Docker meta id: meta uses: docker/metadata-action@v5 @@ -237,14 +240,14 @@ jobs: registry: ${{ env.GHCR_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - + - name: Login to DockerHub uses: docker/login-action@v3 with: registry: ${{ env.DOCKERHUB_REGISTRY }} username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - + - name: Create manifest list and push working-directory: /tmp/digests run: | @@ -255,7 +258,7 @@ jobs: docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ -t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \ $(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) - + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ -t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \ $(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) @@ -267,25 +270,25 @@ jobs: docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ -t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \ $(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) - + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ -t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \ $(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) - + # Small delay to ensure stable tag is fully propagated sleep 5 - + # Now update latest tags docker buildx imagetools create \ -t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable - + docker buildx imagetools create \ -t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable - + fi - + - name: Inspect image run: | docker buildx imagetools inspect ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}