Skip to content

[7418] LocaleSelector control#4821

Open
jvega190 wants to merge 6 commits intocraftercms:developfrom
jvega190:feature/7418-locale-selector
Open

[7418] LocaleSelector control#4821
jvega190 wants to merge 6 commits intocraftercms:developfrom
jvega190:feature/7418-locale-selector

Conversation

@jvega190
Copy link
Copy Markdown
Member

@jvega190 jvega190 commented Mar 3, 2026

craftercms/craftercms#7418

Summary by CodeRabbit

  • New Features
    • Added a locale selector control for forms with human-readable labels (e.g., "English (US)").
    • Automatically sets the site default locale when none is present.
    • Shows a loading skeleton while locales load and an outlined error alert with a retry option on failures.
    • Select is disabled when the field is read-only.
    • Locale selector is lazy-loaded to reduce initial load.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ee57b61-017b-420a-9fc8-c893819736fc

📥 Commits

Reviewing files that changed from the base of the PR and between a306e4b and b9aa015.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx

Walkthrough

Adds a new LocaleSelector React control that lazy-loads, fetches site locales on mount, formats locale labels, sets a default when unset, manages loading/error states with retry, and registers the control in the control map.

Changes

Cohort / File(s) Summary
New LocaleSelector Component
ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx
Adds LocaleSelector export and LocaleSelectorProps. Implements fetching via fetchSiteLocales(siteId), local loading/error state, getLocaleLabel() locale formatting, default value application, select rendering, retry on error, and cleanup on unmount.
Control Map Registration
ui/app/src/components/FormsEngine/lib/controlMap.ts
Replaces null for 'locale-selector' with lazy(() => import('../controls/LocaleSelector')) to enable dynamic import/lazy loading of the new control.

Sequence Diagram(s)

sequenceDiagram
    participant UI as User/Browser
    participant Component as LocaleSelector
    participant Service as SiteService (fetchSiteLocales)
    participant Store as Form State

    UI->>Component: mount (active siteId)
    Component->>Service: fetchSiteLocales(siteId)
    activate Service
    Service-->>Component: locales + defaultLocale OR error
    deactivate Service
    alt success
        Component->>Component: map locales via getLocaleLabel()
        Component-->>UI: render Select (MenuItems, default selected if needed)
        UI->>Component: user selects locale
        Component->>Store: setValue(selectedLocale)
    else error
        Component-->>UI: render Alert with Retry
        UI->>Component: click Retry
        Component->>Service: fetchSiteLocales(siteId) (retry)
    end
    UI->>Component: unmount
    Component->>Component: unsubscribe / cleanup
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • rart
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[7418] LocaleSelector control' clearly and specifically identifies the main change: adding a new LocaleSelector control component, with the ticket reference for context.
Description check ✅ Passed The description provides a direct link to the referenced issue, which fully satisfies the minimal template requirement of 'Ticket reference or full description'.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx`:
- Around line 97-101: getLocaleLabel currently assumes exactly two
underscore-separated parts and treats the second token as the region, which
breaks on inputs like "zh_Hant_TW" or hyphenated tags; update getLocaleLabel to
split the input on both '_' and '-' into parts, use the first part as the
language, detect the region as the last part only if it looks like a region
subtag (e.g. two letters or three digits) and uppercase it, treat any middle
part(s) as script or variant subtags (title-case the script subtag) and include
them between language and region with hyphens, and fall back to returning just
the language when no valid region is detected so labels like "zh-Hant-TW",
"zh-Hant", "en-US" and "en" are produced correctly.
- Around line 67-79: The component currently returns null when !isFetching &&
!localeData, causing the control to disappear after a fetch error; instead
update LocaleSelector to always render the control even if localeData is null by
removing the early return and rendering a disabled/select-with-fallback state
when localeData is missing; in the error handler (where setIsFetching(false) and
dispatch(pushErrorDialog(...)) run) ensure you do not clear the current form
value—use existing refs/setValue to populate the select's value and pass
disabled={true} or show a "Failed to load locales" placeholder so the control
degrades gracefully while keeping the current selection visible.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7bafec1 and 4108fff.

📒 Files selected for processing (2)
  • ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx
  • ui/app/src/components/FormsEngine/lib/controlMap.ts

Comment thread ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx
Comment thread ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx (2)

76-76: Consider removing stable references from dependency array.

setIsFetching (state setter) and refs (ref object from useUpdateRefs) are stable and don't need to be in the dependency array. If setValue isn't stable, it could cause unnecessary re-fetching.

♻️ Suggested dependency array
-	}, [siteId, dispatch, refs, setValue, setIsFetching]);
+	}, [siteId, dispatch, setValue]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx` at line 76,
The effect dependency array on the useEffect that currently lists [siteId,
dispatch, refs, setValue, setIsFetching] should be tightened: remove stable
references refs (from useUpdateRefs) and setIsFetching (state setter) because
they are stable across renders, leaving only true changing values such as siteId
and dispatch; if setValue is not a stable function, either memoize it where it's
created or include it deliberately, but avoid leaving in stable refs that cause
needless runs—update the useEffect dependency array accordingly and, if you keep
setValue, add a comment explaining why it must remain.

85-85: Consider providing a fallback for value to avoid MUI warnings.

If value is undefined initially, MUI's Select may log a warning about switching from uncontrolled to controlled. The component already auto-selects defaultLocaleCode when there's no value, but there's a brief moment where value could be undefined.

♻️ Proposed change
-				<Select value={value} onChange={handleChange} autoFocus={autoFocus}>
+				<Select value={value ?? ''} onChange={handleChange} autoFocus={autoFocus}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx` at line 85,
The Select component may receive undefined briefly causing MUI
uncontrolled->controlled warnings; update the LocaleSelector component so the
Select's value uses a safe fallback (e.g. value ?? defaultLocaleCode or ''),
ensuring the prop passed to Select is never undefined; adjust any related logic
in handleChange or where defaultLocaleCode is determined so the fallback matches
expected option keys and preserves autoFocus behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx`:
- Around line 97-120: The getLocaleLabel function currently calls new
Intl.Locale(normalized) outside the try/catch so malformed or empty localeCode
throws before DisplayNames are attempted; move the parsing (const normalized =
localeCode.replace(...); const parsed = new Intl.Locale(normalized); const lang
= parsed.language; const region = parsed.region; const locale =
parsed.toString();) inside the existing try block that creates
Intl.DisplayNames, so any RangeError from Intl.Locale is caught; in the catch
ensure you set languageLabel = langFallback (use either parsed.language if
available or the original localeCode or an empty string) and regionLabel =
regionFallback (uppercased region if present or undefined) so the function
returns a safe fallback like "en (US)" or the raw code when parsing fails.

---

Nitpick comments:
In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx`:
- Line 76: The effect dependency array on the useEffect that currently lists
[siteId, dispatch, refs, setValue, setIsFetching] should be tightened: remove
stable references refs (from useUpdateRefs) and setIsFetching (state setter)
because they are stable across renders, leaving only true changing values such
as siteId and dispatch; if setValue is not a stable function, either memoize it
where it's created or include it deliberately, but avoid leaving in stable refs
that cause needless runs—update the useEffect dependency array accordingly and,
if you keep setValue, add a comment explaining why it must remain.
- Line 85: The Select component may receive undefined briefly causing MUI
uncontrolled->controlled warnings; update the LocaleSelector component so the
Select's value uses a safe fallback (e.g. value ?? defaultLocaleCode or ''),
ensuring the prop passed to Select is never undefined; adjust any related logic
in handleChange or where defaultLocaleCode is determined so the fallback matches
expected option keys and preserves autoFocus behavior.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4108fff and 9df7677.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx

Comment thread ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx
@jvega190 jvega190 marked this pull request as ready for review March 17, 2026 16:31
@jvega190 jvega190 requested a review from rart as a code owner March 17, 2026 16:31
Comment thread ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx Outdated
@jvega190 jvega190 requested a review from rart April 15, 2026 20:20
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx`:
- Line 37: The component ignores the control contract's readonly flag: update
the props destructuring in LocaleSelector (add readonly to const { field, value,
setValue, autoFocus }) and make the rendered <Select> respect that flag (in the
Select element at the render block around where onChange/setValue are used, pass
disabled or readOnly based on readonly and avoid calling setValue when readonly
so selection changes are no-ops). Ensure both the destructured readonly name and
the Select's disabled/readOnly and onChange behavior are updated so the control
is truly read-only.
- Line 55: The success handler for fetchSiteLocales in LocaleSelector.tsx is
destructuring its parameter directly (next: ({ localeCodes, defaultLocaleCode })
=> ...) which will throw if the callback receives undefined; change the handler
to accept a single param (e.g., result) and safely extract with a fallback (for
example: const { localeCodes = [], defaultLocaleCode } = result || {}) or use
optional chaining/defaults so localeCodes and defaultLocaleCode are always
defined before using them in the function body.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f727a6a-a025-4f9f-bd86-76d88f8c9988

📥 Commits

Reviewing files that changed from the base of the PR and between d725547 and a306e4b.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx

Comment thread ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx Outdated
Comment thread ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx`:
- Around line 60-72: The default-locale write can run before localeData is
populated because setValue(defaultLocaleCode) is outside the if (localeCodes)
guard; move the default-value assignment inside the same if (localeCodes) block
(after calling setLocaleData(...)) or otherwise ensure setLocaleData is always
called when a response contains defaultLocaleCode, so that refs.current.value,
defaultLocaleCode, setLocaleData and setValue are updated atomically and the
component never ends up with a value but no localeData/renderable control.
- Line 105: The JSX in LocaleSelector.tsx currently renders "{error.message}.
{error.remedialAction}" which prints "undefined" when remedialAction is missing;
update the rendering in the component (where error is used) to include
remedialAction only if it is defined or non-empty (e.g., render
"{error.message}{error.remedialAction ? '. ' + error.remedialAction : ''}"), so
the message ends cleanly when error.remedialAction is absent; ensure you
reference the same error object used by the component and keep the punctuation
only when remedialAction exists.
- Around line 70-72: The code currently auto-sets the form value when
defaultLocaleCode exists even if the field is readonly; update the init/cleanup
logic in LocaleSelector so the default write is skipped when readonly is true by
guarding the call to setValue(defaultLocaleCode) with the readonly flag (i.e.
only call setValue when !readonly && !refs.current.value && defaultLocaleCode).
Also ensure the ref object used (refs.current) includes the readonly state so
the component and its Select control remain consistent with the
readonly/disabled behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 424e8d55-96e9-4cc9-8640-3c5db2084507

📥 Commits

Reviewing files that changed from the base of the PR and between a306e4b and b9aa015.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx

Comment on lines +60 to +72
setIsFetching(false);
if (localeCodes) {
setLocaleData({
localeCodes: localeCodes.map((code) => ({
code,
label: getLocaleLabel(code)
})),
defaultLocaleCode
});
}
if (!refs.current.value && defaultLocaleCode) {
setValue(defaultLocaleCode);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Default-locale write can run while localeData remains unset.

The if (!refs.current.value && defaultLocaleCode) block sits outside the if (localeCodes) guard, so a response shaped { defaultLocaleCode, localeCodes: null } will call setValue(defaultLocaleCode) while the component then early-returns null at Line 88 (since localeData stays undefined and error is null). The form ends up with a value but no visible control. Consider nesting the default-value write under the same localeCodes guard, or ensuring localeData is always populated when a response is received.

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

In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx` around lines
60 - 72, The default-locale write can run before localeData is populated because
setValue(defaultLocaleCode) is outside the if (localeCodes) guard; move the
default-value assignment inside the same if (localeCodes) block (after calling
setLocaleData(...)) or otherwise ensure setLocaleData is always called when a
response contains defaultLocaleCode, so that refs.current.value,
defaultLocaleCode, setLocaleData and setValue are updated atomically and the
component never ends up with a value but no localeData/renderable control.

Comment on lines +70 to +72
if (!refs.current.value && defaultLocaleCode) {
setValue(defaultLocaleCode);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't auto-apply default locale when the field is read-only.

The default-value write runs regardless of readonly, which mutates the form value on read-only forms (and defeats the purpose of the disabled Select added at Line 108). Guard the call with the readonly flag.

Proposed fix
-				if (!refs.current.value && defaultLocaleCode) {
+				if (!refs.current.value && defaultLocaleCode && !refs.current.readonly) {
 					setValue(defaultLocaleCode);
 				}

And include readonly in the ref:

-	const refs = useUpdateRefs({ value });
+	const refs = useUpdateRefs({ value, readonly });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx` around lines
70 - 72, The code currently auto-sets the form value when defaultLocaleCode
exists even if the field is readonly; update the init/cleanup logic in
LocaleSelector so the default write is skipped when readonly is true by guarding
the call to setValue(defaultLocaleCode) with the readonly flag (i.e. only call
setValue when !readonly && !refs.current.value && defaultLocaleCode). Also
ensure the ref object used (refs.current) includes the readonly state so the
component and its Select control remain consistent with the readonly/disabled
behavior.

</Button>
}
>
{error.message}. {error.remedialAction}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against undefined remedialAction.

If error.remedialAction is not provided by the API, this renders a trailing ". undefined". Render it conditionally.

Proposed fix
-					{error.message}. {error.remedialAction}
+					{error.message}{error.remedialAction ? `. ${error.remedialAction}` : ''}
📝 Committable suggestion

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

Suggested change
{error.message}. {error.remedialAction}
{error.message}{error.remedialAction ? `. ${error.remedialAction}` : ''}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/src/components/FormsEngine/controls/LocaleSelector.tsx` at line 105,
The JSX in LocaleSelector.tsx currently renders "{error.message}.
{error.remedialAction}" which prints "undefined" when remedialAction is missing;
update the rendering in the component (where error is used) to include
remedialAction only if it is defined or non-empty (e.g., render
"{error.message}{error.remedialAction ? '. ' + error.remedialAction : ''}"), so
the message ends cleanly when error.remedialAction is absent; ensure you
reference the same error object used by the component and keep the punctuation
only when remedialAction exists.

Copy link
Copy Markdown
Member

@rart rart left a comment

Choose a reason for hiding this comment

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

Bot reviews pending.

Avoid pushing fixes one by one so the robot doesn't pause reviews. Batch pushing fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants