Conversation
WalkthroughThis pull request introduces comprehensive internationalization support via ParaglideJS, adding English and Spanish message translations, updating 30+ UI components to use localized strings, creating a new Switch component with theming, and establishing locale/feature-flag state management while removing related documentation. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
There was a problem hiding this comment.
Pull request overview
This PR integrates localization into the application using @inlang/paraglide-js, adding support for English and Spanish languages. The implementation includes a language switcher UI, locale persistence, and comprehensive translation coverage across navigation items, settings, and user-facing messages.
Changes:
- Adds
@inlang/paraglide-jsdependency and Vite plugin for i18n support - Implements Switch UI component with theme recipe for settings toggles
- Creates locale management with Reatom atoms for persistent language selection
- Localizes all user-facing strings across navigation, settings, errors, and page titles
- Adds top bar visibility controls for language switcher, GitHub link, and theme switcher
Reviewed changes
Copilot reviewed 40 out of 42 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Adds paraglide Vite plugin for message compilation |
| theme/recipes/switch.ts | Defines Switch component styling recipe with variants |
| theme/recipes/index.ts | Exports switchRecipe for use in styled components |
| src/widgets/layout/ui/AppShell.tsx | Adds language switcher dropdown, imports locale utilities, renames Menu icon |
| src/shared/model/topBar.ts | Creates atoms for controlling top bar element visibility |
| src/shared/model/locale.ts | Implements locale atom with localStorage persistence and paraglide sync |
| src/shared/model/index.ts | Exports new locale and top bar visibility atoms |
| src/shared/components/ui/switch.tsx | Implements Switch component using Ark UI with Panda CSS styling |
| src/shared/components/ui/index.ts | Exports Switch component namespace |
| src/pages//ui/.tsx | Replaces hardcoded strings with localized message calls throughout all page components |
| src/app/MobileHeader.tsx | Localizes mobile header titles for all routes |
| src/app/App.tsx | Initializes locale atom and localizes app name |
| project.inlang/settings.json | Configures inlang with base locale (en) and Spanish support |
| package.json | Adds @inlang/paraglide-js dependency |
| messages/en.json | Defines English message catalog with 77 translation keys |
| messages/es.json | Defines Spanish message catalog with 77 translation keys |
| docs/installable-park-components.md | Removes Park UI component installation documentation |
| .gitignore | Ignores generated paraglide output directory |
| .config/mise/conf.d/tasks-*.toml | Adds paraglide compilation task to CI and prepare workflows |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| onValueChange={wrap( | ||
| (details: Select.ValueChangeDetails<{ label: string; value: string }>) => { | ||
| const val = details.value[0] | ||
| if (val !== undefined) { | ||
| localeAtom.set(val as 'en' | 'es') | ||
| } | ||
| }, |
There was a problem hiding this comment.
Type casting used for locale value without validation. While isLocale is checked in the AppShell.tsx language switcher, the Settings page directly casts the value to 'en' | 'es' without validation. Consider adding a runtime check using isLocale before setting the value, similar to the pattern in AppShell.tsx lines 51-55, to ensure type safety and prevent invalid locale values from being set.
| export const localeAtom = atom(getLocale(), 'locale').extend( | ||
| withLocalStorage('locale'), | ||
| withChangeHook((value) => void setLocale(value, { reload: false })), | ||
| ) |
There was a problem hiding this comment.
Potential initialization race condition. The localeAtom is initialized with getLocale() and extended with withLocalStorage('locale'). This creates a situation where the atom's initial value comes from paraglide's runtime, but withLocalStorage will also attempt to read from localStorage on initialization. If there's a mismatch between what paraglide returns and what's in localStorage, the behavior may be unpredictable. Consider ensuring these sources are synchronized, or documenting which source takes precedence during initialization.
| plugins: [ | ||
| // | ||
| react(), | ||
| paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide' }), | ||
| enableDevTools && DevTools(), | ||
| ].filter(Boolean), |
There was a problem hiding this comment.
A comment line was removed without replacement. The removed comment marker // at line 49 (in the original) may have served as a placeholder or visual separator in the plugins array. While this is a minor change, it's worth noting that the removal makes the code slightly less visually organized. This is a very minor stylistic change with no functional impact.
| size="sm" | ||
| display={{ base: 'none', md: 'inline-flex' }} | ||
| onClick={cycleTheme} | ||
| aria-label="Toggle theme" |
There was a problem hiding this comment.
Hardcoded aria-label should be localized. The aria-label "Toggle theme" at line 242 is hardcoded in English, while other aria-labels in the same component (like the language switcher at line 211) use localized messages. For consistency and proper internationalization, this should use a message key like m.topbar_theme_switcher_label() to ensure screen reader users see the label in their preferred language.
| Articles | ||
| {m.nav_articles()} | ||
| </styled.h3> | ||
| <IconButton size="xs" variant="plain" aria-label="New article"> |
There was a problem hiding this comment.
Hardcoded aria-label should be localized. The aria-label "New article" is hardcoded in English. For consistency with the localization effort throughout the codebase, this should use a message key like m.article_new() or m.articles_add_button() to ensure screen reader users see the label in their preferred language.
| <IconButton size="xs" variant="plain" aria-label="New article"> | |
| <IconButton size="xs" variant="plain" aria-label={m.article_new()}> |
| interface IndicatorProps extends ComponentProps<typeof StyledIndicator> { | ||
| fallback?: ReactNode | undefined | ||
| } |
There was a problem hiding this comment.
Inconsistent usage of ReactNode type import. Line 25 uses ReactNode (imported from 'react'), while line 40 uses React.ReactNode (namespace import). For consistency with the codebase convention, both should use ReactNode from the direct import at the top of the file.
| interface ThumbIndicatorProps extends ComponentProps<typeof StyledThumbIndicator> { | ||
| fallback?: React.ReactNode | undefined | ||
| } |
There was a problem hiding this comment.
Inconsistent usage of ReactNode type import. Line 40 uses React.ReactNode (namespace import), while line 25 uses ReactNode (imported from 'react'). For consistency with the codebase convention and the import at line 4, this should be ReactNode instead of React.ReactNode.
| @@ -11,12 +13,13 @@ import { SidebarFooterNavigation } from './SidebarFooterNavigation' | |||
| import { SidebarNavigation } from './SidebarNavigation' | |||
|
|
|||
| export const App = reatomComponent(() => { | |||
There was a problem hiding this comment.
The localeAtom() call appears to be present only to ensure the atom is initialized and subscribes to state changes. However, this creates a subscription without using the returned value. Consider adding a comment explaining why this atom needs to be called at the App root level (e.g., to synchronize with localStorage on mount), or consider if there's a more explicit pattern for initializing atoms that need side effects.
| export const App = reatomComponent(() => { | |
| export const App = reatomComponent(() => { | |
| // Initialize and subscribe locale state at the app root (e.g. sync with persistent storage on mount) |
| const emailNotificationsCollection = createListCollection({ | ||
| items: [ | ||
| { label: m.settings_notif_all(), value: 'all' }, | ||
| { label: m.settings_notif_important(), value: 'important' }, | ||
| { label: m.settings_notif_none(), value: 'none' }, | ||
| ], | ||
| itemToString: (item) => item.label, | ||
| itemToValue: (item) => item.value, | ||
| }) | ||
|
|
||
| const desktopNotificationsCollection = createListCollection({ | ||
| items: [ | ||
| { label: m.settings_notif_enabled(), value: 'enabled' }, | ||
| { label: m.settings_notif_disabled(), value: 'disabled' }, | ||
| ], | ||
| itemToString: (item) => item.label, | ||
| itemToValue: (item) => item.value, | ||
| }) | ||
|
|
||
| const themeCollection = createListCollection({ | ||
| items: [ | ||
| { label: m.settings_theme_light(), value: 'light' }, | ||
| { label: m.settings_theme_dark(), value: 'dark' }, | ||
| { label: m.settings_theme_system(), value: 'system' }, | ||
| ], | ||
| itemToString: (item) => item.label, | ||
| itemToValue: (item) => item.value, | ||
| }) | ||
|
|
||
| const densityCollection = createListCollection({ | ||
| items: [ | ||
| { label: m.settings_density_compact(), value: 'compact' }, | ||
| { label: m.settings_density_comfortable(), value: 'comfortable' }, | ||
| { label: m.settings_density_spacious(), value: 'spacious' }, | ||
| ], | ||
| itemToString: (item) => item.label, | ||
| itemToValue: (item) => item.value, | ||
| }) | ||
|
|
||
| const languageCollection = createListCollection({ | ||
| items: [ | ||
| { label: m.language_en(), value: 'en' }, | ||
| { label: m.language_es(), value: 'es' }, | ||
| ], | ||
| itemToString: (item) => item.label, | ||
| itemToValue: (item) => item.value, | ||
| }) |
There was a problem hiding this comment.
List collections are being recreated on every render. The emailNotificationsCollection, desktopNotificationsCollection, themeCollection, densityCollection, and languageCollection are created inside the component body, which means they'll be recreated on every render. Since these collections depend on localized messages via m.*() functions, they need to be recreated when the locale changes, but this could be optimized using useMemo to only recreate when the locale actually changes, reducing unnecessary object allocations on other state changes.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pages/connections/ui/ConnectionNoSelection.tsx (1)
9-22:⚠️ Potential issue | 🟡 MinorStyling diverges from sibling "no selection" components.
The layout props now differ from the structurally identical
ItemNoSelectionandMessageThreadNoSelectioncomponents (seesrc/pages/items/ui/ItemNoSelection.tsxandsrc/pages/chat/ui/MessageThreadNoSelection.tsx):
Prop This component Siblings height minH="400px"h="100%"gap "4""3"padding (removed) p="8"textAlign (removed) textAlign="center"color (removed) color="gray.11"Dropping
textAlign="center"means the text will left-align, and droppingcolor="gray.11"removes the muted text color. If this is intentional (e.g., these styles are now handled elsewhere), please disregard — otherwise, consider aligning with the sibling components for visual consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/connections/ui/ConnectionNoSelection.tsx` around lines 9 - 22, The ConnectionNoSelection component's container props diverge from ItemNoSelection and MessageThreadNoSelection; update the styled.div in ConnectionNoSelection to match the siblings by replacing minH="400px" with h="100%", changing gap="4" to gap="3", re-adding p="8", textAlign="center", and color="gray.11" so the layout, alignment and muted text color are consistent (locate the styled.div inside ConnectionNoSelection and mirror the prop values used in ItemNoSelection and MessageThreadNoSelection).
🧹 Nitpick comments (5)
src/pages/calculator/ui/CalculatorPage.tsx (1)
145-147: Consider a dedicated message key for the page heading.The
nav_calculatorkey is reused for the<h1>title. Nav labels and page headings are semantically distinct UI strings that can diverge in translations (e.g., abbreviated nav labels vs. full headings). A separate key likecalculator_titlewould allow them to be translated independently.✏️ Suggested change
-{m.nav_calculator()} +{m.calculator_title()}And add
"calculator_title": "Calculator"tomessages/en.jsonandmessages/es.json.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/calculator/ui/CalculatorPage.tsx` around lines 145 - 147, The page heading currently uses m.nav_calculator which is a nav label; change the heading to a dedicated message key (e.g., m.calculator_title) in CalculatorPage (replace the use of m.nav_calculator inside the styled.h1) and add the new key "calculator_title": "Calculator" to your locale files (messages/en.json and messages/es.json); also update any TypeScript message typings or message bundle interfaces if you have a generated or typed messages object so m.calculator_title is recognized..config/mise/conf.d/tasks-prepare.toml (1)
30-34: LGTM — minor:sourcesmay miss additionalproject.inlang/config files.The task definition is correct. The
sourcesglob only coversproject.inlang/settings.json, but some Inlang project setups include additional plugin-specific files underproject.inlang/. If those files change, mise won't invalidate the task and the catalog won't be recompiled.💡 Broaden the sources glob (optional)
-sources = ["messages/**/*.json", "project.inlang/settings.json"] +sources = ["messages/**/*.json", "project.inlang/**/*.json"]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.config/mise/conf.d/tasks-prepare.toml around lines 30 - 34, The sources glob for the task [tasks."prepare:paraglide"] is too narrow (only "project.inlang/settings.json"); widen the sources array so changes to any files under project.inlang invalidate the task — update the "sources" entry to include a recursive glob like "project.inlang/**/*" (in addition to or replacing the specific settings.json) so plugin-specific configs are picked up when modified.theme/recipes/index.ts (1)
45-45: Nit: property keyswitchRecipebreaks the naming pattern of sibling entries.All other keys in
recipesuse the bare component name (button,drawer,menu, etc.). Sinceswitchis valid as an object property key in JS/TS (reserved words are allowed in property positions), you could keep this consistent:♻️ Suggested change
- switchRecipe, + switch: switchRecipe,The
className: 'switch'in the recipe definition already governs CSS output, so this is purely a config-level consistency matter.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@theme/recipes/index.ts` at line 45, The property key switchRecipe in the recipes export breaks the naming pattern; rename the key to switch (matching siblings like button, drawer, menu) and keep the recipe definition's className: 'switch' as-is; update any references/imports that currently use switchRecipe to use the new switch key (locate the recipes object and exports where switchRecipe is listed, and adjust usages across codebase such as imports or destructuring that reference switchRecipe).src/app/App.tsx (1)
16-16: Add a comment explaining the barelocaleAtom()subscription.The return value is intentionally discarded — the call solely subscribes this
reatomComponentto locale changes, ensuringm.app_name()(and otherm.*()calls in passed props) re-evaluates on every locale switch. ParaglideJS v2 requires the host framework's own reactivity to trigger re-renders aftersetLocale(), making this subscription load-bearing. Without it, the sidebar header would remain stale after a language change.💡 Suggested clarifying comment
export const App = reatomComponent(() => { - localeAtom() + // Subscribe to locale changes so m.*() calls in this render (e.g. m.app_name()) + // re-evaluate whenever the user switches language. + localeAtom() return (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/App.tsx` at line 16, Add a clarifying inline comment at the call site of localeAtom() explaining that the return value is intentionally discarded because the call only subscribes the reatomComponent to locale changes so that calls like m.app_name() in props re-evaluate on locale switches; mention that ParaglideJS v2 relies on the host framework to re-render after setLocale(), so this subscription is load-bearing to keep the sidebar header in sync. Reference the existing localeAtom() call and reatomComponent / m.app_name() in the comment.src/pages/settings/ui/SettingsPage.tsx (1)
50-96: List collections are recreated on every render.All five
createListCollectioncalls run inside the render body, producing new object references each time any atom changes. On a settings page this is unlikely to cause a noticeable perf issue, but if you want to avoid unnecessary Select re-renders you could memoize these (e.g.,useMemokeyed onlocale) or move the non-localized parts out of the render.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/settings/ui/SettingsPage.tsx` around lines 50 - 96, The five createListCollection calls (emailNotificationsCollection, desktopNotificationsCollection, themeCollection, densityCollection, languageCollection) are being recreated on every render; wrap each call in a memoization so the collection objects are stable across renders (e.g., useMemo) and only recompute when their inputs change (for localized labels depend on locale/m and for static lists use a single module-level constant), ensuring createListCollection is invoked only when necessary to avoid needless Select re-renders.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@messages/en.json`:
- Around line 53-55: Two description strings are missing a trailing period:
update the value for "settings_show_github_link_desc" and the value for
"settings_show_theme_switcher_desc" to end with a period so they match the other
`_desc` entries; locate those keys in messages/en.json and append a period to
each string while preserving the rest of the text.
In `@src/pages/settings/ui/SettingsPage.tsx`:
- Around line 134-138: The displayed role string is hardcoded ("Admin") inside
the FieldRow/ styled.span and should be localized like the rest; replace the
literal "Admin" with a call to the i18n message helper (e.g. use
m.settings_role_admin() or add a new message key and call it) so the span
renders the localized admin label; update the usage in the FieldRow where
styled.span currently contains "Admin" (and add the corresponding message key to
the messages file if it doesn't exist).
- Around line 360-367: Replace the type assertion with Paraglide's runtime
guard: import isLocale from '#paraglide/runtime.js' and inside the onValueChange
handler (the wrapped callback using Select.ValueChangeDetails) call
isLocale(val) and only then call localeAtom.set(val); remove the "as 'en' |
'es'" assertion and ensure the handler safely ignores non-locales.
In `@src/shared/components/ui/switch.tsx`:
- Around line 39-41: The interface ThumbIndicatorProps references
React.ReactNode but the file imports ReactNode as a named type, causing a TS
error; update the interface to use the already-imported ReactNode (i.e., change
React.ReactNode to ReactNode) or alternatively import the React namespace, and
ensure the interface extends ComponentProps<typeof StyledThumbIndicator> with
fallback?: ReactNode | undefined so it compiles; reference symbols:
ThumbIndicatorProps and StyledThumbIndicator.
In `@src/widgets/layout/ui/AppShell.tsx`:
- Line 193: The GitHub link in AppShell.tsx uses a hardcoded aria-label string
("View source on GitHub") while other labels use localization; add a new i18n
message key (e.g., topbar_github_link_label) to your messages bundle and replace
the hardcoded value in the JSX for the GitHub link (the element that currently
sets aria-label directly) with m.topbar_github_link_label() so it matches the
pattern used by m.topbar_language_switcher_label() and other localized labels.
- Around line 187-235: The topbar language Menu (controlled by
showLanguageSwitcher, Menu.Root, Menu.Trigger, and handleLocaleChange) is
correctly hidden on mobile via display={{ base: 'none', md: 'inline-flex' }},
but mobile users still need an accessible place to change locale; add the same
language switch UI (or a simplified variant that uses Menu.RadioItemGroup and
handleLocaleChange) into the mobile drawer or MobileHeader component so language
switching is discoverable on small screens—locate the drawer/sidebar or
MobileHeader rendering in the app shell and copy/adapt the language switcher
there, ensuring it uses the same locale state and handleLocaleChange handler.
- Around line 223-230: The locale options are hardcoded (Menu.RadioItem for
"en"/"es") so new project.inlang locales are missed; import the runtime-exported
readonly array locales and replace the two static Menu.RadioItem entries by
mapping over locales to render a Menu.RadioItem per locale (use each locale as
the value/key), render Menu.ItemText inside each item (use the existing m
translation helpers like m.language_<locale>() when available or fall back to
the raw locale string), and keep Menu.ItemIndicator unchanged so the menu
remains functional (refer to symbols locales, Menu.RadioItem, Menu.ItemText,
Menu.ItemIndicator, and m).
---
Outside diff comments:
In `@src/pages/connections/ui/ConnectionNoSelection.tsx`:
- Around line 9-22: The ConnectionNoSelection component's container props
diverge from ItemNoSelection and MessageThreadNoSelection; update the styled.div
in ConnectionNoSelection to match the siblings by replacing minH="400px" with
h="100%", changing gap="4" to gap="3", re-adding p="8", textAlign="center", and
color="gray.11" so the layout, alignment and muted text color are consistent
(locate the styled.div inside ConnectionNoSelection and mirror the prop values
used in ItemNoSelection and MessageThreadNoSelection).
---
Nitpick comments:
In @.config/mise/conf.d/tasks-prepare.toml:
- Around line 30-34: The sources glob for the task [tasks."prepare:paraglide"]
is too narrow (only "project.inlang/settings.json"); widen the sources array so
changes to any files under project.inlang invalidate the task — update the
"sources" entry to include a recursive glob like "project.inlang/**/*" (in
addition to or replacing the specific settings.json) so plugin-specific configs
are picked up when modified.
In `@src/app/App.tsx`:
- Line 16: Add a clarifying inline comment at the call site of localeAtom()
explaining that the return value is intentionally discarded because the call
only subscribes the reatomComponent to locale changes so that calls like
m.app_name() in props re-evaluate on locale switches; mention that ParaglideJS
v2 relies on the host framework to re-render after setLocale(), so this
subscription is load-bearing to keep the sidebar header in sync. Reference the
existing localeAtom() call and reatomComponent / m.app_name() in the comment.
In `@src/pages/calculator/ui/CalculatorPage.tsx`:
- Around line 145-147: The page heading currently uses m.nav_calculator which is
a nav label; change the heading to a dedicated message key (e.g.,
m.calculator_title) in CalculatorPage (replace the use of m.nav_calculator
inside the styled.h1) and add the new key "calculator_title": "Calculator" to
your locale files (messages/en.json and messages/es.json); also update any
TypeScript message typings or message bundle interfaces if you have a generated
or typed messages object so m.calculator_title is recognized.
In `@src/pages/settings/ui/SettingsPage.tsx`:
- Around line 50-96: The five createListCollection calls
(emailNotificationsCollection, desktopNotificationsCollection, themeCollection,
densityCollection, languageCollection) are being recreated on every render; wrap
each call in a memoization so the collection objects are stable across renders
(e.g., useMemo) and only recompute when their inputs change (for localized
labels depend on locale/m and for static lists use a single module-level
constant), ensuring createListCollection is invoked only when necessary to avoid
needless Select re-renders.
In `@theme/recipes/index.ts`:
- Line 45: The property key switchRecipe in the recipes export breaks the naming
pattern; rename the key to switch (matching siblings like button, drawer, menu)
and keep the recipe definition's className: 'switch' as-is; update any
references/imports that currently use switchRecipe to use the new switch key
(locate the recipes object and exports where switchRecipe is listed, and adjust
usages across codebase such as imports or destructuring that reference
switchRecipe).
| "settings_show_github_link_desc": "Show GitHub repository link in the top bar", | ||
| "settings_show_theme_switcher": "Show Theme Switcher", | ||
| "settings_show_theme_switcher_desc": "Show theme switcher in the top bar", |
There was a problem hiding this comment.
Minor: missing trailing period on two _desc strings, inconsistent with the rest.
All other _desc keys in this file end with a period. settings_show_github_link_desc (line 53) and settings_show_theme_switcher_desc (line 55) do not.
✏️ Proposed fix
- "settings_show_github_link_desc": "Show GitHub repository link in the top bar",
+ "settings_show_github_link_desc": "Show GitHub repository link in the top bar.",
"settings_show_theme_switcher": "Show Theme Switcher",
- "settings_show_theme_switcher_desc": "Show theme switcher in the top bar",
+ "settings_show_theme_switcher_desc": "Show theme switcher in the top bar.",📝 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.
| "settings_show_github_link_desc": "Show GitHub repository link in the top bar", | |
| "settings_show_theme_switcher": "Show Theme Switcher", | |
| "settings_show_theme_switcher_desc": "Show theme switcher in the top bar", | |
| "settings_show_github_link_desc": "Show GitHub repository link in the top bar.", | |
| "settings_show_theme_switcher": "Show Theme Switcher", | |
| "settings_show_theme_switcher_desc": "Show theme switcher in the top bar.", |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@messages/en.json` around lines 53 - 55, Two description strings are missing a
trailing period: update the value for "settings_show_github_link_desc" and the
value for "settings_show_theme_switcher_desc" to end with a period so they match
the other `_desc` entries; locate those keys in messages/en.json and append a
period to each string while preserving the rest of the text.
| <FieldRow label={m.settings_role()}> | ||
| <styled.span fontSize="sm" color="gray.11"> | ||
| Admin | ||
| </styled.span> | ||
| </FieldRow> |
There was a problem hiding this comment.
"Admin" role label is still hardcoded in English.
All other user-facing strings in this file have been localized via m.*(), but line 136 still renders a raw "Admin" string. This looks like a localization miss.
Proposed fix
<FieldRow label={m.settings_role()}>
<styled.span fontSize="sm" color="gray.11">
- Admin
+ {m.settings_role_admin()}
</styled.span>
</FieldRow>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/settings/ui/SettingsPage.tsx` around lines 134 - 138, The displayed
role string is hardcoded ("Admin") inside the FieldRow/ styled.span and should
be localized like the rest; replace the literal "Admin" with a call to the i18n
message helper (e.g. use m.settings_role_admin() or add a new message key and
call it) so the span renders the localized admin label; update the usage in the
FieldRow where styled.span currently contains "Admin" (and add the corresponding
message key to the messages file if it doesn't exist).
| value={[locale]} | ||
| onValueChange={wrap( | ||
| (details: Select.ValueChangeDetails<{ label: string; value: string }>) => { | ||
| const val = details.value[0] | ||
| if (val !== undefined) { | ||
| localeAtom.set(val as 'en' | 'es') | ||
| } | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how localeAtom is typed
ast-grep --pattern 'const localeAtom = $_'Repository: Guria/modern-stack
Length of output: 340
🏁 Script executed:
#!/bin/bash
# Also search for localeAtom definition with rg to catch more variations
rg -A 5 'localeAtom\s*=' --type ts --type tsx | head -50Repository: Guria/modern-stack
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Look for languageCollection definition
rg -B 2 -A 10 'languageCollection' --type ts --type tsx | head -100Repository: Guria/modern-stack
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Search for Paraglide configuration or locale-related constants
rg -i 'paraglide|availableLanguages|supportedLocales' --type ts --type tsx --type js --type json | head -50Repository: Guria/modern-stack
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Read the complete locale.ts file to understand localeAtom type
cat -n src/shared/model/locale.tsRepository: Guria/modern-stack
Length of output: 413
🏁 Script executed:
#!/bin/bash
# Search for languageCollection using ts file type only
rg -B 2 -A 10 'languageCollection' -t tsRepository: Guria/modern-stack
Length of output: 2711
🏁 Script executed:
#!/bin/bash
# Search for locale type definitions
rg -B 2 -A 3 'type.*locale|locale.*type|Locale.*=' -t ts | head -100Repository: Guria/modern-stack
Length of output: 488
🏁 Script executed:
#!/bin/bash
# Look at the SettingsPage.tsx file around lines 360-367
sed -n '350,380p' src/pages/settings/ui/SettingsPage.tsxRepository: Guria/modern-stack
Length of output: 945
🏁 Script executed:
#!/bin/bash
# Find the isLocale function definition
rg -B 2 -A 8 'function isLocale|const isLocale|export.*isLocale' -t tsRepository: Guria/modern-stack
Length of output: 44
🏁 Script executed:
#!/bin/bash
# Check the Paraglide runtime configuration
find . -path ./node_modules -prune -o -type f -name "*.json" -print | xargs grep -l "paraglide\|lang\|locale" 2>/dev/null | head -10Repository: Guria/modern-stack
Length of output: 145
🏁 Script executed:
#!/bin/bash
# Look for Paraglide runtime directory or config
ls -la src/lib 2>/dev/null || ls -la . | grep -i paraRepository: Guria/modern-stack
Length of output: 44
🏁 Script executed:
#!/bin/bash
# Check tsconfig or similar for available locales
rg -i 'paraglide|available.*locale|supported.*language' -t ts -t json | head -30Repository: Guria/modern-stack
Length of output: 2595
🏁 Script executed:
#!/bin/bash
# Check the Paraglide project settings
cat project.inlang/settings.jsonRepository: Guria/modern-stack
Length of output: 432
🏁 Script executed:
#!/bin/bash
# Check if there's any locale type definition in the codebase
rg "type.*Locale|Locale\s*=|as const" -t ts -B 2 -A 2 | head -50Repository: Guria/modern-stack
Length of output: 2278
🏁 Script executed:
#!/bin/bash
# Look at the complete AppShell.tsx section to see how isLocale is used
rg -B 5 -A 10 'isLocale' -t tsRepository: Guria/modern-stack
Length of output: 2203
🏁 Script executed:
#!/bin/bash
# Check if localeAtom type is inferred from Paraglide
rg -B 5 -A 5 'getLocale\(\)' -t tsRepository: Guria/modern-stack
Length of output: 573
Use Paraglide's isLocale() runtime guard instead of type assertion.
The hardcoded type assertion val as 'en' | 'es' bypasses type safety. While project.inlang/settings.json defines the canonical locales (["en", "es"]), this assertion ignores it. If a locale is added to the Paraglide config, TypeScript won't catch the mismatch.
The codebase already has a better pattern in src/widgets/layout/ui/AppShell.tsx:
import { isLocale } from '#paraglide/runtime.js'
if (isLocale(value)) {
localeAtom.set(value)
}
Apply the same approach in SettingsPage.tsx for consistency and runtime safety.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/settings/ui/SettingsPage.tsx` around lines 360 - 367, Replace the
type assertion with Paraglide's runtime guard: import isLocale from
'#paraglide/runtime.js' and inside the onValueChange handler (the wrapped
callback using Select.ValueChangeDetails) call isLocale(val) and only then call
localeAtom.set(val); remove the "as 'en' | 'es'" assertion and ensure the
handler safely ignores non-locales.
| interface ThumbIndicatorProps extends ComponentProps<typeof StyledThumbIndicator> { | ||
| fallback?: React.ReactNode | undefined | ||
| } |
There was a problem hiding this comment.
React.ReactNode is not in scope — TypeScript compilation error.
Line 4 imports { type ComponentProps, type ReactNode } but line 40 references React.ReactNode. Since React is not imported as a namespace, this will fail to compile.
Proposed fix
-interface ThumbIndicatorProps extends ComponentProps<typeof StyledThumbIndicator> {
- fallback?: React.ReactNode | undefined
-}
+interface ThumbIndicatorProps extends ComponentProps<typeof StyledThumbIndicator> {
+ fallback?: ReactNode | undefined
+}📝 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.
| interface ThumbIndicatorProps extends ComponentProps<typeof StyledThumbIndicator> { | |
| fallback?: React.ReactNode | undefined | |
| } | |
| interface ThumbIndicatorProps extends ComponentProps<typeof StyledThumbIndicator> { | |
| fallback?: ReactNode | undefined | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/components/ui/switch.tsx` around lines 39 - 41, The interface
ThumbIndicatorProps references React.ReactNode but the file imports ReactNode as
a named type, causing a TS error; update the interface to use the
already-imported ReactNode (i.e., change React.ReactNode to ReactNode) or
alternatively import the React namespace, and ensure the interface extends
ComponentProps<typeof StyledThumbIndicator> with fallback?: ReactNode |
undefined so it compiles; reference symbols: ThumbIndicatorProps and
StyledThumbIndicator.
| {showGithubLink && ( | ||
| <IconButton | ||
| variant="plain" | ||
| size="sm" | ||
| display={{ base: 'none', md: 'inline-flex' }} | ||
| asChild | ||
| aria-label="View source on GitHub" | ||
| > | ||
| <Github className={css({ w: '4', h: '4' })} /> | ||
| </a> | ||
| </IconButton> | ||
| <IconButton | ||
| variant="plain" | ||
| size="sm" | ||
| display={{ base: 'none', md: 'inline-flex' }} | ||
| onClick={cycleTheme} | ||
| aria-label="Toggle theme" | ||
| > | ||
| {preference === 'system' ? ( | ||
| <Monitor className={css({ w: '4', h: '4' })} /> | ||
| ) : resolved === 'dark' ? ( | ||
| <Moon className={css({ w: '4', h: '4' })} /> | ||
| ) : ( | ||
| <Sun className={css({ w: '4', h: '4' })} /> | ||
| )} | ||
| </IconButton> | ||
| <a | ||
| href="https://github.com/guria/modern-stack" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| > | ||
| <Github className={css({ w: '4', h: '4' })} /> | ||
| </a> | ||
| </IconButton> | ||
| )} | ||
| {showLanguageSwitcher && ( | ||
| <Menu.Root positioning={{ placement: 'bottom-end' }}> | ||
| <Menu.Trigger asChild> | ||
| <IconButton | ||
| variant="plain" | ||
| size="sm" | ||
| display={{ base: 'none', md: 'inline-flex' }} | ||
| aria-label={m.topbar_language_switcher_label()} | ||
| > | ||
| <Languages className={css({ w: '4', h: '4' })} /> | ||
| </IconButton> | ||
| </Menu.Trigger> | ||
| <Menu.Positioner> | ||
| <Menu.Content> | ||
| <Menu.RadioItemGroup | ||
| id="locale" | ||
| value={locale} | ||
| onValueChange={handleLocaleChange} | ||
| > | ||
| <Menu.RadioItem value="en"> | ||
| <Menu.ItemText>{m.language_en()}</Menu.ItemText> | ||
| <Menu.ItemIndicator /> | ||
| </Menu.RadioItem> | ||
| <Menu.RadioItem value="es"> | ||
| <Menu.ItemText>{m.language_es()}</Menu.ItemText> | ||
| <Menu.ItemIndicator /> | ||
| </Menu.RadioItem> | ||
| </Menu.RadioItemGroup> | ||
| </Menu.Content> | ||
| </Menu.Positioner> | ||
| </Menu.Root> | ||
| )} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find all component files and understand the structure
fd "MobileHeader|Drawer" --type fRepository: Guria/modern-stack
Length of output: 291
🏁 Script executed:
# Also search more broadly for mobile components and language switching
rg -l "MobileHeader|Drawer" --type=tsxRepository: Guria/modern-stack
Length of output: 89
🏁 Script executed:
# Search for language/locale references in the codebase
rg -n "locale|language|Language|Languages" --type=tsx -B2 -A2 | head -100Repository: Guria/modern-stack
Length of output: 89
🏁 Script executed:
cat -n src/app/MobileHeader.tsxRepository: Guria/modern-stack
Length of output: 2322
🏁 Script executed:
# Search for locale and language references without the problematic file type
rg "locale|language|Language|Languages" -B2 -A2 | head -150Repository: Guria/modern-stack
Length of output: 9040
🏁 Script executed:
# Look at AppShell.tsx for drawer content
cat -n src/widgets/layout/ui/AppShell.tsx | head -200Repository: Guria/modern-stack
Length of output: 7102
🏁 Script executed:
# Look for the sidebar content and how Settings is exposed
rg "nav_settings" -B3 -A3Repository: Guria/modern-stack
Length of output: 1855
🏁 Script executed:
# Check what sidebar navigation items are available
cat -n src/widgets/layout/ui/AppShell.tsx | grep -A 50 "sidebarContent"Repository: Guria/modern-stack
Length of output: 3611
Language switcher is correctly hidden on mobile, but language switching remains accessible.
The topbar language switcher uses display={{ base: 'none', md: 'inline-flex' }}, which is appropriate for mobile. However, mobile users can switch language via the Settings page (accessible through the sidebar drawer), so the concern that "mobile users are locked into their initial locale" is not accurate. That said, the Settings route is less discoverable than a dedicated topbar control—if improving mobile UX for language switching is desired, consider adding a language switcher directly in the drawer or MobileHeader.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/widgets/layout/ui/AppShell.tsx` around lines 187 - 235, The topbar
language Menu (controlled by showLanguageSwitcher, Menu.Root, Menu.Trigger, and
handleLocaleChange) is correctly hidden on mobile via display={{ base: 'none',
md: 'inline-flex' }}, but mobile users still need an accessible place to change
locale; add the same language switch UI (or a simplified variant that uses
Menu.RadioItemGroup and handleLocaleChange) into the mobile drawer or
MobileHeader component so language switching is discoverable on small
screens—locate the drawer/sidebar or MobileHeader rendering in the app shell and
copy/adapt the language switcher there, ensuring it uses the same locale state
and handleLocaleChange handler.
| size="sm" | ||
| display={{ base: 'none', md: 'inline-flex' }} | ||
| asChild | ||
| aria-label="View source on GitHub" |
There was a problem hiding this comment.
aria-label for GitHub link is hardcoded while other new labels are localized.
Line 211 correctly uses m.topbar_language_switcher_label(), but line 193's "View source on GitHub" is a bare string in the same new block. Consider adding a topbar_github_link_label message key for consistency.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/widgets/layout/ui/AppShell.tsx` at line 193, The GitHub link in
AppShell.tsx uses a hardcoded aria-label string ("View source on GitHub") while
other labels use localization; add a new i18n message key (e.g.,
topbar_github_link_label) to your messages bundle and replace the hardcoded
value in the JSX for the GitHub link (the element that currently sets aria-label
directly) with m.topbar_github_link_label() so it matches the pattern used by
m.topbar_language_switcher_label() and other localized labels.
| <Menu.RadioItem value="en"> | ||
| <Menu.ItemText>{m.language_en()}</Menu.ItemText> | ||
| <Menu.ItemIndicator /> | ||
| </Menu.RadioItem> | ||
| <Menu.RadioItem value="es"> | ||
| <Menu.ItemText>{m.language_es()}</Menu.ItemText> | ||
| <Menu.ItemIndicator /> | ||
| </Menu.RadioItem> |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🌐 Web query:
ParaglideJS v2 runtime availableLocales API documentation
💡 Result:
In ParaglideJS v2, the runtime API that exposes the list of configured locales is documented as:
locales: readonly Locale[]— “The project's locales that have been specified in the settings.” (exported from the generated./paraglide/runtime.js) [1]
Example:
import { locales } from "./paraglide/runtime.js";
console.log(locales); // e.g. ["en", "de"]Notes on naming: the ParaglideJS v2 changelog shows there was a beta rename from runtime.locales → runtime.availableLocales (to match v1) [2], but the current v2 runtime API reference page documents the export as locales [1].
Sources: [1] https://inlang.com/m/gerre34r/library-inlang-paraglideJs/runtime, [2] https://inlang.com/m/gerre34r/library-inlang-paraglideJs/changelog
🏁 Script executed:
# First, let's find and examine the AppShell.tsx file
find . -name "AppShell.tsx" -type fRepository: Guria/modern-stack
Length of output: 98
🏁 Script executed:
# Check the project configuration to understand ParaglideJS setup
find . -name "project.inlang" -o -name "inlang.config.ts" -o -name "inlang.config.js" | head -20Repository: Guria/modern-stack
Length of output: 78
🏁 Script executed:
# Look for any existing runtime imports in the codebase
rg "from.*paraglide/runtime" -A 2 -B 2Repository: Guria/modern-stack
Length of output: 740
🏁 Script executed:
# Read the AppShell.tsx file to see the actual imports and locale handling
cat -n ./src/widgets/layout/ui/AppShell.tsx | head -40Repository: Guria/modern-stack
Length of output: 1365
🏁 Script executed:
# Read the sections around the language switcher (lines 200-235)
sed -n '200,235p' ./src/widgets/layout/ui/AppShell.tsx | cat -nRepository: Guria/modern-stack
Length of output: 1466
🏁 Script executed:
# Check the project.inlang configuration to see what locales are configured
cat ./project.inlangRepository: Guria/modern-stack
Length of output: 99
🏁 Script executed:
# Look for any existing usage of locales export from paraglide runtime
rg "import.*locales.*from.*paraglide" -B 2 -A 2Repository: Guria/modern-stack
Length of output: 44
🏁 Script executed:
# Check the inlang config file structure
find ./project.inlang -type f | head -10Repository: Guria/modern-stack
Length of output: 92
🏁 Script executed:
# Read the main inlang config file
cat -n ./project.inlang/settings.json 2>/dev/null || cat -n ./project.inlang/inlang.config.ts 2>/dev/null || ls -la ./project.inlang/Repository: Guria/modern-stack
Length of output: 516
🏁 Script executed:
# Look for locale configuration
rg "locales?" ./project.inlang/ 2>/dev/null | head -20Repository: Guria/modern-stack
Length of output: 193
🏁 Script executed:
# Check the locale model to understand how locales are currently handled
cat -n ./src/shared/model/locale.tsRepository: Guria/modern-stack
Length of output: 413
Hardcoded locale list will silently miss new languages.
The en/es options are hardcoded, so adding a third locale to project.inlang won't automatically surface it here. ParaglideJS v2's runtime exports locales (a readonly array of configured locales), which can drive the list dynamically.
♻️ Suggested refactor using `locales`
In the import at line 16, add locales:
-import { isLocale } from '#paraglide/runtime.js'
+import { locales, isLocale } from '#paraglide/runtime.js'Replace the hardcoded items (lines 224–230):
-<Menu.RadioItem value="en">
- <Menu.ItemText>{m.language_en()}</Menu.ItemText>
- <Menu.ItemIndicator />
-</Menu.RadioItem>
-<Menu.RadioItem value="es">
- <Menu.ItemText>{m.language_es()}</Menu.ItemText>
- <Menu.ItemIndicator />
-</Menu.RadioItem>
+{locales.map((loc) => (
+ <Menu.RadioItem key={loc} value={loc}>
+ <Menu.ItemText>{m[`language_${loc}` as keyof typeof m]?.() ?? loc}</Menu.ItemText>
+ <Menu.ItemIndicator />
+ </Menu.RadioItem>
+))}📝 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.
| <Menu.RadioItem value="en"> | |
| <Menu.ItemText>{m.language_en()}</Menu.ItemText> | |
| <Menu.ItemIndicator /> | |
| </Menu.RadioItem> | |
| <Menu.RadioItem value="es"> | |
| <Menu.ItemText>{m.language_es()}</Menu.ItemText> | |
| <Menu.ItemIndicator /> | |
| </Menu.RadioItem> | |
| {locales.map((loc) => ( | |
| <Menu.RadioItem key={loc} value={loc}> | |
| <Menu.ItemText>{m[`language_${loc}` as keyof typeof m]?.() ?? loc}</Menu.ItemText> | |
| <Menu.ItemIndicator /> | |
| </Menu.RadioItem> | |
| ))} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/widgets/layout/ui/AppShell.tsx` around lines 223 - 230, The locale
options are hardcoded (Menu.RadioItem for "en"/"es") so new project.inlang
locales are missed; import the runtime-exported readonly array locales and
replace the two static Menu.RadioItem entries by mapping over locales to render
a Menu.RadioItem per locale (use each locale as the value/key), render
Menu.ItemText inside each item (use the existing m translation helpers like
m.language_<locale>() when available or fall back to the raw locale string), and
keep Menu.ItemIndicator unchanged so the menu remains functional (refer to
symbols locales, Menu.RadioItem, Menu.ItemText, Menu.ItemIndicator, and m).



Summary by CodeRabbit
New Features
Documentation