Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Release Notes
=============

Version 0.66.4
--------------

- feat: add podcast episode page and few fixes in podcast page (#3283)
- use Intl to get native language names (#3297)

Version 0.66.3 (Released May 05, 2026)
--------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1443,7 +1443,7 @@ describe("ContractContent", () => {
expect(card).toHaveTextContent("Module in English")

await user.click(languageSelect)
await user.click(await screen.findByRole("option", { name: "Español" }))
await user.click(await screen.findByRole("option", { name: "español" }))

await waitFor(() => {
expect(root.getByTestId("enrollment-card-desktop")).toHaveTextContent(
Expand Down Expand Up @@ -1549,7 +1549,7 @@ describe("ContractContent", () => {
expect(card).toHaveTextContent("Collection English")

await user.click(languageSelect)
await user.click(await screen.findByRole("option", { name: "Español" }))
await user.click(await screen.findByRole("option", { name: "español" }))

await waitFor(() => {
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2543,7 +2543,7 @@ describe("EnrollmentDisplay", () => {
expect(card).toHaveTextContent("Start Course")

await user.click(languageSelect)
await user.click(await screen.findByRole("option", { name: "Español" }))
await user.click(await screen.findByRole("option", { name: "español" }))

const desktopCard = await screen.findByTestId("enrollment-card-desktop")
await within(desktopCard).findByText("Modulo en Espanol")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ type LanguageOptionWithEnrollability = CourseRunLanguageOption & {
}

describe("languageOptions", () => {
const setIntlDisplayNames = (
value: typeof Intl.DisplayNames | undefined,
): void => {
Object.defineProperty(Intl, "DisplayNames", {
value,
configurable: true,
writable: true,
})
}

test("normalizes language keys", () => {
expect(
getLanguageOptionKey({
Expand Down Expand Up @@ -159,7 +169,7 @@ describe("languageOptions", () => {
})
expect(options[1]).toEqual({
value: "language:es",
label: "Español",
label: "español",
})
})

Expand Down Expand Up @@ -213,6 +223,179 @@ describe("languageOptions", () => {
expect(selectedRun?.id).toBe(spanishRun.id)
})

test("uses static fallback labels when Intl.DisplayNames is unavailable", () => {
const originalDisplayNames = Intl.DisplayNames
setIntlDisplayNames(undefined)

try {
const run = factories.courses.courseRun({
id: 7001,
title: "Spanish LATAM",
courseware_id: "cw-es-419",
courseware_url: "https://example.com/cw-es-419",
is_enrollable: true,
})

const course = factories.courses.course({
courseruns: [run],
next_run_id: run.id,
language_options: [
{
id: run.id,
courseware_id: run.courseware_id,
courseware_url: run.courseware_url ?? "",
language: "es-419",
title: run.title,
run_tag: run.run_tag,
},
],
})

const options = getDistinctLanguageOptions([course])
expect(options).toEqual([
{
value: "language:es-419",
label: "español (Latinoamérica)",
},
])
} finally {
setIntlDisplayNames(originalDisplayNames)
}
})

test("falls back to the base language subtag when regional code is unresolved", () => {
const originalDisplayNames = Intl.DisplayNames

class MockDisplayNames {
of(code: string): string | undefined {
if (code === "zz-419") {
return undefined
}
if (code === "zz") {
return "Zed"
}
return undefined
}
}

setIntlDisplayNames(MockDisplayNames as unknown as typeof Intl.DisplayNames)

try {
const run = factories.courses.courseRun({
id: 7002,
title: "Mock Regional",
courseware_id: "cw-zz-419",
courseware_url: "https://example.com/cw-zz-419",
is_enrollable: true,
})

const course = factories.courses.course({
courseruns: [run],
next_run_id: run.id,
language_options: [
{
id: run.id,
courseware_id: run.courseware_id,
courseware_url: run.courseware_url ?? "",
language: "zz-419",
title: run.title,
run_tag: run.run_tag,
},
],
})

const options = getDistinctLanguageOptions([course])
expect(options).toEqual([
{
value: "language:zz-419",
label: "Zed",
},
])
} finally {
setIntlDisplayNames(originalDisplayNames)
}
})

test("memoizes native language labels by language code", () => {
const originalDisplayNames = Intl.DisplayNames
let constructorCalls = 0

class MockDisplayNames {
constructor() {
constructorCalls += 1
}

of(code: string): string | undefined {
if (code === "es") {
return "español"
}
return undefined
}
}

setIntlDisplayNames(MockDisplayNames as unknown as typeof Intl.DisplayNames)

try {
const runA = factories.courses.courseRun({
id: 7101,
title: "Spanish A",
courseware_id: "cw-es-7101",
courseware_url: "https://example.com/cw-es-7101",
is_enrollable: true,
})

const runB = factories.courses.courseRun({
id: 7102,
title: "Spanish B",
courseware_id: "cw-es-7102",
courseware_url: "https://example.com/cw-es-7102",
is_enrollable: true,
})

const courseA = factories.courses.course({
courseruns: [runA],
next_run_id: runA.id,
language_options: [
{
id: runA.id,
courseware_id: runA.courseware_id,
courseware_url: runA.courseware_url ?? "",
language: "es",
title: runA.title,
run_tag: runA.run_tag,
},
],
})

const courseB = factories.courses.course({
courseruns: [runB],
next_run_id: runB.id,
language_options: [
{
id: runB.id,
courseware_id: runB.courseware_id,
courseware_url: runB.courseware_url ?? "",
language: "es",
title: runB.title,
run_tag: runB.run_tag,
},
],
})

const options = getDistinctLanguageOptions([courseA, courseB])

expect(options).toEqual([
{
value: "language:es",
label: "español",
},
])
expect(constructorCalls).toBe(1)
} finally {
setIntlDisplayNames(originalDisplayNames)
}
})

test("keeps language when one of multiple matching runs is enrollable", () => {
const englishRun = factories.courses.courseRun({
id: 6101,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,6 @@ import type {
} from "@mitodl/mitxonline-api-axios/v2"
import { getBestRun, selectBestEnrollment } from "./helpers"

const LANGUAGE_CODE_TO_NATIVE_NAME: Record<string, string> = {
ar: "العربية",
de: "Deutsch",
"de-de": "Deutsch",
el: "Ελληνικά",
es: "Español",
"es-419": "Español (Latinoamérica)",
fr: "Français",
pt: "Português",
ja: "日本語",
"pt-br": "Português (Brasil)",
zh: "中文",
"zh-hans": "简体中文",
en: "English",
}

const getLanguageCode = (option: CourseRunLanguageOption): string | null => {
const normalized = option.language?.trim().toLowerCase().replace(/_/g, "-")
return normalized || null
Expand All @@ -41,19 +25,106 @@ const getLanguageCodeFromOptionKey = (optionKey: string): string | null => {
return code || null
}

const FALLBACK_NATIVE_LANGUAGE_NAMES: Record<string, string> = {
ar: "العربية",
de: "Deutsch",
en: "English",
es: "español",
"es-419": "español (Latinoamérica)",
fr: "français",
hi: "हिन्दी",
it: "italiano",
ja: "日本語",
ko: "한국어",
pt: "português",
"pt-br": "português (Brasil)",
ru: "русский",
zh: "中文",
"zh-cn": "简体中文",
"zh-tw": "繁體中文",
}

const nativeLanguageNameCache = new Map<string, string>()
let cachedDisplayNamesRef: typeof Intl.DisplayNames | undefined =
Intl.DisplayNames

const ensureNativeLanguageNameCacheIsFresh = (): void => {
if (Intl.DisplayNames !== cachedDisplayNamesRef) {
cachedDisplayNamesRef = Intl.DisplayNames
nativeLanguageNameCache.clear()
}
}

const getFallbackNativeLanguageName = (languageCode: string): string | null => {
const exactMatch = FALLBACK_NATIVE_LANGUAGE_NAMES[languageCode]
if (exactMatch) {
return exactMatch
}

const baseLanguageSubtag = languageCode.split("-")[0]
if (!baseLanguageSubtag) {
return null
}

return (
FALLBACK_NATIVE_LANGUAGE_NAMES[baseLanguageSubtag] ?? baseLanguageSubtag
)
}

const getNativeLanguageName = (languageCode: string): string => {
ensureNativeLanguageNameCacheIsFresh()

const normalizedLanguageCode = languageCode.trim().toLowerCase()
const baseLanguageSubtag = normalizedLanguageCode.split("-")[0]

const cachedLabel = nativeLanguageNameCache.get(normalizedLanguageCode)
if (cachedLabel) {
return cachedLabel
}

let resolvedLabel: string | null = null

try {
if (typeof Intl.DisplayNames === "function") {
const displayNames = new Intl.DisplayNames([normalizedLanguageCode], {
type: "language",
})
const label = displayNames.of(normalizedLanguageCode)
if (label && label.toLowerCase() !== normalizedLanguageCode) {
resolvedLabel = label
}

if (
!resolvedLabel &&
baseLanguageSubtag &&
baseLanguageSubtag !== normalizedLanguageCode
) {
const baseLabel = displayNames.of(baseLanguageSubtag)
if (baseLabel && baseLabel.toLowerCase() !== baseLanguageSubtag) {
resolvedLabel = baseLabel
}
}
}
} catch {
// Fall through to static fallback labels.
}

const finalLabel =
resolvedLabel ??
getFallbackNativeLanguageName(normalizedLanguageCode) ??
normalizedLanguageCode

nativeLanguageNameCache.set(normalizedLanguageCode, finalLabel)
return finalLabel
}

const getLanguageOptionLabel = (option: CourseRunLanguageOption): string => {
const languageCode = getLanguageCode(option)
if (!languageCode) {
return ""
}

const exact = LANGUAGE_CODE_TO_NATIVE_NAME[languageCode]
if (exact) {
return exact
}

const baseCode = languageCode.split("-")[0]
return LANGUAGE_CODE_TO_NATIVE_NAME[baseCode] ?? languageCode
return getNativeLanguageName(languageCode)
}

type ExtendedLanguageOption = CourseRunLanguageOption & {
Expand Down
Loading
Loading