Skip to content

Commit 04f0efd

Browse files
authored
Fix homepage group names not using translations (#56245)
1 parent 94f6944 commit 04f0efd

File tree

2 files changed

+248
-5
lines changed

2 files changed

+248
-5
lines changed

src/products/lib/get-product-groups.ts

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import path from 'path'
2+
import fs from 'fs/promises'
23

34
import type { Page, ProductGroup, ProductGroupChild, Context } from '@/types'
4-
import { productMap, data } from './all-products'
5+
import { productMap, data } from '@/products/lib/all-products'
56
import { renderContentWithFallback } from '@/languages/lib/render-with-fallback'
67
import removeFPTFromPath from '@/versions/lib/remove-fpt-from-path'
8+
import frontmatter from '@/frame/lib/read-frontmatter'
9+
import languages from '@/languages/lib/languages'
710

811
type PageMap = Record<string, Page>
912

@@ -93,18 +96,76 @@ interface ProductGroupData {
9396
children: string[]
9497
}
9598

99+
export async function getLocalizedGroupNames(lang: string): Promise<{ [key: string]: string }> {
100+
if (lang === 'en') {
101+
return {}
102+
}
103+
104+
const translationRoot = languages[lang as keyof typeof languages]?.dir
105+
if (!translationRoot) {
106+
return {}
107+
}
108+
109+
try {
110+
const localizedHomepage = path.join(translationRoot, 'content', 'index.md')
111+
const localizedContent = await fs.readFile(localizedHomepage, 'utf8')
112+
const { data: localizedData } = frontmatter(localizedContent)
113+
114+
if (!localizedData?.childGroups) {
115+
return {}
116+
}
117+
118+
return createOcticonToNameMap(localizedData.childGroups)
119+
} catch {
120+
// If localized file doesn't exist or can't be read, return empty map
121+
return {}
122+
}
123+
}
124+
125+
export function createOcticonToNameMap(childGroups: ProductGroupData[]): { [key: string]: string } {
126+
const octiconToName: { [key: string]: string } = {}
127+
128+
childGroups.forEach((group: ProductGroupData) => {
129+
if (group.octicon && group.name) {
130+
octiconToName[group.octicon] = group.name
131+
}
132+
})
133+
134+
return octiconToName
135+
}
136+
137+
export function mapEnglishToLocalizedNames(
138+
englishGroups: ProductGroupData[],
139+
localizedByOcticon: { [key: string]: string },
140+
): { [key: string]: string } {
141+
const nameMap: { [key: string]: string } = {}
142+
143+
englishGroups.forEach((englishGroup: ProductGroupData) => {
144+
if (englishGroup.octicon && localizedByOcticon[englishGroup.octicon]) {
145+
nameMap[englishGroup.name] = localizedByOcticon[englishGroup.octicon]
146+
}
147+
})
148+
149+
return nameMap
150+
}
151+
96152
export async function getProductGroups(
97153
pageMap: PageMap,
98154
lang: string,
99155
context: Context,
100156
): Promise<ProductGroup[]> {
101-
// Handle case where data or childGroups might be undefined
102-
const childGroups = data?.childGroups || []
157+
// Always use English version for structure (octicon, children)
158+
const englishChildGroups = data?.childGroups || []
159+
160+
// Get localized names if available
161+
const localizedByOcticon = await getLocalizedGroupNames(lang)
162+
const localizedNames = mapEnglishToLocalizedNames(englishChildGroups, localizedByOcticon)
103163

104164
return await Promise.all(
105-
childGroups.map(async (group: ProductGroupData) => {
165+
englishChildGroups.map(async (group: ProductGroupData) => {
166+
const localizedName = localizedNames[group.name] || group.name
106167
return {
107-
name: group.name,
168+
name: localizedName,
108169
icon: group.icon || null,
109170
octicon: group.octicon || null,
110171
// Typically the children are product IDs, but we support deeper page paths too
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { describe, expect, test } from 'vitest'
2+
3+
import {
4+
createOcticonToNameMap,
5+
mapEnglishToLocalizedNames,
6+
getLocalizedGroupNames,
7+
} from '@/products/lib/get-product-groups'
8+
9+
describe('get-product-groups helper functions', () => {
10+
describe('createOcticonToNameMap', () => {
11+
test('creates correct mapping from childGroups', () => {
12+
const mockChildGroups = [
13+
{ name: 'Get started', octicon: 'RocketIcon', children: ['get-started'] },
14+
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] },
15+
{ name: 'Security', octicon: 'ShieldLockIcon', children: ['code-security'] },
16+
]
17+
18+
const octiconToName = createOcticonToNameMap(mockChildGroups)
19+
20+
expect(octiconToName['RocketIcon']).toBe('Get started')
21+
expect(octiconToName['CopilotIcon']).toBe('GitHub Copilot')
22+
expect(octiconToName['ShieldLockIcon']).toBe('Security')
23+
expect(Object.keys(octiconToName)).toHaveLength(3)
24+
})
25+
26+
test('handles missing octicon or name gracefully', () => {
27+
const mockChildGroups = [
28+
{ name: 'Valid Group', octicon: 'RocketIcon', children: [] },
29+
{ octicon: 'MissingNameIcon', children: [] }, // missing name
30+
{ name: 'Missing Octicon', children: [] }, // missing octicon
31+
{ name: '', octicon: 'EmptyNameIcon', children: [] }, // empty name
32+
]
33+
34+
const octiconToName = createOcticonToNameMap(mockChildGroups)
35+
36+
expect(octiconToName['RocketIcon']).toBe('Valid Group')
37+
expect(octiconToName['MissingNameIcon']).toBeUndefined()
38+
expect(octiconToName['EmptyNameIcon']).toBeUndefined()
39+
expect(Object.keys(octiconToName)).toHaveLength(1)
40+
})
41+
})
42+
43+
describe('mapEnglishToLocalizedNames', () => {
44+
test('maps English names to localized names using octicon as key', () => {
45+
const englishGroups = [
46+
{ name: 'Get started', octicon: 'RocketIcon', children: [] },
47+
{ name: 'Security', octicon: 'ShieldLockIcon', children: [] },
48+
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: [] },
49+
]
50+
51+
const localizedByOcticon = {
52+
RocketIcon: 'Empezar',
53+
ShieldLockIcon: 'Seguridad',
54+
CopilotIcon: 'GitHub Copilot', // Some names stay the same
55+
}
56+
57+
const nameMap = mapEnglishToLocalizedNames(englishGroups, localizedByOcticon)
58+
59+
expect(nameMap['Get started']).toBe('Empezar')
60+
expect(nameMap['Security']).toBe('Seguridad')
61+
expect(nameMap['GitHub Copilot']).toBe('GitHub Copilot')
62+
expect(Object.keys(nameMap)).toHaveLength(3)
63+
})
64+
65+
test('handles missing translations gracefully', () => {
66+
const englishGroups = [
67+
{ name: 'Get started', octicon: 'RocketIcon', children: [] },
68+
{ name: 'Missing Translation', octicon: 'MissingIcon', children: [] },
69+
{ name: 'No Octicon', children: [] },
70+
]
71+
72+
const localizedByOcticon = {
73+
RocketIcon: 'Empezar',
74+
// MissingIcon is not in the localized map
75+
}
76+
77+
const nameMap = mapEnglishToLocalizedNames(englishGroups, localizedByOcticon)
78+
79+
expect(nameMap['Get started']).toBe('Empezar')
80+
expect(nameMap['Missing Translation']).toBeUndefined()
81+
expect(nameMap['No Octicon']).toBeUndefined()
82+
expect(Object.keys(nameMap)).toHaveLength(1)
83+
})
84+
85+
test('handles different ordering between English and localized groups', () => {
86+
// English groups in one order
87+
const englishGroups = [
88+
{ name: 'Get started', octicon: 'RocketIcon', children: [] },
89+
{ name: 'Security', octicon: 'ShieldLockIcon', children: [] },
90+
]
91+
92+
// Localized groups in different order (but mapped by octicon)
93+
const localizedByOcticon = {
94+
ShieldLockIcon: 'Seguridad', // Security comes first in localized
95+
RocketIcon: 'Empezar', // Get started comes second
96+
}
97+
98+
const nameMap = mapEnglishToLocalizedNames(englishGroups, localizedByOcticon)
99+
100+
// Should correctly map regardless of order
101+
expect(nameMap['Get started']).toBe('Empezar')
102+
expect(nameMap['Security']).toBe('Seguridad')
103+
})
104+
})
105+
106+
describe('getLocalizedGroupNames integration', () => {
107+
test('returns empty object for English language', async () => {
108+
const result = await getLocalizedGroupNames('en')
109+
expect(result).toEqual({})
110+
})
111+
112+
test('returns empty object when no translation root available', () => {
113+
// Test the fallback when translation root is not found
114+
const lang = 'unknown-lang'
115+
const languages = { en: { dir: '/en' }, es: { dir: '/es' } }
116+
117+
const translationRoot = languages[lang]?.dir
118+
const result = translationRoot
119+
? {
120+
/* would proceed */
121+
}
122+
: {}
123+
124+
expect(result).toEqual({})
125+
})
126+
127+
test('handles file read errors gracefully', () => {
128+
// Test the try/catch behavior when file read fails
129+
let result
130+
try {
131+
// Simulate file read error
132+
throw new Error('File not found')
133+
} catch {
134+
result = {}
135+
}
136+
137+
expect(result).toEqual({})
138+
})
139+
})
140+
141+
describe('full translation pipeline', () => {
142+
test('complete flow from English groups to localized names', () => {
143+
// Simulate the complete flow
144+
const englishChildGroups = [
145+
{ name: 'Get started', octicon: 'RocketIcon', children: ['get-started'] },
146+
{ name: 'Security', octicon: 'ShieldLockIcon', children: ['code-security'] },
147+
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] },
148+
]
149+
150+
// Simulate what would come from a Spanish localized file
151+
const mockLocalizedChildGroups = [
152+
{ name: 'Empezar', octicon: 'RocketIcon', children: ['get-started'] },
153+
{ name: 'Seguridad', octicon: 'ShieldLockIcon', children: ['code-security'] },
154+
{ name: 'GitHub Copilot', octicon: 'CopilotIcon', children: ['copilot'] },
155+
]
156+
157+
// Step 1: Create octicon -> localized name mapping
158+
const localizedByOcticon = createOcticonToNameMap(mockLocalizedChildGroups)
159+
160+
// Step 2: Map English names to localized names
161+
const localizedNames = mapEnglishToLocalizedNames(englishChildGroups, localizedByOcticon)
162+
163+
// Step 3: Use in final mapping
164+
const finalResult = englishChildGroups.map((group) => {
165+
const localizedName = localizedNames[group.name] || group.name
166+
return {
167+
name: localizedName,
168+
octicon: group.octicon,
169+
children: group.children,
170+
}
171+
})
172+
173+
expect(finalResult[0].name).toBe('Empezar')
174+
expect(finalResult[1].name).toBe('Seguridad')
175+
expect(finalResult[2].name).toBe('GitHub Copilot')
176+
177+
// Technical data should remain unchanged
178+
expect(finalResult[0].octicon).toBe('RocketIcon')
179+
expect(finalResult[0].children).toEqual(['get-started'])
180+
})
181+
})
182+
})

0 commit comments

Comments
 (0)