From 681efe95af87afab9c220f5f140f7f1338cffc55 Mon Sep 17 00:00:00 2001 From: Husky <39809509+Huskydog9988@users.noreply.github.com> Date: Wed, 4 Jun 2025 19:53:30 -0400 Subject: [PATCH] i18n Support and Task improvements (#80) * fix: release workflow * feat: move mostly to internal tasks system * feat: migrate object clean to new task system * fix: release not getting good base version * chore: set version v0.3.0 * chore: style * feat: basic task concurrency * feat: temp pages to fill in page links * feat: inital i18n support * feat: localize store page * chore: style * fix: weblate doesn't like multifile thing * fix: update nuxt * feat: improved error logging * fix: using old task api * feat: basic translation docs * feat: add i18n eslint plugin * feat: translate store and auth pages * feat: more translation progress * feat: admin dash i18n progress * feat: enable update check by default in prod * fix: using wrong i18n keys * fix: crash in library sources page * feat: finish i18n work * fix: missing i18n translations * feat: use twemoji for emojis * feat: sanatize object ids * fix: EmojiText's alt text * fix: UserWidget not using links * feat: cache and auth for emoji api * fix: add more missing translations --- .github/workflows/release.yml | 27 +- .vscode/extensions.json | 12 + .vscode/settings.json | 17 +- CONTRIBUTING.md | 4 + components/AccountSidebar.vue | 13 +- components/AddLibraryButton.vue | 25 +- components/Auth/OpenID.vue | 6 +- components/Auth/Simple.vue | 19 +- components/CreateCollectionModal.vue | 18 +- components/DeleteCollectionModal.vue | 19 +- components/DeleteNewsModal.vue | 19 +- components/DropWordmark.vue | 6 +- components/EmojiText.vue | 17 + components/GamePanel.vue | 2 +- components/LibraryDirectory.vue | 6 +- components/NewsArticleCreateButton.vue | 94 +- components/NewsDirectory.vue | 38 +- components/NotificationItem.vue | 2 +- components/PlatformSelector.vue | 2 +- components/RelativeTime.vue | 32 + components/SourceOptions/Filesystem.vue | 10 +- components/UploadFileDialog.vue | 9 +- components/UserFooter.vue | 48 +- components/UserHeader.vue | 21 +- .../UserHeader/NotificationWidgetPanel.vue | 14 +- components/UserHeader/SelectLang.vue | 78 + components/UserHeader/UserWidget.vue | 26 +- error.vue | 31 +- eslint.config.mjs | 29 +- i18n/i18n.config.ts | 27 + i18n/localeDetector.ts | 25 + i18n/locales/en_pirate.json | 3 + i18n/locales/en_us.json | 351 ++ layouts/admin.vue | 31 +- nuxt.config.ts | 25 +- package.json | 8 +- pages/account.vue | 11 +- pages/account/devices.vue | 38 +- pages/account/notifications.vue | 21 +- pages/admin/library/[id]/import.vue | 94 +- pages/admin/library/[id]/index.vue | 44 +- pages/admin/library/import.vue | 44 +- pages/admin/library/index.vue | 65 +- pages/admin/library/sources/index.vue | 69 +- pages/admin/metadata/games/[id]/index.vue | 12 +- pages/admin/settings/index.vue | 12 + pages/admin/task/index.vue | 12 + pages/admin/users/index.vue | 4 +- pages/auth/register.vue | 32 +- pages/auth/signin.vue | 9 +- pages/client/[id]/callback.vue | 37 +- pages/community.vue | 8 + pages/library.vue | 11 +- pages/library/collection/[id]/index.vue | 12 +- pages/library/game/[id]/index.vue | 14 +- pages/library/index.vue | 20 +- pages/news.vue | 11 +- pages/news/[id]/index.vue | 7 +- pages/news/index.vue | 24 +- pages/store/[id]/index.vue | 29 +- pages/store/index.vue | 38 +- plugins/error-handler.ts | 5 + server/api/v1/auth/signup/simple.get.ts | 4 +- server/api/v1/emojis/[id]/index.get.ts | 45 + server/api/v1/object/[id]/index.delete.ts | 7 +- server/api/v1/object/[id]/index.get.ts | 7 +- server/api/v1/object/[id]/index.head.ts | 7 +- server/api/v1/object/[id]/index.post.ts | 7 +- .../api/v1/screenshots/[id]/index.delete.ts | 6 +- server/api/v1/screenshots/[id]/index.get.ts | 7 +- server/arktype.ts | 8 +- server/h3.d.ts | 4 - server/internal/config/sys-conf.ts | 29 +- server/internal/library/index.ts | 1 + server/internal/tasks/group.ts | 19 + server/internal/tasks/index.ts | 164 +- server/internal/tasks/registry/invitations.ts | 24 + .../tasks/registry}/objects.ts | 44 +- server/internal/tasks/registry/sessions.ts | 14 + server/internal/tasks/registry/update.ts | 98 + server/plugins/tasks.ts | 8 +- server/tasks/check/update.ts | 150 - server/tasks/cleanup/invitations.ts | 23 - server/tasks/cleanup/sessions.ts | 13 - server/tasks/dailyTasks.ts | 12 + yarn.lock | 5493 ++++++++++------- 86 files changed, 5175 insertions(+), 2816 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 components/EmojiText.vue create mode 100644 components/RelativeTime.vue create mode 100644 components/UserHeader/SelectLang.vue create mode 100644 i18n/i18n.config.ts create mode 100644 i18n/localeDetector.ts create mode 100644 i18n/locales/en_pirate.json create mode 100644 i18n/locales/en_us.json create mode 100644 pages/admin/settings/index.vue create mode 100644 pages/admin/task/index.vue create mode 100644 pages/community.vue create mode 100644 plugins/error-handler.ts create mode 100644 server/api/v1/emojis/[id]/index.get.ts create mode 100644 server/internal/tasks/group.ts create mode 100644 server/internal/tasks/registry/invitations.ts rename server/{tasks/cleanup => internal/tasks/registry}/objects.ts (79%) create mode 100644 server/internal/tasks/registry/sessions.ts create mode 100644 server/internal/tasks/registry/update.ts delete mode 100644 server/tasks/check/update.ts delete mode 100644 server/tasks/cleanup/invitations.ts delete mode 100644 server/tasks/cleanup/sessions.ts create mode 100644 server/tasks/dailyTasks.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 195eb94..b39f2f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,32 +23,23 @@ jobs: fetch-tags: true token: ${{ secrets.GITHUB_TOKEN }} - - name: Get base tag - id: get_base_tag + - name: Determine final version + id: get_final_ver run: | - git fetch --tags - - TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") - echo "Using base tag: $TAG" - echo "base_tag=$TAG" >> $GITHUB_OUTPUT - - - name: Determine final tag - id: get_final_tag - run: | - BASE_TAG=${{ steps.get_base_tag.outputs.base_tag }} + BASE_VER=v$(jq -r '.version' package.json) TODAY=$(date +'%Y.%m.%d') echo "Today will be: $TODAY" echo "today=$TODAY" >> $GITHUB_OUTPUT if [[ "${{ github.event_name }}" == "release" ]]; then - FINAL_TAG="$BASE_TAG" + FINAL_VER="$BASE_VER" else - FINAL_TAG="${BASE_TAG}-nightly.$TODAY" + FINAL_VER="${BASE_VER}-nightly.$TODAY" fi - echo "Drop's release tag will be: $FINAL_TAG" - echo "final_tag=$FINAL_TAG" >> $GITHUB_OUTPUT + echo "Drop's release tag will be: $FINAL_VER" + echo "final_ver=$FINAL_VER" >> $GITHUB_OUTPUT - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -74,7 +65,7 @@ jobs: ghcr.io/drop-OSS/drop tags: | type=schedule,pattern=nightly - type=schedule,pattern=nightly.${{ steps.get_final_tag.outputs.today }} + type=schedule,pattern=nightly.${{ steps.get_final_ver.outputs.today }} type=semver,pattern=v{{version}} type=semver,pattern=v{{major}}.{{minor}} type=semver,pattern=v{{major}} @@ -98,4 +89,4 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max build-args: | - BUILD_DROP_VERSION=${{ steps.get_final_tag.outputs.final_tag }} + BUILD_DROP_VERSION=${{ steps.get_final_ver.outputs.final_ver }} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..2fc7cb9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "lokalise.i18n-ally", + "esbenp.prettier-vscode", + "Prisma.prisma", + "bradlc.vscode-tailwindcss", + "Vue.volar", + "arktypeio.arkdark", + "EditorConfig.EditorConfig", + "dbaeumer.vscode-eslint" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e426a5e..a7c41eb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,20 @@ "strings": "on" }, // prioritize ArkType's "type" for autoimports - "typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"] + "typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"], + // i18n Ally settings + "i18n-ally.sortKeys": true, + "i18n-ally.keepFulfilled": true, + "i18n-ally.extract.autoDetect": true, + "i18n-ally.localesPaths": ["i18n", "i18n/locales"], + "i18n-ally.keystyle": "nested", + "i18n-ally.extract.ignored": [ + "string >= 14", + "string.alphanumeric >= 5", + "/api/v1/admin/import/version/preload?id=${encodeURIComponent(\n gameId,\n )}&version=${encodeURIComponent(version)}" + ], + "i18n-ally.extract.ignoredByFiles": { + "pages/admin/library/sources/index.vue": ["Filesystem"], + "components/NewsArticleCreateButton.vue": ["[", "`", "Enter"] + } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d883ae..1cc3faf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -130,6 +130,10 @@ and [create an issue](#reporting-issues) or [submit a PR](#submitting-pull-reque --- +## Translation + +If you want to help translate Drop, we would love to have your help! You can do so on our weblate instance. Please make sure to read the [message format syntax](https://vue-i18n.intlify.dev/guide/essentials/syntax.html) page before starting. Failure to do so may result in your translations causing errors in Drop. + ## Commit Guidelines Drop uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) diff --git a/components/AccountSidebar.vue b/components/AccountSidebar.vue index 927988b..11335fc 100644 --- a/components/AccountSidebar.vue +++ b/components/AccountSidebar.vue @@ -1,7 +1,7 @@ diff --git a/components/Auth/Simple.vue b/components/Auth/Simple.vue index a38fe8d..e2dfebc 100644 --- a/components/Auth/Simple.vue +++ b/components/Auth/Simple.vue @@ -4,7 +4,7 @@ {{ $t("auth.username") }}
Password{{ $t("auth.password") }}
Remember me{{ $t("auth.signin.rememberMe") }}
- Forgot password?{{ $t("auth.signin.forgot") }}
- Sign in + {{ + $t("auth.signin.signin") + }}
@@ -93,6 +97,7 @@ const error = ref(); const route = useRoute(); const router = useRouter(); +const { t } = useI18n(); function signin_wrapper() { loading.value = true; @@ -101,7 +106,7 @@ function signin_wrapper() { router.push(route.query.redirect?.toString() ?? "/"); }) .catch((response) => { - const message = response.statusMessage || "An unknown error occurred"; + const message = response.statusMessage || t("errors.unknown"); error.value = message; }) .finally(() => { diff --git a/components/CreateCollectionModal.vue b/components/CreateCollectionModal.vue index be4e806..62dd59b 100644 --- a/components/CreateCollectionModal.vue +++ b/components/CreateCollectionModal.vue @@ -3,11 +3,10 @@ @@ -60,6 +59,7 @@ const emit = defineEmits<{ const open = defineModel({ required: true }); +const { t } = useI18n(); const collectionName = ref(""); const createCollectionLoading = ref(false); const collections = await useCollections(); @@ -101,8 +101,10 @@ async function createCollection() { createModal( ModalType.Notification, { - title: "Failed to create collection", - description: `Drop couldn't create your collection: ${err?.statusMessage}`, + title: t("errors.library.collection.create.title"), + description: t("errors.library.collection.create.desc", [ + err?.statusMessage ?? t("errors.unknown"), + ]), }, (_, c) => c(), ); diff --git a/components/DeleteCollectionModal.vue b/components/DeleteCollectionModal.vue index 76e842c..d057666 100644 --- a/components/DeleteCollectionModal.vue +++ b/components/DeleteCollectionModal.vue @@ -6,13 +6,13 @@ as="h3" class="text-lg font-bold font-display text-zinc-100" > - Delete Collection + {{ $t("library.collection.delete") }}

- Are you sure you want to delete "{{ collection?.name }}"? + {{ $t("common.deleteConfirm", [collection?.name]) }}

- This action cannot be undone. + {{ $t("common.cannotUndo") }}

@@ -22,13 +22,13 @@ class="bg-red-600 text-white hover:bg-red-500" @click="() => deleteCollection()" > - Delete + {{ $t("delete") }} @@ -42,6 +42,7 @@ const collection = defineModel(); const deleteLoading = ref(false); const collections = await useCollections(); +const { t } = useI18n(); async function deleteCollection() { try { @@ -62,9 +63,11 @@ async function deleteCollection() { createModal( ModalType.Notification, { - title: "Failed to add game to library", - // @ts-expect-error attempt to display statusMessage on error - description: `Drop couldn't add this game to your library: ${e?.statusMessage}`, + title: t("errors.library.add.title"), + description: t("errors.library.add.desc", [ + // @ts-expect-error attempt to display statusMessage on error + e?.statusMessage ?? t("errors.unknown"), + ]), }, (_, c) => c(), ); diff --git a/components/DeleteNewsModal.vue b/components/DeleteNewsModal.vue index 8f508d7..47915fe 100644 --- a/components/DeleteNewsModal.vue +++ b/components/DeleteNewsModal.vue @@ -6,13 +6,13 @@ as="h3" class="text-lg font-bold font-display text-zinc-100" > - Delete Article + {{ $t("news.delete") }}

- Are you sure you want to delete "{{ article?.title }}"? + {{ $t("common.deleteConfirm", [article?.title]) }}

- This action cannot be undone. + {{ $t("common.cannotUndo") }}

@@ -22,13 +22,13 @@ class="bg-red-600 text-white hover:bg-red-500" @click="() => deleteArticle()" > - Delete + {{ $t("delete") }} @@ -45,6 +45,7 @@ interface Article { const article = defineModel
(); const deleteLoading = ref(false); const router = useRouter(); +const { t } = useI18n(); const news = useNews(); if (!news.value) { news.value = await fetchNews(); @@ -68,9 +69,11 @@ async function deleteArticle() { createModal( ModalType.Notification, { - title: "Failed to delete article", - // @ts-expect-error attempt to display statusMessage on error - description: `Drop couldn't delete this article: ${e?.statusMessage}`, + title: t("errors.news.article.delete.title"), + description: t("errors.news.article.delete.desc", [ + // @ts-expect-error attempt to display statusMessage on error + e?.statusMessage ?? t("errors.unknown"), + ]), }, (_, c) => c(), ); diff --git a/components/DropWordmark.vue b/components/DropWordmark.vue index ae9b39b..7288677 100644 --- a/components/DropWordmark.vue +++ b/components/DropWordmark.vue @@ -11,8 +11,8 @@ /> - Drop + + {{ $t("drop.drop") }} + diff --git a/components/EmojiText.vue b/components/EmojiText.vue new file mode 100644 index 0000000..ee90157 --- /dev/null +++ b/components/EmojiText.vue @@ -0,0 +1,17 @@ + + + diff --git a/components/GamePanel.vue b/components/GamePanel.vue index 9aaa9cc..9307ec1 100644 --- a/components/GamePanel.vue +++ b/components/GamePanel.vue @@ -32,7 +32,7 @@

- > + diff --git a/components/SourceOptions/Filesystem.vue b/components/SourceOptions/Filesystem.vue index 9448c06..9f3e310 100644 --- a/components/SourceOptions/Filesystem.vue +++ b/components/SourceOptions/Filesystem.vue @@ -1,10 +1,12 @@ @@ -84,6 +86,8 @@ import { DocumentIcon } from "@heroicons/vue/24/outline"; import type { Article } from "~/prisma/client"; import type { SerializeObject } from "nitropack/types"; +const { t } = useI18n(); + const { articles } = defineProps<{ articles: SerializeObject< Article & { @@ -93,16 +97,8 @@ const { articles } = defineProps<{ >[]; }>(); -const formatDate = (date: string) => { - return new Date(date).toLocaleDateString("en-AU", { - year: "numeric", - month: "long", - day: "numeric", - }); -}; - useHead({ - title: "News", + title: t("userHeader.links.news"), }); diff --git a/pages/store/[id]/index.vue b/pages/store/[id]/index.vue index 844082d..2a2ef25 100644 --- a/pages/store/[id]/index.vue +++ b/pages/store/[id]/index.vue @@ -44,7 +44,7 @@ type="button" class="inline-flex items-center gap-x-2 rounded-md bg-zinc-800 px-3 py-1 text-sm font-semibold font-display text-white shadow-sm hover:bg-zinc-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 duration-200 hover:scale-105 active:scale-95" > - Open in Admin Dashboard + {{ $t("store.openAdminDashboard") }}