Skip to content
This repository was archived by the owner on Apr 6, 2026. It is now read-only.

feat(web): add dashboard i18n with EN/DE locales#328

Merged
karaktaka merged 1 commit into
mainfrom
feat/dashboard-i18n
Mar 14, 2026
Merged

feat(web): add dashboard i18n with EN/DE locales#328
karaktaka merged 1 commit into
mainfrom
feat/dashboard-i18n

Conversation

@karaktaka
Copy link
Copy Markdown
Contributor

@karaktaka karaktaka commented Mar 14, 2026

Summary

  • Adds a full i18n system to the Vue dashboard with EN and DE locales (~500 translation keys) using a custom useI18n() composable backed by a Pinia locale store
  • Replaces all hardcoded English strings across 19 tab components, login page, navigation shell, shared components, and composables with type-safe t() calls — TypeScript enforces key correctness at build time via a recursive FlatKeys<T> type
  • Adds browser language auto-detection on first visit, a language switcher in the sidebar footer and login page, and localStorage persistence via pinia-plugin-persistedstate

Key implementation details

  • src/i18n/index.ts — module-level singleton composable; EN→locale fallback chain; {param} interpolation; dev-only missing-key warnings deduped via Set
  • src/stores/locale.ts — Pinia store with persist: true; sets document.documentElement.lang on change
  • src/utils/route.ts — extracted toQueryScalar() utility shared across views
  • useAutoSave / useManualSave composables updated to use t("common.save_failed") for fallback error messages
  • ApplicationSubmissionsTab: form filter now uses server-side scoped fetch via applyFormFilter(); client-side filtered computed simplified to status-only filter (dead targetFormName lookup removed)

Test plan

  • First visit auto-detects browser language (EN/DE)
  • Language switcher toggles all visible strings in real time
  • Language persists across page reload
  • EN fallback shown for any missing DE key (no blank strings)
  • All 19 tabs render correctly in both languages
  • Login page renders in both languages
  • Sidebar nav labels, support mode banner, and mockup toolbar all translate
  • TypeScript build passes: npm run build in web/frontend/

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds a typed i18n system (en + de), a persisted Pinia locale store with browser detection, a LanguageSwitcher component and useI18n hook, and replaces hard-coded UI strings with translation keys across many frontend views, tabs, and components.

Changes

Cohort / File(s) Summary
i18n Core & Hook
web/frontend/src/i18n/index.ts
New typed i18n module: I18nKey, useI18n() with dot-key lookup, interpolation, locale fallback and DEV missing-key logging.
Locale Data
web/frontend/src/i18n/locales/en.ts, web/frontend/src/i18n/locales/de.ts
Added comprehensive English and German translation dictionaries covering UI, tabs, and features.
Locale Store
web/frontend/src/stores/locale.ts
New persisted Pinia store useLocaleStore, SUPPORTED_LOCALES/SupportedLocale, browser locale detection, reactive current, setLocale, and document.lang sync.
Language Switcher
web/frontend/src/components/LanguageSwitcher.vue
New Vue 3 TS component bound to locale store; updates locale and exposes accessible label via useI18n.
Small Components
web/frontend/src/components/MockupToolbar.vue
Replaced hard-coded labels with i18n keys; levels/options now use label keys and translated display via t(...).
Views — global / entry
web/frontend/src/views/LoginView.vue, web/frontend/src/views/guild/GuildDetailView.vue
Wired useI18n, replaced static strings with t(...), added LanguageSwitcher; GuildDetailView changed data shapes (label→labelKey, added id and accentClass).
Tabs — bulk localization
web/frontend/src/views/guild/tabs/*.vue
Localized many tabs by replacing UI strings with t(...); several data-shape updates (label→labelKey, STATUS_LABELS→STATUS_LABEL_KEYS, PROFESSION.name→labelKey).
Utilities & Composables
web/frontend/src/utils/route.ts, web/frontend/src/composables/*Save.ts
Added toQueryScalar to normalize route query values; save composables now use i18n for error messages.
Other Views
web/frontend/src/views/guild/FormSubmissionsView.vue, web/frontend/src/views/guild/...
Replaced status label mappings with i18n-keyed mappings and updated error messages to use t(...) across various guild views.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant LangSwitch as LanguageSwitcher
    participant LocaleStore as Locale Store
    participant i18n as i18n Module
    participant UI as UI Component

    User->>LangSwitch: selects language
    LangSwitch->>LocaleStore: setLocale(lang)
    LocaleStore->>LocaleStore: update current (persist)
    LocaleStore->>document: set document.documentElement.lang
    UI->>LocaleStore: read current (reactive)
    UI->>i18n: t(key, params)
    i18n->>i18n: lookup key in current → fallback en → interpolate
    i18n-->>UI: translated string
    UI->>User: render translated UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • nerdycraft/NerpyBot#317 — Touches web/frontend/src/components/MockupToolbar.vue, overlapping i18n label changes and MockupToolbar modifications.

Suggested labels

review

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: adding internationalization with English and German locales to the web dashboard.
Linked Issues check ✅ Passed All coding requirements from #307 are met: custom i18n composable with fallback chain [#307], Pinia locale store with browser detection [#307], LanguageSwitcher component [#307], all UI strings behind t() calls across 19 tabs and shared views [#307], and en/de locale files [#307].
Out of Scope Changes check ✅ Passed All changes are scoped to i18n implementation: new i18n infrastructure, locale files, locale store, LanguageSwitcher component, and translation integration across dashboard views. No unrelated changes detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/dashboard-i18n
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@karaktaka karaktaka force-pushed the feat/dashboard-i18n branch from 2af1db8 to f29f4da Compare March 14, 2026 10:52
@karaktaka karaktaka marked this pull request as ready for review March 14, 2026 10:57
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue (1)

169-171: ⚠️ Potential issue | 🟡 Minor

Localize the add-form required-fields validation message.

This error is still hardcoded in English, so DE users will see untranslated validation feedback.

Proposed fix
-  if (!newConfig.value.channel_id || !newConfig.value.wow_realm_slug || !newConfig.value.wow_guild_name_input) {
-    addError.value = "Region, guild name, realm, and channel are required.";
-    return;
-  }
+  if (!newConfig.value.channel_id || !newConfig.value.wow_realm_slug || !newConfig.value.wow_guild_name_input) {
+    addError.value = t("tabs.wow_guild_news.required_fields");
+    return;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue` around lines 169 -
171, The hardcoded validation message assigned to addError.value in
WowGuildNewsTab.vue should be replaced with a localized string lookup; update
the conditional that checks newConfig.value.channel_id,
newConfig.value.wow_realm_slug, and newConfig.value.wow_guild_name_input to set
addError.value using the project's i18n function (e.g., this.$t or i18n.t)
instead of the English literal so users see the translated "Region, guild name,
realm, and channel are required" message; ensure you reference the appropriate
translation key (create one in the locale files if missing) and use the same
lookup pattern used elsewhere in this component.
web/frontend/src/views/guild/tabs/AutoKickerTab.vue (1)

15-16: ⚠️ Potential issue | 🟡 Minor

Localize the client-side validation error message.

This thrown message is displayed to users and is currently fixed in English.

Proposed fix
-  if (!Number.isInteger(c.kick_after) || c.kick_after < 1) {
-    throw new Error("Kick-after must be at least 1 day.");
-  }
+  if (!Number.isInteger(c.kick_after) || c.kick_after < 1) {
+    throw new Error(t("tabs.auto_kicker.kick_after_invalid"));
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/AutoKickerTab.vue` around lines 15 - 16,
The client-side validation throws a hardcoded English error when c.kick_after is
invalid; replace the literal string in the throw with a localized message (use
the component's i18n helper—e.g., this.$t or the Composition API t function) so
the message is translated, update the thrown Error to use
t('guild.autokicker.kick_after_min') (or similar key) and add that key to the
locale files for all supported languages; ensure you reference the validation
around c.kick_after in AutoKickerTab.vue so the thrown Error uses the i18n key
instead of the English string.
web/frontend/src/views/guild/tabs/WowCraftingTab.vue (1)

319-323: ⚠️ Potential issue | 🟡 Minor

Order status display not using translations.

Line 321 displays order.status.replace("_", " ") directly instead of using the translated labels. The STATUS_LABEL_KEYS are defined but only used for filter buttons, not for individual order status display.

-                <span :class="STATUS_COLORS[order.status] ?? 'text-muted-foreground'" class="capitalize font-medium">
-                  {{ order.status.replace("_", " ") }}
-                </span>
+                <span :class="STATUS_COLORS[order.status] ?? 'text-muted-foreground'" class="font-medium">
+                  {{ t(STATUS_LABEL_KEYS[order.status] ?? `tabs.wow_crafting.status.${order.status}`) }}
+                </span>

Note: This requires ensuring all possible API status values have corresponding translation keys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/WowCraftingTab.vue` around lines 319 - 323,
The order status is rendered raw via order.status.replace("_", " ") instead of
using the existing STATUS_LABEL_KEYS translation mapping; update the display in
WowCraftingTab.vue to look up STATUS_LABEL_KEYS[order.status] and pass that key
through the app's translation function (e.g., $t or i18n.t) while keeping
STATUS_COLORS for styling, and ensure every possible API status value present in
STATUS_LABEL_KEYS has a corresponding translation key defined so missing keys
fall back to a safe label.
web/frontend/src/views/guild/GuildDetailView.vue (1)

228-230: 🧹 Nitpick | 🔵 Trivial

Hardcoded "Guild" fallback not translated.

Lines 229 and 300 use "Guild" as a fallback when guild.current?.name is unavailable. Consider using a translation key like t("nav.sidebar.guild_fallback") for consistency. "NerpyBot" as a brand name is fine to keep hardcoded.

Also applies to: 299-301

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/GuildDetailView.vue` around lines 228 - 230, The
hardcoded fallback string "Guild" in the template expression using guildId and
guild.current?.name should be replaced with a translation key to support i18n;
update the expression in GuildDetailView.vue (the span rendering {{ guildId ?
(guild.current?.name ?? "Guild") : "NerpyBot" }}) to use
t("nav.sidebar.guild_fallback") instead of the literal "Guild", and make the
same change for the other identical occurrence later in the file (the same
ternary that falls back to "Guild"); ensure the t function is available in the
component scope (import/inject/useI18n) if not already.
web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue (1)

177-180: ⚠️ Potential issue | 🟡 Minor

Status badge text is not translated.

Lines 180 and 209 display sub.status and selected.status directly as raw strings ("pending", "approved", "denied") while the filter buttons use t(labelKey) for translated labels. This creates an inconsistency where the filter says "Ausstehend" (German) but the badge shows "pending" (English).

Consider using the STATUS_LABEL_KEYS mapping to translate the badge text:

{{ t(STATUS_LABEL_KEYS[sub.status]) }}

Also applies to: 204-209

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue` around lines
177 - 180, Badge text currently renders raw status strings (sub.status and
selected.status) causing untranslated labels; replace those direct uses with the
translation lookup using STATUS_LABEL_KEYS (e.g. call the i18n function with
STATUS_LABEL_KEYS[sub.status] and STATUS_LABEL_KEYS[selected.status]) and keep
existing CSS class logic using STATUS_BADGES[sub.status] so badges still style
by status; ensure you provide a safe fallback (e.g. the raw status) if
STATUS_LABEL_KEYS[...] is undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/frontend/src/components/LanguageSwitcher.vue`:
- Line 16: The aria-label in LanguageSwitcher.vue is hardcoded to English;
update the template to use the i18n translator (e.g., call $t or the component's
translation helper) with a new key like language_switcher.aria_label and pass
the locale.current (uppercased) as a param so the screen-reader label is
localized; then add the language_switcher.aria_label entry to both
web/frontend/src/i18n/locales/en.ts and web/frontend/src/i18n/locales/de.ts with
appropriate localized strings (e.g., "Language: {lang}" and German equivalent)
so $t('language_switcher.aria_label', { lang: ... }) resolves correctly.

In `@web/frontend/src/components/MockupToolbar.vue`:
- Around line 34-35: In MockupToolbar.vue replace the hardcoded English sentence
"Some sections may be hidden." with a call to the translation helper (t(...)) so
the banner uses localized text; update the template that currently renders
<strong>{{ mockupLevel }}</strong>. Some sections may be hidden. to instead call
t("mockup.sectionsHidden") (or your existing key naming convention) and ensure
the new key is added to the i18n resource files; reference the t(...) helper and
the mockupLevel binding when making the change.

In `@web/frontend/src/i18n/index.ts`:
- Around line 1-7: The locales map currently uses a broad Record<string,
Locales>; define a literal union type SupportedLocale = "en" | "de" (or derive
it from the keys of a const tuple) and change the locales declaration to
Record<SupportedLocale, Locales> (or a mapped type) so the map is strictly keyed
by supported locale names; update any usages (e.g., useLocaleStore, Locales, en,
de) to rely on SupportedLocale to prevent drifting when adding/removing locales.
- Line 32: The missing-key console.warn in the i18n lookup is firing on every
render for the same key; create a module-level Set (e.g., warnedMissingKeys) and
change the condition in the function that compares raw === key to only call
console.warn if the key is not already in the Set; after warning, add the key to
warnedMissingKeys so subsequent renders do not repeat the message (optionally
expose a clear/reset function for tests if needed).

In `@web/frontend/src/views/guild/GuildDetailView.vue`:
- Around line 349-352: The v-for uses group.labelKey as the :key which can break
reactivity if groups are reordered or label keys change; update the
sectionGroups data to include a stable unique identifier (e.g., id) for each
group and change the v-for key to use group.id instead of group.labelKey (update
where sectionGroups is constructed/defined and the template iteration in
GuildDetailView.vue to reference the new id field).

In `@web/frontend/src/views/guild/tabs/ApplicationFormsTab.vue`:
- Around line 205-213: The placeholder for the name input is hardcoded in
English; update the input in ApplicationFormsTab.vue that binds to
formDraft.name to use a localized string instead (e.g. replace the literal "e.g.
Guild Application" with a t(...) lookup such as
t("tabs.application_forms.name_placeholder") or the existing i18n key for name
placeholders), and ensure the corresponding translation key is added to the
locale files so the placeholder is localized for all languages.

In `@web/frontend/src/views/guild/tabs/RoleMappingsTab.vue`:
- Around line 97-105: Replace the hardcoded placeholder strings in the two
DiscordPicker instances with localized keys and add those keys to the locales:
change the placeholders for the component bound to newMapping.source_role_id and
newMapping.target_role_id to use t("tabs.role_mappings.source_placeholder") and
t("tabs.role_mappings.target_placeholder") (or similar key names), then add
matching entries to the translation files for all supported locales so the UI
uses localized text consistently.

---

Outside diff comments:
In `@web/frontend/src/views/guild/GuildDetailView.vue`:
- Around line 228-230: The hardcoded fallback string "Guild" in the template
expression using guildId and guild.current?.name should be replaced with a
translation key to support i18n; update the expression in GuildDetailView.vue
(the span rendering {{ guildId ? (guild.current?.name ?? "Guild") : "NerpyBot"
}}) to use t("nav.sidebar.guild_fallback") instead of the literal "Guild", and
make the same change for the other identical occurrence later in the file (the
same ternary that falls back to "Guild"); ensure the t function is available in
the component scope (import/inject/useI18n) if not already.

In `@web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue`:
- Around line 177-180: Badge text currently renders raw status strings
(sub.status and selected.status) causing untranslated labels; replace those
direct uses with the translation lookup using STATUS_LABEL_KEYS (e.g. call the
i18n function with STATUS_LABEL_KEYS[sub.status] and
STATUS_LABEL_KEYS[selected.status]) and keep existing CSS class logic using
STATUS_BADGES[sub.status] so badges still style by status; ensure you provide a
safe fallback (e.g. the raw status) if STATUS_LABEL_KEYS[...] is undefined.

In `@web/frontend/src/views/guild/tabs/AutoKickerTab.vue`:
- Around line 15-16: The client-side validation throws a hardcoded English error
when c.kick_after is invalid; replace the literal string in the throw with a
localized message (use the component's i18n helper—e.g., this.$t or the
Composition API t function) so the message is translated, update the thrown
Error to use t('guild.autokicker.kick_after_min') (or similar key) and add that
key to the locale files for all supported languages; ensure you reference the
validation around c.kick_after in AutoKickerTab.vue so the thrown Error uses the
i18n key instead of the English string.

In `@web/frontend/src/views/guild/tabs/WowCraftingTab.vue`:
- Around line 319-323: The order status is rendered raw via
order.status.replace("_", " ") instead of using the existing STATUS_LABEL_KEYS
translation mapping; update the display in WowCraftingTab.vue to look up
STATUS_LABEL_KEYS[order.status] and pass that key through the app's translation
function (e.g., $t or i18n.t) while keeping STATUS_COLORS for styling, and
ensure every possible API status value present in STATUS_LABEL_KEYS has a
corresponding translation key defined so missing keys fall back to a safe label.

In `@web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue`:
- Around line 169-171: The hardcoded validation message assigned to
addError.value in WowGuildNewsTab.vue should be replaced with a localized string
lookup; update the conditional that checks newConfig.value.channel_id,
newConfig.value.wow_realm_slug, and newConfig.value.wow_guild_name_input to set
addError.value using the project's i18n function (e.g., this.$t or i18n.t)
instead of the English literal so users see the translated "Region, guild name,
realm, and channel are required" message; ensure you reference the appropriate
translation key (create one in the locale files if missing) and use the same
lookup pattern used elsewhere in this component.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bbdb99a7-e2e2-4c02-8e07-5c20d221fcae

📥 Commits

Reviewing files that changed from the base of the PR and between 7b82492 and f29f4da.

📒 Files selected for processing (27)
  • web/frontend/src/components/LanguageSwitcher.vue
  • web/frontend/src/components/MockupToolbar.vue
  • web/frontend/src/i18n/index.ts
  • web/frontend/src/i18n/locales/de.ts
  • web/frontend/src/i18n/locales/en.ts
  • web/frontend/src/stores/locale.ts
  • web/frontend/src/views/LoginView.vue
  • web/frontend/src/views/guild/GuildDetailView.vue
  • web/frontend/src/views/guild/tabs/ApplicationFormsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue
  • web/frontend/src/views/guild/tabs/AutoDeleteTab.vue
  • web/frontend/src/views/guild/tabs/AutoKickerTab.vue
  • web/frontend/src/views/guild/tabs/LanguageTab.vue
  • web/frontend/src/views/guild/tabs/LeaveMessagesTab.vue
  • web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorDashboardTab.vue
  • web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue
  • web/frontend/src/views/guild/tabs/OperatorModulesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue
  • web/frontend/src/views/guild/tabs/ReactionRolesTab.vue
  • web/frontend/src/views/guild/tabs/RemindersTab.vue
  • web/frontend/src/views/guild/tabs/RoleMappingsTab.vue
  • web/frontend/src/views/guild/tabs/ServerOverviewTab.vue
  • web/frontend/src/views/guild/tabs/SupportTab.vue
  • web/frontend/src/views/guild/tabs/WowCraftingTab.vue
  • web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue

Comment thread web/frontend/src/components/LanguageSwitcher.vue Outdated
Comment thread web/frontend/src/components/MockupToolbar.vue Outdated
Comment thread web/frontend/src/i18n/index.ts Outdated
Comment thread web/frontend/src/i18n/index.ts Outdated
Comment thread web/frontend/src/views/guild/GuildDetailView.vue Outdated
Comment thread web/frontend/src/views/guild/tabs/ApplicationFormsTab.vue
Comment thread web/frontend/src/views/guild/tabs/RoleMappingsTab.vue Outdated
@karaktaka karaktaka force-pushed the feat/dashboard-i18n branch from f29f4da to 53d5ba7 Compare March 14, 2026 11:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
web/frontend/src/views/LoginView.vue (1)

3-16: ⚠️ Potential issue | 🟡 Minor

Handle route.query.error as a potential array.

Lines 14–15 compare route.query.error directly against strings, but Vue Router can provide array values for repeated query parameters. When route.query.error is an array, both checks fail silently.

Suggested fix
-import { useRoute, useRouter } from "vue-router";
+import { useRoute, useRouter, type LocationQueryValue } from "vue-router";
@@
-const isPremiumRequired = computed(() => route.query.error === "premium_required");
-const isSessionExpired = computed(() => route.query.error === "session_expired");
+function firstQueryValue(
+  value: LocationQueryValue | LocationQueryValue[] | undefined,
+): string | null {
+  if (Array.isArray(value)) {
+    return value.find((v): v is string => typeof v === "string") ?? null;
+  }
+  return typeof value === "string" ? value : null;
+}
+
+const queryError = computed(() => firstQueryValue(route.query.error));
+const isPremiumRequired = computed(() => queryError.value === "premium_required");
+const isSessionExpired = computed(() => queryError.value === "session_expired");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/LoginView.vue` around lines 3 - 16, route.query.error
can be an array when query params repeat, so change the computed guards
isPremiumRequired and isSessionExpired (and the initialization of
showExpiredModal) to handle both string and array values: in isPremiumRequired
and isSessionExpired use a check that if route.query.error is an array use
.includes("premium_required") / .includes("session_expired") else compare
directly, and initialize showExpiredModal from isSessionExpired.value (or derive
it from the same normalized check) so it reacts correctly when route.query.error
is an array; update the symbols isPremiumRequired, isSessionExpired, and
showExpiredModal accordingly.
web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue (1)

279-287: ⚠️ Potential issue | 🟡 Minor

Localize the guild-name placeholder as well.

placeholder="Thunderfury" is still hardcoded English, so this input won’t fully switch with locale changes.

Proposed fix
-            placeholder="Thunderfury"
+            :placeholder="t('tabs.wow_guild_news.guild_name_placeholder')"

Also add tabs.wow_guild_news.guild_name_placeholder in both web/frontend/src/i18n/locales/en.ts and web/frontend/src/i18n/locales/de.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue` around lines 279 -
287, Replace the hardcoded placeholder "Thunderfury" in the input bound to
v-model="newConfig.wow_guild_name_input" in WowGuildNewsTab.vue with a localized
string (use the t(...) helper) and add the new translation key
tabs.wow_guild_news.guild_name_placeholder to both locale files
(web/frontend/src/i18n/locales/en.ts and de.ts) with the appropriate English and
German text so the placeholder changes with the selected locale.
web/frontend/src/views/guild/GuildDetailView.vue (1)

41-41: ⚠️ Potential issue | 🟠 Major

Normalize route.query.tab instead of casting it to string.

Line 41 uses route.query.tab as string, which bypasses type safety. Query values are string | null | (string | null)[] at runtime—arrays and null values won't be handled correctly with a direct cast.

Extract and apply the query scalar helper that's already used elsewhere in the codebase:

+function toQueryScalar(v: string | null | (string | null)[] | undefined): string | null | undefined {
+  return Array.isArray(v) ? v[0] : v;
+}
+
-const activeSection = computed(() => (route.query.tab as string) ?? "server-overview");
+const activeSection = computed(() => toQueryScalar(route.query.tab) ?? "server-overview");

This satisfies the guideline: web/frontend/**/*.{ts,tsx,vue}: route.query TypeScript types — query values are LocationQueryValue | LocationQueryValue[] where LocationQueryValue = string | null; helpers must accept (string | null)[].

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/GuildDetailView.vue` at line 41, Replace the
unsafe cast in the computed activeSection by normalizing route.query.tab through
the existing query-scalar helper instead of using "as string"; import and use
the helper (the same one used elsewhere that accepts LocationQueryValue |
LocationQueryValue[]) and write: const activeSection = computed(() =>
(queryScalar(route.query.tab) ?? "server-overview")); update the computed usage
to call queryScalar(route.query.tab) so arrays/nulls are handled safely and type
checks pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/frontend/src/components/MockupToolbar.vue`:
- Around line 34-35: The template is rendering the raw enum value via {{
mockupLevel }} which shows unlocalized strings like "admin", "mod", "member";
replace this with a localized label by mapping mockupLevel to translation keys
and using the existing t() function (e.g. add a computed property or method such
as getMockupLevelLabel or mockupLevelLabel that returns
t(`mockup.levels.${mockupLevel}`) and use that in the template instead of
mockupLevel) so the displayed role names are translated.

In `@web/frontend/src/stores/locale.ts`:
- Around line 7-11: detectBrowserLocale currently only checks
navigator.languages[0] (or navigator.language) and returns "en" if that first
preference is unsupported; instead iterate through all entries in
navigator.languages (falling back to navigator.language if languages is
undefined) normalize each by splitting on "-" and lowercasing, and return the
first matching entry found in SUPPORTED_LOCALES; if none match, return "en".
Update detectBrowserLocale to loop over navigator.languages array (and include
navigator.language as a final candidate) and use SUPPORTED_LOCALES to test
membership.

In `@web/frontend/src/views/guild/tabs/ServerOverviewTab.vue`:
- Around line 14-15: Destructure locale from useI18n() alongside t (e.g., const
{ t, locale } = useI18n()) and then pass the selected locale when formatting
member counts by calling toLocaleString with the locale value (e.g., use
locale.value as the argument to toLocaleString on the members/count variable
used in the ServerOverviewTab.vue render logic) so formatting follows the app's
selected language.

In `@web/frontend/src/views/guild/tabs/WowCraftingTab.vue`:
- Around line 320-322: The current code casts arbitrary order.status to I18nKey
before calling t(), bypassing type checks and causing false missing-key
warnings; change the span to check STATUS_LABEL_KEYS[order.status] first and
only call t() with that value when it exists (e.g. const labelKey =
STATUS_LABEL_KEYS[order.status]; then display t(labelKey) if labelKey is
defined), otherwise render order.status (or a safe fallback like a generic
untranslated string) so you never cast an unknown string to I18nKey; update
references around STATUS_LABEL_KEYS, STATUS_COLORS, t(), and order.status in
WowCraftingTab.vue accordingly.

---

Outside diff comments:
In `@web/frontend/src/views/guild/GuildDetailView.vue`:
- Line 41: Replace the unsafe cast in the computed activeSection by normalizing
route.query.tab through the existing query-scalar helper instead of using "as
string"; import and use the helper (the same one used elsewhere that accepts
LocationQueryValue | LocationQueryValue[]) and write: const activeSection =
computed(() => (queryScalar(route.query.tab) ?? "server-overview")); update the
computed usage to call queryScalar(route.query.tab) so arrays/nulls are handled
safely and type checks pass.

In `@web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue`:
- Around line 279-287: Replace the hardcoded placeholder "Thunderfury" in the
input bound to v-model="newConfig.wow_guild_name_input" in WowGuildNewsTab.vue
with a localized string (use the t(...) helper) and add the new translation key
tabs.wow_guild_news.guild_name_placeholder to both locale files
(web/frontend/src/i18n/locales/en.ts and de.ts) with the appropriate English and
German text so the placeholder changes with the selected locale.

In `@web/frontend/src/views/LoginView.vue`:
- Around line 3-16: route.query.error can be an array when query params repeat,
so change the computed guards isPremiumRequired and isSessionExpired (and the
initialization of showExpiredModal) to handle both string and array values: in
isPremiumRequired and isSessionExpired use a check that if route.query.error is
an array use .includes("premium_required") / .includes("session_expired") else
compare directly, and initialize showExpiredModal from isSessionExpired.value
(or derive it from the same normalized check) so it reacts correctly when
route.query.error is an array; update the symbols isPremiumRequired,
isSessionExpired, and showExpiredModal accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f1c4173e-e8ad-4dfc-86cf-83f0d9531a88

📥 Commits

Reviewing files that changed from the base of the PR and between f29f4da and 53d5ba7.

📒 Files selected for processing (27)
  • web/frontend/src/components/LanguageSwitcher.vue
  • web/frontend/src/components/MockupToolbar.vue
  • web/frontend/src/i18n/index.ts
  • web/frontend/src/i18n/locales/de.ts
  • web/frontend/src/i18n/locales/en.ts
  • web/frontend/src/stores/locale.ts
  • web/frontend/src/views/LoginView.vue
  • web/frontend/src/views/guild/GuildDetailView.vue
  • web/frontend/src/views/guild/tabs/ApplicationFormsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue
  • web/frontend/src/views/guild/tabs/AutoDeleteTab.vue
  • web/frontend/src/views/guild/tabs/AutoKickerTab.vue
  • web/frontend/src/views/guild/tabs/LanguageTab.vue
  • web/frontend/src/views/guild/tabs/LeaveMessagesTab.vue
  • web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorDashboardTab.vue
  • web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue
  • web/frontend/src/views/guild/tabs/OperatorModulesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue
  • web/frontend/src/views/guild/tabs/ReactionRolesTab.vue
  • web/frontend/src/views/guild/tabs/RemindersTab.vue
  • web/frontend/src/views/guild/tabs/RoleMappingsTab.vue
  • web/frontend/src/views/guild/tabs/ServerOverviewTab.vue
  • web/frontend/src/views/guild/tabs/SupportTab.vue
  • web/frontend/src/views/guild/tabs/WowCraftingTab.vue
  • web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue

Comment thread web/frontend/src/components/MockupToolbar.vue Outdated
Comment thread web/frontend/src/stores/locale.ts Outdated
Comment thread web/frontend/src/views/guild/tabs/ServerOverviewTab.vue Outdated
Comment thread web/frontend/src/views/guild/tabs/WowCraftingTab.vue
@karaktaka karaktaka force-pushed the feat/dashboard-i18n branch from 53d5ba7 to f992ebb Compare March 14, 2026 11:47
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue (1)

95-99: ⚠️ Potential issue | 🟡 Minor

Localize the Discord ID placeholder to finish this tab’s i18n.

Line 97 still hardcodes English ("e.g. 123456789012345678"), so DE users get mixed-language UI.

Proposed fix
-        <input
-          v-model="newUserId"
-          placeholder="e.g. 123456789012345678"
+        <input
+          v-model="newUserId"
+          :placeholder="t('tabs.operator_user_management.discord_id_placeholder')"
           class="bg-input border border-border rounded px-3 py-1.5 text-sm font-mono w-56"
           `@keyup.enter`="grantPremium"
         />

Also add tabs.operator_user_management.discord_id_placeholder in both web/frontend/src/i18n/locales/en.ts and web/frontend/src/i18n/locales/de.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue` around lines
95 - 99, The input for the Discord ID in OperatorUserManagementTab.vue is
hardcoded ("e.g. 123456789012345678"); replace this literal placeholder with a
localized string reference (use the component's i18n lookup where the input
element with v-model="newUserId" and `@keyup.enter`="grantPremium" is defined) and
add the key tabs.operator_user_management.discord_id_placeholder to both locale
files (web/frontend/src/i18n/locales/en.ts and
web/frontend/src/i18n/locales/de.ts) with appropriate English and German values
so the placeholder is localized.
web/frontend/src/views/guild/tabs/WowCraftingTab.vue (1)

201-217: ⚠️ Potential issue | 🟡 Minor

Render saved profession names from profession_id, not the API string.

Line 213 still shows m.profession_name, so existing mappings follow whatever language the backend returned instead of the selected dashboard locale. Resolve the label through PROFESSIONS and keep m.profession_name only as a fallback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/WowCraftingTab.vue` around lines 201 - 217,
The UI is rendering m.profession_name directly (line with @{{
roleName(m.role_id) }} → {{ m.profession_name }}), which uses the backend string
instead of the localized dashboard label; change that to resolve the profession
label from the PROFESSIONS list by looking up the entry with id ===
m.profession_id and rendering t(found.labelKey) with m.profession_name as a
fallback (you can do this inline in the template or add a helper method/computed
like getProfessionLabel(professionId) that uses PROFESSIONS.find(p => p.id ===
professionId) and returns t(p.labelKey) || m.profession_name). Ensure the edit
branch still sets editMappingProfessionId and the save/update flow
(updateMapping, editingMappingId) remains unchanged.
web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue (2)

77-77: ⚠️ Potential issue | 🟡 Minor

Translate the fallback load error too.

The "Failed to load" branch is still hardcoded English, so non-Error rejections will show mixed-language UI in this tab. Put that default behind t(...) in both fetch paths.

Also applies to: 101-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue` at line 77,
The fallback error string "Failed to load" is hardcoded in the error.value
assignment (seen at the error.value = e instanceof Error ? e.message : "Failed
to load" occurrences); replace the literal with a translated string by wrapping
it in the i18n translator (t("...")) in both locations (the one around line 77
and the one around line 101) so non-Error rejections also display a localized
message—use the existing t(...) function available in this component and keep
the same ternary structure, only substituting the literal with t("Failed to
load") or the appropriate translation key.

62-63: ⚠️ Potential issue | 🟠 Major

Validate formId before using as a number.

Number(...) on a query string that doesn't represent a valid number produces NaN, which gets passed to the API call. Additionally, the watcher doesn't reset the filter when formId is removed from the URL, leaving a stale filter applied.

🛠️ Proposed fix
+function parseFormId(queryValue: typeof route.query.formId): number | null {
+  const raw = toQueryScalar(queryValue);
+  if (!raw) return null;
+  const parsed = Number(raw);
+  return Number.isFinite(parsed) ? parsed : null;
+}
+
 onMounted(async () => {
-  const rawFormId = toQueryScalar(route.query.formId);
-  const preselected = rawFormId ? Number(rawFormId) : null;
+  const preselected = parseFormId(route.query.formId);
@@
 watch(() => route.query.formId, (newId) => {
-  const id = toQueryScalar(newId);
-  if (id) void applyFormFilter(Number(id));
+  void applyFormFilter(parseFormId(newId));
 });

Also applies to: lines 84-86

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue` around lines
62 - 63, The code assigns preselected = Number(rawFormId) without validating,
which yields NaN for invalid query strings and also the watcher doesn't clear
the filter when formId is removed; update the logic around rawFormId and
preselected (and the analogous block at lines 84–86) to parse and validate the
value (use Number.isFinite or parseInt + isNaN) and only set preselected to a
Number when valid, otherwise set it to null; also update the route/query watcher
to reset the filter to null when route.query.formId is undefined/empty so stale
filters are cleared (refer to variables rawFormId, preselected and the route
watcher).
web/frontend/src/views/guild/tabs/SupportTab.vue (1)

37-37: ⚠️ Potential issue | 🟡 Minor

Translate the generic submit error.

"Failed to send message" bypasses the locale layer on non-Error rejections, so failed submits can still render mixed-language UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/SupportTab.vue` at line 37, The non-Error
branch sets error.value to a hardcoded English string; replace that literal with
the app's i18n lookup so non-Error rejections are localized (e.g., use the
component's t/$t i18n helper to set error.value when e is not an Error). Update
the expression around error.value and e in SupportTab.vue to call the project's
translation key (e.g., "support.failedToSendMessage") instead of the plain
"Failed to send message".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/frontend/src/i18n/locales/en.ts`:
- Around line 2-26: Add standardized fallback error keys to the common locale
object (e.g. load_failed, save_failed, add_failed, delete_failed, remove_failed,
fetch_failed) with concise English messages and any interpolation placeholders
needed (e.g. "{error}" if you want to include error text). Then update the
catch-block fallbacks in ModeratorRolesTab.vue, ApplicationTemplatesTab.vue,
OperatorGuildsTab.vue, and LeaveMessagesTab.vue to call t('common.<key>')
instead of hardcoded English strings (replace each instance on the referenced
lines with the appropriate key like common.load_failed / common.save_failed /
common.add_failed / common.delete_failed / common.remove_failed). Ensure keys
match exactly between en.ts and the t() calls.

In `@web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue`:
- Line 178: The template currently always uses the pluralized key
t("tabs.application_templates.questions_count", { count:
String(tpl.questions.length) }) which renders "1 questions"; change the template
to choose singular vs plural keys based on tpl.questions.length (e.g., if
tpl.questions.length === 1 use
t("tabs.application_templates.questions_count.one") else
t("tabs.application_templates.questions_count.other")), and add corresponding
keys in web/frontend/src/i18n/locales/en.ts and
web/frontend/src/i18n/locales/de.ts (provide both singular and plural strings
for tabs.application_templates.questions_count.one and
tabs.application_templates.questions_count.other).

In `@web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue`:
- Line 105: The template is calling g.member_count.toLocaleString() which uses
the browser locale; extract the dashboard locale from useI18n() (the locale ref
returned by useI18n()) in the OperatorGuildsTab component and pass that locale
into toLocaleString so member counts render with the selected dashboard locale;
locate the expression with g.member_count and t("tabs.operator_guilds.members",
...) and replace the plain toLocaleString() call with
toLocaleString(locale.value or the locale string you expose) so formatting
follows the i18n locale.

In `@web/frontend/src/views/guild/tabs/RemindersTab.vue`:
- Around line 93-99: The translated wrapper currently passes raw English/ISO
values into t(), so pre-format locale-sensitive values before translation: for
the schedule rendering code paths (function handling r.schedule_type: interval,
daily, weekly, monthly) call the localized formatter for the interval
(formatInterval with current locale or equivalent) and format
schedule_time/next_fire using the active locale (e.g., Intl.DateTimeFormat) so
you pass locale-formatted strings into t(); also update the next_fire rendering
at the other usage (the block referenced at lines 349-350) to format next_fire
with the active locale before calling t(). Ensure you still use DOW_LABELS.value
for weekday labels but map it through localization if needed.

In `@web/frontend/src/views/LoginView.vue`:
- Around line 14-20: The showExpiredModal ref is initialized once from
isSessionExpired and won't update when the route.query.error changes; make the
modal reactive by removing the static snapshot and instead initialize
showExpiredModal (e.g., false) and add a watcher on isSessionExpired (or
route.query.error) to set showExpiredModal.value = isSessionExpired.value
whenever the computed changes so the modal opens/closes when
errorParam/isSessionExpired updates; refer to errorParam, isSessionExpired,
showExpiredModal and the route/query reactive source when implementing the
watch.

---

Outside diff comments:
In `@web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue`:
- Line 77: The fallback error string "Failed to load" is hardcoded in the
error.value assignment (seen at the error.value = e instanceof Error ? e.message
: "Failed to load" occurrences); replace the literal with a translated string by
wrapping it in the i18n translator (t("...")) in both locations (the one around
line 77 and the one around line 101) so non-Error rejections also display a
localized message—use the existing t(...) function available in this component
and keep the same ternary structure, only substituting the literal with
t("Failed to load") or the appropriate translation key.
- Around line 62-63: The code assigns preselected = Number(rawFormId) without
validating, which yields NaN for invalid query strings and also the watcher
doesn't clear the filter when formId is removed; update the logic around
rawFormId and preselected (and the analogous block at lines 84–86) to parse and
validate the value (use Number.isFinite or parseInt + isNaN) and only set
preselected to a Number when valid, otherwise set it to null; also update the
route/query watcher to reset the filter to null when route.query.formId is
undefined/empty so stale filters are cleared (refer to variables rawFormId,
preselected and the route watcher).

In `@web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue`:
- Around line 95-99: The input for the Discord ID in
OperatorUserManagementTab.vue is hardcoded ("e.g. 123456789012345678"); replace
this literal placeholder with a localized string reference (use the component's
i18n lookup where the input element with v-model="newUserId" and
`@keyup.enter`="grantPremium" is defined) and add the key
tabs.operator_user_management.discord_id_placeholder to both locale files
(web/frontend/src/i18n/locales/en.ts and web/frontend/src/i18n/locales/de.ts)
with appropriate English and German values so the placeholder is localized.

In `@web/frontend/src/views/guild/tabs/SupportTab.vue`:
- Line 37: The non-Error branch sets error.value to a hardcoded English string;
replace that literal with the app's i18n lookup so non-Error rejections are
localized (e.g., use the component's t/$t i18n helper to set error.value when e
is not an Error). Update the expression around error.value and e in
SupportTab.vue to call the project's translation key (e.g.,
"support.failedToSendMessage") instead of the plain "Failed to send message".

In `@web/frontend/src/views/guild/tabs/WowCraftingTab.vue`:
- Around line 201-217: The UI is rendering m.profession_name directly (line with
@{{ roleName(m.role_id) }} → {{ m.profession_name }}), which uses the backend
string instead of the localized dashboard label; change that to resolve the
profession label from the PROFESSIONS list by looking up the entry with id ===
m.profession_id and rendering t(found.labelKey) with m.profession_name as a
fallback (you can do this inline in the template or add a helper method/computed
like getProfessionLabel(professionId) that uses PROFESSIONS.find(p => p.id ===
professionId) and returns t(p.labelKey) || m.profession_name). Ensure the edit
branch still sets editMappingProfessionId and the save/update flow
(updateMapping, editingMappingId) remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 82ce6d00-9215-4c83-ade8-4c72fdb5a9f4

📥 Commits

Reviewing files that changed from the base of the PR and between 53d5ba7 and f992ebb.

📒 Files selected for processing (28)
  • web/frontend/src/components/LanguageSwitcher.vue
  • web/frontend/src/components/MockupToolbar.vue
  • web/frontend/src/i18n/index.ts
  • web/frontend/src/i18n/locales/de.ts
  • web/frontend/src/i18n/locales/en.ts
  • web/frontend/src/stores/locale.ts
  • web/frontend/src/utils/route.ts
  • web/frontend/src/views/LoginView.vue
  • web/frontend/src/views/guild/GuildDetailView.vue
  • web/frontend/src/views/guild/tabs/ApplicationFormsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue
  • web/frontend/src/views/guild/tabs/AutoDeleteTab.vue
  • web/frontend/src/views/guild/tabs/AutoKickerTab.vue
  • web/frontend/src/views/guild/tabs/LanguageTab.vue
  • web/frontend/src/views/guild/tabs/LeaveMessagesTab.vue
  • web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorDashboardTab.vue
  • web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue
  • web/frontend/src/views/guild/tabs/OperatorModulesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue
  • web/frontend/src/views/guild/tabs/ReactionRolesTab.vue
  • web/frontend/src/views/guild/tabs/RemindersTab.vue
  • web/frontend/src/views/guild/tabs/RoleMappingsTab.vue
  • web/frontend/src/views/guild/tabs/ServerOverviewTab.vue
  • web/frontend/src/views/guild/tabs/SupportTab.vue
  • web/frontend/src/views/guild/tabs/WowCraftingTab.vue
  • web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue

Comment thread web/frontend/src/i18n/locales/en.ts
Comment thread web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue Outdated
Comment thread web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue Outdated
Comment thread web/frontend/src/views/guild/tabs/RemindersTab.vue Outdated
Comment thread web/frontend/src/views/LoginView.vue Outdated
@karaktaka karaktaka force-pushed the feat/dashboard-i18n branch from f992ebb to fd6cc4b Compare March 14, 2026 12:33
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue (1)

166-197: ⚠️ Potential issue | 🟡 Minor

Finish localizing the remaining fallback errors.

This flow now translates required_fields, guild_not_found, and verify_warning, but the nearby fallback branches still emit English (Validation failed, Failed to create tracker). load(), saveEdit(), and confirmDelete() follow the same pattern, so DE users will still see mixed-language failure states.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue` around lines 166 -
197, The code emits hard-coded English fallback messages ("Validation failed",
"Failed to create tracker") in submitAdd (and similarly in load, saveEdit,
confirmDelete), causing mixed-language UIs; replace those literals with i18n
calls (e.g. t("tabs.wow_guild_news.validation_failed") and
t("tabs.wow_guild_news.create_failed") or appropriate existing keys) so all
error branches use t(...), and add the corresponding translation keys to the
locale files; search for the functions submitAdd, load, saveEdit, and
confirmDelete to locate and update each fallback error assignment to use t(...)
instead of raw strings.
web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue (1)

21-21: ⚠️ Potential issue | 🟠 Major

Watch props.guildId and re-fetch role data on guild changes.

The parent view reuses this component instance across guild navigation (no :key="guildId" keying). When the route changes, props.guildId updates reactively, but fetchRoles and roleName remain bound to the initial guild ID because they're destructured from useGuildEntities(props.guildId) in script setup. This causes load() to fetch from the new guild while roleName() still looks up names in the previous guild's cache—creating a stale state split.

Add watch(() => props.guildId, () => { void load(); void fetchRoles(); }) (or re-call useGuildEntities reactively) to synchronize data on guild changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue` at line 21, The
component currently binds fetchRoles and roleName to the initial guild via
useGuildEntities(props.guildId) so when props.guildId changes those helpers
become stale; add a watcher on props.guildId (watch(() => props.guildId, () => {
void load(); void fetchRoles(); })) or otherwise re-initialize useGuildEntities
when props.guildId changes so that load(), fetchRoles(), and roleName() are all
synced to the new guild ID.
web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue (4)

78-79: ⚠️ Potential issue | 🟡 Minor

Hardcoded fallback in deleteTemplate() catch.

Proposed fix
   } catch (e: unknown) {
-    opError.value = e instanceof Error ? e.message : "Delete failed";
+    opError.value = e instanceof Error ? e.message : t("common.delete_failed");
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue` around lines
78 - 79, The catch block in deleteTemplate() sets a hardcoded fallback "Delete
failed" which can hide useful error info; change the assignment to opError.value
to prefer the Error message when e is Error, otherwise use a stringified form of
e (e.g., String(e) or JSON-safe representation), and only fall back to a short
default if that result is empty — update the catch handling around opError.value
in deleteTemplate() to use e instanceof Error ? e.message : String(e) || "Delete
failed".

65-66: ⚠️ Potential issue | 🟡 Minor

Hardcoded fallback in saveTemplate() catch.

Proposed fix
   } catch (e: unknown) {
-    opError.value = e instanceof Error ? e.message : "Save failed";
+    opError.value = e instanceof Error ? e.message : t("common.save_failed");
   } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue` around lines
65 - 66, In the saveTemplate() catch block replace the hardcoded fallback "Save
failed" by extracting a meaningful message from the thrown value: set
opError.value to e instanceof Error ? e.message : String(e) and if that results
in an empty string fall back to a short default; update the catch in
saveTemplate (the block that currently assigns opError.value) to use this logic
so opError.value reflects the actual error content instead of the fixed string.

33-34: ⚠️ Potential issue | 🟡 Minor

Hardcoded fallback in load() catch.

Proposed fix
   } catch (e: unknown) {
-    error.value = e instanceof Error ? e.message : "Failed to load";
+    error.value = e instanceof Error ? e.message : t("common.load_failed");
   } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue` around lines
33 - 34, In load() the catch block sets error.value to a hardcoded "Failed to
load"; instead capture and surface the actual thrown value by replacing the
fallback with a stringified version of the caught value (use e instanceof Error
? e.message : String(e)) so error.value reflects the real error; update the
catch in load() where error.value is assigned to use this stringification (or an
i18n error key if your app has one) rather than the hardcoded literal.

92-93: ⚠️ Potential issue | 🟡 Minor

Hardcoded fallbacks in question operations.

Consider adding common.add_failed or reusing existing keys.

Proposed fix
   } catch (e: unknown) {
-    opError.value = e instanceof Error ? e.message : "Failed to add question";
+    opError.value = e instanceof Error ? e.message : t("common.save_failed");
   }
 }

 async function deleteTemplateQuestion(templateId: number, questionId: number) {
   opError.value = null;
   try {
     await api.delete(`/guilds/${props.guildId}/application-templates/${templateId}/questions/${questionId}`);
     const tpl = templates.value.find((t) => t.id === templateId);
     if (tpl) tpl.questions = tpl.questions.filter((q) => q.id !== questionId);
   } catch (e: unknown) {
-    opError.value = e instanceof Error ? e.message : "Failed to delete question";
+    opError.value = e instanceof Error ? e.message : t("common.delete_failed");
   }

Also applies to: 103-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue` around lines
92 - 93, Replace hardcoded error text in the catch blocks so they use the i18n
key instead of literal strings: when setting opError.value in
ApplicationTemplatesTab.vue (the catch handling around opError.value = ...), use
the translation key (e.g. t('common.add_failed') or this.$t('common.add_failed')
consistent with the component's i18n usage) rather than "Failed to add
question"; make the same replacement for the other catch at the similar spot
(previously flagged at lines 103-104) and ensure the component imports/uses the
existing i18n translator (useI18n or this.$t) so the key resolves correctly.
web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue (2)

48-49: ⚠️ Potential issue | 🟡 Minor

Hardcoded English fallback in revokePremium catch.

Use t("common.delete_failed") for consistency.

Proposed fix
   } catch (e: unknown) {
-    grantError.value = e instanceof Error ? e.message : "Failed to revoke access";
+    grantError.value = e instanceof Error ? e.message : t("common.delete_failed");
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue` around lines
48 - 49, The catch block assigning grantError.value uses a hardcoded English
fallback; replace the literal "Failed to revoke access" with the i18n key
t("common.delete_failed") so the error message follows the app's translations
(update the assignment in the revokePremium catch where grantError.value is set
to use t("common.delete_failed") while keeping the e instanceof Error ?
e.message branch intact).

36-37: ⚠️ Potential issue | 🟡 Minor

Hardcoded English fallback in grantPremium catch.

Use t("common.save_failed") or add a specific key.

Proposed fix
   } catch (e: unknown) {
-    grantError.value = e instanceof Error ? e.message : "Failed to grant access";
+    grantError.value = e instanceof Error ? e.message : t("common.save_failed");
   } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue` around lines
36 - 37, The catch in the grantPremium flow currently sets grantError.value to a
hardcoded English string; replace the fallback "Failed to grant access" with a
localized string (e.g. t("common.save_failed") or a new key like
t("guild.grant_failed")), and ensure the i18n translator (t) is available in the
scope of the grantPremium function (or call useI18n() to obtain t) so the catch
block sets grantError.value = e instanceof Error ? e.message :
t("common.save_failed").
web/frontend/src/views/guild/tabs/LanguageTab.vue (1)

29-30: ⚠️ Potential issue | 🟡 Minor

Hardcoded fallback in loadConfig catch.

Proposed fix
   } catch (e: unknown) {
-    error.value = e instanceof Error ? e.message : "Failed to load";
+    error.value = e instanceof Error ? e.message : t("common.load_failed");
   } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/LanguageTab.vue` around lines 29 - 30, In
the catch block of the loadConfig flow (where error.value is assigned), don't
use the hardcoded fallback "Failed to load"; instead propagate a meaningful
message from the caught value by converting the unknown e to a string or
extracting e.message when available so error.value contains useful diagnostic
text (e.g., use e instanceof Error ? e.message : String(e) or similar) while
keeping the existing error.value assignment in the loadConfig catch scope.
web/frontend/src/views/guild/tabs/OperatorDashboardTab.vue (1)

31-32: ⚠️ Potential issue | 🟡 Minor

Hardcoded English fallback in fetchHealth catch.

Proposed fix
   } catch (e: unknown) {
-    error.value = e instanceof Error ? e.message : "Failed to fetch health data";
+    error.value = e instanceof Error ? e.message : t("common.load_failed");
     health.value = null;
   } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/OperatorDashboardTab.vue` around lines 31 -
32, The catch block for fetchHealth sets a hardcoded English fallback ("Failed
to fetch health data") on error.value; replace that literal with a localized
string using the component's i18n instance (e.g., call useI18n() -> const { t }
= useI18n() or use this.$t) and set error.value = e instanceof Error ? e.message
: t('operatorDashboard.fetchHealthFailed') (or another appropriate key),
updating the translation files with that key; target the catch block that
assigns error.value in OperatorDashboardTab.vue to implement this change.
web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue (1)

23-24: ⚠️ Potential issue | 🟡 Minor

Hardcoded English fallback in catch block.

The error fallback "Failed to fetch guilds" should use t("common.load_failed") for consistency with the i18n approach.

Proposed fix
   } catch (e: unknown) {
-    error.value = e instanceof Error ? e.message : "Failed to fetch guilds";
+    error.value = e instanceof Error ? e.message : t("common.load_failed");
   } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue` around lines 23 -
24, Replace the hardcoded fallback string in the catch block inside
OperatorGuildsTab.vue (the catch (e: unknown) { error.value = ... } block) with
the i18n key by using t("common.load_failed") instead of "Failed to fetch
guilds"; ensure the translation function t is in scope (import/obtain via
useI18n() in the component setup if not already) so error.value = e instanceof
Error ? e.message : t("common.load_failed") compiles and uses the localized
message.
♻️ Duplicate comments (1)
web/frontend/src/views/guild/tabs/RemindersTab.vue (1)

96-111: ⚠️ Potential issue | 🟡 Minor

Localize the remaining schedule tokens.

next_fire is locale-aware now, but formatInterval() still emits hard-coded d/h/m/s, and the daily/weekly/monthly branches still inject raw schedule_time into t(). The wrapper text changes with the locale; the actual schedule values do not.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/RemindersTab.vue` around lines 96 - 111,
formatInterval and scheduleLabel currently inject raw, hard-coded tokens
(d/h/m/s and raw schedule_time) which aren't localized; update
formatInterval(seconds) to return localized unit strings via the i18n translator
(use keys like days/hours/minutes/seconds or the project's existing unit
translation keys) instead of "d/h/m/s", and in scheduleLabel ensure
schedule_time (and schedule_day_of_month if needed) are formatted through the
locale-aware formatter used elsewhere (or Intl.DateTimeFormat) before passing
into t(), keeping use of ReminderSchema, formatInterval, scheduleLabel and
DOW_LABELS to locate where to change. Ensure weekly/daily/monthly branches pass
pre-formatted, localized time strings (and localized unit strings from
formatInterval) into t() rather than raw values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/frontend/src/components/MockupToolbar.vue`:
- Around line 53-60: The select lacks an accessible name; update
MockupToolbar.vue so the <select> is labeled for assistive tech by either adding
aria-label="..." on the select or by giving the visible span a stable id (e.g.
id="mockup-simulate-as") and adding aria-labelledby="mockup-simulate-as" to the
select; ensure this change is applied where the select is rendered (the element
tied to the onSelect handler and iterating over levels) so screen readers will
announce the translated label.

In `@web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue`:
- Around line 43-52: The computed filtered currently resolves targetFormName and
filters submissions by s.form_name which couples to a mutable display field;
instead, keep filtering by the stable ID: when formFilter.value is not null
check s.form_id === formFilter.value (or remove the name-based check entirely
since the fetch is already scoped), and drop targetFormName and any use of
s.form_name; update the computed (referencing filtered, formFilter, forms,
submissions, statusFilter) to use s.form_id for the form filter so
rename-dependent mismatches are avoided.

In `@web/frontend/src/views/guild/tabs/RemindersTab.vue`:
- Line 185: The catch blocks in this component set the error and opError strings
using hard-coded English literals (e.g., "Failed to load", "Toggle failed",
"Delete failed", "Create failed"); update those assignments to use the i18n
translation helper (e.g., this.$t or $t) instead of raw strings so the messages
respect the current locale, and add corresponding translation keys to your
locale files; specifically search for assignments to error and opError in the
component's catch blocks (the load/toggle/delete/create methods) and replace the
literals with translated keys like this.$t('reminders.loadFailed') etc.

In `@web/frontend/src/views/guild/tabs/WowCraftingTab.vue`:
- Around line 324-328: Replace the hard-coded slice on order.create_date with a
locale-aware formatter: parse order.create_date into a Date and format it with
Intl.DateTimeFormat using the current locale (obtainable from your i18n
instance, e.g. useI18n().locale or props/context where locale is available) and
sensible options (e.g. year, month, day) before rendering. Update the template
span that currently uses order.create_date.slice(0, 10) to call a small helper
or computed (e.g. formatOrderDate(order.create_date) or a computed that uses
Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short'|'2-digit', day:
'2-digit' })) so the displayed date follows the selected locale while keeping
the STATUS_COLORS/STATUS_LABEL_KEYS translation logic untouched.

---

Outside diff comments:
In `@web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue`:
- Around line 78-79: The catch block in deleteTemplate() sets a hardcoded
fallback "Delete failed" which can hide useful error info; change the assignment
to opError.value to prefer the Error message when e is Error, otherwise use a
stringified form of e (e.g., String(e) or JSON-safe representation), and only
fall back to a short default if that result is empty — update the catch handling
around opError.value in deleteTemplate() to use e instanceof Error ? e.message :
String(e) || "Delete failed".
- Around line 65-66: In the saveTemplate() catch block replace the hardcoded
fallback "Save failed" by extracting a meaningful message from the thrown value:
set opError.value to e instanceof Error ? e.message : String(e) and if that
results in an empty string fall back to a short default; update the catch in
saveTemplate (the block that currently assigns opError.value) to use this logic
so opError.value reflects the actual error content instead of the fixed string.
- Around line 33-34: In load() the catch block sets error.value to a hardcoded
"Failed to load"; instead capture and surface the actual thrown value by
replacing the fallback with a stringified version of the caught value (use e
instanceof Error ? e.message : String(e)) so error.value reflects the real
error; update the catch in load() where error.value is assigned to use this
stringification (or an i18n error key if your app has one) rather than the
hardcoded literal.
- Around line 92-93: Replace hardcoded error text in the catch blocks so they
use the i18n key instead of literal strings: when setting opError.value in
ApplicationTemplatesTab.vue (the catch handling around opError.value = ...), use
the translation key (e.g. t('common.add_failed') or this.$t('common.add_failed')
consistent with the component's i18n usage) rather than "Failed to add
question"; make the same replacement for the other catch at the similar spot
(previously flagged at lines 103-104) and ensure the component imports/uses the
existing i18n translator (useI18n or this.$t) so the key resolves correctly.

In `@web/frontend/src/views/guild/tabs/LanguageTab.vue`:
- Around line 29-30: In the catch block of the loadConfig flow (where
error.value is assigned), don't use the hardcoded fallback "Failed to load";
instead propagate a meaningful message from the caught value by converting the
unknown e to a string or extracting e.message when available so error.value
contains useful diagnostic text (e.g., use e instanceof Error ? e.message :
String(e) or similar) while keeping the existing error.value assignment in the
loadConfig catch scope.

In `@web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue`:
- Line 21: The component currently binds fetchRoles and roleName to the initial
guild via useGuildEntities(props.guildId) so when props.guildId changes those
helpers become stale; add a watcher on props.guildId (watch(() => props.guildId,
() => { void load(); void fetchRoles(); })) or otherwise re-initialize
useGuildEntities when props.guildId changes so that load(), fetchRoles(), and
roleName() are all synced to the new guild ID.

In `@web/frontend/src/views/guild/tabs/OperatorDashboardTab.vue`:
- Around line 31-32: The catch block for fetchHealth sets a hardcoded English
fallback ("Failed to fetch health data") on error.value; replace that literal
with a localized string using the component's i18n instance (e.g., call
useI18n() -> const { t } = useI18n() or use this.$t) and set error.value = e
instanceof Error ? e.message : t('operatorDashboard.fetchHealthFailed') (or
another appropriate key), updating the translation files with that key; target
the catch block that assigns error.value in OperatorDashboardTab.vue to
implement this change.

In `@web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue`:
- Around line 23-24: Replace the hardcoded fallback string in the catch block
inside OperatorGuildsTab.vue (the catch (e: unknown) { error.value = ... }
block) with the i18n key by using t("common.load_failed") instead of "Failed to
fetch guilds"; ensure the translation function t is in scope (import/obtain via
useI18n() in the component setup if not already) so error.value = e instanceof
Error ? e.message : t("common.load_failed") compiles and uses the localized
message.

In `@web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue`:
- Around line 48-49: The catch block assigning grantError.value uses a hardcoded
English fallback; replace the literal "Failed to revoke access" with the i18n
key t("common.delete_failed") so the error message follows the app's
translations (update the assignment in the revokePremium catch where
grantError.value is set to use t("common.delete_failed") while keeping the e
instanceof Error ? e.message branch intact).
- Around line 36-37: The catch in the grantPremium flow currently sets
grantError.value to a hardcoded English string; replace the fallback "Failed to
grant access" with a localized string (e.g. t("common.save_failed") or a new key
like t("guild.grant_failed")), and ensure the i18n translator (t) is available
in the scope of the grantPremium function (or call useI18n() to obtain t) so the
catch block sets grantError.value = e instanceof Error ? e.message :
t("common.save_failed").

In `@web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue`:
- Around line 166-197: The code emits hard-coded English fallback messages
("Validation failed", "Failed to create tracker") in submitAdd (and similarly in
load, saveEdit, confirmDelete), causing mixed-language UIs; replace those
literals with i18n calls (e.g. t("tabs.wow_guild_news.validation_failed") and
t("tabs.wow_guild_news.create_failed") or appropriate existing keys) so all
error branches use t(...), and add the corresponding translation keys to the
locale files; search for the functions submitAdd, load, saveEdit, and
confirmDelete to locate and update each fallback error assignment to use t(...)
instead of raw strings.

---

Duplicate comments:
In `@web/frontend/src/views/guild/tabs/RemindersTab.vue`:
- Around line 96-111: formatInterval and scheduleLabel currently inject raw,
hard-coded tokens (d/h/m/s and raw schedule_time) which aren't localized; update
formatInterval(seconds) to return localized unit strings via the i18n translator
(use keys like days/hours/minutes/seconds or the project's existing unit
translation keys) instead of "d/h/m/s", and in scheduleLabel ensure
schedule_time (and schedule_day_of_month if needed) are formatted through the
locale-aware formatter used elsewhere (or Intl.DateTimeFormat) before passing
into t(), keeping use of ReminderSchema, formatInterval, scheduleLabel and
DOW_LABELS to locate where to change. Ensure weekly/daily/monthly branches pass
pre-formatted, localized time strings (and localized unit strings from
formatInterval) into t() rather than raw values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 25fd5969-9d39-4851-8a6b-65a32d432c3c

📥 Commits

Reviewing files that changed from the base of the PR and between f992ebb and fd6cc4b.

📒 Files selected for processing (28)
  • web/frontend/src/components/LanguageSwitcher.vue
  • web/frontend/src/components/MockupToolbar.vue
  • web/frontend/src/i18n/index.ts
  • web/frontend/src/i18n/locales/de.ts
  • web/frontend/src/i18n/locales/en.ts
  • web/frontend/src/stores/locale.ts
  • web/frontend/src/utils/route.ts
  • web/frontend/src/views/LoginView.vue
  • web/frontend/src/views/guild/GuildDetailView.vue
  • web/frontend/src/views/guild/tabs/ApplicationFormsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue
  • web/frontend/src/views/guild/tabs/AutoDeleteTab.vue
  • web/frontend/src/views/guild/tabs/AutoKickerTab.vue
  • web/frontend/src/views/guild/tabs/LanguageTab.vue
  • web/frontend/src/views/guild/tabs/LeaveMessagesTab.vue
  • web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorDashboardTab.vue
  • web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue
  • web/frontend/src/views/guild/tabs/OperatorModulesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue
  • web/frontend/src/views/guild/tabs/ReactionRolesTab.vue
  • web/frontend/src/views/guild/tabs/RemindersTab.vue
  • web/frontend/src/views/guild/tabs/RoleMappingsTab.vue
  • web/frontend/src/views/guild/tabs/ServerOverviewTab.vue
  • web/frontend/src/views/guild/tabs/SupportTab.vue
  • web/frontend/src/views/guild/tabs/WowCraftingTab.vue
  • web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue

Comment thread web/frontend/src/components/MockupToolbar.vue Outdated
Comment thread web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue Outdated
Comment thread web/frontend/src/views/guild/tabs/RemindersTab.vue
Comment thread web/frontend/src/views/guild/tabs/WowCraftingTab.vue Outdated
@karaktaka karaktaka force-pushed the feat/dashboard-i18n branch 2 times, most recently from b2d9a67 to 382a685 Compare March 14, 2026 13:08
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue (1)

97-101: ⚠️ Potential issue | 🔴 Critical

guildId is undefined in template; use props.guildId.

Line 99 passes guildId to :guild-id, but the prop is not auto-exposed in the template. All script references use props.guildId; the template must do the same to avoid a runtime error.

Fix
          <DiscordPicker
            v-model="newRoleId"
-           :guild-id="guildId"
+           :guild-id="props.guildId"
            kind="role"
          />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue` around lines 97 -
101, The template is passing an undefined local name guildId to the
DiscordPicker; change the prop reference to use props.guildId instead. Locate
the DiscordPicker usage (component name DiscordPicker) and update its :guild-id
binding to use props.guildId (matching how the script accesses the prop) while
keeping v-model="newRoleId" and kind="role" unchanged so the component receives
the correct guild id.
web/frontend/src/views/guild/FormSubmissionsView.vue (1)

8-29: ⚠️ Potential issue | 🟠 Major

Partial i18n migration leaves this screen mixed-language.

You added i18n scaffolding here, but several user-facing strings are still hardcoded (for example Line 77, Line 80, Line 82, Line 90, Line 116, Line 139, Line 146, Line 158, Line 164, Line 173, Line 178, Line 179, Line 182, Line 193). In DE locale this view will remain partly English, which breaks the dashboard localization objective.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/FormSubmissionsView.vue` around lines 8 - 29,
The view has partial i18n: user-facing strings remaining hardcoded; update all
hardcoded labels/messages in this component (including options, button/text
labels, table headers, empty states and status labels) to use the existing
useI18n() t function and the STATUS_LABEL_KEYS mapping (or add new I18nKey
entries) so every displayed string calls t("...") with an appropriate key; add
new keys to the locale files for any new messages and replace occurrences in the
component (refer to useI18n, t, STATUS_LABEL_KEYS, selectedId, statusFilter and
any template bindings) to ensure consistent localization across all texts.
♻️ Duplicate comments (1)
web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue (1)

43-50: ⚠️ Potential issue | 🟠 Major

Filter by stable form identity, not mutable form_name.

At Line 44 and Line 49, the client-side filter re-couples results to form_name. Renames or duplicate form names can hide submissions that the form_id-scoped fetch already returned.

Proposed fix
-const filtered = computed(() => {
-  const targetFormName = formFilter.value !== null
-    ? (forms.value.find((f) => f.id === formFilter.value)?.name ?? null)
-    : null;
-  return submissions.value.filter((s) => {
-    if (statusFilter.value && s.status !== statusFilter.value) return false;
-    if (targetFormName !== null && s.form_name !== targetFormName) return false;
-    return true;
-  });
-});
+const filtered = computed(() =>
+  submissions.value.filter((s) => !statusFilter.value || s.status === statusFilter.value),
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue` around lines
43 - 50, The client-side computed "filtered" currently resolves targetFormName
from forms and filters submissions by s.form_name, which breaks when form names
change or duplicate; instead derive the stable ID (use formFilter.value or
resolve to the selected form's id) and filter by s.form_id (and keep the
statusFilter check). Update the computed referenced as filtered and variables
formFilter/forms/submissions/statusFilter so the second check reads equivalently
to: if (targetFormId !== null && s.form_id !== targetFormId) return false,
ensuring null handling and preserving the existing statusFilter logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@web/frontend/src/views/guild/FormSubmissionsView.vue`:
- Around line 8-29: The view has partial i18n: user-facing strings remaining
hardcoded; update all hardcoded labels/messages in this component (including
options, button/text labels, table headers, empty states and status labels) to
use the existing useI18n() t function and the STATUS_LABEL_KEYS mapping (or add
new I18nKey entries) so every displayed string calls t("...") with an
appropriate key; add new keys to the locale files for any new messages and
replace occurrences in the component (refer to useI18n, t, STATUS_LABEL_KEYS,
selectedId, statusFilter and any template bindings) to ensure consistent
localization across all texts.

In `@web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue`:
- Around line 97-101: The template is passing an undefined local name guildId to
the DiscordPicker; change the prop reference to use props.guildId instead.
Locate the DiscordPicker usage (component name DiscordPicker) and update its
:guild-id binding to use props.guildId (matching how the script accesses the
prop) while keeping v-model="newRoleId" and kind="role" unchanged so the
component receives the correct guild id.

---

Duplicate comments:
In `@web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue`:
- Around line 43-50: The client-side computed "filtered" currently resolves
targetFormName from forms and filters submissions by s.form_name, which breaks
when form names change or duplicate; instead derive the stable ID (use
formFilter.value or resolve to the selected form's id) and filter by s.form_id
(and keep the statusFilter check). Update the computed referenced as filtered
and variables formFilter/forms/submissions/statusFilter so the second check
reads equivalently to: if (targetFormId !== null && s.form_id !== targetFormId)
return false, ensuring null handling and preserving the existing statusFilter
logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 76afb614-e239-40d2-b8fa-3f990178d521

📥 Commits

Reviewing files that changed from the base of the PR and between fd6cc4b and b2d9a67.

📒 Files selected for processing (31)
  • web/frontend/src/components/LanguageSwitcher.vue
  • web/frontend/src/components/MockupToolbar.vue
  • web/frontend/src/composables/useAutoSave.ts
  • web/frontend/src/composables/useManualSave.ts
  • web/frontend/src/i18n/index.ts
  • web/frontend/src/i18n/locales/de.ts
  • web/frontend/src/i18n/locales/en.ts
  • web/frontend/src/stores/locale.ts
  • web/frontend/src/utils/route.ts
  • web/frontend/src/views/LoginView.vue
  • web/frontend/src/views/guild/FormSubmissionsView.vue
  • web/frontend/src/views/guild/GuildDetailView.vue
  • web/frontend/src/views/guild/tabs/ApplicationFormsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vue
  • web/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vue
  • web/frontend/src/views/guild/tabs/AutoDeleteTab.vue
  • web/frontend/src/views/guild/tabs/AutoKickerTab.vue
  • web/frontend/src/views/guild/tabs/LanguageTab.vue
  • web/frontend/src/views/guild/tabs/LeaveMessagesTab.vue
  • web/frontend/src/views/guild/tabs/ModeratorRolesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorDashboardTab.vue
  • web/frontend/src/views/guild/tabs/OperatorGuildsTab.vue
  • web/frontend/src/views/guild/tabs/OperatorModulesTab.vue
  • web/frontend/src/views/guild/tabs/OperatorUserManagementTab.vue
  • web/frontend/src/views/guild/tabs/ReactionRolesTab.vue
  • web/frontend/src/views/guild/tabs/RemindersTab.vue
  • web/frontend/src/views/guild/tabs/RoleMappingsTab.vue
  • web/frontend/src/views/guild/tabs/ServerOverviewTab.vue
  • web/frontend/src/views/guild/tabs/SupportTab.vue
  • web/frontend/src/views/guild/tabs/WowCraftingTab.vue
  • web/frontend/src/views/guild/tabs/WowGuildNewsTab.vue

@karaktaka karaktaka force-pushed the feat/dashboard-i18n branch 2 times, most recently from 30e1f42 to 4f17011 Compare March 14, 2026 13:26
Adds a custom i18n system to the Vue dashboard with full English and German translations across all 19 tab components, the login page, sidebar navigation, and shared components. Includes browser language auto-detection, localStorage-persisted locale preference, and a language switcher in the sidebar footer and login page.

- New: `src/i18n/` composable with dot-path `t(key, params?)`, fallback chain (locale → EN → raw key), and dev-mode missing-key warnings
- New: `src/stores/locale.ts` Pinia store with `persist: true` and browser language detection
- New: `src/components/LanguageSwitcher.vue` dropdown in sidebar footer and login
- Updated: all 19 tab components, `GuildDetailView`, `LoginView`, `MockupToolbar` to use `t()`
- ~500 translation keys each in EN and DE locale files

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant