feat(web): add dashboard i18n with EN/DE locales#328
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
2af1db8 to
f29f4da
Compare
There was a problem hiding this comment.
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 | 🟡 MinorLocalize 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 | 🟡 MinorLocalize 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 | 🟡 MinorOrder status display not using translations.
Line 321 displays
order.status.replace("_", " ")directly instead of using the translated labels. TheSTATUS_LABEL_KEYSare 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 | 🔵 TrivialHardcoded "Guild" fallback not translated.
Lines 229 and 300 use
"Guild"as a fallback whenguild.current?.nameis unavailable. Consider using a translation key liket("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 | 🟡 MinorStatus badge text is not translated.
Lines 180 and 209 display
sub.statusandselected.statusdirectly as raw strings ("pending", "approved", "denied") while the filter buttons uset(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_KEYSmapping 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
📒 Files selected for processing (27)
web/frontend/src/components/LanguageSwitcher.vueweb/frontend/src/components/MockupToolbar.vueweb/frontend/src/i18n/index.tsweb/frontend/src/i18n/locales/de.tsweb/frontend/src/i18n/locales/en.tsweb/frontend/src/stores/locale.tsweb/frontend/src/views/LoginView.vueweb/frontend/src/views/guild/GuildDetailView.vueweb/frontend/src/views/guild/tabs/ApplicationFormsTab.vueweb/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vueweb/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vueweb/frontend/src/views/guild/tabs/AutoDeleteTab.vueweb/frontend/src/views/guild/tabs/AutoKickerTab.vueweb/frontend/src/views/guild/tabs/LanguageTab.vueweb/frontend/src/views/guild/tabs/LeaveMessagesTab.vueweb/frontend/src/views/guild/tabs/ModeratorRolesTab.vueweb/frontend/src/views/guild/tabs/OperatorDashboardTab.vueweb/frontend/src/views/guild/tabs/OperatorGuildsTab.vueweb/frontend/src/views/guild/tabs/OperatorModulesTab.vueweb/frontend/src/views/guild/tabs/OperatorUserManagementTab.vueweb/frontend/src/views/guild/tabs/ReactionRolesTab.vueweb/frontend/src/views/guild/tabs/RemindersTab.vueweb/frontend/src/views/guild/tabs/RoleMappingsTab.vueweb/frontend/src/views/guild/tabs/ServerOverviewTab.vueweb/frontend/src/views/guild/tabs/SupportTab.vueweb/frontend/src/views/guild/tabs/WowCraftingTab.vueweb/frontend/src/views/guild/tabs/WowGuildNewsTab.vue
f29f4da to
53d5ba7
Compare
There was a problem hiding this comment.
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 | 🟡 MinorHandle
route.query.erroras a potential array.Lines 14–15 compare
route.query.errordirectly against strings, but Vue Router can provide array values for repeated query parameters. Whenroute.query.erroris 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 | 🟡 MinorLocalize 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_placeholderin bothweb/frontend/src/i18n/locales/en.tsandweb/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 | 🟠 MajorNormalize
route.query.tabinstead of casting it tostring.Line 41 uses
route.query.tab as string, which bypasses type safety. Query values arestring | 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
📒 Files selected for processing (27)
web/frontend/src/components/LanguageSwitcher.vueweb/frontend/src/components/MockupToolbar.vueweb/frontend/src/i18n/index.tsweb/frontend/src/i18n/locales/de.tsweb/frontend/src/i18n/locales/en.tsweb/frontend/src/stores/locale.tsweb/frontend/src/views/LoginView.vueweb/frontend/src/views/guild/GuildDetailView.vueweb/frontend/src/views/guild/tabs/ApplicationFormsTab.vueweb/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vueweb/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vueweb/frontend/src/views/guild/tabs/AutoDeleteTab.vueweb/frontend/src/views/guild/tabs/AutoKickerTab.vueweb/frontend/src/views/guild/tabs/LanguageTab.vueweb/frontend/src/views/guild/tabs/LeaveMessagesTab.vueweb/frontend/src/views/guild/tabs/ModeratorRolesTab.vueweb/frontend/src/views/guild/tabs/OperatorDashboardTab.vueweb/frontend/src/views/guild/tabs/OperatorGuildsTab.vueweb/frontend/src/views/guild/tabs/OperatorModulesTab.vueweb/frontend/src/views/guild/tabs/OperatorUserManagementTab.vueweb/frontend/src/views/guild/tabs/ReactionRolesTab.vueweb/frontend/src/views/guild/tabs/RemindersTab.vueweb/frontend/src/views/guild/tabs/RoleMappingsTab.vueweb/frontend/src/views/guild/tabs/ServerOverviewTab.vueweb/frontend/src/views/guild/tabs/SupportTab.vueweb/frontend/src/views/guild/tabs/WowCraftingTab.vueweb/frontend/src/views/guild/tabs/WowGuildNewsTab.vue
53d5ba7 to
f992ebb
Compare
There was a problem hiding this comment.
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 | 🟡 MinorLocalize 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_placeholderin bothweb/frontend/src/i18n/locales/en.tsandweb/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 | 🟡 MinorRender 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 throughPROFESSIONSand keepm.profession_nameonly 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 | 🟡 MinorTranslate the fallback load error too.
The
"Failed to load"branch is still hardcoded English, so non-Errorrejections will show mixed-language UI in this tab. Put that default behindt(...)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 | 🟠 MajorValidate
formIdbefore using as a number.
Number(...)on a query string that doesn't represent a valid number producesNaN, which gets passed to the API call. Additionally, the watcher doesn't reset the filter whenformIdis 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 | 🟡 MinorTranslate the generic submit error.
"Failed to send message"bypasses the locale layer on non-Errorrejections, 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
📒 Files selected for processing (28)
web/frontend/src/components/LanguageSwitcher.vueweb/frontend/src/components/MockupToolbar.vueweb/frontend/src/i18n/index.tsweb/frontend/src/i18n/locales/de.tsweb/frontend/src/i18n/locales/en.tsweb/frontend/src/stores/locale.tsweb/frontend/src/utils/route.tsweb/frontend/src/views/LoginView.vueweb/frontend/src/views/guild/GuildDetailView.vueweb/frontend/src/views/guild/tabs/ApplicationFormsTab.vueweb/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vueweb/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vueweb/frontend/src/views/guild/tabs/AutoDeleteTab.vueweb/frontend/src/views/guild/tabs/AutoKickerTab.vueweb/frontend/src/views/guild/tabs/LanguageTab.vueweb/frontend/src/views/guild/tabs/LeaveMessagesTab.vueweb/frontend/src/views/guild/tabs/ModeratorRolesTab.vueweb/frontend/src/views/guild/tabs/OperatorDashboardTab.vueweb/frontend/src/views/guild/tabs/OperatorGuildsTab.vueweb/frontend/src/views/guild/tabs/OperatorModulesTab.vueweb/frontend/src/views/guild/tabs/OperatorUserManagementTab.vueweb/frontend/src/views/guild/tabs/ReactionRolesTab.vueweb/frontend/src/views/guild/tabs/RemindersTab.vueweb/frontend/src/views/guild/tabs/RoleMappingsTab.vueweb/frontend/src/views/guild/tabs/ServerOverviewTab.vueweb/frontend/src/views/guild/tabs/SupportTab.vueweb/frontend/src/views/guild/tabs/WowCraftingTab.vueweb/frontend/src/views/guild/tabs/WowGuildNewsTab.vue
f992ebb to
fd6cc4b
Compare
There was a problem hiding this comment.
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 | 🟡 MinorFinish localizing the remaining fallback errors.
This flow now translates
required_fields,guild_not_found, andverify_warning, but the nearby fallback branches still emit English (Validation failed,Failed to create tracker).load(),saveEdit(), andconfirmDelete()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 | 🟠 MajorWatch
props.guildIdand 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.guildIdupdates reactively, butfetchRolesandroleNameremain bound to the initial guild ID because they're destructured fromuseGuildEntities(props.guildId)in script setup. This causesload()to fetch from the new guild whileroleName()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-calluseGuildEntitiesreactively) 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 | 🟡 MinorHardcoded 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 | 🟡 MinorHardcoded 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 | 🟡 MinorHardcoded 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 | 🟡 MinorHardcoded fallbacks in question operations.
Consider adding
common.add_failedor 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 | 🟡 MinorHardcoded 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 | 🟡 MinorHardcoded 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 | 🟡 MinorHardcoded 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 | 🟡 MinorHardcoded 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 | 🟡 MinorHardcoded English fallback in catch block.
The error fallback
"Failed to fetch guilds"should uset("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 | 🟡 MinorLocalize the remaining schedule tokens.
next_fireis locale-aware now, butformatInterval()still emits hard-codedd/h/m/s, and the daily/weekly/monthly branches still inject rawschedule_timeintot(). 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
📒 Files selected for processing (28)
web/frontend/src/components/LanguageSwitcher.vueweb/frontend/src/components/MockupToolbar.vueweb/frontend/src/i18n/index.tsweb/frontend/src/i18n/locales/de.tsweb/frontend/src/i18n/locales/en.tsweb/frontend/src/stores/locale.tsweb/frontend/src/utils/route.tsweb/frontend/src/views/LoginView.vueweb/frontend/src/views/guild/GuildDetailView.vueweb/frontend/src/views/guild/tabs/ApplicationFormsTab.vueweb/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vueweb/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vueweb/frontend/src/views/guild/tabs/AutoDeleteTab.vueweb/frontend/src/views/guild/tabs/AutoKickerTab.vueweb/frontend/src/views/guild/tabs/LanguageTab.vueweb/frontend/src/views/guild/tabs/LeaveMessagesTab.vueweb/frontend/src/views/guild/tabs/ModeratorRolesTab.vueweb/frontend/src/views/guild/tabs/OperatorDashboardTab.vueweb/frontend/src/views/guild/tabs/OperatorGuildsTab.vueweb/frontend/src/views/guild/tabs/OperatorModulesTab.vueweb/frontend/src/views/guild/tabs/OperatorUserManagementTab.vueweb/frontend/src/views/guild/tabs/ReactionRolesTab.vueweb/frontend/src/views/guild/tabs/RemindersTab.vueweb/frontend/src/views/guild/tabs/RoleMappingsTab.vueweb/frontend/src/views/guild/tabs/ServerOverviewTab.vueweb/frontend/src/views/guild/tabs/SupportTab.vueweb/frontend/src/views/guild/tabs/WowCraftingTab.vueweb/frontend/src/views/guild/tabs/WowGuildNewsTab.vue
b2d9a67 to
382a685
Compare
There was a problem hiding this comment.
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
guildIdis undefined in template; useprops.guildId.Line 99 passes
guildIdto:guild-id, but the prop is not auto-exposed in the template. All script references useprops.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 | 🟠 MajorPartial 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 | 🟠 MajorFilter 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 theform_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
📒 Files selected for processing (31)
web/frontend/src/components/LanguageSwitcher.vueweb/frontend/src/components/MockupToolbar.vueweb/frontend/src/composables/useAutoSave.tsweb/frontend/src/composables/useManualSave.tsweb/frontend/src/i18n/index.tsweb/frontend/src/i18n/locales/de.tsweb/frontend/src/i18n/locales/en.tsweb/frontend/src/stores/locale.tsweb/frontend/src/utils/route.tsweb/frontend/src/views/LoginView.vueweb/frontend/src/views/guild/FormSubmissionsView.vueweb/frontend/src/views/guild/GuildDetailView.vueweb/frontend/src/views/guild/tabs/ApplicationFormsTab.vueweb/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vueweb/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vueweb/frontend/src/views/guild/tabs/AutoDeleteTab.vueweb/frontend/src/views/guild/tabs/AutoKickerTab.vueweb/frontend/src/views/guild/tabs/LanguageTab.vueweb/frontend/src/views/guild/tabs/LeaveMessagesTab.vueweb/frontend/src/views/guild/tabs/ModeratorRolesTab.vueweb/frontend/src/views/guild/tabs/OperatorDashboardTab.vueweb/frontend/src/views/guild/tabs/OperatorGuildsTab.vueweb/frontend/src/views/guild/tabs/OperatorModulesTab.vueweb/frontend/src/views/guild/tabs/OperatorUserManagementTab.vueweb/frontend/src/views/guild/tabs/ReactionRolesTab.vueweb/frontend/src/views/guild/tabs/RemindersTab.vueweb/frontend/src/views/guild/tabs/RoleMappingsTab.vueweb/frontend/src/views/guild/tabs/ServerOverviewTab.vueweb/frontend/src/views/guild/tabs/SupportTab.vueweb/frontend/src/views/guild/tabs/WowCraftingTab.vueweb/frontend/src/views/guild/tabs/WowGuildNewsTab.vue
30e1f42 to
4f17011
Compare
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>
4f17011 to
5557db0
Compare
Summary
useI18n()composable backed by a Pinia locale storet()calls — TypeScript enforces key correctness at build time via a recursiveFlatKeys<T>typelocalStoragepersistence viapinia-plugin-persistedstateKey implementation details
src/i18n/index.ts— module-level singleton composable; EN→locale fallback chain;{param}interpolation; dev-only missing-key warnings deduped viaSetsrc/stores/locale.ts— Pinia store withpersist: true; setsdocument.documentElement.langon changesrc/utils/route.ts— extractedtoQueryScalar()utility shared across viewsuseAutoSave/useManualSavecomposables updated to uset("common.save_failed")for fallback error messagesApplicationSubmissionsTab: form filter now uses server-side scoped fetch viaapplyFormFilter(); client-sidefilteredcomputed simplified to status-only filter (deadtargetFormNamelookup removed)Test plan
npm run buildinweb/frontend/🤖 Generated with Claude Code