Skip to content

refactor(i18n): move updateLanguage to runtime as shared funtionality - #289

Open
dcoa wants to merge 7 commits into
openedx:mainfrom
eduNEXT:dcoa/lang-selec-refactor
Open

refactor(i18n): move updateLanguage to runtime as shared funtionality#289
dcoa wants to merge 7 commits into
openedx:mainfrom
eduNEXT:dcoa/lang-selec-refactor

Conversation

@dcoa

@dcoadcoa commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Description

The current PR aims to consolidate a reusable feature that enables updating the language preference across different UI components, ensuring consistent behavior.

What changes were implemented?

  • Moved the updateSiteLanguage function from shell/footer to runtime/i18n, making it reusable across any component in the site. For example, it can now be used by the LanguageMenu in the Footer and the Language Preference Selector in the Account App.

  • Added two optional configuration properties to the i18n runtime:

    • defaultLanguage (string, default: 'en'): Defines the fallback locale when a user's preferred locale is not supported. Open edX provides the LANGUAGE_CODE setting to configure the platform's default language (for any non-English site), but this setting is not currently read by the client-side code. This property provides equivalent flexibility on the client side.
    • supportedLanguages (string[], default: []): Defines an allowlist of supported language codes. When non-empty, only these languages are displayed in the language picker and considered supported during locale resolution. An empty array means that all loaded locales are considered supported.
  • Updated handleRtl() to set both the lang and dir attributes on <html>. Previously, the lang attribute was not being updated. Updating lang ensures browsers, screen readers, and other accessibility tools correctly identify the page's language and provide appropriate language-specific behavior.

  • Performs a optimistic UI update, updateLocale changes the UI, then the user preference and the cookie is updated via API.

  • Displays a Toast message when the preference language API or user preference API fails.

How to test

  • Use the test-site
  • Add the two new optional configuration for test limited dropdown options displayed and non-English default language. Example:
// ..."defaultLanguage": "es-419","supportedLanguages": ["es-419","ar"],// ...
  • You will need to update the i18n folder to expose the translations (use make extract_translations or you can get support of AI). For example:
importes419from'./messages/es-419.json';importarfrom'./messages/ar.json';importptBRfrom'./messages/pt_BR.json';exportdefault[{'es-419': es419,
ar,'pt-br': ptBR,},];
  • Load the testing application and verify the html and the language selector displays defaultLanguage
  • Change the language and verify the UI is updated and the cookie is set with the selected language.
  • Log in/ Log out and verify the cookie is not duplicated.
  • Test the Error message is visible if the API fails.
image
Screencast.from.2026-08-14.23-51-01.webm

Additional Information

LLM use

Supported by Opencode - Deepseek V4

@openedx-webhooksopenedx-webhooks added open-source-contribution PR author is not from Axim or 2U core contributor PR author is a Core Contributor (who may or may not have write access to this repo). labels Aug 14, 2026
@openedx-webhooks

openedx-webhooks commented Aug 14, 2026

Copy link
Copy Markdown

Thanks for the pull request, @dcoa!

This repository is currently maintained by @openedx/axim-engineering.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@github-project-automationgithub-project-automationBot moved this to Needs Triage in ContributionsAug 14, 2026
@dcoadcoa changed the title Dcoa/lang selec refactorrefactor: move updateLanguage to runtime as shared funtionalityAug 14, 2026
@dcoa
dcoaforce-pushed the dcoa/lang-selec-refactor branch from a7923f1 to 93bf13eCompareAugust 14, 2026 10:23
@dcoa
dcoaforce-pushed the dcoa/lang-selec-refactor branch from 4b9b67f to 3432147CompareAugust 14, 2026 11:13
@dcoadcoa changed the title refactor: move updateLanguage to runtime as shared funtionalityrefactor(i18n): move updateLanguage to runtime as shared funtionalityAug 14, 2026
@dcoa
dcoa marked this pull request as ready for review August 14, 2026 13:34
@mphilbrick211mphilbrick211 moved this from Needs Triage to Ready for Review in ContributionsAug 19, 2026
@dcoa
dcoaforce-pushed the dcoa/lang-selec-refactor branch 2 times, most recently from 0236562 to 9e46401CompareAugust 21, 2026 04:31
@dcoa
dcoaforce-pushed the dcoa/lang-selec-refactor branch from 9e46401 to ad58514CompareAugust 21, 2026 04:47

@arbrandesarbrandes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looking pretty good, but I found a bug and a bunch of nits, with Claude's help. Mind taking a look? Thanks!

Comment threadruntime/i18n/lib.ts
Comment on lines +124 to +129
const isLocaleSupported = (code) => {
if (supportedLanguages.length > 0) {
return supportedLanguages.includes(code) && messages[code] !== undefined;
}
return messages[code] !== undefined;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is the major issue with the PR as is: English disappears when defaultLanguage is not en.

English messages are never in the messages map. isLocaleSupported('en') is therefore always false, so findSupportedLocale('en') returns defaultLanguage. The other half is in getSupportedLanguageList below.

So, for example, on a Spanish-default site an English speaker has no way back to English: the option is absent from the footer menu, and explicitly allowlisting en in supportedLanguages does not restore it. Selecting it programmatically via updateSiteLanguage('en') also silently lands on es-419 while persisting pref-lang=en to the API, leaving the cookie and the UI disagreeing.

The fix is to treat en as always supported, and to keep adding it to the language list alongside defaultLanguage.

Comment threadruntime/i18n/lib.ts
Comment on lines +196 to +204
let locales = Object.keys(messages);

if (!locales.includes(defaultLanguage)) {
locales.push(defaultLanguage);
}

if (supportedLanguages.length > 0) {
locales = locales.filter((locale) => supportedLanguages.includes(locale));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Other half of the blocking issue on findSupportedLocale above: the old unconditional locales.push('en') was replaced by pushing defaultLanguage, so en is only in the list when it is the default.

Comment threadruntime/i18n/lib.ts
}

if (messages[locale] !== undefined) {
const { defaultLanguage, supportedLanguages = [] } = getSiteConfig();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use the same defaultLanguage = 'en' destructuring default that getSupportedLanguageList does.

Comment on lines +28 to +32
if (user !== null) {
await patchUserPreferences(user.username, locale);
}

await setSessionLanguage(locale);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the preferences PATCH fails, it's also going to skip the cookie write. Suggestion: attempt both calls and aggregate the failures, rather than short-circuiting.

Comment on lines +74 to +79
const formData = new FormData();
formData.append('language', locale);

// Post to the LMS setlang endpoint for server-side persistence.
// Use the authenticated HTTP client to ensure that the request includes the CSRF token.
// Works for both authenticated and anonymous users, since the LMS setlang endpoint is public.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Delete the FormData construction and update the comment - the request is a JSON PATCH to lang_pref/update_language, not a form post to setlang.

Comment threaddocs/how_tos/i18n.rst
************

This is a step by step guide to making your React app ready to accept translations. The instructions here are very specific to the edX setup.
This is a step by step guide to making your React app ready to accept translations. The instructions here are very specific to the Openedx setup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"Openedx" should be "Open edX".

});

it('should filter by supportedLanguages when configured', () => {
mergeSiteConfig({ supportedLanguages: ['en', 'es-419'] });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Site config is leaking between test blocks. Restore the default in an afterEach so :184 doesn't have to mutate getSiteConfig().supportedLanguages directly.

This mergeSiteConfig in particular is never undone, which also weakens the updateLocale block: the leaked allowlist excludes ar, so "should take precedence over the language preference cookie" isn't testing against a locale the cookie path could actually have returned.

});

handleRtl();
expect(setAttribute).toHaveBeenCalledWith('lang', 'es-419');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This test block is called handleRtl, but here it's being removed. These assertions now fire on setAttribute calls that only configureI18n triggers, so the function under test is exercised only incidentally.

<Dropdown.Item key={language.code} onClick={handleClick}>
<Dropdown.Item
key={language.code}
className={isActive ? 'active' : ''}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use the active prop instead of the class. className={isActive ? 'active' : ''} produces the same markup as active={isActive} in this react-bootstrap version, but the prop is the intended API.

const mockUpdateLocale = updateLocale as jest.MockedFunction<typeof updateLocale>;

describe('updateSiteLanguage', () => {
const mockAuthHttpClient = { patch: jest.fn(), post: jest.fn() };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: post is left over from the setlang POST and is never used.

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

Labels

core contributorPR author is a Core Contributor (who may or may not have write access to this repo).open-source-contributionPR author is not from Axim or 2U

Projects

Status: Ready for Review

Development

Successfully merging this pull request may close these issues.

Make site language switching a supported runtime API

4 participants

@dcoa@openedx-webhooks@arbrandes@mphilbrick211