Skip to content

Commit 9da57ee

Browse files
committed
feat: add i18n support and fix table pagination
- Add comprehensive i18n for all pages, layouts, and components - Add language switcher in app header and dashboard command palette - Fix LinksTable pagination using server-side total instead of client-side count - Replace getPaginationRowModel with manualPagination for proper server-side paging
1 parent 291e048 commit 9da57ee

74 files changed

Lines changed: 4803 additions & 3319 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.vite-hooks/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vp staged

CLAUDE.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,24 @@ These commands map to their corresponding tools. For example, `vp dev --port 300
6363

6464
- **Using the package manager directly:** Do not use pnpm, npm, or Yarn directly. Vite+ can handle all package manager operations.
6565
- **Always use Vite commands to run tools:** Don't attempt to run `vp vitest` or `vp oxlint`. They do not exist. Use `vp test` and `vp lint` instead.
66-
- **Running scripts:** Vite+ commands take precedence over `package.json` scripts. If there is a `test` script defined in `scripts` that conflicts with the built-in `vp test` command, run it using `vp run test`.
66+
- **Running scripts:** Vite+ built-in commands (`vp dev`, `vp build`, `vp test`, etc.) always run the Vite+ built-in tool, not any `package.json` script of the same name. To run a custom script that shares a name with a built-in command, use `vp run <script>`. For example, if you have a custom `dev` script that runs multiple services concurrently, run it with `vp run dev`, not `vp dev` (which always starts Vite's dev server).
6767
- **Do not install Vitest, Oxlint, Oxfmt, or tsdown directly:** Vite+ wraps these tools. They must not be installed directly. You cannot upgrade these tools by installing their latest versions. Always use Vite+ commands.
6868
- **Use Vite+ wrappers for one-off binaries:** Use `vp dlx` instead of package-manager-specific `dlx`/`npx` commands.
6969
- **Import JavaScript modules from `vite-plus`:** Instead of importing from `vite` or `vitest`, all modules should be imported from the project's `vite-plus` dependency. For example, `import { defineConfig } from 'vite-plus';` or `import { expect, test, vi } from 'vite-plus/test';`. You must not install `vitest` to import test utilities.
7070
- **Type-Aware Linting:** There is no need to install `oxlint-tsgolint`, `vp lint --type-aware` works out of the box.
7171

72+
## CI Integration
73+
74+
For GitHub Actions, consider using [`voidzero-dev/setup-vp`](https://github.com/voidzero-dev/setup-vp) to replace separate `actions/setup-node`, package-manager setup, cache, and install steps with a single action.
75+
76+
```yaml
77+
- uses: voidzero-dev/setup-vp@v1
78+
with:
79+
cache: true
80+
- run: vp check
81+
- run: vp test
82+
```
83+
7284
## Review Checklist for Agents
7385
7486
- [ ] Run `vp install` after pulling remote changes and before getting started.

app/components/OrgsMenu.vue

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { Organization } from "better-auth/plugins";
44
55
import { authClient } from "~/utils/auth";
66
7+
const { t } = useI18n();
8+
79
defineProps<{
810
collapsed?: boolean;
911
}>();
@@ -46,7 +48,7 @@ const currentEntity = computed(() => {
4648
};
4749
}
4850
return {
49-
label: "Select Organization",
51+
label: t("common.selectOrganization"),
5052
icon: "i-lucide-users",
5153
type: "none",
5254
};
@@ -65,8 +67,8 @@ async function switchContext(entityId: string) {
6567
6668
if (error) {
6769
toast.add({
68-
title: "Switch Failed",
69-
description: error.message || "Failed to switch to organization",
70+
title: t("common.failedToSwitchOrg"),
71+
description: error.message || t("common.failedToSwitchOrg"),
7072
color: "error",
7173
});
7274
return;
@@ -76,19 +78,19 @@ async function switchContext(entityId: string) {
7678
const org = orgList?.find((o: Organization) => o.id === entityId);
7779
const isPersonal = org?.slug?.startsWith("user_");
7880
toast.add({
79-
title: isPersonal ? "Personal Workspace" : "Switched to Organization",
81+
title: isPersonal ? t("common.personalWorkspace") : t("common.switchedToOrg"),
8082
description: isPersonal
81-
? "You are now in your personal workspace"
82-
: `You are now working in ${org?.name || "the organization"}`,
83+
? t("common.inPersonalWorkspace")
84+
: t("common.nowInOrg", { name: org?.name || "..." }),
8385
color: "success",
8486
});
8587
8688
// useActiveOrganization hook will auto-update
8789
// useAsyncData with watch will auto-refetch organizations
8890
} catch (error) {
8991
toast.add({
90-
title: "Switch Failed",
91-
description: error instanceof Error ? error.message : "An error occurred",
92+
title: t("common.failedToSwitchOrg"),
93+
description: error instanceof Error ? error.message : t("common.unexpectedError"),
9294
color: "error",
9395
});
9496
} finally {
@@ -129,7 +131,7 @@ const items = computed<DropdownMenuItem[][]>(() => {
129131
organizationOptions,
130132
[
131133
{
132-
label: "Create organization",
134+
label: t("dashboard.createOrg"),
133135
icon: "i-lucide-circle-plus",
134136
onSelect: () => navigateTo("/dashboard/create-org"),
135137
},

app/components/UserMenu.vue

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { DropdownMenuItem } from "@nuxt/ui";
33
44
import { authClient } from "~/utils/auth";
55
6+
const { t } = useI18n();
7+
68
defineProps<{
79
collapsed?: boolean;
810
}>();
@@ -41,8 +43,8 @@ async function handleSignOut() {
4143
4244
if (error) {
4345
toast.add({
44-
title: "Sign Out Error",
45-
description: error.message || "Failed to sign out",
46+
title: t("common.signOutError"),
47+
description: error.message || t("common.failedToSignOut"),
4648
color: "error",
4749
});
4850
return;
@@ -52,16 +54,16 @@ async function handleSignOut() {
5254
await authClient.getSession();
5355
5456
toast.add({
55-
title: "Signed Out",
56-
description: "You have been successfully signed out",
57+
title: t("common.signedOut"),
58+
description: t("common.signedOutDesc"),
5759
color: "success",
5860
});
5961
6062
await navigateTo("/");
6163
} catch (error) {
6264
toast.add({
63-
title: "Sign Out Error",
64-
description: "An error occurred while signing out",
65+
title: t("common.signOutError"),
66+
description: t("common.signOutErrorDesc"),
6567
color: "error",
6668
});
6769
}
@@ -81,43 +83,43 @@ const items = computed<DropdownMenuItem[][]>(() => {
8183
],
8284
[
8385
{
84-
label: isInAdmin.value ? "Go to Dashboard" : "Dashboard",
86+
label: isInAdmin.value ? t("common.goToDashboard") : t("dashboard.title"),
8587
icon: "i-lucide-layout-dashboard",
8688
onSelect: navigateToDashboard,
8789
},
8890
...(isAdmin.value
8991
? [
9092
{
91-
label: "Admin",
93+
label: t("admin.title"),
9294
icon: "i-lucide-shield",
9395
onSelect: navigateToAdmin,
9496
},
9597
]
9698
: []),
9799
{
98-
label: "Profile",
100+
label: t("dashboard.profile"),
99101
icon: "i-lucide-user",
100102
onSelect: () => navigateTo("/dashboard/settings"),
101103
},
102104
{
103-
label: "Security",
105+
label: t("common.security"),
104106
icon: "i-lucide-shield",
105107
onSelect: () => navigateTo("/dashboard/settings/security"),
106108
},
107109
{
108-
label: "Invitations",
110+
label: t("dashboard.invitations"),
109111
icon: "i-lucide-mail",
110112
onSelect: () => navigateTo("/dashboard/settings/invitations"),
111113
},
112114
{
113-
label: "API Keys",
115+
label: t("dashboard.apiKeys"),
114116
icon: "i-lucide-key",
115117
onSelect: () => navigateTo("/dashboard/apikeys"),
116118
},
117119
],
118120
[
119121
{
120-
label: "Sign Out",
122+
label: t("common.signOut"),
121123
icon: "i-lucide-log-out",
122124
color: "error",
123125
onSelect: handleSignOut,

app/components/dashboard/DomainDeleteModal.vue

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { Domain } from "~~/shared/types/domain";
44
55
import { authClient } from "~/utils/auth";
66
7+
const { t } = useI18n();
8+
79
const props = defineProps<{
810
domain: Domain | null;
911
}>();
@@ -19,7 +21,7 @@ const loading = ref(false);
1921
2022
const schema = z.object({
2123
confirmText: z.string().refine((val) => val === props.domain?.domainName, {
22-
message: "Please enter domain name to confirm deletion",
24+
message: t("dashboard.enterDomainConfirm"),
2325
}),
2426
});
2527
@@ -32,8 +34,8 @@ const state = reactive<Partial<Schema>>({
3234
async function deleteDomain() {
3335
if (!props.domain?.id) {
3436
toast.add({
35-
title: "Error",
36-
description: "Domain ID is required",
37+
title: t("common.error"),
38+
description: t("dashboard.domainIdRequired"),
3739
color: "error",
3840
});
3941
return;
@@ -47,16 +49,16 @@ async function deleteDomain() {
4749
4850
if (result.error) {
4951
toast.add({
50-
title: "Error",
51-
description: result.error.message || "Failed to delete domain",
52+
title: t("common.error"),
53+
description: result.error.message || t("dashboard.failedToCreateDomain"),
5254
color: "error",
5355
});
5456
return;
5557
}
5658
5759
toast.add({
58-
title: "Success",
59-
description: `Domain ${props.domain.domainName} has been deleted`,
60+
title: t("common.success"),
61+
description: t("dashboard.domainDeleted", { name: props.domain.domainName }),
6062
color: "success",
6163
});
6264
@@ -65,8 +67,8 @@ async function deleteDomain() {
6567
state.confirmText = "";
6668
} catch (error) {
6769
toast.add({
68-
title: "Error",
69-
description: "An unexpected error occurred",
70+
title: t("common.error"),
71+
description: t("common.unexpectedError"),
7072
color: "error",
7173
});
7274
} finally {
@@ -76,29 +78,34 @@ async function deleteDomain() {
7678
</script>
7779

7880
<template>
79-
<UModal v-model:open="open" title="Delete Domain" description="This action cannot be undone">
81+
<UModal
82+
v-model:open="open"
83+
:title="t('dashboard.deleteDomain')"
84+
:description="t('dashboard.cannotBeUndone')"
85+
>
8086
<slot />
8187

8288
<template #body>
8389
<div class="space-y-4">
84-
<UAlert v-if="domain" color="error" icon="i-lucide-alert-triangle" title="Warning">
90+
<UAlert
91+
v-if="domain"
92+
color="error"
93+
icon="i-lucide-alert-triangle"
94+
:title="t('common.warning')"
95+
>
8596
<template #description>
8697
<div class="space-y-1">
87-
<p>You are about to delete the domain:</p>
98+
<p>{{ t("dashboard.confirmDeleteDomainDesc") }}</p>
8899
<p class="font-mono text-sm font-semibold">{{ domain.domainName }}</p>
89-
<p class="text-xs">
90-
This action cannot be undone. All links associated with this domain will need to be
91-
reassigned before deletion.
92-
</p>
93100
</div>
94101
</template>
95102
</UAlert>
96103

97104
<UForm :schema="schema" :state="state" class="space-y-4" @submit="deleteDomain">
98105
<UFormField
99106
name="confirmText"
100-
:label="`Type '${domain?.domainName}' to confirm`"
101-
description="Please enter the domain name to confirm deletion"
107+
:label="t('common.typeToConfirm', { name: domain?.domainName })"
108+
:description="t('dashboard.enterDomainConfirm')"
102109
>
103110
<UInput
104111
v-model="state.confirmText"
@@ -109,7 +116,9 @@ async function deleteDomain() {
109116
</UFormField>
110117

111118
<div class="flex justify-end gap-2">
112-
<UButton variant="outline" @click="open = false" :disabled="loading"> Cancel </UButton>
119+
<UButton variant="outline" @click="open = false" :disabled="loading">
120+
{{ t("common.cancel") }}
121+
</UButton>
113122
<UButton
114123
color="error"
115124
type="submit"
@@ -119,7 +128,7 @@ async function deleteDomain() {
119128
<template #leading>
120129
<UIcon name="i-lucide-trash" />
121130
</template>
122-
Delete Domain
131+
{{ t("dashboard.deleteDomain") }}
123132
</UButton>
124133
</div>
125134
</UForm>

0 commit comments

Comments
 (0)