Skip to content

feat: translations now use crowdin (translate.unraid.net)#1739

Merged
elibosley merged 30 commits intomainfrom
feat/i18n
Oct 13, 2025
Merged

feat: translations now use crowdin (translate.unraid.net)#1739
elibosley merged 30 commits intomainfrom
feat/i18n

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Oct 3, 2025

Summary by CodeRabbit

  • New Features

    • App-wide internationalization: dynamic locale loading, LocaleSwitcher UI, many locale bundles added, and JSONForms i18n support.
    • CLI helpers: i18n:extract and sort scripts for extracting/syncing translation keys.
  • Accessibility

    • Brand button now supports keyboard activation (Enter/Space).
  • Documentation

    • Internationalization sections added to API and Web READMEs.
  • Refactor

    • Unified i18n loader and migration to composition-API i18n usage across UI.
  • Tests

    • Test utilities updated to provide i18n and localized assertions.

@claude
Copy link

claude bot commented Oct 3, 2025

Claude encountered an error —— View job


I'll analyze this and get back to you.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 3, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds end-to-end internationalization: locale bundles and many new locale files, extraction and sorting scripts, a web i18n-loader with window.LOCALE, JsonForms i18n adapter, migration of many components/tests to vue‑i18n, removal of legacy PHP translations, and type/schema changes to carry i18n metadata.

Changes

Cohort / File(s) Summary
Tooling & config
crowdin.yml, package.json, api/package.json, web/package.json
Adds Crowdin mapping and i18n scripts (i18n:extract, i18n:sort) and related dev deps; root/script wiring to run extraction across packages.
Extraction & sorting scripts
web/scripts/extract-translations.mjs, web/scripts/sort-translations.mjs, api/scripts/extract-translations.mjs
New Node scripts to extract translation keys from sources (AST/SFC), reconcile locale JSONs, merge new keys into en.json, and deterministically sort locale files.
Web i18n runtime loader
web/src/helpers/i18n-loader.ts, web/src/helpers/globals.d.ts
New i18n loader: DEFAULT_LOCALE, locale normalization/resolution, lazy loaders, createI18nInstance, ensureLocale, getWindowLocale; adds window.LOCALE typing.
JsonForms i18n adapter
web/src/helpers/jsonforms-i18n.ts, components using JsonForms (e.g. web/src/components/ApiKey/ApiKeyCreate.vue, .../RCloneConfig.vue)
Adds useJsonFormsI18n helper and wires JsonForms i18n into form-rendering components.
Locale data (web)
web/src/locales/en.json, web/src/locales/{ar,bn,ca,cs,da,de,es,fr,hi,hr,hu,it,...}.json, removed web/src/locales/en_US.json, removed _template.json
Introduces canonical en.json and many translated locale bundles; removes legacy en_US.json and _template.json.
Locale data (api)
api/src/i18n/en.json, api/src/i18n/{ar,ca,cs,da,de,es,fr,hu,it,ja,ko,nl,no,pl,pt,ro,ru,sv,bn,hi,hr,lv,uk,zh}.json
Adds en.json and many empty/placeholder locale files for the API.
UI component i18n migration
web/src/components/**/* (Activation, Logs, Notifications, Registration, UpdateOs, UserProfile, sso, Modal.vue, BrandButton.vue, KeyActions.vue, etc.)
Replaces literal strings/old keys with namespaced i18n keys, adopts useI18n and useJsonFormsI18n, removes prop-passed t in many components, and small UX tweaks (keyboard handling in BrandButton).
Mounting & registry changes
web/src/components/Wrapper/mount-engine.ts, web/src/components/Wrapper/auto-mount.ts, web/src/components/Wrapper/component-registry.ts, web/components.d.ts, web/public/test-pages/all-components.html, web/src/components/LocaleSwitcher.vue, web/src/components/DevSettings.vue
Shifts i18n init to loader-driven async flow (mount functions now async), adds error handling for auto-mount, registers LocaleSwitcher component and test-page usage, and exposes initializeGlobalDependencies.
Form utils & types (API)
api/src/unraid-api/graph/utils/form-utils.ts, api/src/unraid-api/types/json-forms.ts, api/src/types/jsonforms-i18n.d.ts
Adds optional i18nKey params to helpers, extends schema types with optional i18n (I18nJsonSchema), and augments JsonSchema typings to include i18n?: string.
API UI i18n wiring
api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts, .../settings/settings.service.ts, .../sso/core/oidc-config.service.ts
Introduces centralized i18n key constants and attaches i18n metadata to API UI schemas; adds ApiKeyFormData interface.
Tests & test utilities
web/__test__/utils/i18n.ts, many web/__test__/**/*.test.ts
Adds createTestI18n and testTranslate helpers; updates tests to use plugin-based i18n, adjusts mocks, and marks/skips some legacy tests.
Legacy PHP translation removal & plugin tweaks
plugin/source/.../include/translations.php, plugin/source/.../include/myservers2.php, plugin/plugins/dynamix.unraid.net.plg
Removes legacy WebComponentTranslations PHP code, stops backing/restoring translations.php, and replaces LOCALE_DATA injection with window.LOCALE usage.
Misc: linters, globals, exports
api/.eslintrc.ts, web/src/components/Wrapper/mount-engine.ts, web/components.d.ts
ESLint updated to accept .mjs/.cjs; global Window typing adjusted (LOCALE); component registry global type adds LocaleSwitcher.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Browser
  participant App as Web App
  participant Loader as i18n-loader
  participant I18n as Vue I18n
  participant Window as window

  Browser->>App: load page
  App->>Loader: createI18nInstance()
  Loader-->>App: i18n instance (DEFAULT)
  App->>Loader: getWindowLocale()
  Loader->>Window: read LOCALE
  Window-->>Loader: "fr_FR" or undefined
  App->>Loader: ensureLocale(i18n, requested?)
  alt requested supported
    Loader->>I18n: load & register messages, set locale
  else fail or unsupported
    Loader->>I18n: keep DEFAULT locale
  end
  App-->>Browser: render UI with active locale
Loading
sequenceDiagram
  autonumber
  actor Dev
  participant Script as extract-translations
  participant FS as File System
  participant Parser as AST/SFC Parser

  Dev->>Script: run i18n:extract
  Script->>FS: glob source files (*.ts,*.js,*.vue)
  Script->>Parser: parse & collect keys (t(), i18n.*, jsonforms.*)
  Script->>FS: read `en.json`
  Script->>FS: merge discovered keys, write `en.json` if changed
  loop per non-en locale
    Script->>FS: read locale.json
    Script->>FS: add missing keys (from en or null) and write if changed
  end
  Script-->>Dev: report added/failed keys
Loading
sequenceDiagram
  autonumber
  participant API as API code
  participant FormUtil as form-utils
  participant JsonForms as JsonForms schema

  API->>FormUtil: createLabeledControl(label, control, i18nKey?)
  alt i18nKey provided
    FormUtil->>JsonForms: emit LabelElement with i18n
    FormUtil->>JsonForms: emit ControlElement with i18nKey
  else
    FormUtil->>JsonForms: emit elements without i18n metadata
  end
  JsonForms-->>API: schema annotated (optional i18n)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Poem

A hop, a nibble, keys unfurled,
Locales sprout across the world.
Scripts hum, loaders fetch and bind,
Forms find words, old PHP resigned.
A rabbit cheers — translations hop! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately captures the introduction of Crowdin for translation management, which is a significant component of this changeset, even though the PR also encompasses broader internationalization enhancements across the codebase.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 636586e and ab2e3e5.

📒 Files selected for processing (45)
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (38 hunks)
  • web/__test__/components/ChangelogModal.test.ts (3 hunks)
  • web/__test__/components/CheckUpdateResponseModal.test.ts (3 hunks)
  • web/__test__/components/Modal.test.ts (10 hunks)
  • web/__test__/components/Modals.test.ts (2 hunks)
  • web/scripts/sort-translations.mjs (1 hunks)
  • web/src/components/DowngradeOs.standalone.vue (2 hunks)
  • web/src/components/HeaderOsVersion.standalone.vue (7 hunks)
  • web/src/components/Modal.vue (4 hunks)
  • web/src/components/Modals.standalone.vue (1 hunks)
  • web/src/components/Registration.standalone.vue (11 hunks)
  • web/src/components/Registration/KeyLinkedStatus.vue (3 hunks)
  • web/src/components/Registration/ReplaceCheck.vue (2 hunks)
  • web/src/components/Registration/UpdateExpiration.vue (2 hunks)
  • web/src/components/Registration/UpdateExpirationAction.vue (4 hunks)
  • web/src/components/ReleaseNotesModal.vue (0 hunks)
  • web/src/components/UpdateOs.standalone.vue (2 hunks)
  • web/src/components/UpdateOs/CallbackButton.vue (2 hunks)
  • web/src/components/UpdateOs/ChangelogModal.vue (5 hunks)
  • web/src/components/UpdateOs/CheckUpdateResponseModal.vue (18 hunks)
  • web/src/components/UpdateOs/Downgrade.vue (6 hunks)
  • web/src/components/UpdateOs/IgnoredRelease.vue (2 hunks)
  • web/src/components/UpdateOs/RawChangelogRenderer.vue (4 hunks)
  • web/src/components/UpdateOs/Status.vue (13 hunks)
  • web/src/components/UpdateOs/TestUpdateModal.standalone.vue (0 hunks)
  • web/src/components/UpdateOs/ThirdPartyDrivers.vue (2 hunks)
  • web/src/components/UpdateOs/Update.vue (9 hunks)
  • web/src/components/UpdateOs/UpdateIneligible.vue (6 hunks)
  • web/src/components/UserProfile/CallbackFeedback.vue (12 hunks)
  • web/src/components/UserProfile/DropdownConnectStatus.vue (3 hunks)
  • web/src/components/UserProfile/DropdownContent.vue (9 hunks)
  • web/src/components/UserProfile/DropdownError.vue (2 hunks)
  • web/src/components/UserProfile/DropdownItem.vue (1 hunks)
  • web/src/components/UserProfile/DropdownLaunchpad.vue (3 hunks)
  • web/src/components/UserProfile/Trial.vue (3 hunks)
  • web/src/locales/ar.json (1 hunks)
  • web/src/locales/bn.json (1 hunks)
  • web/src/locales/ca.json (1 hunks)
  • web/src/locales/cs.json (1 hunks)
  • web/src/locales/da.json (1 hunks)
  • web/src/locales/de.json (1 hunks)
  • web/src/locales/es.json (1 hunks)
  • web/src/locales/fr.json (1 hunks)
  • web/src/locales/hi.json (1 hunks)
  • web/src/locales/hr.json (1 hunks)

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

@github-actions
Copy link
Contributor

github-actions bot commented Oct 3, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

Comment on lines 149 to 155

<div class="prose text-base leading-relaxed whitespace-normal opacity-75">
<p>
{{
t(
'Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes – keeping your server up-to-date ensures the best experience that Unraid has to offer.'
)
}}
{{ t('updateOs.update.receiveTheLatestAndGreatestFor') }}
</p>
<p v-if="available">
{{ flashBackupCopy }}

Choose a reason for hiding this comment

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

P1 Badge Provide translation function to template

The template calls t('updateOs.update.receiveTheLatestAndGreatestFor'), but t is never defined in <script setup>. Here defineProps is assigned to props, so Vue does not auto-expose individual props to the template. At runtime this will throw ReferenceError: t is not defined, breaking the Update panel. Either destructure the prop (const { t } = defineProps…) or reference props.t from the template.

Useful? React with 👍 / 👎.

Comment on lines 63 to 83
<ArrowUturnDownIcon class="w-5 shrink-0" />
<span class="inline-flex flex-wrap items-baseline justify-start gap-2 leading-none">
<span class="text-xl">
{{ t('Downgrade Unraid OS to {0}', [version]) }}
{{ t('updateOs.downgrade.downgradeUnraidOsTo', [version]) }}
</span>
<span
v-if="releaseDate && formattedReleaseDate !== 'Invalid Date'"
class="shrink text-base opacity-75"
>
{{ t('Original release date {0}', [formattedReleaseDate]) }}
{{ t('updateOs.downgrade.originalReleaseDate', [formattedReleaseDate]) }}
</span>
</span>
</h3>
<div class="prose text-base leading-relaxed whitespace-normal opacity-75">
<p>{{ t(`Downgrades are only recommended if you're unable to solve a critical issue.`) }}</p>
<p>{{ t('updateOs.downgrade.downgradesAreOnlyRecommendedIfYou') }}</p>
<p>
{{
t(
'In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.'
)
}}
{{ t('updateOs.downgrade.inTheRareEventYouNeed') }}
</p>
<p>
{{
t(
'Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.'
)
}}
{{ t('updateOs.downgrade.downloadTheDiagnosticsZipThenPlease') }}
</p>

Choose a reason for hiding this comment

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

P1 Badge Destructure t before using in downgrade template

The Downgrade view also assigns defineProps to props but the template invokes t('updateOs.downgrade…') directly. Without a const { t } = props or useI18n, t is undefined and the component will fail to render whenever the downgrade UI is shown. Expose the prop before using it or reference props.t in the template.

Useful? React with 👍 / 👎.

Comment on lines 113 to 117
:icon="EyeIcon"
:icon-right="ArrowTopRightOnSquareIcon"
:name="updateButton?.name"
:text="t('View Changelog')"
:text="t('updateOs.updateIneligible.viewChangelog')"
class="flex-none"

Choose a reason for hiding this comment

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

P1 Badge Fix undefined t in update-ineligible CTA

Similar to the other Update components, UpdateIneligible.vue stores the props object in props but references t('updateOs.updateIneligible…') directly in the template. Vue will not inject t automatically in this case, so these bindings will throw when the component renders. Destructure t from props or use props.t to avoid the runtime error.

Useful? React with 👍 / 👎.

Comment on lines 188 to 197
>
{{
available
? t('Unraid {0} Available', [available])
? t('updateOs.status.unraidAvailable', [available])
: availableWithRenewal
? t('Up-to-date with eligible releases')
: t('Up-to-date')
? t('updateOs.status.upToDateWithEligibleReleases')
: t('updateOs.status.upToDate')
}}
</Badge>
<Badge

Choose a reason for hiding this comment

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

P1 Badge Ensure t is defined before using in status badges

The status component references t('updateOs.status…') throughout the template but only defines const props = defineProps… in script. Because the prop isn’t destructured, t isn’t available in the template scope and rendering this component raises a t is not defined error. Extract t from props or rewrite template references to props.t.

Useful? React with 👍 / 👎.

Comment on lines 150 to 156
>
<ResponsiveModalHeader>
<ResponsiveModalTitle>
{{ t('Unraid OS {0} Changelog', [currentRelease.version]) }}
{{ t('updateOs.changelogModal.unraidOsChangelog', [currentRelease.version]) }}
</ResponsiveModalTitle>
</ResponsiveModalHeader>

Choose a reason for hiding this comment

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

P1 Badge Changelog modal template uses undefined t

ChangelogModal.vue sets const props = defineProps but the modal title uses {{ t('updateOs.changelogModal…') }}. Because t is only available on props, this expression will crash when opening the changelog modal. Destructure t (const { t } = props) or switch template bindings to props.t.

Useful? React with 👍 / 👎.

Comment on lines 406 to 410
<TooltipTrigger as-child>
<Button variant="ghost" @click="accountStore.updateOs()">
<ArrowTopRightOnSquareIcon class="mr-2 h-4 w-4" />
{{ t('More Options') }}
{{ t('updateOs.checkUpdateResponseModal.moreOptions') }}
</Button>

Choose a reason for hiding this comment

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

P1 Badge Check-update modal references t without exposing it

CheckUpdateResponseModal.vue expects t via props but binds {{ t('updateOs.checkUpdateResponseModal…') }} in the template. Since the script assigns the props to const props and never destructures the translation function, Vue will throw when the modal renders. Destructure the prop or replace usages with props.t(...).

Useful? React with 👍 / 👎.

Copy link
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: 12

Caution

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

⚠️ Outside diff range comments (10)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php (6)

431-435: Critical: json_encode flags misused; UNESCAPED_UNICODE not applied

The third argument to json_encode is depth, not an additional flag. Unicode will be escaped unintentionally. OR the flags in the second arg and omit the third arg.

Apply this diff:

-        if ($urlEncode) {
-            return rawurlencode(json_encode($this->getTranslations(), JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE));
-        }
-        return json_encode($this->getTranslations(), JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE);
+        if ($urlEncode) {
+            return rawurlencode(json_encode($this->getTranslations(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
+        }
+        return json_encode($this->getTranslations(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

78-78: Stray '>' in HTML output

There’s an extra '>' inside the closing list item; results in a visible stray character in UI.

Apply this diff:

-            '... 5 minutes') . '</li><li>' . _('Please make sure there is a DNS server specified') . '</li>>' . '</ul>',
+            '... 5 minutes') . '</li><li>' . _('Please make sure there is a DNS server specified') . '</li>' . '</ul>',

200-200: Typo in key: "It\s" → "It's"

Backslash-s appears instead of an apostrophe in the key; inconsistent with the value and likely with frontend keys.

Apply this diff:

-            'It\s highly recommended to review the changelog before continuing your update.' => _('It\'s highly recommended to review the changelog before continuing your update.'),
+            'It\'s highly recommended to review the changelog before continuing your update.' => _('It\'s highly recommended to review the changelog before continuing your update.'),

219-219: Mismatch: "Unraidnet" vs "Unraid.net"

Dot missing in the translated string.

Apply this diff:

-            'Linked to Unraid.net account' => _('Linked to Unraidnet account'),
+            'Linked to Unraid.net account' => _('Linked to Unraid.net account'),

82-82: Spelling: "Acklowledge" → "Acknowledge"

User-facing typo.

Apply this diff:

-            'Acklowledge that you have made a Flash Backup to enable this action' => _('Acklowledge that you have made a Flash Backup to enable this action'),
+            'Acknowledge that you have made a Flash Backup to enable this action' => _('Acknowledge that you have made a Flash Backup to enable this action'),

273-273: Grammar: "Whether it new features" → "Whether it's new features"

Minor copy fix.

Apply this diff:

-            'Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes – keeping your server up-to-date ensures the best experience that Unraid has to offer.' => _('Receive the latest and greatest for Unraid OS.') . ' ' . _('Whether it new features, security patches, or bug fixes – keeping your server up-to-date ensures the best experience that Unraid has to offer.'),
+            'Receive the latest and greatest for Unraid OS. Whether it\'s new features, security patches, or bug fixes — keeping your server up-to-date ensures the best experience that Unraid has to offer.' => _('Receive the latest and greatest for Unraid OS.') . ' ' . _('Whether it\'s new features, security patches, or bug fixes — keeping your server up-to-date ensures the best experience that Unraid has to offer.'),
web/src/components/Activation/WelcomeModal.standalone.vue (1)

40-44: SSR safety: guard window access

Accessing window during setup can crash on SSR. Guard it.

-const isLoginPage = computed(() => window.location.pathname.includes('login'));
-// Initialize showModal based on conditions
-const showModal = ref(isLoginPage.value || isInitialSetup.value);
+const isClient = typeof window !== 'undefined';
+const isLoginPage = computed(() => (isClient ? window.location.pathname.includes('login') : false));
+// Initialize showModal based on conditions
+const showModal = ref((isLoginPage.value) || isInitialSetup.value);
api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts (1)

148-176: Fix CI: enum must be string[]; Resource[] breaks JsonSchema typing

TypeScript fails because enum expects string[] but getAvailableResources() returns Resource[].

Apply this fix to return string[] safely and satisfy JsonSchema types:

@@
-    private getAvailableResources(): Resource[] {
-        return Object.values(Resource);
-    }
+    // Ensure JSON Schema enums are string arrays
+    private getAvailableResources(): string[] {
+        return Object.values(Resource).filter((v): v is string => typeof v === 'string');
+    }

No changes needed where it’s used; the schema already declares items.type = 'string'.

Optionally do the same for getAvailableRoles() for consistency.

web/src/components/UpdateOs/CheckUpdateResponseModal.vue (1)

361-373: Avoid double-translation in extraLinks.

extraLinks sets text to a translated string, but the template calls t(item.text) again.

-              :text="t(item.text ?? '')"
-              :title="item.title ? t(item.title) : undefined"
+              :text="item.text ?? ''"
+              :title="item.title ? item.title : undefined"

Alternatively, store keys in extraLinks and translate in the template consistently.

web/src/components/Logs/SingleLogViewer.vue (1)

403-407: Fix canLoadMore predicate: use computed value

Passing the computed itself returns a truthy ref, not a boolean. Use the ref’s value.

-        { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore },
+        { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore.value },
🧹 Nitpick comments (25)
web/src/components/sso/useSsoAuth.ts (1)

2-2: Add .js extension to import for ESM compatibility.

The import from vue-i18n lacks the .js extension required by the coding guidelines for TypeScript ESM compatibility.

As per coding guidelines, apply this diff:

-import { useI18n } from 'vue-i18n';
+import { useI18n } from 'vue-i18n.js';
web/src/components/Notifications/Item.vue (1)

125-125: Error message placement may not match expected behavior.

The learning from PR #941 specifies that when delete or archive mutations fail, the error message should be shown below the action buttons. Currently, the error is displayed on line 125, which appears above the buttons div that starts on line 127.

Consider moving the error display to after the closing </div> tag of the buttons section to match the expected behavior.

Based on learnings.

Apply this diff to move the error below the action buttons:

-    <p v-if="mutationError" class="text-red-600">{{ t('common.error') }}: {{ mutationError }}</p>
-
     <div class="flex items-baseline justify-end gap-4">
       <a
         v-if="link"
@@ -148,6 +146,8 @@
         <span class="text-sm">{{ t('notifications.item.delete') }}</span>
       </Button>
     </div>
+
+    <p v-if="mutationError" class="text-red-600">{{ t('common.error') }}: {{ mutationError }}</p>
   </div>
 </template>
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php (1)

52-52: Use positional specifiers in translatable strings: %1$s, %2$s

To support languages that reorder placeholders, prefer %1$s, %2$s, etc., in translatable strings instead of bare %s or width-based %1s/%2s.

Example:

-            '{0} {1} Key…' => sprintf(_('%1s %2s Key…'), '{0}', '{1}'),
+            '{0} {1} Key…' => sprintf(_('%1$s %2$s Key…'), '{0}', '{1}'),
-            '{1} Key {0} Successfully' => sprintf(_('%2s Key %1s Successfully'), '{0}', '{1}'),
+            '{1} Key {0} Successfully' => sprintf(_('%2$s Key %1$s Successfully'), '{0}', '{1}'),

Also applies to: 60-60

api/src/i18n/ar.json (1)

1-1: Confirm BCP‑47 code choice if variants needed.

Using generic "ar" is fine; if region-specific (e.g., ar-SA, ar-EG) are planned, document fallback/aliasing.

api/src/i18n/de.json (1)

1-1: LGTM: locale scaffold.

No issues. Keep consistent formatting (minified JSON) across locales for smaller diffs.

api/src/i18n/ca.json (1)

1-1: LGTM: locale scaffold.

All good. If you plan key ordering, ensure the sorter (if any) is applied on API locales too, not only web.

web/src/locales/hu.json (1)

1-603: LGTM! Comprehensive Hungarian locale addition.

The locale file provides extensive translation coverage across all application namespaces. The use of placeholders ({0}, {n}) and HTML markup in translations is standard practice for i18n frameworks like vue-i18n.

Optional: Consider HTML-free translations where possible.

Some translations embed HTML markup (e.g., lines 304, 349, 371), which can complicate maintenance if the markup structure needs to change across all locales. Where feasible, consider using component-level markup with translation keys for text content only. This is not blocking and can be addressed incrementally.

web/src/components/DownloadApiLogs.standalone.vue (1)

21-23: Consider consolidating multiple t() calls in paragraph.

The paragraph uses three separate t() calls (lines 21-23). While this provides translation flexibility, it may result in awkward sentence flow in some languages. Consider whether these could be combined into a single translation key with proper sentence structure, or if the current approach is intentional for translation purposes.

web/src/components/UserProfile/DropdownContent.vue (1)

81-83: Good migration to namespaced i18n keys.

Keys and positional params look consistent with locales. One nit: showKeyline computes a non-boolean (number/object). Consider coercing to boolean for clarity.

Example minimal change:

-const showKeyline = computed(
-  () =>
-    (showConnectStatus.value && (keyActions.value?.length || links.value.length)) ||
-    unraidConnectWelcome.value
-);
+const showKeyline = computed(() =>
+  !!(
+    (showConnectStatus.value && (keyActions.value?.length || links.value.length)) ||
+    unraidConnectWelcome.value
+  )
+);

Also applies to: 93-94, 105-107, 118-120, 145-147, 162-164, 172-174, 196-198

web/src/components/Registration/KeyLinkedStatus.vue (1)

37-42: Use a localized fallback instead of 'Unknown'.

Prefer t('common.unknown') over t('Unknown') to ensure proper localization.

-{{ t(keyLinkedOutput.text ?? 'Unknown') }}
+{{ t(keyLinkedOutput.text ?? 'common.unknown') }}

Apply the same change at both occurrences.

web/src/components/Activation/WelcomeModal.standalone.vue (1)

34-36: Prefer named params to avoid list-order bugs

Use object params for clarity and to match typical message templates.

-  partnerInfo.value?.partnerName
-    ? t('activation.welcomeModal.welcomeToYourNewSystemPowered', [partnerInfo.value?.partnerName])
+  partnerInfo.value?.partnerName
+    ? t('activation.welcomeModal.welcomeToYourNewSystemPowered', { partnerName: partnerInfo.value?.partnerName })
     : t('activation.welcomeModal.welcomeToUnraid')
web/src/components/ApiKey/ApiKeyCreate.vue (1)

438-442: Remove redundant t() fallback

t always exists from useI18n(). Simplify and ensure all branches are localized.

-            ? 'Authorize API Key Access'
-            : editingKey
-              ? t
-                ? t('apiKey.apiKeyCreate.editApiKey')
-                : 'Edit API Key'
-              : t
-                ? t('apiKey.apiKeyCreate.createApiKey')
-                : 'Create API Key'
+            ? t('apiKey.apiKeyCreate.authorizeAccess')
+            : editingKey
+              ? t('apiKey.apiKeyCreate.editApiKey')
+              : t('apiKey.apiKeyCreate.createApiKey')

Note: add apiKey.apiKeyCreate.authorizeAccess to locales.

api/src/unraid-api/types/json-forms.ts (1)

17-22: Optional: allow params for schema-level i18n

If you plan parameterized labels, consider supporting an object.

-export type I18nJsonSchema = JsonSchema & {
-    i18n?: string;
-};
+export type I18nJsonSchema = JsonSchema & {
+    i18n?: string | { key: string; params?: Record<string, unknown> };
+};
web/src/components/ConnectSettings/ConnectSettings.standalone.vue (1)

118-118: Include punctuation in the translation

Avoid appending “:” outside the i18n key so locales can control punctuation.

-<Label>{{ t('downloadApiLogs.downloadUnraidApiLogs') }}:</Label>
+<Label>{{ t('downloadApiLogs.downloadUnraidApiLogsWithColon') }}</Label>

Or include the colon in the existing key’s message.

web/src/components/UpdateOs/UpdateIneligible.vue (1)

53-59: Remove redundant conditional in watchEffect.

Both branches do the same thing.

-watchEffect(() => {
-  if (availableWithRenewal.value) {
-    updateButton.value = updateOsActionsStore.updateCallbackButton();
-  } else {
-    updateButton.value = updateOsActionsStore.updateCallbackButton();
-  }
-});
+watchEffect(() => {
+  updateButton.value = updateOsActionsStore.updateCallbackButton();
+});
web/src/helpers/jsonforms-i18n.ts (1)

9-29: Tighten types for translate/translateError to avoid implicit any.

Align with JsonForms expectations and our TS guidelines.

As per coding guidelines:

-    translate: (id, defaultMessage, values) => {
+    translate: (id: string, defaultMessage?: string, values?: Record<string, unknown> | unknown[]) => {
       if (id && te(id)) {
         const result = t(id, values);
         return typeof result === 'string' ? result : String(result);
       }
       if (defaultMessage) {
         return defaultMessage;
       }
       return id;
     },
-    translateError: (error) => {
+    translateError: (error: { keyword?: string; message?: string; params?: Record<string, unknown> }) => {
       const key = error.keyword ? `jsonforms.errors.${error.keyword}` : undefined;
       if (key && te(key)) {
         const translated = t(key, error.params ?? {});
         return typeof translated === 'string' ? translated : String(translated);
       }
       return error.message ?? error.keyword ?? '';
     },
web/__test__/components/Wrapper/mount-engine.test.ts (1)

151-188: Test doesn’t verify JSON prop parsing behavior.

The test asserts the attribute value, not that the component received parsed JSON. Consider asserting rendered output or adjusting the test component to accept/display structured input.

Example: change TestComponent prop type to [String, Object] and render JSON.stringify when object, then assert rendered text.

web/src/components/UpdateOs/CheckUpdateResponseModal.vue (1)

318-321: Untranslated literal: “Released on …”.

Localize this string to keep UI fully i18n.

-              <p v-if="userFormattedReleaseDate" class="text-muted-foreground mt-2 text-center text-sm">
-                Released on {{ userFormattedReleaseDate }}
-              </p>
+              <p v-if="userFormattedReleaseDate" class="text-muted-foreground mt-2 text-center text-sm">
+                {{ t('updateOs.checkUpdateResponseModal.releasedOn', [userFormattedReleaseDate]) }}
+              </p>

Ensure the key exists in locales.

web/package.json (1)

73-73: Update vue-i18n-extract to the latest 2.x patch version

  • Bump vue-i18n-extract from 2.0.4 to 2.0.7
web/src/components/Registration/UpdateExpirationAction.vue (1)

41-44: Avoid concatenating translation strings; use a single key with interpolation

Concatenation breaks grammar in many locales. Prefer one key that includes the whole sentence with placeholders.

Example:

  • Define a single message like registration.updateExpirationAction.eligibleForUpdatesReleasedOnOrAndExtend with a {date} placeholder.
  • Use: props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOrAndExtend', { date: formattedRegExp.value })
web/src/components/UpdateOs/Update.vue (1)

66-82: Prefer full-sentence translation keys over assembling paragraphs

Building paragraphs by concatenating multiple t() calls risks awkward spacing/grammar and complicates localization.

  • Create complete strings per branch, e.g.:
    • updateOs.update.flashBackup.copyAlreadyActivated
    • updateOs.update.flashBackup.copyNotActivated
    • updateOs.update.flashBackup.copyManualOnly
  • Each key should include all sentences in the branch, with placeholders as needed.
web/src/components/Logs/LogViewer.standalone.vue (2)

7-7: Verify convert API and tighten typing for formatFileSize.

Ensure convert(bytes, 'B').to('best', 'metric') exists and returns { quantity, unit } as expected. Current (best.quantity as number) hints missing types.

Consider a small type guard to avoid casts:

+type BestSize = { quantity: number; unit: string };
+const isBestSize = (v: unknown): v is BestSize =>
+  !!v && typeof (v as any).quantity === 'number' && typeof (v as any).unit === 'string';

-    const best = convert(bytes, 'B').to('best', 'metric');
-    const formattedValue = new Intl.NumberFormat(undefined, {
+    const res = convert(bytes, 'B').to('best', 'metric');
+    if (!isBestSize(res)) throw new Error('Unexpected convert() result');
+    const formattedValue = new Intl.NumberFormat(undefined, {
       maximumFractionDigits: 2,
       minimumFractionDigits: 0,
-    }).format(best.quantity as number);
+    }).format(res.quantity);
-
-    const unit = unitLabels.value[best.unit] ?? best.unit;
+    const unit = unitLabels.value[res.unit] ?? res.unit;

If convert doesn’t guarantee “metric” symbols (e.g., returns kB vs KB), extend unitLabels or normalize the unit key before lookup.

Also applies to: 75-82, 91-104


218-219: Avoid exposing raw backend error messages to users.

Interpolating logFilesError.message can leak internal details. Prefer a generic message and log details to console/telemetry.

-        {{ t('logs.viewer.errorLoadingLogFiles', { error: logFilesError.message }) }}
+        {{ t('logs.viewer.errorLoadingLogFiles') }}
web/src/helpers/i18n-loader.ts (2)

43-47: Normalize by comparing base language parts.

Current check compares full candidate (e.g., en_US) to base (en) and never matches. Compare base segments.

-  for (const candidate of supportedLocaleCache) {
-    if (candidate.toLowerCase() === base.toLowerCase()) {
-      return candidate;
-    }
-  }
+  for (const candidate of supportedLocaleCache) {
+    const candidateBase = candidate.split(/[-_]/)[0];
+    if (candidateBase.toLowerCase() === base.toLowerCase()) {
+      return candidate;
+    }
+  }

101-115: Remove unsafe casts and unnecessary mutation of availableLocales.

No need to coerce types or push into the array; setLocaleMessage makes the locale available.

-  const availableLocales = i18n.global.availableLocales as unknown as string[];
-
-  if (!availableLocales.includes(targetLocale)) {
+  const hasLocale = (i18n.global.availableLocales as readonly string[]).includes(targetLocale);
+
+  if (!hasLocale) {
     const messages = await loadLocaleMessages(targetLocale);
     if (!messages) {
       i18n.global.locale.value = DEFAULT_LOCALE;
       return DEFAULT_LOCALE;
     }
-    i18n.global.setLocaleMessage(targetLocale as typeof DEFAULT_LOCALE, messages);
-    availableLocales.push(targetLocale);
+    i18n.global.setLocaleMessage(targetLocale, messages);
   }
 
-  i18n.global.locale.value = targetLocale as typeof DEFAULT_LOCALE;
+  i18n.global.locale.value = targetLocale;
   return targetLocale;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed594e9 and 7583331.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (102)
  • api/README.md (1 hunks)
  • api/dev/configs/api.json (1 hunks)
  • api/package.json (1 hunks)
  • api/scripts/extract-translations.mjs (1 hunks)
  • api/src/i18n/ar.json (1 hunks)
  • api/src/i18n/ca.json (1 hunks)
  • api/src/i18n/cs.json (1 hunks)
  • api/src/i18n/da.json (1 hunks)
  • api/src/i18n/de.json (1 hunks)
  • api/src/i18n/en.json (1 hunks)
  • api/src/i18n/es.json (1 hunks)
  • api/src/i18n/fr.json (1 hunks)
  • api/src/i18n/hu.json (1 hunks)
  • api/src/i18n/it.json (1 hunks)
  • api/src/i18n/ja.json (1 hunks)
  • api/src/i18n/ko.json (1 hunks)
  • api/src/i18n/nl.json (1 hunks)
  • api/src/i18n/no.json (1 hunks)
  • api/src/i18n/pl.json (1 hunks)
  • api/src/i18n/pt.json (1 hunks)
  • api/src/i18n/ro.json (1 hunks)
  • api/src/i18n/ru.json (1 hunks)
  • api/src/i18n/sv.json (1 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts (13 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (32 hunks)
  • api/src/unraid-api/graph/utils/form-utils.ts (3 hunks)
  • api/src/unraid-api/types/json-forms.ts (1 hunks)
  • crowdin.yml (1 hunks)
  • package.json (1 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php (1 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php (1 hunks)
  • unraid-ui/src/components/brand/BrandButton.vue (3 hunks)
  • unraid-ui/src/forms/AccordionLayout.vue (7 hunks)
  • web/README.md (1 hunks)
  • web/__test__/components/Wrapper/mount-engine.test.ts (3 hunks)
  • web/package.json (4 hunks)
  • web/scripts/extract-translations.mjs (1 hunks)
  • web/scripts/sort-translations.mjs (1 hunks)
  • web/src/components/Activation/ActivationModal.vue (3 hunks)
  • web/src/components/Activation/WelcomeModal.standalone.vue (2 hunks)
  • web/src/components/ApiKey/ApiKeyCreate.vue (4 hunks)
  • web/src/components/ConnectSettings/ConnectSettings.standalone.vue (7 hunks)
  • web/src/components/ConnectSettings/OidcDebugLogs.vue (3 hunks)
  • web/src/components/DowngradeOs.standalone.vue (2 hunks)
  • web/src/components/DownloadApiLogs.standalone.vue (5 hunks)
  • web/src/components/HeaderOsVersion.standalone.vue (7 hunks)
  • web/src/components/Logs/LogFilterInput.vue (4 hunks)
  • web/src/components/Logs/LogViewer.standalone.vue (6 hunks)
  • web/src/components/Logs/LogViewerToolbar.vue (5 hunks)
  • web/src/components/Logs/SingleLogViewer.vue (9 hunks)
  • web/src/components/Modal.vue (2 hunks)
  • web/src/components/Notifications/Item.vue (4 hunks)
  • web/src/components/Notifications/List.vue (4 hunks)
  • web/src/components/Notifications/Sidebar.vue (10 hunks)
  • web/src/components/Registration.standalone.vue (11 hunks)
  • web/src/components/Registration/KeyLinkedStatus.vue (3 hunks)
  • web/src/components/Registration/ReplaceCheck.vue (2 hunks)
  • web/src/components/Registration/UpdateExpiration.vue (1 hunks)
  • web/src/components/Registration/UpdateExpirationAction.vue (4 hunks)
  • web/src/components/UpdateOs.standalone.vue (2 hunks)
  • web/src/components/UpdateOs/CallbackButton.vue (1 hunks)
  • web/src/components/UpdateOs/ChangelogModal.vue (3 hunks)
  • web/src/components/UpdateOs/CheckUpdateResponseModal.vue (14 hunks)
  • web/src/components/UpdateOs/Downgrade.vue (4 hunks)
  • web/src/components/UpdateOs/IgnoredRelease.vue (1 hunks)
  • web/src/components/UpdateOs/RawChangelogRenderer.vue (3 hunks)
  • web/src/components/UpdateOs/Status.vue (11 hunks)
  • web/src/components/UpdateOs/ThirdPartyDrivers.vue (1 hunks)
  • web/src/components/UpdateOs/Update.vue (6 hunks)
  • web/src/components/UpdateOs/UpdateIneligible.vue (4 hunks)
  • web/src/components/UserProfile.standalone.vue (2 hunks)
  • web/src/components/UserProfile/CallbackFeedback.vue (10 hunks)
  • web/src/components/UserProfile/DropdownConnectStatus.vue (1 hunks)
  • web/src/components/UserProfile/DropdownContent.vue (8 hunks)
  • web/src/components/UserProfile/DropdownLaunchpad.vue (1 hunks)
  • web/src/components/UserProfile/DropdownTrigger.vue (1 hunks)
  • web/src/components/UserProfile/ServerState.vue (2 hunks)
  • web/src/components/UserProfile/Trial.vue (2 hunks)
  • web/src/components/UserProfile/UptimeExpire.vue (1 hunks)
  • web/src/components/WanIpCheck.standalone.vue (2 hunks)
  • web/src/components/Wrapper/mount-engine.ts (3 hunks)
  • web/src/components/sso/SsoButtons.vue (1 hunks)
  • web/src/components/sso/useSsoAuth.ts (1 hunks)
  • web/src/composables/dateTime.ts (1 hunks)
  • web/src/helpers/globals.d.ts (1 hunks)
  • web/src/helpers/i18n-loader.ts (1 hunks)
  • web/src/helpers/jsonforms-i18n.ts (1 hunks)
  • web/src/locales/_template.json (0 hunks)
  • web/src/locales/ar.json (1 hunks)
  • web/src/locales/bn.json (1 hunks)
  • web/src/locales/ca.json (1 hunks)
  • web/src/locales/cs.json (1 hunks)
  • web/src/locales/da.json (1 hunks)
  • web/src/locales/de.json (1 hunks)
  • web/src/locales/en.json (1 hunks)
  • web/src/locales/en_US.json (0 hunks)
  • web/src/locales/es.json (1 hunks)
  • web/src/locales/fr.json (1 hunks)
  • web/src/locales/hi.json (1 hunks)
  • web/src/locales/hr.json (1 hunks)
  • web/src/locales/hu.json (1 hunks)
💤 Files with no reviewable changes (2)
  • web/src/locales/_template.json
  • web/src/locales/en_US.json
🧰 Additional context used
📓 Path-based instructions (9)
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/Notifications/Item.vue
  • web/src/components/UpdateOs/Downgrade.vue
  • web/src/components/UserProfile/DropdownLaunchpad.vue
  • web/src/components/Activation/ActivationModal.vue
  • web/src/components/UserProfile/DropdownTrigger.vue
  • web/src/components/ApiKey/ApiKeyCreate.vue
  • web/src/components/UserProfile/CallbackFeedback.vue
  • web/src/components/UserProfile/DropdownContent.vue
  • web/src/components/Notifications/Sidebar.vue
  • web/src/components/HeaderOsVersion.standalone.vue
  • web/src/components/Registration/UpdateExpiration.vue
  • web/src/components/WanIpCheck.standalone.vue
  • web/src/components/Modal.vue
  • web/src/components/Registration/KeyLinkedStatus.vue
  • web/src/components/UpdateOs/CallbackButton.vue
  • web/src/components/UserProfile/ServerState.vue
  • web/src/components/UpdateOs/ThirdPartyDrivers.vue
  • web/src/components/Notifications/List.vue
  • web/src/components/DowngradeOs.standalone.vue
  • web/src/components/Registration.standalone.vue
  • web/src/components/UpdateOs/RawChangelogRenderer.vue
  • web/src/components/DownloadApiLogs.standalone.vue
  • web/src/components/UserProfile/Trial.vue
  • unraid-ui/src/components/brand/BrandButton.vue
  • web/src/components/Registration/UpdateExpirationAction.vue
  • web/src/components/UpdateOs/ChangelogModal.vue
  • web/src/components/UpdateOs/Status.vue
  • web/src/components/UpdateOs/UpdateIneligible.vue
  • web/src/components/sso/SsoButtons.vue
  • web/src/components/Registration/ReplaceCheck.vue
  • web/src/components/Logs/LogFilterInput.vue
  • web/src/components/UpdateOs/CheckUpdateResponseModal.vue
  • web/src/components/UserProfile.standalone.vue
  • web/src/components/ConnectSettings/ConnectSettings.standalone.vue
  • web/src/components/UpdateOs/Update.vue
  • web/src/components/Logs/LogViewerToolbar.vue
  • web/src/components/Activation/WelcomeModal.standalone.vue
  • web/src/components/Logs/LogViewer.standalone.vue
  • web/src/components/UpdateOs/IgnoredRelease.vue
  • web/src/components/UserProfile/DropdownConnectStatus.vue
  • web/src/components/ConnectSettings/OidcDebugLogs.vue
  • web/src/components/Logs/SingleLogViewer.vue
  • web/src/components/UserProfile/UptimeExpire.vue
  • web/src/components/UpdateOs.standalone.vue
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/src/components/sso/useSsoAuth.ts
  • api/src/unraid-api/types/json-forms.ts
  • web/src/helpers/globals.d.ts
  • web/src/helpers/i18n-loader.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
  • web/src/composables/dateTime.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/src/helpers/jsonforms-i18n.ts
  • web/src/components/Wrapper/mount-engine.ts
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/types/json-forms.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/types/json-forms.ts
  • api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
🧠 Learnings (15)
📚 Learning: 2024-11-01T17:52:55.311Z
Learnt from: pujitm
PR: unraid/api#941
File: web/components/Notifications/Item.vue:0-0
Timestamp: 2024-11-01T17:52:55.311Z
Learning: In the `web/components/Notifications/Item.vue` component, ensure that when delete or archive mutations fail, the notification remains displayed, and an error message is shown below the action buttons to inform the user.

Applied to files:

  • web/src/components/Notifications/Item.vue
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/{store,stores}/**/*.ts : In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Applied to files:

  • web/src/components/Notifications/Item.vue
  • web/src/components/Notifications/Sidebar.vue
  • web/src/components/Notifications/List.vue
  • web/src/components/ConnectSettings/ConnectSettings.standalone.vue
  • web/src/components/Logs/LogViewer.standalone.vue
  • web/src/components/Logs/SingleLogViewer.vue
📚 Learning: 2025-03-12T13:35:43.900Z
Learnt from: pujitm
PR: unraid/api#1211
File: web/codegen.ts:14-14
Timestamp: 2025-03-12T13:35:43.900Z
Learning: The JSON scalar type in web/codegen.ts was temporarily changed from 'string' to 'any' for compatibility with JsonForms integration. This change facilitates the implementation of the Connect settings web component.

Applied to files:

  • api/src/unraid-api/types/json-forms.ts
📚 Learning: 2025-09-02T19:35:14.427Z
Learnt from: elibosley
PR: unraid/api#1639
File: web/components/standalone-mount.ts:38-54
Timestamp: 2025-09-02T19:35:14.427Z
Learning: In the unraid/api project, exposing Apollo client and GraphQL utilities (apolloClient, gql, graphqlParse) on the global window object is intentional to allow developers to interact with GraphQL from the browser console without needing to be inside Vue components. Developer experience is prioritized over minimizing global exposure in this context.

Applied to files:

  • web/README.md
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Always run pnpm codegen for GraphQL in the web directory

Applied to files:

  • api/package.json
  • web/package.json
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Always run `pnpm codegen` for GraphQL code generation in the web directory

Applied to files:

  • api/package.json
  • web/package.json
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
PR: unraid/api#975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.

Applied to files:

  • web/src/components/Logs/LogFilterInput.vue
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
PR: unraid/api#1417
File: web/components/ConnectSettings/ConnectSettings.ce.vue:11-18
Timestamp: 2025-06-13T17:14:21.739Z
Learning: The project’s build tooling auto-imports common Vue/Pinia helpers such as `storeToRefs`, so explicit import statements for them are not required.

Applied to files:

  • web/src/components/ConnectSettings/ConnectSettings.standalone.vue
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
🧬 Code graph analysis (7)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php (1)
web/scripts/extract-translations.mjs (1)
  • locale (97-97)
web/src/helpers/i18n-loader.ts (1)
web/scripts/extract-translations.mjs (3)
  • rawLocale (96-96)
  • rawLocale (129-129)
  • locale (97-97)
web/scripts/extract-translations.mjs (1)
api/scripts/extract-translations.mjs (9)
  • raw (90-90)
  • ignorePatterns (10-17)
  • files (133-137)
  • keys (54-54)
  • keys (145-145)
  • content (143-143)
  • sourceFile (144-144)
  • added (108-108)
  • added (150-150)
api/scripts/extract-translations.mjs (1)
web/scripts/extract-translations.mjs (8)
  • ignorePatterns (32-39)
  • sourceFile (58-58)
  • keys (53-53)
  • visit (60-68)
  • raw (21-21)
  • added (188-188)
  • files (41-41)
  • content (57-57)
web/__test__/components/Wrapper/mount-engine.test.ts (1)
web/src/components/Wrapper/mount-engine.ts (1)
  • mountUnifiedApp (80-198)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/helpers/i18n-loader.ts (3)
  • createI18nInstance (59-69)
  • ensureLocale (93-115)
  • getWindowLocale (117-122)
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)
api/src/unraid-api/graph/utils/form-utils.ts (1)
  • createSimpleLabeledControl (7-55)
🪛 GitHub Actions: CI - Main (API)
package.json

[error] 1-1: Command failed with exit code 1.

api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts

[error] 152-152: Type '{ type: string; properties: { resources: { type: string; i18n: "jsonforms.apiKey.customPermissions.resources"; title: string; items: { type: string; enum: Resource[]; }; uniqueItems: true; minItems: number; default: Resource[]; }; actions: { ...; }; }; required: string[]; }' is not assignable to type 'JsonSchema4 | JsonSchema7 | JsonSchema4[] | JsonSchema7[] | undefined'.

api/package.json

[error] 1-1: Command failed with exit code 1.

web/__test__/components/Wrapper/mount-engine.test.ts

[error] 59-59: pnpm run type-check failed. TypeScript error TS2558: Expected 0-1 type arguments, but got 2. (test/components/Wrapper/mount-engine.test.ts:59:35)

web/package.json

[error] 1-1: Command failed with exit code 1.

<DispatchRenderer
:schema="layout.schema"
:uischema="element as UISchemaElement"
:uischema="element as unknown as UISchemaElement"
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify the double cast necessity.

The cast element as unknown as UISchemaElement bypasses TypeScript's type checking and suggests that element may not fully satisfy UISchemaElement's requirements. This could hide runtime issues if required properties are missing.

Consider:

  1. Checking if element should be typed as UISchemaElement from the start (e.g., by constraining the elements array type)
  2. Verifying that all elements in the filtered array actually have the properties DispatchRenderer expects
  3. If the mismatch is deliberate (e.g., using a subset of UISchemaElement), documenting why the double cast is safe

Run the following script to verify how elements are typed and whether they consistently match UISchemaElement:


🏁 Script executed:

#!/bin/bash
# Description: Check the types of elements used in AccordionLayout and related components

# Search for UISchemaElement and BaseUISchemaElement usage patterns
ast-grep --pattern $'interface BaseUISchemaElement {
  $$$
}'

# Search for where elements are defined/constrained
rg -nP --type=ts 'elements\s*:\s*\w+\[\]' unraid-ui/src/forms/

# Search for other uses of similar double casts in the codebase
rg -nP 'as unknown as UISchemaElement' unraid-ui/

Length of output: 106


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' unraid-ui/src/forms/AccordionLayout.vue

Length of output: 5644


Remove the unnecessary double cast
Strongly type the computed elements as UISchemaElement[] (e.g. const elements = computed<UISchemaElement[]>(() => { … })) so that in the template you can use

:uischema="element"

directly without as unknown as UISchemaElement.

🤖 Prompt for AI Agents
In unraid-ui/src/forms/AccordionLayout.vue around line 24, the template uses an
unnecessary double cast ":uischema=\"element as unknown as UISchemaElement\"";
strongly type the computed elements as UISchemaElement[] (e.g., define the
computed with computed<UISchemaElement[]>() in the script setup or component
code and ensure UISchemaElement is imported/available) so each item is already
typed and then replace the template binding with the direct
":uischema=\"element\"" and remove the double cast.

Comment on lines +31 to +39
const apiSourceRoot = path.resolve(process.cwd(), '../api/src');
const ignorePatterns = [
'**/__tests__/**',
'**/__test__/**',
'**/*.spec.ts',
'**/*.spec.js',
'**/*.test.ts',
'**/*.test.js',
];
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify path assumption for ../api/src

The script assumes it’s run with CWD at web/, resolving ../api/src. This may break under different runners.

Run this from the repo root to confirm expected layout and that locales exist:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[1] CWD:"
pwd

echo "[2] Check web locales dir:"
fd -t d -a '^locales$' web/src || true
fd -t f -a '\.json$' web/src/locales || true

echo "[3] Check api src dir as resolved by script:"
if [ -d "web" ]; then
  (cd web && node -e "console.log(require('node:path').resolve(process.cwd(), '../api/src'))")
fi
fd -t d -a '^src$' api || true
fd -t f -a '\.ts$' api/src | head -n 5

Length of output: 1648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[4] Resolve api path from repo root:"
node -e "console.log(require('node:path').resolve(process.cwd(), '../api/src'))"
if [ -d "$(node -e "console.log(require('node:path').resolve(process.cwd(), '../api/src'))")" ]; then
  echo "Exists"
else
  echo "Does not exist"
fi

Length of output: 74


Resolve apiSourceRoot relative to script file, not CWD
path.resolve(process.cwd(), '../api/src') breaks if the script is run from the repo root. Replace it with resolution based on the script’s directory, for example:

import { fileURLToPath } from 'url';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const apiSourceRoot = path.resolve(__dirname, '../../api/src');

This ensures the correct path regardless of working directory.

🤖 Prompt for AI Agents
In web/scripts/extract-translations.mjs around lines 31 to 39, apiSourceRoot is
being resolved from process.cwd(), which breaks when the script is invoked from
different working directories; change resolution to be based on the script file
location by deriving __dirname from import.meta.url (using fileURLToPath and
path.dirname) and then resolve apiSourceRoot relative to that directory (e.g.,
path.resolve(__dirname, '../../api/src')) so the path is correct regardless of
CWD.

Comment on lines +168 to 175
<Label for="log-file-select">{{ t('logs.viewer.logFileLabel') }}</Label>
<Select
v-model="selectedLogFile"
:items="logFileOptions"
placeholder="Select a log file"
:placeholder="t('logs.viewer.selectLogFilePlaceholder')"
class="w-full"
/>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix label-to-control association for Selects (add ids).

Labels use for="log-file-select" and for="highlight-language" but the Selects lack matching id attributes. Screen readers won’t associate them.

Apply:

-          <Select
+          <Select id="log-file-select"
             v-model="selectedLogFile"
             :items="logFileOptions"
             :placeholder="t('logs.viewer.selectLogFilePlaceholder')"
             class="w-full"
           />
-          <Select
+          <Select id="highlight-language"
             v-model="highlightLanguage"
             :items="highlightLanguageOptions"
             :placeholder="t('logs.viewer.selectLanguagePlaceholder')"
             class="w-full"
           />

Also applies to: 189-197

🤖 Prompt for AI Agents
In web/src/components/Logs/LogViewer.standalone.vue around lines 168-175 and
189-197 the Label elements use for="log-file-select" and
for="highlight-language" but the corresponding Select components lack matching
id attributes; add id="log-file-select" to the Select bound to selectedLogFile
and add id="highlight-language" to the Select (or component) for highlight
language so the label for attributes correctly associate with their controls for
accessibility; ensure the id values exactly match the Label for values and keep
them unique within the page.

Comment on lines +241 to 243
:text="t('updateOs.update.viewAvailableUpdates')"
:title="!acknowledgeBackup ? t('updateOs.update.acklowledgeThatYouHaveMadeA') : ''"
class="flex-none"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Typo in translation key will break lookup

Key has a misspelling: acklowledge → acknowledge.

Apply this fix:

-          :title="!acknowledgeBackup ? t('updateOs.update.acklowledgeThatYouHaveMadeA') : ''"
+          :title="!acknowledgeBackup ? t('updateOs.update.acknowledgeThatYouHaveMadeA') : ''"
🤖 Prompt for AI Agents
In web/src/components/UpdateOs/Update.vue around lines 241 to 243 the
translation key used in the :title binding is misspelled as
"acklowledgeThatYouHaveMadeA" which will break lookup; change the key to the
correct spelling "acknowledgeThatYouHaveMadeA" (update the string in the :title
attribute and any other occurrences) so the i18n lookup resolves properly.

Comment on lines 41 to 45
if (availableWithRenewal.value) {
return props.t('Unraid OS {0} Released', [availableWithRenewal.value]);
return props.t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value]);
}
return props.t('License Key Updates Expired');
return props.t('updateOs.updateIneligible.licenseKeyUpdatesExpired');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect interpolation value for heading; boolean passed to t().

availableWithRenewal.value is boolean. The heading should interpolate the release/version string from the update response.

Suggest using the version from updateOsResponse (or the release object) instead:

-  if (availableWithRenewal.value) {
-    return props.t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value]);
-  }
+  if (availableWithRenewal.value) {
+    return props.t('headerOsVersion.unraidOsReleased', [updateOsResponse.value?.version ?? '']);
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (availableWithRenewal.value) {
return props.t('Unraid OS {0} Released', [availableWithRenewal.value]);
return props.t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value]);
}
return props.t('License Key Updates Expired');
return props.t('updateOs.updateIneligible.licenseKeyUpdatesExpired');
});
if (availableWithRenewal.value) {
return props.t(
'headerOsVersion.unraidOsReleased',
[updateOsResponse.value?.version ?? '']
);
}
return props.t('updateOs.updateIneligible.licenseKeyUpdatesExpired');
});
🤖 Prompt for AI Agents
web/src/components/UpdateOs/UpdateIneligible.vue lines 41-45: the code is
passing the boolean availableWithRenewal.value into the translation
interpolation, but the heading expects the release/version string from the
update response; replace the boolean with the actual version/release string from
updateOsResponse (or the release object) when calling props.t, and add a safe
null/undefined check (fall back to a sensible default string) so you pass a
string version like updateOsResponse.latestRelease.version (or the release.name)
rather than the boolean.

@@ -0,0 +1,124 @@
import { createI18n } from 'vue-i18n';

import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use .js extension for local TS ESM import.

Align with ESM compatibility guidance.

[As per coding guidelines]

-import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
+import { createHtmlEntityDecoder } from '~/helpers/i18n-utils.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
import { createHtmlEntityDecoder } from '~/helpers/i18n-utils.js';
🤖 Prompt for AI Agents
In web/src/helpers/i18n-loader.ts at line 3, the local ESM import lacks a .js
extension which breaks TS ESM runtime resolution; update the import specifier to
include the .js extension (e.g. import from '~/helpers/i18n-utils.js') so the
runtime can resolve the module, and ensure the referenced file is emitted with a
.js extension by your build setup.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 3, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

1 similar comment
@github-actions
Copy link
Contributor

github-actions bot commented Oct 3, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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: 1

🧹 Nitpick comments (2)
web/src/locales/en.json (1)

304-306: HTML in translation strings may complicate maintenance and localization.

Several translation values contain raw HTML markup (e.g., <p>, <em>, <ul>, <li>, <a>, <strong>). This approach can make translations harder to manage and may pose risks if user-supplied data is ever interpolated into these strings.

Consider:

  • Moving HTML structure to component templates and using separate translation keys for text fragments.
  • Using markdown or a safe markup library if rich text is needed.
  • Ensuring that any dynamic content interpolated into these strings is properly sanitized.

Also applies to: 349-350, 356-356, 371-396

web/__test__/utils/i18n.ts (1)

15-41: Consider escaping special regex characters in placeholder names.

The replaceParams function constructs regular expressions from variable input (index at line 24, placeholder at line 31). While the risk is low in a test context with controlled locale keys, if placeholder names ever contain regex special characters (e.g., {foo.bar}, {test[0]}), this could cause regex errors or unexpected behavior.

To harden the implementation, escape special regex characters before constructing the RegExp:

+function escapeRegex(str: string | number): string {
+  return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
 function replaceParams(template: string, params?: unknown): string {
   if (params === undefined || params === null) {
     return template;
   }

   let result = template;

   if (Array.isArray(params)) {
     params.forEach((value, index) => {
-      result = result.replace(new RegExp(`\\{${index}\\}`, 'g'), String(value));
+      result = result.replace(new RegExp(`\\{${escapeRegex(index)}\\}`, 'g'), String(value));
     });
     return result;
   }

   if (typeof params === 'object') {
     Object.entries(params as AnyObject).forEach(([placeholder, value]) => {
-      result = result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), String(value));
+      result = result.replace(new RegExp(`\\{${escapeRegex(placeholder)}\\}`, 'g'), String(value));
     });
     return result;
   }

   if (typeof params === 'number' || typeof params === 'string' || typeof params === 'boolean') {
     return result.replace(/\{0\}/g, String(params));
   }

   return result;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7583331 and f48c85c.

📒 Files selected for processing (20)
  • api/src/i18n/bn.json (1 hunks)
  • api/src/i18n/hi.json (1 hunks)
  • api/src/i18n/hr.json (1 hunks)
  • api/src/i18n/lv.json (1 hunks)
  • api/src/i18n/uk.json (1 hunks)
  • api/src/i18n/zh.json (1 hunks)
  • plugin/plugins/dynamix.unraid.net.plg (0 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php (0 hunks)
  • web/__test__/components/Activation/ActivationModal.test.ts (2 hunks)
  • web/__test__/components/Activation/WelcomeModal.test.ts (3 hunks)
  • web/__test__/components/CheckUpdateResponseModal.test.ts (2 hunks)
  • web/__test__/components/DowngradeOs.test.ts (6 hunks)
  • web/__test__/components/DownloadApiLogs.test.ts (7 hunks)
  • web/__test__/components/HeaderOsVersion.test.ts (4 hunks)
  • web/__test__/components/Logs/SingleLogViewer.test.ts (5 hunks)
  • web/__test__/components/Registration.test.ts (2 hunks)
  • web/__test__/components/SsoButton.test.ts (2 hunks)
  • web/__test__/components/UpdateOs.test.ts (3 hunks)
  • web/__test__/utils/i18n.ts (1 hunks)
  • web/src/locales/en.json (1 hunks)
💤 Files with no reviewable changes (2)
  • plugin/plugins/dynamix.unraid.net.plg
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php
✅ Files skipped from review due to trivial changes (7)
  • api/src/i18n/uk.json
  • api/src/i18n/lv.json
  • api/src/i18n/zh.json
  • api/src/i18n/hi.json
  • api/src/i18n/hr.json
  • api/src/i18n/bn.json
  • web/test/components/Logs/SingleLogViewer.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/DowngradeOs.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/DowngradeOs.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/DowngradeOs.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/utils/i18n.ts
  • web/__test__/components/HeaderOsVersion.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/DowngradeOs.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/DowngradeOs.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/utils/i18n.ts
  • web/__test__/components/HeaderOsVersion.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/DowngradeOs.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
🧠 Learnings (14)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/DowngradeOs.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • web/__test__/components/Registration.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/DowngradeOs.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)

Applied to files:

  • web/__test__/components/Activation/WelcomeModal.test.ts
  • web/__test__/components/DownloadApiLogs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/Activation/ActivationModal.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/DowngradeOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/DowngradeOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: This Nuxt app is tested with vitest outside the Nuxt runtime; structure tests accordingly

Applied to files:

  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing

Applied to files:

  • web/__test__/components/DownloadApiLogs.test.ts
  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to api/**/*.{test,spec}.{ts,tsx} : API test suite is Vitest; do not use Jest

Applied to files:

  • web/__test__/components/DownloadApiLogs.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to {api,web}/**/*.{test,spec}.{ts,tsx} : Use mocks as nouns, not verbs

Applied to files:

  • web/__test__/components/DownloadApiLogs.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Mock external dependencies and services in component tests

Applied to files:

  • web/__test__/components/DownloadApiLogs.test.ts
🧬 Code graph analysis (10)
web/__test__/components/Registration.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • testTranslate (43-47)
web/__test__/components/Activation/WelcomeModal.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • testTranslate (43-47)
web/__test__/components/Activation/ActivationModal.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • testTranslate (43-47)
web/__test__/components/CheckUpdateResponseModal.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • testTranslate (43-47)
web/__test__/components/SsoButton.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • testTranslate (43-47)
web/__test__/components/DowngradeOs.test.ts (1)
web/__test__/utils/i18n.ts (2)
  • testTranslate (43-47)
  • createTestI18n (49-58)
web/__test__/components/DownloadApiLogs.test.ts (1)
web/__test__/utils/i18n.ts (2)
  • testTranslate (43-47)
  • createTestI18n (49-58)
web/__test__/components/UpdateOs.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • testTranslate (43-47)
web/__test__/utils/i18n.ts (1)
web/src/helpers/i18n-loader.ts (1)
  • DEFAULT_LOCALE (124-124)
web/__test__/components/HeaderOsVersion.test.ts (1)
web/__test__/utils/i18n.ts (2)
  • testTranslate (43-47)
  • createTestI18n (49-58)
🪛 ast-grep (0.39.5)
web/__test__/utils/i18n.ts

[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${index}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 30-30: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${placeholder}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (12)
web/__test__/components/Activation/ActivationModal.test.ts (1)

13-13: LGTM! Correct adoption of centralized i18n test utilities.

The import of testTranslate and its assignment to mockT correctly replaces the local translation mock with the centralized test utility, aligning with the PR's i18n testing strategy.

Also applies to: 40-40

web/__test__/components/UpdateOs.test.ts (1)

12-12: LGTM! Proper spy wrapper for testTranslate.

The pattern of wrapping testTranslate in vi.fn() allows the test to track translation calls while using the real implementation from the centralized utilities. The beforeEach correctly resets the mock to ensure test isolation.

Also applies to: 36-36, 71-71

web/__test__/components/Registration.test.ts (1)

19-19: LGTM! Clean async mock pattern.

The async mock pattern preserves the original vue-i18n module while overriding only useI18n to return testTranslate. This is a cleaner approach than fully replacing the module and aligns with the PR's centralized i18n testing strategy.

Also applies to: 116-124

web/__test__/components/SsoButton.test.ts (1)

13-13: LGTM! Straightforward migration to centralized test utilities.

The import and usage of testTranslate correctly replaces the local translation mock with the centralized test utility.

Also applies to: 33-36

web/__test__/components/HeaderOsVersion.test.ts (1)

20-20: LGTM! Complete i18n test setup.

The test correctly imports and uses both testTranslate and createTestI18n, providing a complete i18n context for the component. The async mock pattern preserves the original vue-i18n module while overriding the translation function, and the i18n plugin is consistently added to all mount operations.

Also applies to: 64-72, 101-101, 156-156

web/__test__/components/DowngradeOs.test.ts (1)

13-13: LGTM! Consistent i18n setup across all tests.

The test correctly imports and applies both createTestI18n and testTranslate, ensuring consistent i18n context across all mounting operations. The async mock pattern preserves the original module while providing the test translation function.

Also applies to: 34-42, 79-79, 96-96, 124-124, 150-150

web/__test__/components/CheckUpdateResponseModal.test.ts (1)

9-9: LGTM! Correct type casting for prop-based i18n.

The import of testTranslate and type cast to ComposerTranslation correctly adapts the test utility for use as a component prop, aligning with the component's prop-based i18n pattern.

Also applies to: 11-11

web/__test__/components/DownloadApiLogs.test.ts (1)

12-12: LGTM! Complete i18n test migration with assertion improvements.

The test correctly adopts both createTestI18n and testTranslate, providing a complete i18n context. The use of testTranslate() in assertions (lines 62, 80, 83, 86, 107-109) is particularly good practice, as it verifies that translations are correctly applied while making tests resilient to translation text changes.

Also applies to: 21-29, 41-41, 62-62, 68-68, 97-97

web/__test__/components/Activation/WelcomeModal.test.ts (2)

13-13: LGTM! Test updated correctly for i18n.

The test now uses testTranslate from the shared i18n test utilities, which provides real translation lookups against en.json. The switch from inline mock to testTranslate ensures tests validate actual translation keys and their interpolation logic.

Also applies to: 40-40, 119-128


130-146: LGTM! Title assertions correctly validate translated output.

Tests now assert against the actual translated strings returned by testTranslate, including placeholder replacement for the partner name scenario. This approach ensures that translation keys remain valid and that parameter interpolation works as expected.

web/__test__/utils/i18n.ts (2)

43-47: LGTM! testTranslate provides a practical test helper.

The testTranslate function effectively mimics the vue-i18n ComposerTranslation signature for test purposes. The type cast is acceptable here as it's a test utility that matches the expected interface.


49-58: LGTM! createTestI18n correctly initializes a test i18n instance.

The factory function creates a properly configured vue-i18n instance with:

  • Composition API mode (legacy: false)
  • Consistent locale and fallback (en_US)
  • Real locale messages from en.json

This allows tests to use a real i18n setup rather than mocks.

"userProfile.serverState.purchaseKey": "Purchase Key",
"userProfile.serverState.upgrade": "Upgrade",
"userProfile.serverState.upgradeKey": "Upgrade Key",
"userProfile.trial.errorCreatiingATrialKeyPlease": "Error creatiing a trial key. Please try again later.",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in translation key.

Line 579 contains a typo: "userProfile.trial.errorCreatiingATrialKeyPlease" should be "errorCreatingATrialKeyPlease" (double 'i' in "Creatiing").

Apply this diff to fix the typo:

-  "userProfile.trial.errorCreatiingATrialKeyPlease": "Error creatiing a trial key. Please try again later.",
+  "userProfile.trial.errorCreatingATrialKeyPlease": "Error creating a trial key. Please try again later.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"userProfile.trial.errorCreatiingATrialKeyPlease": "Error creatiing a trial key. Please try again later.",
"userProfile.trial.errorCreatingATrialKeyPlease": "Error creating a trial key. Please try again later.",
🤖 Prompt for AI Agents
In web/src/locales/en.json around line 579 the translation key has a typo:
"userProfile.trial.errorCreatiingATrialKeyPlease" (double 'i' in "Creatiing");
update the key to "userProfile.trial.errorCreatingATrialKeyPlease" preserving
the existing value string exactly and ensure no other occurrences (search for
the old misspelled key and replace them if present) so references continue to
work.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 3, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

1 similar comment
@github-actions
Copy link
Contributor

github-actions bot commented Oct 3, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@codecov
Copy link

codecov bot commented Oct 3, 2025

Codecov Report

❌ Patch coverage is 28.83549% with 1155 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.64%. Comparing base (fabe6a2) to head (ab2e3e5).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
web/scripts/extract-translations.mjs 0.00% 488 Missing and 1 partial ⚠️
web/src/helpers/i18n-loader.ts 0.00% 93 Missing and 1 partial ⚠️
web/src/components/Logs/LogViewer.standalone.vue 0.00% 64 Missing ⚠️
...pi/graph/resolvers/sso/core/oidc-config.service.ts 45.61% 62 Missing ⚠️
...eb/src/components/UserProfile/CallbackFeedback.vue 0.00% 62 Missing ⚠️
web/scripts/sort-translations.mjs 0.00% 52 Missing and 1 partial ⚠️
web/src/components/Logs/LogFilterInput.vue 0.00% 26 Missing ⚠️
web/src/helpers/jsonforms-i18n.ts 0.00% 25 Missing and 1 partial ⚠️
api/src/unraid-api/graph/utils/form-utils.ts 36.84% 24 Missing ⚠️
unraid-ui/src/components/brand/BrandButton.vue 0.00% 24 Missing ⚠️
... and 27 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1739      +/-   ##
==========================================
- Coverage   53.35%   52.64%   -0.72%     
==========================================
  Files         860      866       +6     
  Lines       48278    49335    +1057     
  Branches     4929     4940      +11     
==========================================
+ Hits        25759    25972     +213     
- Misses      22450    23290     +840     
- Partials       69       73       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 3, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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

♻️ Duplicate comments (2)
web/__test__/components/Wrapper/mount-engine.test.ts (1)

49-65: Verified: TS2558 fix applied.

The previous TS2558 error has been resolved by using the correct Vitest function type annotation vi.fn<() => string | undefined>() at line 59.

web/scripts/extract-translations.mjs (1)

263-263: Path resolution still needs fixing.

As flagged in a previous review, apiSourceRoot at line 263 is resolved relative to process.cwd(), which breaks when the script is invoked from different working directories.

Resolve the path relative to the script file location instead:

+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
 async function collectJsonFormsKeys() {
-  const apiSourceRoot = path.resolve(process.cwd(), '../api/src');
+  const apiSourceRoot = path.resolve(__dirname, '../../api/src');
   const ignorePatterns = [
🧹 Nitpick comments (2)
web/__test__/components/Brand/Avatar.test.ts (1)

22-31: Clarify vue-i18n mock comment
Update the comment to reflect that this mock is for stores used in this component test, for example:

- // Mock vue-i18n for store tests
+ // Mock vue-i18n for stores used in component tests

This mock remains necessary because serverStore getters call useI18n.

web/__test__/components/SsoButton.test.ts (1)

13-13: Remove unused import.

testTranslate is imported but never used in this test file. Only createTestI18n is needed for the i18n plugin injection.

Apply this diff:

-import { createTestI18n, testTranslate } from '../utils/i18n';
+import { createTestI18n } from '../utils/i18n';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f48c85c and f25a468.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (23)
  • api/.eslintrc.ts (1 hunks)
  • api/src/types/jsonforms-i18n.d.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (32 hunks)
  • api/src/unraid-api/graph/utils/form-utils.ts (4 hunks)
  • web/__test__/components/Activation/ActivationModal.test.ts (3 hunks)
  • web/__test__/components/Activation/ActivationSteps.test.ts (2 hunks)
  • web/__test__/components/Auth.test.ts (5 hunks)
  • web/__test__/components/Brand/Avatar.test.ts (6 hunks)
  • web/__test__/components/KeyActions.test.ts (9 hunks)
  • web/__test__/components/Modals.test.ts (0 hunks)
  • web/__test__/components/Registration.test.ts (4 hunks)
  • web/__test__/components/SsoButton.test.ts (10 hunks)
  • web/__test__/components/UpdateOs.test.ts (9 hunks)
  • web/__test__/components/Wrapper/mount-engine.test.ts (3 hunks)
  • web/__test__/store/server.test.ts (1 hunks)
  • web/package.json (5 hunks)
  • web/scripts/extract-translations.mjs (1 hunks)
  • web/src/components/Activation/ActivationModal.vue (5 hunks)
  • web/src/components/Activation/ActivationSteps.vue (4 hunks)
  • web/src/components/KeyActions.vue (1 hunks)
  • web/src/components/Modals.standalone.vue (1 hunks)
  • web/src/components/Notifications/List.vue (4 hunks)
  • web/src/locales/en.json (1 hunks)
💤 Files with no reviewable changes (1)
  • web/test/components/Modals.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • web/src/components/Notifications/List.vue
  • web/test/components/UpdateOs.test.ts
  • web/test/components/Activation/ActivationModal.test.ts
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
  • web/src/locales/en.json
🧰 Additional context used
📓 Path-based instructions (11)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/store/server.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/__test__/components/Registration.test.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
  • web/__test__/components/KeyActions.test.ts
  • api/src/types/jsonforms-i18n.d.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/store/server.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/store/server.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/store/server.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/store/server.test.ts
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/utils/form-utils.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/utils/form-utils.ts
  • api/src/types/jsonforms-i18n.d.ts
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/KeyActions.vue
  • web/src/components/Activation/ActivationSteps.vue
  • web/src/components/Modals.standalone.vue
  • web/src/components/Activation/ActivationModal.vue
**/__test__/store/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/store/**/*.ts: For Pinia store unit tests, initialize with setActivePinia(createPinia()) rather than createTestingPinia, unless testing features from createTestingPinia are explicitly needed
Do not mock the store under test; allow the store to initialize with its natural default state
In store tests, verify action side effects and state changes, and assert that actions are called with the correct parameters
In store tests, mock external dependencies used by the store and verify interactions with those mocks
Test computed properties/getters by accessing them directly and verifying derived state
Thoroughly test async store actions: assert intermediate loading state, await completion, and check final state

Files:

  • web/__test__/store/server.test.ts
**/{store,stores}/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Files:

  • web/__test__/store/server.test.ts
🧠 Learnings (24)
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Activation/ActivationSteps.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/store/server.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use createTestingPinia() for mocking stores in components

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/store/server.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/store/server.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
  • web/__test__/store/server.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/Registration.test.ts
  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript import specifiers with .js extensions for ESM compatibility

Applied to files:

  • api/.eslintrc.ts
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
PR: unraid/api#1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • api/.eslintrc.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/components/KeyActions.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)

Applied to files:

  • web/__test__/components/KeyActions.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()

Applied to files:

  • web/__test__/components/Auth.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : For store tests, use createPinia() and setActivePinia

Applied to files:

  • web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : For Pinia store unit tests, initialize with setActivePinia(createPinia()) rather than createTestingPinia, unless testing features from createTestingPinia are explicitly needed

Applied to files:

  • web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior

Applied to files:

  • web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Always run pnpm codegen for GraphQL in the web directory

Applied to files:

  • web/package.json
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Always run `pnpm codegen` for GraphQL code generation in the web directory

Applied to files:

  • web/package.json
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store under test; allow the store to initialize with its natural default state

Applied to files:

  • web/__test__/store/server.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Let stores initialize with natural default state; don’t mock the store under test

Applied to files:

  • web/__test__/store/server.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Mock external dependencies and services in component tests

Applied to files:

  • web/__test__/store/server.test.ts
🧬 Code graph analysis (9)
web/__test__/components/Registration.test.ts (1)
web/__test__/utils/i18n.ts (2)
  • testTranslate (43-47)
  • createTestI18n (49-58)
web/scripts/extract-translations.mjs (1)
api/scripts/extract-translations.mjs (8)
  • raw (90-90)
  • content (143-143)
  • sourceFile (144-144)
  • keys (54-54)
  • keys (145-145)
  • firstArg (71-71)
  • ignorePatterns (10-17)
  • files (133-137)
web/__test__/components/Activation/ActivationSteps.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • createTestI18n (49-58)
web/__test__/components/KeyActions.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • createTestI18n (49-58)
web/__test__/components/Wrapper/mount-engine.test.ts (1)
web/src/components/Wrapper/mount-engine.ts (1)
  • mountUnifiedApp (80-198)
web/__test__/components/Auth.test.ts (1)
web/__test__/utils/i18n.ts (2)
  • testTranslate (43-47)
  • createTestI18n (49-58)
web/__test__/components/Brand/Avatar.test.ts (1)
web/__test__/utils/i18n.ts (2)
  • testTranslate (43-47)
  • createTestI18n (49-58)
web/__test__/components/SsoButton.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • createTestI18n (49-58)
web/__test__/store/server.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • testTranslate (43-47)
🪛 GitHub Check: CodeQL
web/__test__/components/KeyActions.test.ts

[notice] 30-30: Unused variable, import, function or class
Unused variable t.

web/__test__/components/SsoButton.test.ts

[notice] 13-13: Unused variable, import, function or class
Unused import testTranslate.

web/__test__/store/server.test.ts

[notice] 22-22: Unused variable, import, function or class
Unused import createTestI18n.

🔇 Additional comments (25)
api/.eslintrc.ts (1)

44-48: LGTM! Correct ESM import extension rules.

The updated configuration properly enforces ESM import conventions:

  • mjs: 'always' and cjs: 'always' ensure module variant extensions are explicit
  • ts: 'never' and tsx: 'never' correctly prevent TypeScript source extensions in imports (since imports resolve to compiled .js files)

This change aligns with the project's ESM setup and supports the new .mjs scripts added in this PR (e.g., extract-translations.mjs).

Based on learnings: The codebase uses ESM and requires .js extensions in import statements for TypeScript files.

web/__test__/components/Wrapper/mount-engine.test.ts (2)

80-87: LGTM: Proper mock isolation.

Mock cleanup correctly uses mockClear() for call history and mockReset() for implementation reset, ensuring test isolation.


412-427: LGTM: i18n setup tests verify correct behavior.

Tests properly verify that ensureLocale is called with the expected locale (undefined for default, or the window locale when available), focusing on observable behavior rather than implementation details.

web/__test__/components/Brand/Avatar.test.ts (2)

9-9: LGTM!

The import of test i18n utilities is correctly placed and follows explicit import guidelines.


54-54: LGTM!

The addition of createTestI18n() to the global plugins array in each test mount is correct. This ensures the Avatar component has access to i18n functionality during testing, aligning with the test i18n utilities introduced in this PR.

Also applies to: 72-72, 90-90, 109-109

web/src/components/KeyActions.vue (1)

3-3: LGTM! Clean i18n migration.

The component correctly migrates from receiving t as a prop to using the i18n composition API. The translation function is properly initialized and used in the template for both action.text and action.title.

Also applies to: 13-13, 60-61

web/package.json (2)

33-35: LGTM! Well-organized i18n workflow scripts.

The new scripts properly chain extraction and sorting, providing a clean workflow for managing translations.


116-116: No duplicate 'convert' entry detected. The convert package is listed only under dependencies, not in devDependencies.

Likely an incorrect or invalid review comment.

web/__test__/components/KeyActions.test.ts (1)

15-15: LGTM! Proper i18n test setup.

The test correctly imports and uses createTestI18n() to provide i18n context during component mounting. This aligns with the broader i18n testing strategy.

Also applies to: 46-48

web/scripts/extract-translations.mjs (1)

485-514: Good protection against over-deletion.

The script correctly preserves protected keys (jsonforms, descriptions, labels, TS translation keys) and maybeDynamic keys to prevent breaking dynamic translations. This prevents the extractor from removing keys that are referenced indirectly or computed at runtime.

web/__test__/components/Activation/ActivationSteps.test.ts (1)

10-10: LGTM! Consistent i18n test integration.

The test properly adds createTestI18n() to provide i18n context, consistent with the broader test migration strategy.

Also applies to: 63-65

web/src/components/Modals.standalone.vue (1)

34-34: LGTM! ActivationModal migration complete.

ActivationModal now uses the i18n composition API internally and no longer requires the t prop. The gradual migration approach (other modals still using prop-based translation) is appropriate.

web/__test__/store/server.test.ts (1)

24-29: LGTM! Proper i18n mocking for store tests.

The test correctly mocks vue-i18n's useI18n to return testTranslate, enabling the store to resolve translations during tests without requiring full i18n infrastructure.

Also applies to: 127-127

web/__test__/components/Registration.test.ts (1)

19-19: LGTM! Comprehensive i18n test integration.

The test properly:

  • Imports both createTestI18n and testTranslate
  • Mocks vue-i18n while preserving the actual module exports
  • Injects createTestI18n() into the component mount plugins
  • Uses testTranslate for translation resolution

This follows the established testing pattern for i18n-enabled components.

Also applies to: 30-39, 127-127, 178-178

web/__test__/components/Auth.test.ts (2)

16-26: LGTM! Static analysis warning is a false positive.

The i18n mock setup correctly uses the test utilities. testTranslate is used within the mock factory (line 23) to provide the t function to components under test. The static analysis tool doesn't recognize usage within vi.mock factory functions.


73-73: LGTM! Consistent i18n plugin injection.

All mount calls correctly inject the test i18n plugin via createTestI18n(), providing translation context for the Auth component in tests. This aligns with the broader i18n testing strategy across the codebase.

Also applies to: 97-97, 121-121, 142-142

web/__test__/components/SsoButton.test.ts (1)

149-149: LGTM! Consistent i18n plugin injection across all tests.

All nine test cases correctly inject the test i18n plugin via createTestI18n(), ensuring translation context is available for the SsoButtons component.

Also applies to: 174-174, 203-203, 241-241, 290-290, 330-330, 374-374, 413-413, 468-468

web/src/components/Activation/ActivationSteps.vue (3)

2-3: LGTM! Necessary imports for i18n integration.

The addition of computed and useI18n enables the component to dynamically generate localized step content.


44-44: LGTM! Standard i18n setup.

Correctly initializes the translation function using the useI18n composition API.


46-77: LGTM – translation keys verified
All activation.activationSteps.* keys are present in web/src/locales.

web/src/components/Activation/ActivationModal.vue (4)

3-3: LGTM! Standard i18n composition API setup.

The migration from prop-based translation to composition API is correct. This is a breaking change in the component's public API—it no longer accepts t as a prop and instead uses the global i18n instance.

Also applies to: 19-19


28-31: LGTM! Proper reactive translation setup.

Title and description correctly use computed with translation keys for reactive localization.


32-51: LGTM! Documentation buttons properly localized.

The docsButtons computed correctly uses translation keys for button text while preserving all other button properties (links, icons, variants).


78-78: All ActivationModal translation keys verified. Button text properly localized; no missing keys.

api/src/types/jsonforms-i18n.d.ts (1)

6-40: Consolidate redundant module augmentations.
We detected no imports of @jsonforms/core/(lib|src)/models/jsonSchema4(.js)? or jsonSchema7 in the codebase, so it’s unclear which specifiers your consumers actually use. Verify which paths are imported (or re-exported via your package’s exports), then remove unused augmentations and keep only the canonical module(s).

Comment on lines +1 to +4
import '@jsonforms/core/lib/models/jsonSchema4';
import '@jsonforms/core/lib/models/jsonSchema7';
import '@jsonforms/core/src/models/jsonSchema4';
import '@jsonforms/core/src/models/jsonSchema7';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify necessity of side-effect imports for module augmentation.

In TypeScript declaration files, module augmentation via declare module typically does not require explicit side-effect imports. These imports may be redundant.

Additionally, per the coding guidelines, TypeScript imports should use .js extensions for ESM compatibility. If these imports are necessary, consider:

-import '@jsonforms/core/lib/models/jsonSchema4';
-import '@jsonforms/core/lib/models/jsonSchema7';
-import '@jsonforms/core/src/models/jsonSchema4';
-import '@jsonforms/core/src/models/jsonSchema7';
+import '@jsonforms/core/lib/models/jsonSchema4.js';
+import '@jsonforms/core/lib/models/jsonSchema7.js';
+import '@jsonforms/core/src/models/jsonSchema4.js';
+import '@jsonforms/core/src/models/jsonSchema7.js';

However, if these imports serve no purpose beyond TypeScript module resolution, they can likely be removed entirely.

As per coding guidelines.

Run the following script to check @jsonforms/core package structure and determine which module paths are actually exported:


🏁 Script executed:

#!/bin/bash
# Description: Examine @jsonforms/core package.json to identify exported module paths.

# Check if package.json exists and extract exports/main fields
fd -t f "package.json" api/node_modules/@jsonforms/core --exec cat {} \; | jq -r '.exports, .main, .module, .types'

Length of output: 119


🏁 Script executed:

#!/bin/bash
# Fetch package metadata for @jsonforms/core from npm registry
npm view @jsonforms/core --json | jq -r '.exports, .main, .module, .types'

Length of output: 61


Consolidate JSONForms module augmentations to .js ESM paths

  • Remove imports and declare module blocks for @jsonforms/core/src/models/* (these source‐path modules aren’t published).
  • Update remaining side-effect imports and declare module specifiers to use .js extensions, e.g.:
    • @jsonforms/core/lib/models/jsonSchema4.js
    • @jsonforms/core/lib/models/jsonSchema7.js
🤖 Prompt for AI Agents
In api/src/types/jsonforms-i18n.d.ts around lines 1 to 4, remove the side-effect
imports that reference non-published source paths
(@jsonforms/core/src/models/jsonSchema4 and jsonSchema7) and update the
remaining module augmentation imports and any declare module specifiers to use
the published ESM .js paths (e.g. @jsonforms/core/lib/models/jsonSchema4.js and
@jsonforms/core/lib/models/jsonSchema7.js); delete the two src/* import lines
and replace the lib/* import specifiers to include the .js extension so
TypeScript resolves the published ESM modules correctly.

Comment on lines +22 to +42
const labelElement = {
type: 'Label',
text: label,
options: {
description,
},
} as LabelElement;

if (i18nKey) {
(labelElement as any).i18n = i18nKey;
}

const controlElement = {
type: 'Control',
scope: scope,
options: controlOptions,
} as ControlElement;

if (i18nKey) {
(controlElement as any).i18n = i18nKey;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Eliminate as any on the JSONForms elements.

We now violate the no-any rule while attaching i18n. Widen the element types so the property can be set without casting.

-    const labelElement = {
-        type: 'Label',
-        text: label,
-        options: {
-            description,
-        },
-    } as LabelElement;
-
-    if (i18nKey) {
-        (labelElement as any).i18n = i18nKey;
-    }
+    const labelElement: LabelElement & { i18n?: string } = {
+        type: 'Label',
+        text: label,
+        options: {
+            description,
+        },
+    };
+
+    if (i18nKey) {
+        labelElement.i18n = i18nKey;
+    }
 
-    const controlElement = {
-        type: 'Control',
-        scope: scope,
-        options: controlOptions,
-    } as ControlElement;
-
-    if (i18nKey) {
-        (controlElement as any).i18n = i18nKey;
-    }
+    const controlElement: ControlElement & { i18n?: string } = {
+        type: 'Control',
+        scope,
+        options: controlOptions,
+    };
+
+    if (i18nKey) {
+        controlElement.i18n = i18nKey;
+    }

As per coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const labelElement = {
type: 'Label',
text: label,
options: {
description,
},
} as LabelElement;
if (i18nKey) {
(labelElement as any).i18n = i18nKey;
}
const controlElement = {
type: 'Control',
scope: scope,
options: controlOptions,
} as ControlElement;
if (i18nKey) {
(controlElement as any).i18n = i18nKey;
}
const labelElement: LabelElement & { i18n?: string } = {
type: 'Label',
text: label,
options: {
description,
},
};
if (i18nKey) {
labelElement.i18n = i18nKey;
}
const controlElement: ControlElement & { i18n?: string } = {
type: 'Control',
scope,
options: controlOptions,
};
if (i18nKey) {
controlElement.i18n = i18nKey;
}
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/utils/form-utils.ts around lines 22 to 42, the code
uses `as any` to attach an `i18n` property to JSONForms Label and Control
elements; instead, update the TypeScript types so `i18n` can be set without
casting. Add or import widened interfaces (e.g., declare interfaces
LocalizedLabelElement extends LabelElement { i18n?: string } and
LocalizedControlElement extends ControlElement { i18n?: string } or use
intersection types LabelElement & { i18n?: string } and ControlElement & {
i18n?: string }), then type `labelElement` and `controlElement` with those
widened types and set `.i18n = i18nKey` without any casting.

Comment on lines +83 to +101
const labelElement = {
type: 'Label',
text: label,
options: { ...labelOptions, description },
} as LabelElement;

if (i18nKey) {
(labelElement as any).i18n = i18nKey;
}

const controlElement = {
type: 'Control',
scope: scope,
options: controlOptions,
} as ControlElement;

if (i18nKey) {
(controlElement as any).i18n = i18nKey;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Apply the same typed approach here.

This block repeats the as any pattern; please widen the element types instead.

-    const labelElement = {
-        type: 'Label',
-        text: label,
-        options: { ...labelOptions, description },
-    } as LabelElement;
-
-    if (i18nKey) {
-        (labelElement as any).i18n = i18nKey;
-    }
+    const labelElement: LabelElement & { i18n?: string } = {
+        type: 'Label',
+        text: label,
+        options: { ...labelOptions, description },
+    };
+
+    if (i18nKey) {
+        labelElement.i18n = i18nKey;
+    }
 
-    const controlElement = {
-        type: 'Control',
-        scope: scope,
-        options: controlOptions,
-    } as ControlElement;
-
-    if (i18nKey) {
-        (controlElement as any).i18n = i18nKey;
-    }
+    const controlElement: ControlElement & { i18n?: string } = {
+        type: 'Control',
+        scope,
+        options: controlOptions,
+    };
+
+    if (i18nKey) {
+        controlElement.i18n = i18nKey;
+    }

As per coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const labelElement = {
type: 'Label',
text: label,
options: { ...labelOptions, description },
} as LabelElement;
if (i18nKey) {
(labelElement as any).i18n = i18nKey;
}
const controlElement = {
type: 'Control',
scope: scope,
options: controlOptions,
} as ControlElement;
if (i18nKey) {
(controlElement as any).i18n = i18nKey;
}
const labelElement: LabelElement & { i18n?: string } = {
type: 'Label',
text: label,
options: { ...labelOptions, description },
};
if (i18nKey) {
labelElement.i18n = i18nKey;
}
const controlElement: ControlElement & { i18n?: string } = {
type: 'Control',
scope,
options: controlOptions,
};
if (i18nKey) {
controlElement.i18n = i18nKey;
}
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/utils/form-utils.ts around lines 83 to 101, the code
uses (labelElement as any).i18n and (controlElement as any).i18n; change this to
a typed approach by widening the element types: either add an optional i18n?:
string property to the LabelElement and ControlElement interfaces (preferred)
or, if you cannot modify the shared interfaces, use local intersection types
when creating the objects (e.g., const labelElement: LabelElement & { i18n?:
string } = ... and const controlElement: ControlElement & { i18n?: string } =
...) and then assign i18nKey without using any casts.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 5, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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

♻️ Duplicate comments (1)
web/src/locales/da.json (1)

466-466: Translate remaining English string

This entry is still English; please supply the Danish translation (e.g., “Bekræftelse på opdatering af Unraid OS kræves”) to keep the locale consistent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f25a468 and 57bef96.

📒 Files selected for processing (12)
  • web/src/locales/ar.json (1 hunks)
  • web/src/locales/bn.json (1 hunks)
  • web/src/locales/ca.json (1 hunks)
  • web/src/locales/cs.json (1 hunks)
  • web/src/locales/da.json (1 hunks)
  • web/src/locales/de.json (1 hunks)
  • web/src/locales/es.json (1 hunks)
  • web/src/locales/fr.json (1 hunks)
  • web/src/locales/hi.json (1 hunks)
  • web/src/locales/hr.json (1 hunks)
  • web/src/locales/hu.json (1 hunks)
  • web/src/locales/it.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • web/src/locales/es.json
🚧 Files skipped from review as they are similar to previous changes (8)
  • web/src/locales/ar.json
  • web/src/locales/hu.json
  • web/src/locales/ca.json
  • web/src/locales/fr.json
  • web/src/locales/bn.json
  • web/src/locales/hr.json
  • web/src/locales/de.json
  • web/src/locales/cs.json

Comment on lines +147 to +156
"logs.filterLabel": "फिल्टर",
"logs.filterPlaceholder": "लॉग्स छानें...",
"logs.hideLogs": "लॉग्स छुपाएँ",
"logs.presets.auth": "प्रमाणीकरण",
"logs.presets.error": "त्रुटियाँ",
"logs.presets.none": "कोई फिल्टर नहीं",
"logs.presets.oidc": "OIDC लॉग्स",
"logs.presets.warning": "चेतावनियाँ",
"logs.quickFilterLabel": "त्वरित {लेबल}",
"logs.refreshLogs": "लॉग्स ताज़ा करें",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore the exact placeholder tokens.

{label} is part of the runtime interpolation contract; translating it to {लेबल} causes Vue I18n to fall back to the literal token so users will see braces instead of the dynamic value. Same issue in both lines. Please keep the placeholder name untouched.

-  "logs.customFilterLabel": "कस्टम {लेबल}",
+  "logs.customFilterLabel": "कस्टम {label}",
...
-  "logs.quickFilterLabel": "त्वरित {लेबल}",
+  "logs.quickFilterLabel": "त्वरित {label}",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"logs.filterLabel": "फिल्टर",
"logs.filterPlaceholder": "लॉग्स छानें...",
"logs.hideLogs": "लॉग्स छुपाएँ",
"logs.presets.auth": "प्रमाणीकरण",
"logs.presets.error": "त्रुटियाँ",
"logs.presets.none": "कोई फिल्टर नहीं",
"logs.presets.oidc": "OIDC लॉग्स",
"logs.presets.warning": "चेतावनियाँ",
"logs.quickFilterLabel": "त्वरित {लेबल}",
"logs.refreshLogs": "लॉग्स ताज़ा करें",
"logs.customFilterLabel": "कस्टम {label}",
"logs.quickFilterLabel": "त्वरित {label}",
🤖 Prompt for AI Agents
In web/src/locales/hi.json around lines 147 to 156, the placeholder tokens were
translated (e.g., "{लेबल}") which breaks runtime interpolation; restore the
exact placeholder names used in code (replace "{लेबल}" with "{label}" in any
affected strings such as "logs.quickFilterLabel" and any other occurrences) so
Vue I18n can substitute the dynamic value, preserving original token spelling
and braces exactly.

"notifications.item.delete": "हटाएं",
"notifications.item.viewLink": "देखें",
"notifications.list.noNotifications": "यहां देखने के लिए कोई सूचनाएं नहीं हैं!",
"notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {महत्व} सूचनाएं नहीं हैं!",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep placeholder identifiers untranslated.

Interpolated key {importance} was localized to {महत्व}, which breaks substitution in the UI. Restore the original token so the importance value renders properly.

-  "notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {महत्व} सूचनाएं नहीं हैं!",
+  "notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {importance} सूचनाएं नहीं हैं!",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {महत्व} सूचनाएं नहीं हैं!",
"notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {importance} सूचनाएं नहीं हैं!",
🤖 Prompt for AI Agents
In web/src/locales/hi.json around line 209, the interpolation token
`{importance}` was translated to `{महत्व}`, breaking runtime substitution;
restore the original placeholder `{importance}` (keep placeholder identifiers
untranslated) so the UI can correctly substitute the importance value, and save
the file.

"server.state.enoflash.message": "<p>आपके USB फ्लैश बूट डिवाइस तक पहुँचने में भौतिक समस्या है</p>",
"server.state.enokeyfile.heading": "चलो आपके हार्डवेयर को बाहर निकालें",
"server.state.enokeyfile.humanReadable": "कोई कीफाइल नहीं",
"server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix rel attribute typo in embedded link.

The HTML snippet misspells noreferrer, so the protective attribute is lost. Please correct the value to match the English source.

-  "server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर  <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>",
+  "server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर  <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>",
"server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>",
🤖 Prompt for AI Agents
In web/src/locales/hi.json around line 323, the embedded link's rel attribute is
misspelled as "noreffer noopener"; update it to "noreferrer noopener" to match
the English source and restore the intended protective attributes.

"server.state.enoflash.message": "<p>C'è un problema fisico nell'accesso al tuo dispositivo di avvio USB Flash</p>",
"server.state.enokeyfile.heading": "Liberiamo le tue risorse hardware",
"server.state.enokeyfile.humanReadable": "Nessun File Chiave",
"server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore rel="noreferrer" for security parity.

The typo rel="noreffer noopener" drops the noreferrer token, so referer headers will leak where the English source intentionally suppressed them. Please fix the attribute spelling.

-  "server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>",
+  "server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>",
"server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>",
🤖 Prompt for AI Agents
In web/src/locales/it.json around line 323, the rel attribute in the anchor is
misspelled as "noreffer noopener"; update it to "noreferrer noopener" so the
HTML becomes rel="noreferrer noopener" (preserve the existing surrounding string
quoting and escape sequences).

@github-actions
Copy link
Contributor

github-actions bot commented Oct 6, 2025

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57bef96 and e61ee74.

📒 Files selected for processing (4)
  • web/__test__/components/Activation/ActivationModal.test.ts (3 hunks)
  • web/__test__/components/KeyActions.test.ts (8 hunks)
  • web/__test__/components/SsoButton.test.ts (10 hunks)
  • web/__test__/store/server.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/test/store/server.test.ts
  • web/test/components/Activation/ActivationModal.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/KeyActions.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/KeyActions.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/KeyActions.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/KeyActions.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/KeyActions.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/SsoButton.test.ts
  • web/__test__/components/KeyActions.test.ts
🧠 Learnings (11)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior

Applied to files:

  • web/__test__/components/KeyActions.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)

Applied to files:

  • web/__test__/components/KeyActions.test.ts
🧬 Code graph analysis (2)
web/__test__/components/SsoButton.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • createTestI18n (49-58)
web/__test__/components/KeyActions.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • createTestI18n (49-58)

import type { ServerStateDataAction, ServerStateDataActionType } from '~/types/server';

import KeyActions from '~/components/KeyActions.vue';
import { createTestI18n } from '../utils/i18n';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add the .js extension to the ESM import.

Our TypeScript files must use ESM-friendly specifiers. Importing ../utils/i18n without the .js suffix will break resolution under NodeNext/Bundler module resolution and violates the project’s TS guideline. Please switch the specifier to ../utils/i18n.js.

[As per coding guidelines]

-import { createTestI18n } from '../utils/i18n';
+import { createTestI18n } from '../utils/i18n.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createTestI18n } from '../utils/i18n';
import { createTestI18n } from '../utils/i18n.js';
🤖 Prompt for AI Agents
In web/__test__/components/KeyActions.test.ts around line 15, the ESM import
specifier "import { createTestI18n } from '../utils/i18n';" is missing the
required .js extension; update the import to use the ESM-friendly path
"../utils/i18n.js" so NodeNext/bundlers resolve the module correctly and comply
with the project's TypeScript ESM guidelines.

import type { Mock, MockInstance } from 'vitest';

import SsoButtons from '~/components/sso/SsoButtons.vue';
import { createTestI18n } from '../utils/i18n';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add the .js extension for ESM compatibility

Per our TS/ESM rules, specifiers must include the .js suffix so the compiled code resolves at runtime. Please update the import accordingly. As per coding guidelines.

-import { createTestI18n } from '../utils/i18n';
+import { createTestI18n } from '../utils/i18n.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createTestI18n } from '../utils/i18n';
import { createTestI18n } from '../utils/i18n.js';
🤖 Prompt for AI Agents
In web/__test__/components/SsoButton.test.ts around line 13, the import
specifier for createTestI18n is missing the required .js extension for ESM;
update the import to include the .js suffix (i.e., import from
'../utils/i18n.js') so compiled ESM code resolves at runtime and adjust any
related test/import paths if necessary.

…ripts

- Added `crowdin.yml` for Crowdin integration to manage translation files.
- Introduced `i18n:extract` scripts in both `api` and `web` packages to automate extraction of translation keys from source files.
- Created initial English locale files in both `api/src/i18n/en.json` and `web/src/locales/en.json`.
- Updated README files to include instructions for running the extraction scripts and details on the internationalization process.
- Refactored i18n setup in the application to utilize the new extraction and loading mechanisms.

This commit enhances the application's internationalization capabilities, allowing for easier management and addition of translations.
…ing of the script

This commit cleans up the `extract-translations.mjs` file by removing an extraneous newline, improving the overall readability of the script without altering its functionality.
…mponents

- Introduced i18n keys for API key and OIDC configuration forms, improving localization capabilities.
- Updated form utility functions to support i18n integration for labels and descriptions.
- Added a new helper for managing i18n state in Vue components, ensuring consistent translation handling.
- Enhanced existing components to utilize the new i18n features, including ApiKeyCreate and ConnectSettings.
- Updated English locale files with new keys for API settings and OIDC provider configurations.

This commit significantly improves the application's internationalization, making it easier to manage translations and enhance user experience across different languages.
This commit adds a newline at the beginning of the `extract-translations.mjs` file to enhance the overall readability of the script. This change does not affect the functionality of the script.
- Added a new script `sort-translations.mjs` to automate the sorting of translation keys in locale JSON files, improving organization and maintainability.
- Updated the `i18n:extract` command in `package.json` to include the new sorting step, ensuring translations are consistently ordered after extraction.
- Refactored various components to utilize new i18n keys for improved localization, enhancing user experience across the application.

This commit significantly improves the internationalization process, making it easier to manage and maintain translation files.
…nents

- Integrated i18n support in ConnectSettings, updating labels and messages to utilize translation keys for improved user experience.
- Refactored OidcDebugLogs and LogViewerToolbar components to leverage i18n for dynamic text rendering, enhancing consistency across the application.
- Updated LogFilterInput to support localized preset filters and placeholders, improving accessibility and usability.
- Added new translation keys to the English locale file for better management of UI text.

These changes significantly improve the internationalization of the application, making it more user-friendly and adaptable to different languages.
…ponents

- Integrated i18n support in LogViewer and SingleLogViewer, updating labels, messages, and error handling to utilize translation keys for improved user experience.
- Refactored log file size formatting and loading messages to be dynamically translated, enhancing accessibility and usability.
- Updated English locale file with new translation keys for log viewer functionalities, ensuring consistent internationalization across the application.

These changes significantly improve the internationalization of the logging components, making them more user-friendly and adaptable to different languages.
- Changed translation file paths in `crowdin.yml` to use two-letter locale codes for consistency.
- Added `convert` package version 5.12.0 to `pnpm-lock.yaml` and `package.json`.
- Restored `glob` package version 11.0.3 in `package.json` after it was previously removed.

These changes enhance the localization setup and ensure proper dependency management for the project.
…ences

- Deleted the translations.php file from the dynamix.my.servers plugin, streamlining the localization process.
- Updated backup and exit scripts to remove references to the deleted translations.php file, ensuring consistency across the application.
- Enhanced test files to utilize a new test translation utility, improving internationalization support in various components.

These changes simplify the localization setup and improve the overall maintainability of the codebase.
…ation

- Introduced `i18n` property to JSON Schema 4 and 7 interfaces, enhancing localization capabilities for schemas.
- Updated OIDC configuration to correct `i18n` references for allowed domains, emails, and user IDs, ensuring accurate localization.
- Added `i18n` support in accordion layout configuration, improving internationalization for UI components.

These changes enhance the internationalization framework, making it more robust and user-friendly across the application.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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 (3)
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)

1104-1111: Use distinct i18n keys for semantically different fields.

Both authorizationMode (lines 1104-1111) and authorizationRuleMode (lines 1229-1236) use the same i18n key OIDC_I18N.rules.mode, but they control different aspects:

  • authorizationMode: UI presentation (simple vs advanced interface)
  • authorizationRuleMode: Rule evaluation logic (OR vs AND)

These distinct concepts should have separate i18n keys to ensure accurate translations.

Add a new key to the OIDC_I18N constant:

 const OIDC_I18N = {
     // ...
     rules: {
         mode: 'jsonforms.oidc.rules.mode',
+        evaluationMode: 'jsonforms.oidc.rules.evaluationMode',
         claim: 'jsonforms.oidc.rules.claim',
         operator: 'jsonforms.oidc.rules.operator',
         value: 'jsonforms.oidc.rules.value',
         collection: 'jsonforms.oidc.rules.collection',
         sectionTitle: 'jsonforms.oidc.rules.title',
         sectionDescription: 'jsonforms.oidc.rules.description',
     },
     // ...
 } as const;

Then update line 1235 to use the new key:

-                                                            i18nKey: OIDC_I18N.rules.mode,
+                                                            i18nKey: OIDC_I18N.rules.evaluationMode,

And update the schema property definition around line 794:

                             authorizationRuleMode: {
                                 type: 'string',
-                                i18n: OIDC_I18N.rules.mode,
+                                i18n: OIDC_I18N.rules.evaluationMode,
                                 title: 'Rule Mode',

Also applies to: 1229-1236

web/src/components/Logs/SingleLogViewer.vue (2)

403-406: Bug: canLoadMore returns a ComputedRef, not boolean

Pass the boolean value, not the ref.

-        { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore },
+        { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore.value },

260-310: Avoid duplicate subscriptions; clean up on refresh

startLogSubscription is called on mount and again after refresh, but no unsubscribe is tracked. This can create multiple active subscriptions.

-const startLogSubscription = () => {
+let stopSubscription: (() => void) | null = null;
+const startLogSubscription = () => {
   if (!props.logFilePath) return;
   try {
-    subscribeToMore({
+    // Stop previous subscription before starting a new one
+    stopSubscription?.();
+    const maybeStop = subscribeToMore({
       document: LOG_FILE_SUBSCRIPTION,
       variables: { path: props.logFilePath },
       updateQuery: (prev, { subscriptionData }) => {
         ...
       },
     });
+    // Some apollo clients return an unsubscribe function; if available, keep it
+    if (typeof maybeStop === 'function') stopSubscription = maybeStop as () => void;
     state.isSubscriptionActive = true;
   } catch (error) {
     console.error('Error starting log subscription:', error);
     state.isSubscriptionActive = false;
   }
};
+onUnmounted(() => stopSubscription?.());

If subscribeToMore doesn’t return an unsubscribe function in this wrapper, store and call the result of client.subscribe/observable instead.

Also applies to: 319-328

♻️ Duplicate comments (11)
api/src/unraid-api/graph/utils/form-utils.ts (2)

22-42: Duplicate: Eliminate as any casts on JSONForms elements.

The as any casts on lines 31 and 41 still violate the coding guidelines ("Never use the any type"). The fix was already suggested in a previous review: widen the element types using intersection types so the i18n property can be set without casting.

Apply the previously suggested diff:

-    const labelElement = {
-        type: 'Label',
-        text: label,
-        options: {
-            description,
-        },
-    } as LabelElement;
-
-    if (i18nKey) {
-        (labelElement as any).i18n = i18nKey;
-    }
+    const labelElement: LabelElement & { i18n?: string } = {
+        type: 'Label',
+        text: label,
+        options: {
+            description,
+        },
+    };
+
+    if (i18nKey) {
+        labelElement.i18n = i18nKey;
+    }
 
-    const controlElement = {
-        type: 'Control',
-        scope: scope,
-        options: controlOptions,
-    } as ControlElement;
-
-    if (i18nKey) {
-        (controlElement as any).i18n = i18nKey;
-    }
+    const controlElement: ControlElement & { i18n?: string } = {
+        type: 'Control',
+        scope,
+        options: controlOptions,
+    };
+
+    if (i18nKey) {
+        controlElement.i18n = i18nKey;
+    }

As per coding guidelines.


83-101: Duplicate: Apply the same typed approach here.

This block repeats the as any pattern on lines 90 and 100. Please widen the element types using intersection types as suggested in the previous review.

Apply the previously suggested diff:

-    const labelElement = {
-        type: 'Label',
-        text: label,
-        options: { ...labelOptions, description },
-    } as LabelElement;
-
-    if (i18nKey) {
-        (labelElement as any).i18n = i18nKey;
-    }
+    const labelElement: LabelElement & { i18n?: string } = {
+        type: 'Label',
+        text: label,
+        options: { ...labelOptions, description },
+    };
+
+    if (i18nKey) {
+        labelElement.i18n = i18nKey;
+    }
 
-    const controlElement = {
-        type: 'Control',
-        scope: scope,
-        options: controlOptions,
-    } as ControlElement;
-
-    if (i18nKey) {
-        (controlElement as any).i18n = i18nKey;
-    }
+    const controlElement: ControlElement & { i18n?: string } = {
+        type: 'Control',
+        scope,
+        options: controlOptions,
+    };
+
+    if (i18nKey) {
+        controlElement.i18n = i18nKey;
+    }

As per coding guidelines.

web/src/components/Modal.vue (2)

100-100: Duplicate: Missing i18n key already flagged.

This issue has been previously identified and verified. The key modal.clickToCloseModal is missing from the locale files.


129-129: Duplicate: Missing i18n key already flagged.

This issue has been previously identified and verified. The key common.close is missing from the locale files.

unraid-ui/src/forms/AccordionLayout.vue (1)

24-24: Critical: Double cast worsens type safety.

The cast changed from element as UISchemaElement to element as unknown as UISchemaElement, which is MORE permissive and completely bypasses TypeScript's type checking. This is a regression from the previous version and conflicts with the past review suggestion.

Additionally, there's a type inconsistency: your helper functions (getAccordionTitle, getAccordionDescription) accept BaseUISchemaElement (lines 130, 140), but here you're forcing a cast to UISchemaElement. This suggests either:

  1. Elements should be typed as UISchemaElement[] if that's what DispatchRenderer requires
  2. Or DispatchRenderer should accept BaseUISchemaElement if that's the actual type

Apply this diff to strongly type the elements computed property and remove the unsafe cast:

 const elements = computed(() => {
   const allElements = props.uischema.elements || [];
-
   // Filter elements based on visibility rules
-  return allElements.filter((element) => {
+  return allElements.filter((element): element is UISchemaElement => {
     const elementWithRule = element as BaseUISchemaElement;
     if (!elementWithRule.rule) {
       // No rule means always visible
       return true;
     }
     // ... rest of filter logic
-  });
+  }) as UISchemaElement[];
 });

Then in the template, remove the cast:

 <DispatchRenderer
   :schema="layout.schema"
-  :uischema="element as unknown as UISchemaElement"
+  :uischema="element"
   :path="layout.path || ''"
   :enabled="layout.enabled"
   :renderers="layout.renderers"
   :cells="layout.cells"
 />
web/src/components/UpdateOs/UpdateIneligible.vue (1)

41-45: Fix incorrect boolean interpolation in heading.

Line 42 passes availableWithRenewal.value (a boolean) to the translation, but the heading expects the release version string. This will result in displaying "Unraid OS true Released" instead of "Unraid OS 7.0.0 Released".

Apply this fix:

   if (availableWithRenewal.value) {
-    return props.t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value]);
+    return props.t('headerOsVersion.unraidOsReleased', [updateOsResponse.value?.version ?? '']);
   }
web/src/locales/da.json (1)

323-323: Fix typo in rel attribute value.

The rel attribute value contains "noreffer noopener" instead of "noreferrer noopener". This typo defeats the referrer protection and should be corrected.

Apply this diff:

-  "server.state.enokeyfile.message": "<p>Vælg en mulighed nedenfor, og brug vores <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">Kom godt i gang Guide</a> til at konfigurere dit array på mindre end 15 minutter.</p>",
+  "server.state.enokeyfile.message": "<p>Vælg en mulighed nedenfor, og brug vores <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">Kom godt i gang Guide</a> til at konfigurere dit array på mindre end 15 minutter.</p>",
web/scripts/extract-translations.mjs (1)

173-176: Resolve paths relative to script file, not CWD

CWD-based resolution breaks under different runners. Derive paths from import.meta.url. This was raised earlier.

-import process from 'node:process';
+import process from 'node:process';
+import { fileURLToPath } from 'node:url';
...
-async function collectTsTranslationKeys() {
-  const sourceRoot = path.resolve(process.cwd(), 'src');
+async function collectTsTranslationKeys() {
+  const __dirname = path.dirname(fileURLToPath(import.meta.url));
+  const projectRoot = path.resolve(__dirname, '..'); // web/
+  const sourceRoot = path.resolve(projectRoot, 'src');
   ...
}
...
-async function collectJsonFormsKeys() {
-  const apiSourceRoot = path.resolve(process.cwd(), '../api/src');
+async function collectJsonFormsKeys() {
+  const __dirname = path.dirname(fileURLToPath(import.meta.url));
+  const projectRoot = path.resolve(__dirname, '..'); // web/
+  const apiSourceRoot = path.resolve(projectRoot, '../api/src');
   ...
}

Also applies to: 262-279

web/__test__/components/KeyActions.test.ts (1)

15-15: Add .js extension to ESM import.

The import is missing the required .js extension for ESM compatibility. This was flagged in a previous review and should be corrected.

As per coding guidelines:

-import { createTestI18n } from '../utils/i18n';
+import { createTestI18n } from '../utils/i18n.js';
web/src/components/UpdateOs/Status.vue (1)

137-147: Previous “t is not defined” is resolved.

Props now includes t and the template references t(...) correctly. LGTM.

web/src/components/Logs/LogViewer.standalone.vue (1)

168-175: A11y: Associate Labels with Selects (add matching ids).

Labels have for="log-file-select" / "highlight-language" but the Selects lack ids. Add ids to ensure screen reader association.

@@
-          <Label for="log-file-select">{{ t('logs.viewer.logFileLabel') }}</Label>
-          <Select
+          <Label for="log-file-select">{{ t('logs.viewer.logFileLabel') }}</Label>
+          <Select id="log-file-select"
             v-model="selectedLogFile"
             :items="logFileOptions"
             :placeholder="t('logs.viewer.selectLogFilePlaceholder')"
             class="w-full"
           />
@@
-          <Label for="highlight-language">{{ t('logs.viewer.syntaxLabel') }}</Label>
-          <Select
+          <Label for="highlight-language">{{ t('logs.viewer.syntaxLabel') }}</Label>
+          <Select id="highlight-language"
             v-model="highlightLanguage"
             :items="highlightLanguageOptions"
             :placeholder="t('logs.viewer.selectLanguagePlaceholder')"
             class="w-full"
           />

Also applies to: 189-197

🧹 Nitpick comments (8)
web/src/components/HeaderOsVersion.standalone.vue (2)

165-168: Verify interpolation style for titles

You’re passing list params (array) to t(). Ensure the strings use positional placeholders (e.g., {0}). Consider named params for clarity:

- title: availableWithRenewal.value
-   ? t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value])
-   : t('headerOsVersion.unraidOsUpdateAvailable', [available.value]),
+ title: availableWithRenewal.value
+   ? t('headerOsVersion.unraidOsReleased', { version: availableWithRenewal.value })
+   : t('headerOsVersion.unraidOsUpdateAvailable', { version: available.value }),

272-279: Localize remaining literal prop

Localize “Open in new tab” for consistency.

-      view-docs-label="Open in new tab"
+      :view-docs-label="t('common.openInNewTab')"

If common.openInNewTab doesn’t exist, add it to locales.

web/src/components/Logs/SingleLogViewer.vue (1)

250-254: Replace alert() with non-blocking UI toast

Use the app’s notification/toast system for errors; avoid leaking raw error strings in user-facing text.

-    alert(
-      t('logs.singleViewer.errorDownloadingLogFile', {
-        error: error instanceof Error ? error.message : String(error),
-      })
-    );
+    // e.g., use a toast/notification composable
+    // showToast({ variant: 'destructive', title: t('logs.singleViewer.downloadFailedTitle'), description: t('logs.singleViewer.errorDownloadingLogFile') })
web/scripts/extract-translations.mjs (2)

419-421: Include TSX/JSX in extractor patterns

Avoid misreporting keys as unused when used in TSX/JSX.

-    report = await createI18NReport({
-      vueFiles: 'src/**/*.{vue,ts,js}',
+    report = await createI18NReport({
+      vueFiles: 'src/**/*.{vue,ts,tsx,js,jsx}',
       languageFiles: 'src/locales/*.json',
     });

384-391: Add CI verification for extracted translation keys in en.json

Include a grouped jq check to avoid boolean-pipe errors and ensure all nested keys exist:

#!/usr/bin/env bash
set -euo pipefail
file="web/src/locales/en.json"
jq -e '
  has("headerOsVersion") and
  (.headerOsVersion | has("versionInformation","unraidOs","unraidApi","viewOsReleaseNotes","viewApiChangelog")) and
  has("logs") and
  (.logs | has("singleViewer","totalLines","watchingLogFileTooltip","download","downloading","refresh","loadingLogContent","errorLoadingLogContent","loadingMoreLines")) and
  has("downgradeOs") and
  (.downgradeOs | has("pleaseFinishTheInitiatedUpdateTo","downgradeUnraidOs"))
' "$file" >/dev/null
echo "All required keys present in $file"

Also apply this check at lines 529–536.

web/__test__/components/HeaderOsVersion.test.ts (2)

111-116: Use testTranslate for robust selector

Avoid hardcoding translated text. Use testTranslate to resolve the expected title.

-    const versionButton = wrapper.find('[title="Version Information"]');
+    const versionButton = wrapper.find(`[title="${testTranslate('headerOsVersion.versionInformation')}"]`);

As per coding guidelines


64-72: Optional: rely on i18n plugin instead of mocking vue-i18n

You already install createTestI18n(). Consider removing the vue-i18n mock to avoid divergence between mock and plugin behavior.

-vi.mock('vue-i18n', async (importOriginal) => {
-  const actual = (await importOriginal()) as typeof import('vue-i18n');
-  return {
-    ...actual,
-    useI18n: () => ({
-      t: testTranslate,
-    }),
-  };
-});
...
-      global: {
-        plugins: [testingPinia, createTestI18n()],
-      },
+      global: { plugins: [testingPinia, createTestI18n()] },
...
-      global: {
-        plugins: [testingPinia, createTestI18n()],
-      },
+      global: { plugins: [testingPinia, createTestI18n()] },

Based on learnings

Also applies to: 99-103, 155-157

web/src/components/UpdateOs/Status.vue (1)

53-55: Unify t usage (destructure from props).

Avoid mixing props.t in script and t(...) in template. Destructure t once and use consistently.

@@
-const { outputDateTimeReadableDiff: readableDiffRegExp, outputDateTimeFormatted: formattedRegExp } =
-  useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
+const { outputDateTimeReadableDiff: readableDiffRegExp, outputDateTimeFormatted: formattedRegExp } =
+  useDateTimeHelper(dateTimeFormat.value, t, true, regExp.value);
@@
-  return {
-    text: regUpdatesExpired.value
-      ? `${props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [formattedRegExp.value])} ${props.t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}`
-      : props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [
-          formattedRegExp.value,
-        ]),
-    title: regUpdatesExpired.value
-      ? props.t('registration.updateExpirationAction.ineligibleAsOf', [readableDiffRegExp.value])
-      : props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesFor', [
-          readableDiffRegExp.value,
-        ]),
-  };
+  return {
+    text: regUpdatesExpired.value
+      ? `${t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [formattedRegExp.value])} ${t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}`
+      : t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [
+          formattedRegExp.value,
+        ]),
+    title: regUpdatesExpired.value
+      ? t('registration.updateExpirationAction.ineligibleAsOf', [readableDiffRegExp.value])
+      : t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesFor', [
+          readableDiffRegExp.value,
+        ]),
+  };

Add once after props:

@@
 const props = withDefaults(defineProps<Props>(), {
   downgradeNotAvailable: false,
   showExternalDowngrade: false,
   title: undefined,
   subtitle: undefined,
 });
+
+const { t } = props;

Also applies to: 61-71, 137-147

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e61ee74 and 5ab15ac.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (131)
  • api/.eslintrc.ts (1 hunks)
  • api/README.md (1 hunks)
  • api/package.json (1 hunks)
  • api/scripts/extract-translations.mjs (1 hunks)
  • api/src/i18n/ar.json (1 hunks)
  • api/src/i18n/bn.json (1 hunks)
  • api/src/i18n/ca.json (1 hunks)
  • api/src/i18n/cs.json (1 hunks)
  • api/src/i18n/da.json (1 hunks)
  • api/src/i18n/de.json (1 hunks)
  • api/src/i18n/en.json (1 hunks)
  • api/src/i18n/es.json (1 hunks)
  • api/src/i18n/fr.json (1 hunks)
  • api/src/i18n/hi.json (1 hunks)
  • api/src/i18n/hr.json (1 hunks)
  • api/src/i18n/hu.json (1 hunks)
  • api/src/i18n/it.json (1 hunks)
  • api/src/i18n/ja.json (1 hunks)
  • api/src/i18n/ko.json (1 hunks)
  • api/src/i18n/lv.json (1 hunks)
  • api/src/i18n/nl.json (1 hunks)
  • api/src/i18n/no.json (1 hunks)
  • api/src/i18n/pl.json (1 hunks)
  • api/src/i18n/pt.json (1 hunks)
  • api/src/i18n/ro.json (1 hunks)
  • api/src/i18n/ru.json (1 hunks)
  • api/src/i18n/sv.json (1 hunks)
  • api/src/i18n/uk.json (1 hunks)
  • api/src/i18n/zh.json (1 hunks)
  • api/src/types/jsonforms-i18n.d.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts (13 hunks)
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (32 hunks)
  • api/src/unraid-api/graph/utils/form-utils.ts (4 hunks)
  • api/src/unraid-api/types/json-forms.ts (1 hunks)
  • crowdin.yml (1 hunks)
  • package.json (1 hunks)
  • plugin/plugins/dynamix.unraid.net.plg (0 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php (1 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php (0 hunks)
  • unraid-ui/src/components/brand/BrandButton.vue (3 hunks)
  • unraid-ui/src/forms/AccordionLayout.vue (7 hunks)
  • web/README.md (1 hunks)
  • web/__test__/components/Activation/ActivationModal.test.ts (3 hunks)
  • web/__test__/components/Activation/ActivationSteps.test.ts (2 hunks)
  • web/__test__/components/Activation/WelcomeModal.test.ts (3 hunks)
  • web/__test__/components/Auth.test.ts (5 hunks)
  • web/__test__/components/Brand/Avatar.test.ts (6 hunks)
  • web/__test__/components/CheckUpdateResponseModal.test.ts (2 hunks)
  • web/__test__/components/DowngradeOs.test.ts (6 hunks)
  • web/__test__/components/DownloadApiLogs.test.ts (7 hunks)
  • web/__test__/components/HeaderOsVersion.test.ts (4 hunks)
  • web/__test__/components/KeyActions.test.ts (8 hunks)
  • web/__test__/components/Logs/SingleLogViewer.test.ts (5 hunks)
  • web/__test__/components/Modals.test.ts (0 hunks)
  • web/__test__/components/Registration.test.ts (4 hunks)
  • web/__test__/components/SsoButton.test.ts (10 hunks)
  • web/__test__/components/UpdateOs.test.ts (9 hunks)
  • web/__test__/components/Wrapper/mount-engine.test.ts (3 hunks)
  • web/__test__/store/server.test.ts (1 hunks)
  • web/__test__/utils/i18n.ts (1 hunks)
  • web/package.json (5 hunks)
  • web/scripts/extract-translations.mjs (1 hunks)
  • web/scripts/sort-translations.mjs (1 hunks)
  • web/src/components/Activation/ActivationModal.vue (5 hunks)
  • web/src/components/Activation/ActivationSteps.vue (4 hunks)
  • web/src/components/Activation/WelcomeModal.standalone.vue (2 hunks)
  • web/src/components/ApiKey/ApiKeyCreate.vue (4 hunks)
  • web/src/components/ConnectSettings/ConnectSettings.standalone.vue (7 hunks)
  • web/src/components/ConnectSettings/OidcDebugLogs.vue (3 hunks)
  • web/src/components/DowngradeOs.standalone.vue (2 hunks)
  • web/src/components/DownloadApiLogs.standalone.vue (5 hunks)
  • web/src/components/HeaderOsVersion.standalone.vue (7 hunks)
  • web/src/components/KeyActions.vue (1 hunks)
  • web/src/components/Logs/LogFilterInput.vue (4 hunks)
  • web/src/components/Logs/LogViewer.standalone.vue (6 hunks)
  • web/src/components/Logs/LogViewerToolbar.vue (5 hunks)
  • web/src/components/Logs/SingleLogViewer.vue (9 hunks)
  • web/src/components/Modal.vue (2 hunks)
  • web/src/components/Modals.standalone.vue (1 hunks)
  • web/src/components/Notifications/Item.vue (4 hunks)
  • web/src/components/Notifications/List.vue (4 hunks)
  • web/src/components/Notifications/Sidebar.vue (10 hunks)
  • web/src/components/Registration.standalone.vue (11 hunks)
  • web/src/components/Registration/KeyLinkedStatus.vue (3 hunks)
  • web/src/components/Registration/ReplaceCheck.vue (2 hunks)
  • web/src/components/Registration/UpdateExpiration.vue (1 hunks)
  • web/src/components/Registration/UpdateExpirationAction.vue (4 hunks)
  • web/src/components/UpdateOs.standalone.vue (2 hunks)
  • web/src/components/UpdateOs/CallbackButton.vue (1 hunks)
  • web/src/components/UpdateOs/ChangelogModal.vue (3 hunks)
  • web/src/components/UpdateOs/CheckUpdateResponseModal.vue (14 hunks)
  • web/src/components/UpdateOs/Downgrade.vue (4 hunks)
  • web/src/components/UpdateOs/IgnoredRelease.vue (1 hunks)
  • web/src/components/UpdateOs/RawChangelogRenderer.vue (3 hunks)
  • web/src/components/UpdateOs/Status.vue (11 hunks)
  • web/src/components/UpdateOs/ThirdPartyDrivers.vue (1 hunks)
  • web/src/components/UpdateOs/Update.vue (6 hunks)
  • web/src/components/UpdateOs/UpdateIneligible.vue (4 hunks)
  • web/src/components/UserProfile.standalone.vue (2 hunks)
  • web/src/components/UserProfile/CallbackFeedback.vue (10 hunks)
  • web/src/components/UserProfile/DropdownConnectStatus.vue (1 hunks)
  • web/src/components/UserProfile/DropdownContent.vue (8 hunks)
  • web/src/components/UserProfile/DropdownLaunchpad.vue (1 hunks)
  • web/src/components/UserProfile/DropdownTrigger.vue (1 hunks)
  • web/src/components/UserProfile/ServerState.vue (2 hunks)
  • web/src/components/UserProfile/Trial.vue (2 hunks)
  • web/src/components/UserProfile/UptimeExpire.vue (1 hunks)
  • web/src/components/WanIpCheck.standalone.vue (2 hunks)
  • web/src/components/Wrapper/mount-engine.ts (3 hunks)
  • web/src/components/sso/SsoButtons.vue (1 hunks)
  • web/src/components/sso/useSsoAuth.ts (1 hunks)
  • web/src/composables/dateTime.ts (1 hunks)
  • web/src/helpers/globals.d.ts (1 hunks)
  • web/src/helpers/i18n-loader.ts (1 hunks)
  • web/src/helpers/jsonforms-i18n.ts (1 hunks)
  • web/src/locales/_template.json (0 hunks)
  • web/src/locales/ar.json (1 hunks)
  • web/src/locales/bn.json (1 hunks)
  • web/src/locales/ca.json (1 hunks)
  • web/src/locales/cs.json (1 hunks)
  • web/src/locales/da.json (1 hunks)
  • web/src/locales/de.json (1 hunks)
  • web/src/locales/en.json (1 hunks)
  • web/src/locales/en_US.json (0 hunks)
  • web/src/locales/es.json (1 hunks)
  • web/src/locales/fr.json (1 hunks)
  • web/src/locales/hi.json (1 hunks)
  • web/src/locales/hr.json (1 hunks)
  • web/src/locales/hu.json (1 hunks)
  • web/src/locales/it.json (1 hunks)
💤 Files with no reviewable changes (5)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php
  • web/src/locales/_template.json
  • web/test/components/Modals.test.ts
  • plugin/plugins/dynamix.unraid.net.plg
  • web/src/locales/en_US.json
✅ Files skipped from review due to trivial changes (8)
  • api/src/i18n/hi.json
  • web/src/components/DownloadApiLogs.standalone.vue
  • api/src/i18n/nl.json
  • api/src/i18n/zh.json
  • api/src/i18n/uk.json
  • web/src/locales/cs.json
  • api/src/i18n/hr.json
  • api/src/i18n/sv.json
🚧 Files skipped from review as they are similar to previous changes (72)
  • web/src/components/UpdateOs/IgnoredRelease.vue
  • api/src/i18n/ro.json
  • api/src/i18n/cs.json
  • web/src/components/UpdateOs/ThirdPartyDrivers.vue
  • web/test/components/DownloadApiLogs.test.ts
  • web/README.md
  • api/src/i18n/en.json
  • web/src/helpers/globals.d.ts
  • web/test/components/SsoButton.test.ts
  • api/src/i18n/bn.json
  • package.json
  • api/src/i18n/no.json
  • web/test/components/Registration.test.ts
  • api/src/i18n/ko.json
  • web/src/components/UpdateOs/CheckUpdateResponseModal.vue
  • web/src/components/UpdateOs/RawChangelogRenderer.vue
  • web/src/components/sso/SsoButtons.vue
  • web/src/locales/de.json
  • web/src/components/Activation/ActivationModal.vue
  • web/src/components/Registration/UpdateExpirationAction.vue
  • web/src/locales/es.json
  • api/src/i18n/ar.json
  • web/src/components/ApiKey/ApiKeyCreate.vue
  • web/src/locales/hi.json
  • api/src/i18n/it.json
  • web/src/locales/fr.json
  • api/src/unraid-api/types/json-forms.ts
  • web/src/components/UserProfile.standalone.vue
  • web/src/locales/it.json
  • api/src/i18n/ru.json
  • api/src/types/jsonforms-i18n.d.ts
  • api/src/i18n/de.json
  • api/src/unraid-api/graph/resolvers/settings/settings.service.ts
  • api/README.md
  • web/src/components/Activation/WelcomeModal.standalone.vue
  • api/src/i18n/es.json
  • web/src/components/UserProfile/ServerState.vue
  • web/test/components/Activation/WelcomeModal.test.ts
  • web/test/components/DowngradeOs.test.ts
  • web/src/components/UpdateOs/Update.vue
  • web/src/locales/bn.json
  • web/src/components/Notifications/List.vue
  • web/src/locales/en.json
  • api/scripts/extract-translations.mjs
  • web/test/components/Activation/ActivationModal.test.ts
  • web/src/components/Registration/ReplaceCheck.vue
  • api/src/i18n/ca.json
  • api/src/i18n/pl.json
  • web/src/components/UserProfile/DropdownLaunchpad.vue
  • web/src/components/Registration/KeyLinkedStatus.vue
  • web/src/components/UserProfile/DropdownConnectStatus.vue
  • web/src/components/UpdateOs/CallbackButton.vue
  • web/test/store/server.test.ts
  • web/src/components/UpdateOs/ChangelogModal.vue
  • web/src/components/UpdateOs/Downgrade.vue
  • crowdin.yml
  • web/test/components/Wrapper/mount-engine.test.ts
  • web/src/components/Registration.standalone.vue
  • web/src/components/Notifications/Item.vue
  • api/src/i18n/ja.json
  • api/src/i18n/lv.json
  • web/src/helpers/i18n-loader.ts
  • web/src/helpers/jsonforms-i18n.ts
  • web/test/components/Activation/ActivationSteps.test.ts
  • web/src/components/UserProfile/DropdownTrigger.vue
  • web/src/locales/ca.json
  • web/src/components/KeyActions.vue
  • web/src/locales/hr.json
  • web/src/components/UserProfile/Trial.vue
  • api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts
  • api/src/i18n/pt.json
  • web/test/components/Logs/SingleLogViewer.test.ts
🧰 Additional context used
📓 Path-based instructions (9)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/__test__/components/UpdateOs.test.ts
  • web/src/composables/dateTime.ts
  • web/src/components/Wrapper/mount-engine.ts
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/src/components/sso/useSsoAuth.ts
  • web/__test__/components/HeaderOsVersion.test.ts
  • web/__test__/utils/i18n.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
  • web/__test__/utils/i18n.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/CheckUpdateResponseModal.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/KeyActions.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/Modal.vue
  • web/src/components/DowngradeOs.standalone.vue
  • web/src/components/WanIpCheck.standalone.vue
  • web/src/components/ConnectSettings/OidcDebugLogs.vue
  • web/src/components/Registration/UpdateExpiration.vue
  • web/src/components/Modals.standalone.vue
  • web/src/components/Logs/LogViewerToolbar.vue
  • web/src/components/Activation/ActivationSteps.vue
  • web/src/components/UpdateOs/Status.vue
  • web/src/components/UpdateOs/UpdateIneligible.vue
  • web/src/components/UserProfile/DropdownContent.vue
  • web/src/components/Logs/SingleLogViewer.vue
  • web/src/components/UpdateOs.standalone.vue
  • web/src/components/UserProfile/UptimeExpire.vue
  • web/src/components/Logs/LogFilterInput.vue
  • web/src/components/ConnectSettings/ConnectSettings.standalone.vue
  • web/src/components/HeaderOsVersion.standalone.vue
  • web/src/components/UserProfile/CallbackFeedback.vue
  • unraid-ui/src/components/brand/BrandButton.vue
  • web/src/components/Notifications/Sidebar.vue
  • web/src/components/Logs/LogViewer.standalone.vue
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
  • api/src/unraid-api/graph/utils/form-utils.ts
🧠 Learnings (25)
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Auth.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: This Nuxt app is tested with vitest outside the Nuxt runtime; structure tests accordingly

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/Auth.test.ts
  • web/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/UpdateOs.test.ts
  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/Auth.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use createTestingPinia() for mocking stores in components

Applied to files:

  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/Auth.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : For store tests, use createPinia() and setActivePinia

Applied to files:

  • web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : For Pinia store unit tests, initialize with setActivePinia(createPinia()) rather than createTestingPinia, unless testing features from createTestingPinia are explicitly needed

Applied to files:

  • web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior

Applied to files:

  • web/__test__/components/Brand/Avatar.test.ts
  • web/__test__/components/KeyActions.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Always run pnpm codegen for GraphQL in the web directory

Applied to files:

  • web/package.json
  • api/package.json
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Always run `pnpm codegen` for GraphQL code generation in the web directory

Applied to files:

  • web/package.json
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript import specifiers with .js extensions for ESM compatibility

Applied to files:

  • api/.eslintrc.ts
  • web/__test__/components/KeyActions.test.ts
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
PR: unraid/api#1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • api/.eslintrc.ts
  • web/__test__/components/KeyActions.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/{store,stores}/**/*.ts : In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports

Applied to files:

  • web/src/components/Logs/SingleLogViewer.vue
  • web/src/components/ConnectSettings/ConnectSettings.standalone.vue
  • web/src/components/Notifications/Sidebar.vue
  • web/src/components/Logs/LogViewer.standalone.vue
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
PR: unraid/api#975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.

Applied to files:

  • web/src/components/Logs/LogFilterInput.vue
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
PR: unraid/api#1417
File: web/components/ConnectSettings/ConnectSettings.ce.vue:11-18
Timestamp: 2025-06-13T17:14:21.739Z
Learning: The project’s build tooling auto-imports common Vue/Pinia helpers such as `storeToRefs`, so explicit import statements for them are not required.

Applied to files:

  • web/src/components/ConnectSettings/ConnectSettings.standalone.vue
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()

Applied to files:

  • web/__test__/components/Auth.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)

Applied to files:

  • web/__test__/components/KeyActions.test.ts
🧬 Code graph analysis (10)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php (1)
web/scripts/extract-translations.mjs (1)
  • locale (398-398)
web/__test__/components/UpdateOs.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • createTestI18n (49-58)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/helpers/i18n-loader.ts (3)
  • createI18nInstance (59-69)
  • ensureLocale (93-115)
  • getWindowLocale (117-122)
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)
api/src/unraid-api/graph/utils/form-utils.ts (1)
  • createSimpleLabeledControl (7-55)
web/__test__/components/Brand/Avatar.test.ts (1)
web/__test__/utils/i18n.ts (2)
  • testTranslate (43-47)
  • createTestI18n (49-58)
web/__test__/components/CheckUpdateResponseModal.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • testTranslate (43-47)
web/scripts/extract-translations.mjs (1)
api/scripts/extract-translations.mjs (7)
  • content (143-143)
  • sourceFile (144-144)
  • keys (54-54)
  • keys (145-145)
  • firstArg (71-71)
  • ignorePatterns (10-17)
  • files (133-137)
web/__test__/components/Auth.test.ts (1)
web/__test__/utils/i18n.ts (2)
  • testTranslate (43-47)
  • createTestI18n (49-58)
web/__test__/components/KeyActions.test.ts (1)
web/__test__/utils/i18n.ts (1)
  • createTestI18n (49-58)
web/__test__/components/HeaderOsVersion.test.ts (1)
web/__test__/utils/i18n.ts (2)
  • testTranslate (43-47)
  • createTestI18n (49-58)
🪛 ast-grep (0.39.6)
web/__test__/utils/i18n.ts

[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${index}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 30-30: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${placeholder}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

// Get the root data from JSONForms context for rule evaluation
const rootData = jsonFormsContext?.core?.data || {};
const formData = props.data || rootData;
const formData = (layout.value?.data as unknown) ?? rootData;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unsafe cast bypasses type checking.

The cast (layout.value?.data as unknown) completely bypasses TypeScript's type safety and suggests a type mismatch in the useJsonFormsLayout composable's return type. This could hide runtime type errors.

Consider these alternatives:

  1. If layout.value.data has the correct type, remove the cast
  2. If there's a genuine type mismatch, use proper type narrowing or type guards
  3. Document why the cast is necessary if it's intentional

Run the following script to examine the useJsonFormsLayout return type:

#!/bin/bash
# Description: Examine useJsonFormsLayout composable type definition

# Search for useJsonFormsLayout definition and return type
ast-grep --pattern $'function useJsonFormsLayout($$$) {
  $$$
}'

# Search for useJsonFormsLayout type definitions
rg -nP --type=ts 'useJsonFormsLayout.*:\s*\{' node_modules/@jsonforms/vue/

# Check other usages of layout.value.data in the codebase
rg -nP --type=vue --type=ts -C3 'layout\.value\.data'
🤖 Prompt for AI Agents
In unraid-ui/src/forms/AccordionLayout.vue around line 74, the expression "const
formData = (layout.value?.data as unknown) ?? rootData;" unsafely casts
layout.value.data to unknown which bypasses TypeScript checks; fix by removing
the cast and ensuring formData has the correct type: either update the
useJsonFormsLayout return type to include the proper data type, or narrow
layout.value with a type guard/conditional (e.g., check for existence and
expected shape) before assigning, or use a generic type parameter on
useJsonFormsLayout so you can write const formData = layout.value?.data ??
rootData while preserving typing; if the cast is intentional, add an inline
comment referencing why and where the type was validated.

Comment on lines +9 to +41
const flatMessages = enUS as unknown as Record<string, string>;

function resolveMessage(key: string): string | undefined {
return flatMessages[key];
}

function replaceParams(template: string, params?: unknown): string {
if (params === undefined || params === null) {
return template;
}

let result = template;

if (Array.isArray(params)) {
params.forEach((value, index) => {
result = result.replace(new RegExp(`\\{${index}\\}`, 'g'), String(value));
});
return result;
}

if (typeof params === 'object') {
Object.entries(params as AnyObject).forEach(([placeholder, value]) => {
result = result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), String(value));
});
return result;
}

if (typeof params === 'number' || typeof params === 'string' || typeof params === 'boolean') {
return result.replace(/\{0\}/g, String(params));
}

return result;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Nested keys don’t resolve; replace custom resolver with composer t.

flatMessages = enUS as Record<string, string> will not resolve dotted paths (e.g., logs.viewer.title), causing fallbacks to the key and weakening tests. Also avoids dynamic RegExp construction.

Apply:

@@
-import enUS from '~/locales/en.json';
+import enUS from '~/locales/en.json';
@@
-const flatMessages = enUS as unknown as Record<string, string>;
-
-function resolveMessage(key: string): string | undefined {
-  return flatMessages[key];
-}
-
-function replaceParams(template: string, params?: unknown): string {
-  if (params === undefined || params === null) {
-    return template;
-  }
-
-  let result = template;
-
-  if (Array.isArray(params)) {
-    params.forEach((value, index) => {
-      result = result.replace(new RegExp(`\\{${index}\\}`, 'g'), String(value));
-    });
-    return result;
-  }
-
-  if (typeof params === 'object') {
-    Object.entries(params as AnyObject).forEach(([placeholder, value]) => {
-      result = result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), String(value));
-    });
-    return result;
-  }
-
-  if (typeof params === 'number' || typeof params === 'string' || typeof params === 'boolean') {
-    return result.replace(/\{0\}/g, String(params));
-  }
-
-  return result;
-}
+// Build a standalone composer to resolve nested keys and interpolation exactly like runtime.
+const testI18n = createI18n({
+  legacy: false,
+  locale: DEFAULT_LOCALE,
+  fallbackLocale: DEFAULT_LOCALE,
+  messages: { [DEFAULT_LOCALE]: enUS },
+});
@@
-export const testTranslate = ((key: string, params?: unknown) => {
-  const message = resolveMessage(key);
-  const template = message ?? key;
-  return replaceParams(template, params);
-}) as unknown as import('vue-i18n').ComposerTranslation;
+export const testTranslate = testI18n.global.t as unknown as import('vue-i18n').ComposerTranslation;

Also applies to: 43-48

🧰 Tools
🪛 ast-grep (0.39.6)

[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${index}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 30-30: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${placeholder}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

Comment on lines +35 to +38
? `${props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [outputDateTimeFormatted.value])} ${props.t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}`
: props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [
outputDateTimeFormatted.value,
]),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consolidate concatenated translation keys.

Concatenating two separate translation keys with template literals breaks proper internationalization, as different languages may require different word orders or sentence structures that cannot be achieved by simple concatenation.

Consider consolidating into a single translation key:

-      ? `${props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [outputDateTimeFormatted.value])} ${props.t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}`
+      ? props.t('registration.updateExpirationAction.eligibleForUpdatesWithExtensionPrompt', [outputDateTimeFormatted.value])

The new key should contain the complete sentence with proper placeholders, allowing translators to reorder components as needed for their language.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
? `${props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [outputDateTimeFormatted.value])} ${props.t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}`
: props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [
outputDateTimeFormatted.value,
]),
? props.t('registration.updateExpirationAction.eligibleForUpdatesWithExtensionPrompt', [
outputDateTimeFormatted.value,
])
: props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [
outputDateTimeFormatted.value,
]),
🤖 Prompt for AI Agents
In web/src/components/Registration/UpdateExpiration.vue around lines 35 to 38,
the code concatenates two translation keys which breaks internationalization;
replace the concatenation with a single consolidated translation key that
contains the full sentence and placeholders (e.g., for the date and for the
phrase "extend your license..."), update the i18n resource files with that new
key (allowing translators to reorder placeholders as needed), and change the
component to call props.t once with the new key and pass
outputDateTimeFormatted.value and any other dynamic text as interpolation
parameters.

import { client } from '~/helpers/create-apollo-client';
import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
import en_US from '~/locales/en_US.json';
import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader';
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add .js extension to the import specifier.

The import path should include the .js extension for ESM compatibility.

As per coding guidelines

Apply this diff:

-import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader';
+import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader';
import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader.js';
🤖 Prompt for AI Agents
In web/src/components/Wrapper/mount-engine.ts around line 9, the import
statement "import { createI18nInstance, ensureLocale, getWindowLocale } from
'~/helpers/i18n-loader';" must include the .js extension for ESM compatibility;
update the specifier to import from '~/helpers/i18n-loader.js' so the runtime
can resolve the module correctly.

"userProfile.callbackFeedback.thenGoToToolsRegistrationTo": "Gå derefter til Værktøjer > Registrering for manuelt at installere det",
"userProfile.callbackFeedback.thisDowngradeWillRequireAReboot": "Denne nedgradering kræver en genstart",
"userProfile.callbackFeedback.thisUpdateWillRequireAReboot": "Denne opdatering kræver en genstart",
"userProfile.callbackFeedback.updateUnraidOsConfirmationRequired": "Confirmation of Update Unraid OS required",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Untranslated entry remains in English.

The value for userProfile.callbackFeedback.updateUnraidOsConfirmationRequired is still in English. Consider translating it to Danish for consistency (e.g., "Bekræftelse af opdatering af Unraid OS påkrævet").

🤖 Prompt for AI Agents
In web/src/locales/da.json around line 466 the value for
"userProfile.callbackFeedback.updateUnraidOsConfirmationRequired" is still in
English; replace the English string with the Danish translation (for example
"Bekræftelse af opdatering af Unraid OS påkrævet") so the locale file remains
fully translated and consistent with other entries.

- Introduced BigInt scalar type in code generation configurations for both API and web.
- Updated GraphQL schema to reflect BigInt as a number type for input and output.
- Enhanced DockerContainer and related types to utilize BigInt for sizeRootFs.
- Improved documentation for sizeRootFs field to clarify its purpose and data type.
- Ensured consistent export formatting in generated files.
@elibosley elibosley changed the title feat: full i18n feat: translations now use crowdin (unraid.crowdin.net) Oct 13, 2025
@elibosley elibosley changed the title feat: translations now use crowdin (unraid.crowdin.net) feat: translations now use crowdin (translate.unraid.net) Oct 13, 2025
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

1 similar comment
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Updated component registry tests to mock the mount-engine module correctly, ensuring proper hoisting of mock functions.
- Refactored mount-engine tests to await asynchronous operations for better reliability and accuracy in test execution.
- Marked several test cases as skipped to reflect changes in the unified app architecture and focus on relevant tests.

These improvements enhance the clarity and maintainability of the test suite, ensuring more reliable test outcomes.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
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: 0

♻️ Duplicate comments (1)
web/src/components/Wrapper/mount-engine.ts (1)

9-9: Add .js extension to the import specifier.

The import path should include the .js extension for ESM compatibility.

As per coding guidelines

Apply this diff:

-import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader';
+import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader.js';
🧹 Nitpick comments (1)
web/__test__/components/Wrapper/mount-engine.test.ts (1)

59-59: Optional: Simplify mock by removing redundant type parameter.

The type parameter <() => string | undefined> is redundant when an implementation with an explicit return type is provided. TypeScript can infer the type from the implementation.

Apply this diff to simplify:

-const mockGetWindowLocale = vi.fn<() => string | undefined>(() => undefined);
+const mockGetWindowLocale = vi.fn((): string | undefined => undefined);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ab15ac and 1ecc7e6.

📒 Files selected for processing (4)
  • web/__test__/components/Wrapper/mount-engine.test.ts (17 hunks)
  • web/__test__/components/component-registry.test.ts (6 hunks)
  • web/src/components/Wrapper/auto-mount.ts (1 hunks)
  • web/src/components/Wrapper/mount-engine.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
**/__test__/components/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/src/components/Wrapper/mount-engine.ts
  • web/__test__/components/component-registry.test.ts
  • web/src/components/Wrapper/auto-mount.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
web/__test__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place Vue component tests under web/test; run with pnpm test

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.{test,spec}.{ts,tsx}: Use mount from Vue Test Utils for component testing
Stub complex child components that aren’t the focus of the test
Mock external dependencies and services in component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test

Files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
🧠 Learnings (15)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)

Applied to files:

  • web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript import specifiers with .js extensions for ESM compatibility

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
PR: unraid/api#1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to {api,web}/**/*.{test,spec}.{ts,tsx} : Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Applied to files:

  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Mock external dependencies and services in component tests

Applied to files:

  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/mocks/**/*.ts : Frequently used mocks can also be placed under __test__/mocks

Applied to files:

  • web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: This Nuxt app is tested with vitest outside the Nuxt runtime; structure tests accordingly

Applied to files:

  • web/__test__/components/component-registry.test.ts
🧬 Code graph analysis (3)
web/__test__/components/Wrapper/mount-engine.test.ts (1)
web/src/components/Wrapper/mount-engine.ts (2)
  • mountUnifiedApp (80-198)
  • autoMountAllComponents (201-203)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/helpers/i18n-loader.ts (3)
  • createI18nInstance (59-69)
  • ensureLocale (93-115)
  • getWindowLocale (117-122)
web/src/components/Wrapper/auto-mount.ts (1)
web/src/components/Wrapper/mount-engine.ts (1)
  • autoMountAllComponents (201-203)
🔇 Additional comments (9)
web/__test__/components/component-registry.test.ts (2)

3-10: LGTM! Proper hoisted mock setup.

The hoisted mocks for the mount-engine module follow best practices: using vi.hoisted(), placing mocks at the top level, and including clear documentation. This ensures proper hoisting order and avoids common pitfalls.

Based on learnings


141-210: Track re-enablement plan for skipped tests.

Six tests have been skipped due to the unified app architecture transition. While the architectural reasoning is clear from the comments, there's no TODO, issue reference, or timeline for re-enabling these tests.

Consider:

  1. Adding a TODO comment with an issue link to track the work needed to adapt these tests to the unified architecture
  2. Documenting whether the hoisted mocks (lines 4-5) can be removed or are still needed by the active test (lines 132-139)
  3. Clarifying if these tests will be updated or replaced with new ones

Do you have an issue tracking the work to update these tests for the unified app architecture?

As per coding guidelines

web/src/components/Wrapper/auto-mount.ts (2)

48-52: LGTM! Proper error handling for async mount flows.

The error handling correctly prevents unhandled promise rejections in both the DOMContentLoaded event path and the immediate mount scenario. The consistent logging approach makes debugging easier.

Also applies to: 55-57


62-62: LGTM! New public export aligns with i18n initialization strategy.

Exposing initializeGlobalDependencies as a public export enables manual initialization scenarios, which aligns with the loader-based i18n approach introduced in this PR.

web/src/components/Wrapper/mount-engine.ts (3)

22-22: LGTM! Type declaration aligns with loader-based i18n approach.

The Window.LOCALE?: string declaration correctly replaces the previous LOCALE_DATA approach, supporting the new i18n loader flow where the locale identifier is retrieved via getWindowLocale().


30-34: LGTM! Async i18n setup eliminates race condition.

Making setupI18n async and awaiting ensureLocale ensures locale messages are fully loaded before mounting components, preventing the brief flash of English content that could occur with fire-and-forget locale loading. Line 88 correctly awaits the setup.

Also applies to: 88-88


80-80: LGTM! Async mounting flow properly orchestrated.

The async conversion of mountUnifiedApp and autoMountAllComponents correctly propagates the async control flow, ensuring i18n and shared context are fully prepared before mounting. Error handling is properly delegated to callers in auto-mount.ts.

Also applies to: 201-203

web/__test__/components/Wrapper/mount-engine.test.ts (2)

74-88: Excellent mock management and test isolation.

The beforeEach properly resets all mocks and restores i18n state to defaults, ensuring test independence.


412-427: Well-structured i18n setup tests.

The tests properly verify both default locale initialization and window-provided locale handling, ensuring the i18n-loader integration works correctly.

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Removed the `t` prop from various components, transitioning to the `useI18n` composable for translation management.
- Updated test files to utilize `createTestI18n` for improved internationalization consistency.
- Enhanced modal and registration components to reflect the new translation approach, ensuring a cleaner and more maintainable codebase.

These changes improve the overall integration of i18n across the application, promoting better localization practices.
Copy link
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

🧹 Nitpick comments (3)
web/src/components/LocaleSwitcher.vue (2)

59-61: Remove redundant initialization in onMounted.

The currentLocale is already initialized with locale.value at line 35, making this onMounted hook redundant. The ref is created with the correct initial value during component setup.

Apply this diff to remove the redundant code:

-// Initialize current locale on mount
-onMounted(() => {
-  currentLocale.value = locale.value;
-});

47-47: Consider the user experience impact of full page reloads.

While the full page reload ensures all components pick up the new locale (as noted in the comment and UI message), it results in a jarring user experience. Modern SPAs typically handle locale changes reactively without reloads.

If the reload is necessary due to the mount engine architecture or legacy PHP component integration, ensure:

  1. The current page state can be recovered after reload (e.g., scroll position, form data)
  2. Users are aware that unsaved changes will be lost
  3. Consider adding a confirmation dialog if there's user input at risk

Verify whether the full reload is truly necessary, or if the i18n system can be configured to reactively update all mounted components through vue-i18n's built-in reactivity.

api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)

24-81: Several keys defined but unused.

The following keys are defined in the constant but never referenced in the code:

  • provider.discoveryToggle
  • provider.userInfoEndpoint
  • restrictions.sectionHelp
  • rules.sectionDescription
  • buttons.sectionTitle
  • buttons.sectionDescription

Consider removing these unused keys or documenting if they're reserved for future use.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ecc7e6 and 636586e.

📒 Files selected for processing (9)
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (38 hunks)
  • web/components.d.ts (1 hunks)
  • web/public/test-pages/all-components.html (1 hunks)
  • web/scripts/extract-translations.mjs (1 hunks)
  • web/src/components/DevSettings.vue (2 hunks)
  • web/src/components/LocaleSwitcher.vue (1 hunks)
  • web/src/components/RClone/RCloneConfig.vue (2 hunks)
  • web/src/components/Wrapper/component-registry.ts (1 hunks)
  • web/src/locales/en.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/scripts/extract-translations.mjs
  • web/src/locales/en.json
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • web/components.d.ts
  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
  • web/src/components/Wrapper/component-registry.ts
**/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)

Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests

Files:

  • web/src/components/DevSettings.vue
  • web/src/components/RClone/RCloneConfig.vue
  • web/src/components/LocaleSwitcher.vue
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
🧠 Learnings (2)
📚 Learning: 2025-02-18T17:36:22.670Z
Learnt from: pujitm
PR: unraid/api#1143
File: web/components/DummyServerSwitcher.vue:16-19
Timestamp: 2025-02-18T17:36:22.670Z
Learning: The DummyServerSwitcher component in web/components/DummyServerSwitcher.vue is a development tool for testing different server states, and includes intentional debug elements to aid development.

Applied to files:

  • web/src/components/DevSettings.vue
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
PR: unraid/api#1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.

Applied to files:

  • web/src/components/Wrapper/component-registry.ts
🧬 Code graph analysis (1)
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)
api/src/unraid-api/graph/utils/form-utils.ts (1)
  • createSimpleLabeledControl (7-55)
🔇 Additional comments (6)
web/src/components/RClone/RCloneConfig.vue (1)

7-7: LGTM! JsonForms i18n integration is correctly implemented.

The changes properly integrate internationalization support:

  • Line 7 imports the composable helper
  • Line 14 instantiates the i18n state
  • Line 233 wires it into JsonForms

The implementation follows the standard JsonForms i18n pattern and aligns with the broader i18n effort described in the PR summary. All required imports are explicit per the coding guidelines.

Also applies to: 14-14, 233-233

web/src/components/DevSettings.vue (1)

6-6: LGTM!

The LocaleSwitcher import and integration into DevSettings follows the established component pattern. The fixed width and spacing for the popover content provides a consistent layout for the developer settings UI.

Also applies to: 16-20

web/public/test-pages/all-components.html (1)

243-246: LGTM!

The <unraid-locale-switcher> web component is properly integrated into the test page, providing visibility for the new language selection functionality in the test environment.

web/components.d.ts (1)

71-71: LGTM!

The GlobalComponents type declaration for LocaleSwitcher follows the established pattern and enables proper TypeScript support for the component.

web/src/components/Wrapper/component-registry.ts (1)

124-128: LGTM!

The LocaleSwitcher component registration follows the established pattern for web component mappings, with proper async loading and consistent selector naming.

web/src/components/LocaleSwitcher.vue (1)

2-3: LGTM!

Explicit imports from vue are correctly included, which is essential for components in this directory to work properly in test environments where Nuxt auto-imports may not apply.

Based on coding guidelines

… tests

- Removed the `t` prop from `ChangelogModal` and `CheckUpdateResponseModal`, transitioning to the `useI18n` composable for translation management.
- Updated test files to consistently use `createTestI18n`, enhancing internationalization practices.
- Adjusted modal components to reflect the new translation approach, promoting cleaner and more maintainable code.

These changes continue to improve the integration of i18n across the application, ensuring better localization practices.
- Removed the `t` prop from multiple components, including `DowngradeOs`, `HeaderOsVersion`, and various modals, transitioning to the `useI18n` composable for improved translation management.
- Updated components to ensure consistent use of the `useI18n` approach, enhancing code clarity and maintainability.
- These changes further streamline internationalization practices across the application, promoting better localization and reducing prop dependencies.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Updated the OIDC_I18N structure to streamline the localization of providers and defaultAllowedOrigins by using direct keys instead of nested objects.
- Adjusted the i18n references in the OidcConfigPersistence class to utilize the updated OIDC_I18N structure, enhancing consistency in translation handling.
- Added LOCALE property to the global window type for improved locale management.

These changes contribute to a more cohesive internationalization strategy within the OIDC configuration.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Eliminated the `t` prop from multiple components, including modals and user profile components, transitioning to a cleaner codebase without unnecessary dependencies.
- Updated the sorting function in `sort-translations.mjs` to use a shared collator for consistent string comparison across locales.
- These changes enhance the maintainability of the code and streamline internationalization practices throughout the application.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1739/dynamix.unraid.net.plg

@elibosley elibosley merged commit 31c4102 into main Oct 13, 2025
11 of 14 checks passed
@elibosley elibosley deleted the feat/i18n branch October 13, 2025 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant