From b49867dacab841bd0d02f6d1403723c90e65925f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Demirel?= Date: Thu, 19 Mar 2026 22:12:53 +0300 Subject: [PATCH 1/6] feat: internationalization implemented --- package.json | 2 +- scripts/generate-og.mjs | 89 ++++--- scripts/update-data.mjs | 35 ++- src/components/LanguageSelector.astro | 99 ++++++++ src/components/ShareButtons.astro | 7 +- src/components/Sidebar.astro | 72 ++++-- src/components/TopBar.astro | 45 +++- src/config.mjs | 20 ++ src/content.config.ts | 3 +- src/content/articles/en/neural-networks.md | 38 +++ .../articles/tr/yapay-sinir-ag-hlari.md | 38 +++ src/data/articles.json | 1 - src/data/categories.json | 1 - src/data/en/articles.json | 7 + src/data/en/categories.json | 10 + src/data/en/labels.json | 68 ++++++ src/data/en/tags.json | 23 ++ src/data/tags.json | 1 - src/data/tr/articles.json | 7 + src/data/tr/categories.json | 10 + src/data/tr/labels.json | 68 ++++++ src/data/tr/tags.json | 23 ++ src/helper.mjs | 31 +++ src/i18n/index.ts | 61 +++++ src/layouts/BaseLayout.astro | 18 +- src/layouts/DocsLayout.astro | 23 +- src/pages/404.astro | 20 +- src/pages/articles/[slug].astro | 72 +++--- src/pages/articles/index.astro | 79 ++++--- src/pages/categories/[slug].astro | 51 +++- src/pages/categories/index.astro | 25 +- src/pages/en/404.astro | 59 +++++ src/pages/en/articles/[slug].astro | 222 ++++++++++++++++++ src/pages/en/articles/index.astro | 68 ++++++ src/pages/en/categories/[slug].astro | 84 +++++++ src/pages/en/categories/index.astro | 45 ++++ src/pages/en/index.astro | 122 ++++++++++ src/pages/en/tags/[slug].astro | 89 +++++++ src/pages/en/tags/index.astro | 38 +++ src/pages/index.astro | 137 ++++++----- src/pages/tags/[slug].astro | 58 +++-- src/pages/tags/index.astro | 21 +- 42 files changed, 1721 insertions(+), 269 deletions(-) create mode 100644 src/components/LanguageSelector.astro create mode 100644 src/content/articles/en/neural-networks.md create mode 100644 src/content/articles/tr/yapay-sinir-ag-hlari.md delete mode 100644 src/data/articles.json delete mode 100644 src/data/categories.json create mode 100644 src/data/en/articles.json create mode 100644 src/data/en/categories.json create mode 100644 src/data/en/labels.json create mode 100644 src/data/en/tags.json delete mode 100644 src/data/tags.json create mode 100644 src/data/tr/articles.json create mode 100644 src/data/tr/categories.json create mode 100644 src/data/tr/labels.json create mode 100644 src/data/tr/tags.json create mode 100644 src/helper.mjs create mode 100644 src/i18n/index.ts create mode 100644 src/pages/en/404.astro create mode 100644 src/pages/en/articles/[slug].astro create mode 100644 src/pages/en/articles/index.astro create mode 100644 src/pages/en/categories/[slug].astro create mode 100644 src/pages/en/categories/index.astro create mode 100644 src/pages/en/index.astro create mode 100644 src/pages/en/tags/[slug].astro create mode 100644 src/pages/en/tags/index.astro diff --git a/package.json b/package.json index b9bd7b6..85e4257 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "prepare": "husky", "check": "astro check", "generate:og": "node scripts/generate-og.mjs", - "generate:data": "node scripts/genedate-data.mjs" + "generate:data": "node scripts/update-data.mjs --lang=tr && node scripts/update-data.mjs --lang=en" }, "dependencies": { "@astrojs/partytown": "^2.1.5", diff --git a/scripts/generate-og.mjs b/scripts/generate-og.mjs index c7eaf28..a51d7a5 100644 --- a/scripts/generate-og.mjs +++ b/scripts/generate-og.mjs @@ -13,7 +13,27 @@ const fontBold = await fetch( const DEFAULT_COLORS = { bg: '#1a1a2e', text: '#e2e8f0' } -async function makeSvg(title, color, isArticle) { +const LANG_TEXTS = { + tr: { + explore: 'Makaleleri Keşfet', + category: 'KATEGORİ', + article: 'MAKALE', + tagline: 'Sinir ağları, transformerlar, pekiştirmeli öğrenme ve daha fazlası', + description: 'Makine öğrenimi kavramlarının net açıklamaları', + suffix: '4byte.dev — ML Kavramları Açıklandı', + }, + en: { + explore: 'Explore Articles', + category: 'CATEGORY', + article: 'ARTICLE', + tagline: 'Neural networks, transformers, reinforcement learning & more', + description: 'Clear explanations of machine learning concepts', + suffix: '4byte.dev — ML Concepts Explained', + }, +} + +async function makeSvg(title, color, isArticle, lang = 'en') { + const texts = LANG_TEXTS[lang] || LANG_TEXTS.en const bg = color || DEFAULT_COLORS.bg const svg = await satori( { @@ -83,7 +103,7 @@ async function makeSvg(title, color, isArticle) { fontWeight: '500', fontFamily: 'Inter', }, - children: isArticle ? 'Machine Learning & AI' : 'Explore Articles', + children: isArticle ? 'Machine Learning & AI' : texts.explore, }, }, ], @@ -105,7 +125,7 @@ async function makeSvg(title, color, isArticle) { letterSpacing: '2px', fontFamily: 'Inter', }, - children: isArticle ? 'ARTICLE' : 'LEARN MACHINE LEARNING', + children: isArticle ? texts.article : texts.category, }, }, { @@ -132,9 +152,7 @@ async function makeSvg(title, color, isArticle) { fontFamily: 'Inter', maxWidth: '800px', }, - children: isArticle - ? 'Clear explanations of machine learning concepts' - : 'Neural networks, transformers, reinforcement learning & more', + children: isArticle ? texts.description : texts.tagline, }, }, ], @@ -168,7 +186,7 @@ async function makeSvg(title, color, isArticle) { type: 'span', props: { style: { fontSize: '18px', color: '#94a3b8', fontFamily: 'Inter' }, - children: '4byte.dev — ML Concepts Explained', + children: texts.suffix, }, }, ], @@ -189,32 +207,47 @@ async function makeSvg(title, color, isArticle) { return svg } -const outDir = path.join(process.cwd(), 'public', 'og') +const repoRoot = path.join(process.cwd()) +const outDir = path.join(repoRoot, 'public', 'og') fs.mkdirSync(outDir, { recursive: true }) -const dataDir = path.join(process.cwd(), 'src', 'data') -const categories = JSON.parse(fs.readFileSync(path.join(dataDir, 'categories.json'), 'utf8')) -const articles = JSON.parse(fs.readFileSync(path.join(dataDir, 'articles.json'), 'utf8')) +const SUPPORTED_LANGS = ['tr', 'en'] -const categoryColorMap = {} -for (const cat of categories) { - categoryColorMap[cat.name] = cat.color || null -} +for (const lang of SUPPORTED_LANGS) { + const langOutDir = path.join(outDir, lang) + fs.mkdirSync(langOutDir, { recursive: true }) + + const dataDir = path.join(repoRoot, 'src', 'data', lang) + let categories = [] + let articles = [] + + try { + categories = JSON.parse(fs.readFileSync(path.join(dataDir, 'categories.json'), 'utf8')) + articles = JSON.parse(fs.readFileSync(path.join(dataDir, 'articles.json'), 'utf8')) + } catch { + console.log(`No data found for language: ${lang}`) + } + + const categoryColorMap = {} + for (const cat of categories) { + categoryColorMap[cat.name] = cat.color || null + } -const svgIndex = await makeSvg('4byte.dev', null, false) -const resvgIndex = new Resvg(svgIndex, { fitTo: { mode: 'width', value: 1200 } }) -fs.writeFileSync(path.join(outDir, 'index.png'), Buffer.from(resvgIndex.render().asPng())) -console.log('Generated public/og/index.png') + const svgIndex = await makeSvg('4byte.dev', null, false, lang) + const resvgIndex = new Resvg(svgIndex, { fitTo: { mode: 'width', value: 1200 } }) + fs.writeFileSync(path.join(langOutDir, 'index.png'), Buffer.from(resvgIndex.render().asPng())) + console.log(`Generated public/og/${lang}/index.png`) -if (articles.length === 0) { - console.log('No articles found – skipping article OG images.') -} else { - for (const { slug, title, category } of articles) { - const color = categoryColorMap[category] || null - const svgArticle = await makeSvg(title, color, true) - const resvgArticle = new Resvg(svgArticle, { fitTo: { mode: 'width', value: 1200 } }) - fs.writeFileSync(path.join(outDir, `${slug}.png`), Buffer.from(resvgArticle.render().asPng())) - console.log(`Generated public/og/${slug}.png`) + if (articles.length === 0) { + console.log(`No articles for ${lang} – skipping article OG images.`) + } else { + for (const { slug, title, category } of articles) { + const color = categoryColorMap[category] || null + const svgArticle = await makeSvg(title, color, true, lang) + const resvgArticle = new Resvg(svgArticle, { fitTo: { mode: 'width', value: 1200 } }) + fs.writeFileSync(path.join(langOutDir, `${slug}.png`), Buffer.from(resvgArticle.render().asPng())) + console.log(`Generated public/og/${lang}/${slug}.png`) + } } } diff --git a/scripts/update-data.mjs b/scripts/update-data.mjs index 15b5232..c9e9039 100755 --- a/scripts/update-data.mjs +++ b/scripts/update-data.mjs @@ -7,13 +7,22 @@ import slugify from 'slugify' const readJSON = async (file) => JSON.parse(await fs.readFile(file, 'utf8')) const writeJSON = async (file, data) => await fs.writeFile(file, JSON.stringify(data, null, 2), 'utf8') +const langArg = process.argv.find((arg) => arg.startsWith('--lang=')) +const targetLang = langArg ? langArg.split('=')[1] : 'tr' + +const SUPPORTED_LANGS = ['tr', 'en'] +if (!SUPPORTED_LANGS.includes(targetLang)) { + console.error(`Unsupported language: ${targetLang}. Supported: ${SUPPORTED_LANGS.join(', ')}`) + process.exit(1) +} + ;(async () => { const __dirname = new URL('.', import.meta.url).pathname const repoRoot = path.resolve(__dirname, '..') - const contentDir = path.join(repoRoot, 'src/content/articles') - const tagsPath = path.join(repoRoot, 'src/data/tags.json') - const categoriesPath = path.join(repoRoot, 'src/data/categories.json') - const articlesPath = path.join(repoRoot, 'src/data/articles.json') + const contentDir = path.join(repoRoot, 'src/content/articles', targetLang) + const tagsPath = path.join(repoRoot, 'src/data', targetLang, 'tags.json') + const categoriesPath = path.join(repoRoot, 'src/data', targetLang, 'categories.json') + const articlesPath = path.join(repoRoot, 'src/data', targetLang, 'articles.json') const tags = (await readJSON(tagsPath)) ?? [] const categories = (await readJSON(categoriesPath)) ?? [] @@ -24,7 +33,14 @@ const writeJSON = async (file, data) => await fs.writeFile(file, JSON.stringify( const addedTags = new Set() const addedCategories = new Set() - const mdFiles = (await fs.readdir(contentDir)).filter((f) => f.endsWith('.md')) + let mdFiles = [] + try { + mdFiles = (await fs.readdir(contentDir)).filter((f) => f.endsWith('.md')) + } catch { + console.log(`No content directory for language '${targetLang}': ${contentDir}`) + console.log('Skipping article generation.') + } + const articles = [] for (const file of mdFiles) { @@ -80,18 +96,23 @@ const writeJSON = async (file, data) => await fs.writeFile(file, JSON.stringify( await writeJSON(categoriesPath, filteredCategories) await writeJSON(articlesPath, articles) + console.log(`Updated data for language: ${targetLang}`) + console.log(` Articles: ${articles.length}`) + console.log(` Categories: ${filteredCategories.length}`) + console.log(` Tags: ${filteredTags.length}`) + if (process.env.GITHUB_EVENT_PATH) { const event = JSON.parse(await fs.readFile(process.env.GITHUB_EVENT_PATH, 'utf8')) const prNumber = event.pull_request?.number if (prNumber) { const lines = [] if (addedTags.size) { - lines.push('## 📌 New tags added (please add descriptions)') + lines.push(`## New tags added for ${targetLang} (please add descriptions)`) for (const t of addedTags) lines.push(`- \`${t}\``) lines.push('') } if (addedCategories.size) { - lines.push('## 📂 New categories added (please add descriptions)') + lines.push(`## New categories added for ${targetLang} (please add descriptions)`) for (const c of addedCategories) lines.push(`- \`${c}\``) lines.push('') } diff --git a/src/components/LanguageSelector.astro b/src/components/LanguageSelector.astro new file mode 100644 index 0000000..00881b4 --- /dev/null +++ b/src/components/LanguageSelector.astro @@ -0,0 +1,99 @@ +--- +import { LANGUAGES } from '../helper.mjs' + +interface Props { + currentLang: string + currentPath: string + labels: Record + direction?: 'up' | 'down' +} + +const { currentLang, currentPath, labels, direction = 'down' } = Astro.props + +const langLabels = labels.lang as Record | undefined + +function getOtherLangPath(lang: string, path: string): string { + if (lang === 'en') { + return path.replace(/^\/en/, '') || '/' + } + return '/en' + path +} + +const otherLangPath = getOtherLangPath(currentLang, currentPath) + +const isUp = direction === 'up' +const dropdownClass = isUp + ? 'hidden absolute bottom-full left-0 mb-1 w-32' + : 'hidden absolute top-full left-0 mt-1 w-32' +--- + +
+ + +
+ { + Object.entries(LANGUAGES).map(([code, config]) => ( + + {langLabels?.[code] || code.toUpperCase()} + {config.name} + + )) + } +
+
+ + diff --git a/src/components/ShareButtons.astro b/src/components/ShareButtons.astro index e6aff47..fa0e27a 100644 --- a/src/components/ShareButtons.astro +++ b/src/components/ShareButtons.astro @@ -1,10 +1,13 @@ --- +import { t } from '../i18n/index.ts' + interface Props { title: string url: string + labels?: Record } -const { title, url } = Astro.props +const { title, url, labels = {} } = Astro.props const encodedTitle = encodeURIComponent(title) const encodedUrl = encodeURIComponent(url) @@ -14,7 +17,7 @@ const encodedUrl = encodeURIComponent(url) - Share + {t(labels, 'share.label')} +} + +const { lang, currentPath, labels } = Astro.props + +const categories = lang === 'en' ? categoriesEn : categoriesTr +const tags = lang === 'en' ? tagsEn : tagsTr + +const navLabels = labels.nav as Record | undefined +const themeLabels = labels.theme as Record | undefined const navItems = [ - { title: 'Home', url: '/' }, - { title: 'Articles', url: '/articles' }, - { title: 'Categories', url: '/categories' }, - { title: 'Tags', url: '/tags' }, + { title: navLabels?.home || 'Home', url: lang === 'en' ? '/en/' : '/' }, + { title: navLabels?.articles || 'Articles', url: lang === 'en' ? '/en/articles' : '/articles' }, + { title: navLabels?.categories || 'Categories', url: lang === 'en' ? '/en/categories' : '/categories' }, + { title: navLabels?.tags || 'Tags', url: lang === 'en' ? '/en/tags' : '/tags' }, ] -const currentPath = Astro.url.pathname - function isActive(path: string): boolean { - if (path === '/') return currentPath === '/' + if (path === '/' || path === '/en/') return currentPath === path return currentPath.startsWith(path) } const topCategories = [...categories].sort((a, b) => b.articleCount - a.articleCount).slice(0, 5) - const topTags = [...tags].sort((a, b) => b.articleCount - a.articleCount).slice(0, 8) import logoDark from '../assets/logo-dark.png' @@ -29,7 +44,7 @@ import logoLight from '../assets/logo-light.png' class="w-64 shrink-0 border-r border-border dark:border-border-dark bg-background dark:bg-background-dark hidden lg:flex flex-col h-screen sticky top-0" >
- + 4byte logo @@ -39,7 +54,7 @@ import logoLight from '../assets/logo-light.png'
- Navigation + {t(labels, 'nav.home')}
{ navItems.map((item) => ( @@ -51,10 +66,10 @@ import logoLight from '../assets/logo-light.png' : 'text-muted-foreground dark:text-muted-dark-foreground hover:bg-muted dark:hover:bg-muted-dark hover:text-foreground dark:hover:text-foreground-dark' }`} > - {item.title === 'Home' && } - {item.title === 'Articles' && } - {item.title === 'Categories' && } - {item.title === 'Tags' && } + {item.title === (navLabels?.home || 'Home') && } + {item.title === (navLabels?.articles || 'Articles') && } + {item.title === (navLabels?.categories || 'Categories') && } + {item.title === (navLabels?.tags || 'Tags') && } {item.title} )) @@ -63,14 +78,14 @@ import logoLight from '../assets/logo-light.png'
- Categories + {t(labels, 'nav.categories') || 'Categories'}
{ topCategories.map((cat) => ( - Tags + {t(labels, 'nav.tags') || 'Tags'}
{ topTags.map((tag) => ( - -
+
+ +
diff --git a/src/pages/en/articles/[slug].astro b/src/pages/en/articles/[slug].astro new file mode 100644 index 0000000..df91de4 --- /dev/null +++ b/src/pages/en/articles/[slug].astro @@ -0,0 +1,222 @@ +--- +import DocsLayout from '../../../layouts/DocsLayout.astro' +import ShareButtons from '../../../components/ShareButtons.astro' +import { getCollection } from 'astro:content' +import { SITE } from '../../../config.mjs' +import { t } from '../../../i18n/index.ts' + +const lang = 'en' +const labels = await import(`../../../data/${lang}/labels.json`).then((m) => m.default) + +export async function getStaticPaths() { + const allArticles = await getCollection('articles') + return allArticles + .filter((a) => a.data.lang === 'en') + .map((article) => ({ + params: { slug: article.data.slug }, + props: { article }, + })) +} + +const { article } = Astro.props + +if (!article || article.data.status !== 'Published') { + return Astro.redirect('/en/404') +} + +const categorySlug = article.data.category.toLowerCase().replace(/\s+/g, '-') +const articleUrl = `${SITE.origin}/en/articles/${article.data.slug}` +const ogImageUrl = `/og/${article.data.slug}.png` + +const publishedDate = new Date(article.data.date) +const isoDate = publishedDate.toISOString() + +const sections = (article.body ?? '') + .split('\n') + .filter((line) => line.startsWith('## ')) + .map((line) => line.replace('## ', '')) + +const jsonLdArticle = { + '@context': 'https://schema.org', + '@type': 'Article', + headline: article.data.title, + description: article.data.excerpt, + image: `${SITE.origin}${ogImageUrl}`, + url: articleUrl, + datePublished: isoDate, + dateModified: isoDate, + author: { + '@type': 'Person', + name: article.data.author, + url: SITE.origin, + }, + publisher: { + '@type': 'Organization', + name: SITE.name, + url: SITE.origin, + }, + inLanguage: 'en', + keywords: article.data.tags.join(', '), + mainEntityOfPage: { + '@type': 'WebPage', + '@id': articleUrl, + }, +} + +const homeLabel = t(labels, 'article.home') +const articlesLabel = t(labels, 'nav.articles') +const byLabel = t(labels, 'article.by') +const onThisPageLabel = t(labels, 'article.onThisPage') +--- + + +