Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ WolfStar.rocks is built for Discord server administrators and bot users who need
- [Lighthouse accessibility tests](#lighthouse-accessibility-tests)
- [Lighthouse performance tests](#lighthouse-performance-tests)
- [End-to-end tests](#end-to-end-tests)
- [Localization (i18n)](#localization-i18n)
- [i18n commands](#i18n-commands)
- [Adding a locale](#adding-a-locale)
- [Translation status (Lunaria)](#translation-status-lunaria)
- [Storybook](#storybook)
- [Submitting changes](#submitting-changes)
- [Before submitting](#before-submitting)
Expand Down Expand Up @@ -143,6 +147,14 @@ pnpm prisma:push # Push schema changes (development)
pnpm prisma:migrate:dev # Create and apply migration
pnpm prisma:generate # Regenerate Prisma client
pnpm prisma:studio # Visual database editor (http://localhost:5555)

# Localization (vp tasks + fix scripts)
pnpm vp run i18n:check # Audit locale files against en.json
pnpm i18n:check:fix # Add missing keys (EN placeholders) / remove extras
pnpm vp run i18n:report # Detect missing, unused, or dynamic keys in code
pnpm i18n:report:fix # Remove unused keys from all locale files
pnpm vp run i18n:schema # Regenerate i18n/schema.json from en.json
pnpm vp run build:lunaria # Build /lunaria dashboard + status.json
```

### GitHub Actions security analysis
Expand Down Expand Up @@ -383,6 +395,47 @@ pnpm test:browser:ui # Run with Playwright UI

Make sure to read about [Playwright best practices](https://playwright.dev/docs/best-practices) and prefer user-facing locators (`getByRole`, `getByLabel`, `getByText`) over selectors based on classes or IDs.

## Localization (i18n)

WolfStar.rocks uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for the dashboard UI. Guild bot-response languages (`en-US`, `es-ES`, …) are separate from UI locale preference.

- Source of truth: [`i18n/locales/en.json`](../i18n/locales/en.json)
- Other locales: [`i18n/locales/`](../i18n/locales) (currently `es-ES`, matching WolfStar/Skyra language keys)
- Locale registry: [`config/i18n.ts`](../config/i18n.ts)
- Lunaria config: [`lunaria.config.json`](../lunaria.config.json)

### i18n commands

| Command | Purpose |
| ------------------------------ | ------------------------------------------------------------------------- |
| `pnpm vp run i18n:check` | Compare locales to `en.json` (reports missing/extra keys; removes extras) |
| `pnpm i18n:check:fix [locale]` | Add missing keys with English placeholders (optionally for one locale) |
| `pnpm vp run i18n:report` | Fail on missing, unused, or dynamic keys used in `app/**` |
| `pnpm i18n:report:fix` | Remove unused keys from all locale files |
| `pnpm vp run i18n:schema` | Regenerate `i18n/schema.json` for IDE validation |
| `pnpm vp run build:lunaria` | Build `dist/lunaria/` dashboard + `status.json` |

CI runs `i18n:report` and checks that `i18n/schema.json` is up to date. Autofix runs `i18n:check` and `build:lunaria`. The Lunaria PR workflow posts a translation overview comment.

### Adding a locale

1. Create `i18n/locales/<code>.json` (start from `en.json`)
2. Register it in `config/i18n.ts` (`locales` array)
3. Add it to `lunaria.config.json` → `locales`
4. Run `pnpm i18n:check:fix` and `pnpm vp run i18n:schema`

Prefer static string keys with `$t('…')` / `t('…')` so `i18n:report` can analyze usage. For HTML inside translations, use [`i18n-t`](https://vue-i18n.intlify.dev/guide/advanced/component.html).

### Translation status (Lunaria)

We track translation progress with [Lunaria](https://lunaria.dev/):

- Built dashboard: `/lunaria/` (generated at build time)
- JSON status for the app: `/lunaria/status.json`
- In-app page: `/translation-status`

Use the [i18n-ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) VS Code extension (recommended in `.vscode/extensions.json`) for editing locale files.

## Storybook

Stories are co-located with pages as `*.stories.ts` files under `app/pages/`. Storybook configuration lives in `.storybook/`.
Expand Down Expand Up @@ -426,7 +479,7 @@ Format: `type(scope): description`

Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`, `types`

Scopes (optional): `auth`, `api`, `guild`, `ui`, `db`, `deps`
Scopes (optional): `auth`, `api`, `guild`, `ui`, `db`, `deps`, `i18n`

Examples:

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ jobs:
cache: true
sfw: true

- name: 🌐 Compare translations
run: vp run i18n:check

- name: 🌍 Update lunaria data
run: vp run build:lunaria

- name: 🔠 Fix lint errors
run: vp run lint:fix

Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,28 @@ jobs:

- name: 🧹 Check for unused code
run: vp run knip

i18n:
name: 🌐 i18n validation
runs-on: blacksmith-4vcpu-ubuntu-2404-arm

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- uses: voidzero-dev/setup-vp@2dec1e33f4ab2c6d5bce1b0c4607961bb1a3f7a1 # v1.12.0
with:
node-version: lts/*
sfw: true
# root only, no scripts
run-install: |
- args: ["--filter", ".", "--ignore-scripts"]

- name: 🌐 Check for missing or dynamic i18n keys
run: vp run i18n:report

- name: 🌐 Check i18n schema is up to date
run: |
vp run i18n:schema
git diff --exit-code i18n/schema.json
37 changes: 37 additions & 0 deletions .github/workflows/lunaria.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: lunaria

on:
pull_request_target:
types: [opened, synchronize]

concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }}
cancel-in-progress: true

permissions: {}

jobs:
lunaria-overview:
name: 🌝 Generate Lunaria Overview
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
permissions:
contents: read
pull-requests: write # post Lunaria overview comments on pull requests

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Necessary for Lunaria to work properly
# Makes the action clone the entire git history
fetch-depth: 0
persist-credentials: false
Comment on lines +22 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Checkout PR head
This pull_request_target workflow checks out the base repository default ref because no PR ref is supplied, so Lunaria runs against main instead of the PR's locale changes. The action will post an overview for stale base-branch files on every PR update; checkout needs ref: ${{ github.event.pull_request.head.sha }} before generating the overview.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/lunaria.yml
Line: 22-27

Comment:
**Checkout PR head**
This `pull_request_target` workflow checks out the base repository default ref because no PR `ref` is supplied, so Lunaria runs against `main` instead of the PR's locale changes. The action will post an overview for stale base-branch files on every PR update; checkout needs `ref: ${{ github.event.pull_request.head.sha }}` before generating the overview.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Cursor Cloud Agents


- uses: voidzero-dev/setup-vp@2dec1e33f4ab2c6d5bce1b0c4607961bb1a3f7a1 # v1.12.0
with:
node-version: lts/*
cache: true
sfw: true

- name: Generate Lunaria Overview
uses: lunariajs/action@4911ad0736d1e3b20af4cb70f5079aea2327ed8e # astro-docs
5 changes: 5 additions & 0 deletions .github/zizmor.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Existing privileged PR automation is intentionally isolated to these workflows:
# they do not checkout pull request head code, and they only comment or set status.
rules:
stale-action-refs:
ignore:
# lunariajs/action has no tag refs; keep the branch commit hash-pinned.
- lunaria.yml:37
dangerous-triggers:
ignore:
- enforce-release-source.yml
- lunaria.yml
- semantic-pull-requests.yml
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"formulahendry.auto-rename-tag",
"vitest.explorer",
"coderabbit.coderabbit-vscode",
"VoidZero.vite-plus-extension-pack"
"VoidZero.vite-plus-extension-pack",
"lokalise.i18n-ally"
]
}
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"eslint.enable": false,
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["./i18n/locales"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "en",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"oxc.fmt.configPath": "./vite.config.ts",
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ pnpm knip:fix # Auto-fix unused files, exports, and dependenc
pnpm preview # Preview production build locally
pnpm lint:fix # Run linter and auto-fix issues (oxlint + oxfmt)
pnpm typecheck # TypeScript type checking
pnpm vp run i18n:check # Audit locale files against en.json
pnpm i18n:check:fix # Sync locale keys (EN placeholders for missing)
pnpm vp run i18n:report # Fail on missing/unused/dynamic i18n keys in app/**
pnpm i18n:report:fix # Remove unused keys from all locale files
pnpm vp run i18n:schema # Regenerate i18n/schema.json from en.json
pnpm vp run build:lunaria # Build Lunaria dashboard + status.json
pnpm test # Run all Vitest projects
pnpm test:unit # Run unit tests
pnpm test:nuxt # Nuxt component/API tests
Expand Down
12 changes: 12 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ useAuthIdentity();

const router = useRouter();
const appName = ref<"wolfstar" | "staryl">("wolfstar");
const { locale, locales } = useI18n();

const localeMap = Object.fromEntries(
locales.value.map((entry) => [entry.code, entry.dir ?? "ltr"]),
);

useHead({
htmlAttrs: {
lang: () => locale.value,
dir: () => localeMap[locale.value] ?? "ltr",
},
});

// Watch for route changes to update appName
watch(
Expand Down
52 changes: 43 additions & 9 deletions app/components/UserMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
? 'w-48 bg-base-200/90 border border-base-200 shadow-md rounded-md'
: 'w-(--reka-dropdown-menu-trigger-width) bg-base-200/90 border border-base-200 shadow-md rounded-md',
}"
aria-label="User account menu"
:aria-label="t('user_menu.account_menu')"
>
<UButton
v-bind="{
Expand All @@ -24,19 +24,26 @@
:ui="{
trailingIcon: 'text-dimmed',
}"
:aria-label="collapsed ? 'User menu' : `User menu for ${user?.name}`"
:aria-label="
collapsed
? t('header.user_menu')
: t('user_menu.user_menu_for', { name: user?.name ?? '' })
"
aria-haspopup="true"
/>
</UDropdownMenu>
</template>

<script setup lang="ts">
import type { DropdownMenuItem } from "@nuxt/ui";
import { isAppLocaleCode } from "~/utils/is-app-locale";

const { collapsed } = defineProps<{
collapsed?: boolean;
}>();

const { t, locale, locales, setLocale } = useI18n();
const { setPreferredLocale } = usePreferredLocale();
const isFeedbackOpen = ref(false);
const colorMode = useColorMode();
const { user: authUser, signOut } = useUserSession();
Expand All @@ -45,12 +52,34 @@ const src = computed(() => authUser.value?.image ?? undefined);

const user = ref({
avatar: {
alt: authUser.value?.name ? `${authUser.value.name}'s avatar` : "User avatar",
alt: authUser.value?.name
? t("user_menu.avatar_alt", { name: authUser.value.name })
: t("user_menu.avatar_fallback"),
src: src.value,
},
name: authUser.value?.name,
});

const languageChildren = computed<DropdownMenuItem[]>(() =>
locales.value.map((entry) => ({
checked: locale.value === entry.code,
label: entry.name ?? entry.code,
onSelect(e: Event) {
e.preventDefault();
if (!isAppLocaleCode(entry.code)) return;
setPreferredLocale(entry.code);
void setLocale(entry.code);
},
onUpdateChecked(checked: boolean) {
if (checked && isAppLocaleCode(entry.code)) {
setPreferredLocale(entry.code);
void setLocale(entry.code);
}
},
type: "checkbox" as const,
})),
);

const items = computed<DropdownMenuItem[][]>(() => [
[
{
Expand All @@ -62,25 +91,30 @@ const items = computed<DropdownMenuItem[][]>(() => [
[
{
icon: "lucide:user",
label: "Profile",
label: t("user_menu.profile"),
to: "/profile",
},
{
icon: "lucide:bug",
label: "Report a Bug",
label: t("user_menu.report_bug"),
onSelect(e: Event) {
e.preventDefault();
isFeedbackOpen.value = true;
},
},
],
[
{
children: languageChildren.value,
icon: "lucide:languages",
label: t("common.language"),
},
{
children: [
{
checked: colorMode.value === "light",
icon: "lucide:sun",
label: "Light",
label: t("common.light"),
onSelect(e: Event) {
e.preventDefault();

Expand All @@ -91,7 +125,7 @@ const items = computed<DropdownMenuItem[][]>(() => [
{
checked: colorMode.value === "dark",
icon: "lucide:moon",
label: "Dark",
label: t("common.dark"),
onSelect(e: Event) {
e.preventDefault();
},
Expand All @@ -104,13 +138,13 @@ const items = computed<DropdownMenuItem[][]>(() => [
},
],
icon: "lucide:sun-moon",
label: "Appearance",
label: t("common.appearance"),
},
],
[
{
icon: "lucide:log-out",
label: "Sign out",
label: t("user_menu.sign_out"),
async onSelect(e: Event) {
e.preventDefault();
await signOut();
Expand Down
Loading
Loading