diff --git a/packages/database/src/lib/entry.ts b/packages/database/src/lib/entry.ts index f9a984ea..90c8916d 100644 --- a/packages/database/src/lib/entry.ts +++ b/packages/database/src/lib/entry.ts @@ -33,10 +33,10 @@ export async function createEntry( lastUpdated: extraData.lastUpdated ?? file.lastModified, url: extraData.url ?? getUrl(file.path), githubUrl: extraData.githubUrl ?? undefined, - language: extraData.language ?? getLanguageFromFile(file.path), + language: extraData.language ?? file.meta.language ?? getLanguageFromFile(file.path), level: file.meta.level ?? 'beginner', audience: parseCsvOrArray(file.meta.audience) ?? ['students', 'pro devs'], - ...(searchTranslations ? { translations: await findTranslations(file.path) } : {}) + ...(searchTranslations ? { translations: await findTranslations(file.path, file) } : {}) }; return { ...entryWithoutId, diff --git a/packages/database/src/lib/language.ts b/packages/database/src/lib/language.ts index c91415ce..fdae01a1 100644 --- a/packages/database/src/lib/language.ts +++ b/packages/database/src/lib/language.ts @@ -1,11 +1,14 @@ import path from 'path'; import glob from 'fast-glob'; +import { promises as fs } from 'fs'; import { defaultLanguage } from '../../../website/src/app/shared/constants.js'; import { ContentEntry } from '../../../website/src/app/catalog/content-entry.js'; import { getWorkshops } from './workshop.js'; import { createEntry } from './entry.js'; +import { FileInfo } from './workshop.js'; +import { parseCsvOrArray } from './util.js'; -const languageRegex = /.*?\.([a-zA-Z]{2})\.md$/; +const languageRegex = /.*?\.([a-zA-Z]{2}(?:_[A-Z]{2})?)\.md$/; const translationsFolder = 'translations'; export function getLanguageFromFile(filePath: string): string { @@ -13,14 +16,36 @@ export function getLanguageFromFile(filePath: string): string { return match ? match[1] : defaultLanguage; } -export async function findTranslations(filePath: string): Promise { +export async function findTranslations(filePath: string, fileInfo?: FileInfo): Promise { const dir = path.dirname(filePath); const originalLanguage = getLanguageFromFile(filePath); const extension = (originalLanguage !== defaultLanguage ? `.${originalLanguage}` : '') + path.extname(filePath); const baseName = path.basename(filePath, extension); const translationsDir = path.join(dir, translationsFolder); + + // Find translations from filesystem const translations = glob.sync(`${translationsDir}/${baseName}*.md`); - const translatedFiles = await getWorkshops(translations); + + // Also check metadata for translations list + const metadataTranslations: string[] = []; + if (fileInfo?.meta?.translations) { + const translationCodes = parseCsvOrArray(fileInfo.meta.translations); + for (const langCode of translationCodes) { + const translationPath = path.join(translationsDir, `${baseName}.${langCode}.md`); + // Only add if file exists and not already in the list + try { + await fs.access(translationPath); + if (!translations.includes(translationPath)) { + metadataTranslations.push(translationPath); + } + } catch { + console.warn(`Translation file not found for language '${langCode}': ${translationPath}`); + } + } + } + + const allTranslations = [...translations, ...metadataTranslations]; + const translatedFiles = await getWorkshops(allTranslations); const entriesPromises = translatedFiles.map((file) => createEntry(file, false)); return Promise.all(entriesPromises); } diff --git a/packages/website/src/app/shared/components/header.component.ts b/packages/website/src/app/shared/components/header.component.ts index 6f3a0034..bd9c987d 100644 --- a/packages/website/src/app/shared/components/header.component.ts +++ b/packages/website/src/app/shared/components/header.component.ts @@ -2,12 +2,13 @@ import { Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; import { IconComponent } from './icon.component'; import { SidebarComponent } from './sidebar.component'; +import { LanguageSelectorComponent, LanguageOption } from './language-selector.component'; import { Link } from '../link'; @Component({ selector: 'app-header', standalone: true, - imports: [CommonModule, IconComponent], + imports: [CommonModule, IconComponent, LanguageSelectorComponent], template: `