-
Notifications
You must be signed in to change notification settings - Fork 9
feat: full i18n #1739
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: full i18n #1739
Conversation
…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.
Claude encountered an error —— View job I'll analyze this and get back to you. |
Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughAdds comprehensive internationalization: new extraction/sorting scripts, an i18n loader and jsonforms-i18n helper, many web and api locale files, type and form-utils updates to carry i18n metadata, widespread UI text migrations to translation keys, test helpers and test updates, plus minor docs/config tweaks. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant App as Web App
participant Loader as i18n-loader
participant Bundles as Locale Bundles (import.meta.glob)
participant VueI18n as Vue I18n
User->>App: open page
App->>Loader: createI18nInstance()
Loader-->>App: i18n instance (locale=DEFAULT_LOCALE)
App->>Loader: ensureLocale(getWindowLocale())
Loader->>Loader: resolveLocale(candidate)
alt target locale not loaded
Loader->>Bundles: dynamic import(locale)
Bundles-->>Loader: messages
Loader->>VueI18n: setLocaleMessage(locale, messages)
else already available
Loader-->>App: locale already available
end
Loader->>VueI18n: set active locale
App-->>User: render UI with translations
note right of Loader: on failure -> fallback to DEFAULT_LOCALE
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this 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
|
||
<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 }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
<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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
:icon="EyeIcon" | ||
:icon-right="ArrowTopRightOnSquareIcon" | ||
:name="updateButton?.name" | ||
:text="t('View Changelog')" | ||
:text="t('updateOs.updateIneligible.viewChangelog')" | ||
class="flex-none" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
> | ||
{{ | ||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
> | ||
<ResponsiveModalHeader> | ||
<ResponsiveModalTitle> | ||
{{ t('Unraid OS {0} Changelog', [currentRelease.version]) }} | ||
{{ t('updateOs.changelogModal.unraidOsChangelog', [currentRelease.version]) }} | ||
</ResponsiveModalTitle> | ||
</ResponsiveModalHeader> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
<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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
There was a problem hiding this 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 appliedThe 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 outputThere’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 accessAccessing 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 typingTypeScript 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
setstext
to a translated string, but the template callst(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 valuePassing 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$sTo 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 bugsUse 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() fallbackt 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 i18nIf 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 translationAvoid 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.7web/src/components/Registration/UpdateExpirationAction.vue (1)
41-44
: Avoid concatenating translation strings; use a single key with interpolationConcatenation 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 paragraphsBuilding 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
: Verifyconvert
API and tighten typing forformatFileSize
.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), extendunitLabels
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 ofavailableLocales
.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
⛔ 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" |
There was a problem hiding this comment.
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:
- Checking if
element
should be typed asUISchemaElement
from the start (e.g., by constraining the elements array type) - Verifying that all elements in the filtered array actually have the properties
DispatchRenderer
expects - 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.
const apiSourceRoot = path.resolve(process.cwd(), '../api/src'); | ||
const ignorePatterns = [ | ||
'**/__tests__/**', | ||
'**/__test__/**', | ||
'**/*.spec.ts', | ||
'**/*.spec.js', | ||
'**/*.test.ts', | ||
'**/*.test.js', | ||
]; |
There was a problem hiding this comment.
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.
<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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
<div | ||
:class="cn('fixed inset-0 z-0 transition-opacity', overlayColor, overlayOpacity)" | ||
:title="showCloseX ? t('Click to close modal') : undefined" | ||
:title="showCloseX ? t('modal.clickToCloseModal') : undefined" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ Verification inconclusive
Verify the i18n key exists in the base locale.
The key modal.clickToCloseModal
follows good namespacing practices. Ensure it exists in web/src/locales/en.json
to avoid runtime translation failures.
Run the following script to verify the key exists:
🏁 Script executed:
#!/bin/bash
# Description: Verify modal.clickToCloseModal exists in en.json
rg -n '"modal"\s*:' web/src/locales/en.json -A 10 | rg 'clickToCloseModal'
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Broad search for the i18n key in en.json
rg -n '"clickToCloseModal"' web/src/locales/en.json
Length of output: 0
Add missing i18n key
The key modal.clickToCloseModal
is not defined in web/src/locales/en.json; add it under the modal
section (e.g., "clickToCloseModal": "Click to close modal"
).
🤖 Prompt for AI Agents
In web/src/components/Modal.vue around line 100, the template references the
i18n key modal.clickToCloseModal which is missing from the English locale; open
web/src/locales/en.json and add "clickToCloseModal": "Click to close modal" (or
the desired copy) under the existing "modal" object so the template can resolve
the translation.
:text="t('updateOs.update.viewAvailableUpdates')" | ||
:title="!acknowledgeBackup ? t('updateOs.update.acklowledgeThatYouHaveMadeA') : ''" | ||
class="flex-none" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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'; |
There was a problem hiding this comment.
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.
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.
"registration.messages.yourLicenseKeySOsUpdate": "Din licensnøgles OS-opdateringsberettigelse er udløbet. Forny venligst din licensnøgle for at aktivere opdateringer udgivet efter din udløbsdato.", | ||
"registration.messages.yourServerWillNotBeUsable": "<p>Din server vil ikke kunne bruges, før du køber en Registreringsnøgle eller installerer en gratis 30 dages <em>Prøvenøgle</em>. En <em>Prøvenøgle</em> giver alle funktionerne i en Unleashed Registreringsnøgle.</p><p>Registreringsnøgler er bundet til din USB Flash opstartsenheds serienummer (GUID). Brug venligst en højkvalitets enhedsmærke mindst 1GB i størrelse.</p><p>Bemærk: USB kortlæserne er generelt ikke understøttede, da de fleste ikke har unikke serienumre.</p><p><strong>Vigtigt:</strong></p><ul class='list-disc pl-16px'><li>Sørg for, at din servertid er korrekt afstemt inden for 5 minutter</li><li>Sørg for, at der er angivet en DNS-server</li></ul>", | ||
"registration.messages.yourTrialKeyIncludesAllThe": "<p>Din <em>Prøvenøgle</em> inkluderer alle funktionerne og enhedsunderstøttelsen af en <em>Unleashed</em> nøgle.</p><p>Efter din <em>Prøveperiode</em> er udløbet, fungerer din server <strong>normalt</strong>, indtil næste gang du standser arrays eller genstarter din server.</p><p>På dette tidspunkt kan du enten købe en licensnøgle eller anmode om en <em>Prøve</em> forlængelse.</p>", | ||
"registration.messages.yourTrialKeyRequiresAnInternet": "<p>Din Prøvenøgle kræver en internetforbindelse.</p><p><a href='/Settings/NetworkSettings class='underline'>Tjek venligst Indstillinger > Netværk</a></p>", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fix malformed HTML attributes (broken href/class quoting).
The anchor’s href/class quotes are misordered, which will break rendering and could affect link behavior.
Apply:
-"registration.messages.yourTrialKeyRequiresAnInternet": "<p>Din Prøvenøgle kræver en internetforbindelse.</p><p><a href='/Settings/NetworkSettings class='underline'>Tjek venligst Indstillinger > Netværk</a></p>",
+"registration.messages.yourTrialKeyRequiresAnInternet": "<p>Din Prøvenøgle kræver en internetforbindelse.</p><p><a href='/Settings/NetworkSettings' class='underline'>Tjek venligst Indstillinger > Netværk</a></p>",
Optionally, prefer double quotes for attributes to reduce escaping issues in JSON strings.
Also consider scanning for similar issues:
🏁 Script executed:
#!/bin/bash
# Search for likely malformed attribute quoting patterns in locale files
rg -nP "href=['\"][^\"']+\\s+class=['\"]" web/src/locales/*.json
Length of output: 5119
Fix malformed HTML attribute quoting across all locale JSON files.
In every web/src/locales/*.json
, change:
- <a href='/Settings/NetworkSettings class='underline'>
+ <a href='/Settings/NetworkSettings' class='underline'>
Optionally use double quotes for both attributes to simplify JSON escaping.
📝 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.
"registration.messages.yourTrialKeyRequiresAnInternet": "<p>Din Prøvenøgle kræver en internetforbindelse.</p><p><a href='/Settings/NetworkSettings class='underline'>Tjek venligst Indstillinger > Netværk</a></p>", | |
"registration.messages.yourTrialKeyRequiresAnInternet": "<p>Din Prøvenøgle kræver en internetforbindelse.</p><p><a href='/Settings/NetworkSettings' class='underline'>Tjek venligst Indstillinger > Netværk</a></p>", |
🤖 Prompt for AI Agents
In web/src/locales/da.json around line 306 (and apply across all
web/src/locales/*.json files), the HTML attribute quoting in the string is
malformed (the href attribute and class attribute quotes are
mixed/unterminated). Fix each locale string by properly quoting HTML attributes
and escaping quotes for JSON: use double quotes for HTML attributes and escape
them in the JSON string (or use single quotes for HTML attributes consistently
and ensure the JSON string delimiters remain valid). Update all locale files to
the same convention so attributes like href and class are correctly quoted and
JSON remains valid.
"userProfile.callbackFeedback.thisDowngradeWillRequireAReboot": "Denne nedgradering kræver en genstart", | ||
"userProfile.callbackFeedback.thisUpdateWillRequireAReboot": "Denne opdatering kræver en genstart", | ||
"userProfile.callbackFeedback.unraid": "Unraid {0}", | ||
"userProfile.callbackFeedback.updateUnraidOsConfirmationRequired": "Confirmation of Update Unraid OS required", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Untranslated entry remains in English.
"userProfile.callbackFeedback.updateUnraidOsConfirmationRequired" is still English. Not blocking, but worth translating for consistency.
🤖 Prompt for AI Agents
In web/src/locales/da.json around line 543 the value for
"userProfile.callbackFeedback.updateUnraidOsConfirmationRequired" is still in
English; replace it with a Danish translation such as "Bekræftelse kræves for
opdatering af Unraid OS" (or an equivalent Danish phrasing) so the locale file
is fully translated and consistent.
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
…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.
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this 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
📒 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 tomockT
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
invi.fn()
allows the test to track translation calls while using the real implementation from the centralized utilities. ThebeforeEach
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 onlyuseI18n
to returntestTranslate
. 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
andcreateTestI18n
, 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
andtestTranslate
, 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 toComposerTranslation
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
andtestTranslate
, providing a complete i18n context. The use oftestTranslate()
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 againsten.json
. The switch from inline mock totestTranslate
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-i18nComposerTranslation
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.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
"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.
…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.
- Updated JSON schema interfaces to adjust `i18n` properties, ensuring correct localization for both JSON Schema 4 and 7. - Corrected `i18n` references in OIDC configuration for allowed domains, emails, and user IDs, enhancing localization accuracy. - Added `i18n` support to accordion layout configuration, improving internationalization for UI components. These changes enhance the internationalization framework, ensuring a more consistent and user-friendly experience across the application.
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
- Updated the mock function for retrieving the window locale to ensure proper type inference. - Adjusted the infinite scroll directive in the Notifications List component for improved functionality. These changes enhance the test reliability and component behavior, contributing to a more robust application.
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1739 +/- ##
==========================================
- Coverage 53.35% 52.57% -0.78%
==========================================
Files 860 865 +5
Lines 48275 49198 +923
Branches 4929 4940 +11
==========================================
+ Hits 25756 25868 +112
- Misses 22450 23257 +807
- Partials 69 73 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This plugin has been deployed to Cloudflare R2 and is available for testing.
|
- Updated the i18n key assignment in createSimpleLabeledControl and createLabeledControl functions to directly use the provided i18nKey without appending '.label'. - This change streamlines the localization process for form controls, ensuring more accurate internationalization. These modifications enhance the clarity and functionality of the i18n integration in form utilities.
- Added support for the `@vue/compiler-sfc` version 3.5.20 in `package.json` and updated related dependencies in `pnpm-lock.yaml`. - Refactored translation keys in `ActivationSteps.vue` to utilize the `useI18n` composable for dynamic localization. - Enhanced the `extract-translations.mjs` script to improve the collection of translation keys, including new functions for handling JSON forms and TypeScript translation keys. - Updated English locale file with new translation keys and refined existing ones for better clarity and consistency. These changes significantly improve the internationalization framework, ensuring a more robust and user-friendly experience across the application.
- Updated test files across various components to utilize the `createTestI18n` utility for improved internationalization. - Refactored translation handling in `Auth.test.ts`, `KeyActions.test.ts`, `Registration.test.ts`, and others to streamline localization processes. - Removed direct translation key assignments in favor of using the `useI18n` composable, ensuring consistency and clarity in translation usage. These changes improve the robustness of the i18n framework in tests, facilitating better localization practices across the application.
})); | ||
|
||
const t = (key: string) => `translated_${key}`; | ||
const t = (key: string) => `${key}`; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 14 hours ago
To fix this issue, simply remove the declaration of the unused variable t
from line 30. This means deleting the line:
const t = (key: string) => `${key}`;
No other code needs to be changed. There are no imports or dependencies required. This edit should be performed in web/test/components/KeyActions.test.ts, at line 30 (as shown).
@@ -27,7 +27,6 @@ | ||
})), | ||
})); | ||
|
||
const t = (key: string) => `${key}`; | ||
|
||
describe('KeyActions', () => { | ||
beforeEach(() => { |
import type { Mock, MockInstance } from 'vitest'; | ||
|
||
import SsoButtons from '~/components/sso/SsoButtons.vue'; | ||
import { createTestI18n, testTranslate } from '../utils/i18n'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 14 hours ago
To fix the issue, the unused import testTranslate
should be removed from the import statement on line 13 in the file web/__test__/components/SsoButton.test.ts
. The rest of the code should be left unchanged, and only the unused named import should be removed.
-
Copy modified line R13
@@ -10,7 +10,7 @@ | ||
import type { Mock, MockInstance } from 'vitest'; | ||
|
||
import SsoButtons from '~/components/sso/SsoButtons.vue'; | ||
import { createTestI18n, testTranslate } from '../utils/i18n'; | ||
import { createTestI18n } from '../utils/i18n'; | ||
|
||
// Mock the child components | ||
const SsoProviderButtonStub = { |
|
||
import { WebguiState } from '~/composables/services/webgui'; | ||
import { useServerStore } from '~/store/server'; | ||
import { createTestI18n, testTranslate } from '../utils/i18n'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 14 hours ago
To fix the problem, the unused imported identifier createTestI18n
should be removed from the import statement. This involves editing the line:
import { createTestI18n, testTranslate } from '../utils/i18n';
to import only the actually used testTranslate
:
import { testTranslate } from '../utils/i18n';
This change should only be made to the one relevant line (line 22), and no other changes or additions are necessary.
-
Copy modified line R22
@@ -19,7 +19,7 @@ | ||
|
||
import { WebguiState } from '~/composables/services/webgui'; | ||
import { useServerStore } from '~/store/server'; | ||
import { createTestI18n, testTranslate } from '../utils/i18n'; | ||
import { testTranslate } from '../utils/i18n'; | ||
|
||
// Mock vue-i18n for store tests | ||
vi.mock('vue-i18n', () => ({ |
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this 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 toprocess.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 testsThis mock remains necessary because
serverStore
getters calluseI18n
.web/__test__/components/SsoButton.test.ts (1)
13-13
: Remove unused import.
testTranslate
is imported but never used in this test file. OnlycreateTestI18n
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
⛔ 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'
andcjs: 'always'
ensure module variant extensions are explicitts: 'never'
andtsx: '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 andmockReset()
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 bothaction.text
andaction.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. Theconvert
package is listed only underdependencies
, not indevDependencies
.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 thet
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
'suseI18n
to returntestTranslate
, 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
andtestTranslate
- Mocks
vue-i18n
while preserving the actual module exports- Injects
createTestI18n()
into the component mount plugins- Uses
testTranslate
for translation resolutionThis 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 thet
function to components under test. The static analysis tool doesn't recognize usage withinvi.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
anduseI18n
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)?
orjsonSchema7
in the codebase, so it’s unclear which specifiers your consumers actually use. Verify which paths are imported (or re-exported via your package’sexports
), then remove unused augmentations and keep only the canonical module(s).
import '@jsonforms/core/lib/models/jsonSchema4'; | ||
import '@jsonforms/core/lib/models/jsonSchema7'; | ||
import '@jsonforms/core/src/models/jsonSchema4'; | ||
import '@jsonforms/core/src/models/jsonSchema7'; |
There was a problem hiding this comment.
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.
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; | ||
} |
There was a problem hiding this comment.
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.
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.
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; | ||
} |
There was a problem hiding this comment.
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.
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.
})); | ||
|
||
const t = (key: string) => `translated_${key}`; | ||
const t = (key: string) => `${key}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove unused local translation function.
The local t
function defined at line 30 is unused, as confirmed by static analysis. The test now relies on createTestI18n()
to provide translation context, making this local function redundant.
Apply this diff to remove the unused function:
-const t = (key: string) => `${key}`;
-
describe('KeyActions', () => {
📝 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.
const t = (key: string) => `${key}`; | |
describe('KeyActions', () => { |
🧰 Tools
🪛 GitHub Check: CodeQL
[notice] 30-30: Unused variable, import, function or class
Unused variable t.
🤖 Prompt for AI Agents
In web/__test__/components/KeyActions.test.ts around line 30, there's an unused
local translation function `const t = (key: string) => `${key}`;` which is
redundant because tests use createTestI18n(); remove this line from the file to
eliminate the dead code and keep imports/fixtures intact.
|
||
import { WebguiState } from '~/composables/services/webgui'; | ||
import { useServerStore } from '~/store/server'; | ||
import { createTestI18n, testTranslate } from '../utils/i18n'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove unused import.
createTestI18n
is imported but never used in this store test file, as confirmed by static analysis. Store tests only need testTranslate
for mocking useI18n
.
Apply this diff:
-import { createTestI18n, testTranslate } from '../utils/i18n';
+import { testTranslate } from '../utils/i18n';
📝 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.
import { createTestI18n, testTranslate } from '../utils/i18n'; | |
import { testTranslate } from '../utils/i18n'; |
🧰 Tools
🪛 GitHub Check: CodeQL
[notice] 22-22: Unused variable, import, function or class
Unused import createTestI18n.
🤖 Prompt for AI Agents
In web/__test__/store/server.test.ts around line 22, the import statement
includes createTestI18n which is unused; remove createTestI18n from the import
list so only testTranslate is imported to eliminate the unused import warning
and keep the test file minimal.
Summary by CodeRabbit
New Features
UI
Documentation
Chores
Tests