Skip to content
Open
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ If your question needs a new tag, propose it in the PR description.

English is the source of truth. Translated files live in `docs/{lang}/q/` mirroring `docs/q/`.

Currently supported: English (en), Spanish (es). Looking for contributors for: Chinese (zh), Portuguese (pt), Japanese (ja), Korean (ko), French (fr), German (de), Russian (ru), Arabic (ar), Vietnamese (vi), Hindi (hi). See the [translation issues](https://github.com/miguelfernandezdev/vuewiki.dev/issues?q=label%3Aenhancement+i18n) for details.
Currently supported: English (en), Spanish (es), Arabic (ar). Looking for contributors for: Chinese (zh), Portuguese (pt), Japanese (ja), Korean (ko), French (fr), German (de), Russian (ru), Vietnamese (vi), Hindi (hi). See the [translation issues](https://github.com/miguelfernandezdev/vuewiki.dev/issues?q=label%3Aenhancement+i18n) for details.

1. Pick a question file from `docs/{lang}/q/` that still has English content or needs improvement.
1. Translate the markdown body. Keep code examples in English, translate surrounding text.
Expand Down
101 changes: 97 additions & 4 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export default defineConfig({

const isHome =
pageData.relativePath === 'index.md' ||
pageData.relativePath === 'es/index.md'
pageData.relativePath === 'es/index.md' ||
pageData.relativePath === 'ar/index.md'
const title = isHome
? 'VueWiki — 170+ Vue.js Interview Questions with Answers'
: pageData.frontmatter.title || 'VueWiki'
Expand Down Expand Up @@ -69,7 +70,7 @@ export default defineConfig({
.replace(/\.md$/, '')
.replaceAll('/', '-')
const outDir = siteConfig.outDir
const lang = pageData.relativePath.startsWith('es/') ? 'es' : 'en'
const lang = pageData.relativePath.startsWith('es/') ? 'es' : pageData.relativePath.startsWith('ar/') ? 'ar' : 'en'
const filename = await generateOgImage(
slug,
pageData.frontmatter.title,
Expand All @@ -90,18 +91,31 @@ export default defineConfig({

if (pageData.relativePath.startsWith('q/')) {
const esUrl = `${siteUrl}/es/${path}`
const arUrl = `${siteUrl}/ar/${path}`
head.push([
'link',
{ rel: 'alternate', hreflang: 'en', href: canonicalUrl }
])
head.push(['link', { rel: 'alternate', hreflang: 'es', href: esUrl }])
head.push(['link', { rel: 'alternate', hreflang: 'ar', href: arUrl }])
} else if (pageData.relativePath.startsWith('es/q/')) {
const enUrl = `${siteUrl}/${path.replace('es/', '')}`
const arUrl = `${siteUrl}/ar/${path.replace('es/', '')}`
head.push([
'link',
{ rel: 'alternate', hreflang: 'es', href: canonicalUrl }
])
head.push(['link', { rel: 'alternate', hreflang: 'en', href: enUrl }])
head.push(['link', { rel: 'alternate', hreflang: 'ar', href: arUrl }])
} else if (pageData.relativePath.startsWith('ar/q/')) {
const enUrl = `${siteUrl}/${path.replace('ar/', '')}`
const esUrl = `${siteUrl}/es/${path.replace('ar/', '')}`
head.push([
'link',
{ rel: 'alternate', hreflang: 'ar', href: canonicalUrl }
])
head.push(['link', { rel: 'alternate', hreflang: 'en', href: enUrl }])
head.push(['link', { rel: 'alternate', hreflang: 'es', href: esUrl }])
}

if (isQuestion && pageData.frontmatter.title) {
Expand Down Expand Up @@ -136,9 +150,9 @@ export default defineConfig({
details: `
170 questions covering Vue 3, Composition API, reactivity, components, TypeScript,
testing, Nuxt, SSR, performance, architecture, and common interview scenarios.
Available in English and Spanish.
Available in English, Spanish, and Arabic.
`.trim(),
ignoreFiles: ['index.md', 'es/index.md']
ignoreFiles: ['index.md', 'es/index.md', 'ar/index.md']
})
]
},
Expand Down Expand Up @@ -206,6 +220,66 @@ Available in English and Spanish.
code: '404'
}
}
},
ar: {
label: 'العربية',
lang: 'ar',
dir: 'rtl',
description:
'أسئلة مقابلات Vue.js مع إجابات وأمثلة على الكود',
themeConfig: {
returnToTopLabel: 'العودة إلى الأعلى',
sidebarMenuLabel: 'القائمة',
darkModeSwitchLabel: 'المظهر',
lightModeSwitchTitle: 'التبديل إلى المظهر الفاتح',
darkModeSwitchTitle: 'التبديل إلى المظهر الداكن',
outline: {
label: 'في هذه الصفحة'
},
docFooter: {
prev: 'الصفحة السابقة',
next: 'الصفحة التالية'
},
lastUpdated: {
text: 'آخر تحديث'
},
editLink: {
pattern:
'https://github.com/miguelfernandezdev/vuewiki.dev/edit/main/docs/:path',
text: 'اقترح تغييرات على هذه الصفحة'
},
footer: {
message: 'منشور تحت رخصة MIT.',
copyright: 'حقوق النشر © 2026-الحالي Miguel Fernández Carratalá'
},
nav: [
{ text: 'الأسئلة', link: '/ar/', activeMatch: '^/ar/$' },
{ text: 'حسب الصعوبة', link: '/ar/questions' },
{ text: 'بطاقات التعلم', link: '/ar/flashcards' },
{ text: 'الموارد', link: '/ar/resources' },
{
text: 'روابط',
items: [
{
text: 'المساهمة',
link: 'https://github.com/miguelfernandezdev/vuewiki.dev/blob/main/CONTRIBUTING.md'
},
{
text: 'سجل التغييرات',
link: 'https://github.com/miguelfernandezdev/vuewiki.dev/commits/main'
}
]
}
],
notFound: {
title: 'الصفحة غير موجودة',
quote:
'ولكن إذا لم تغير اتجاهك وواصلت البحث، فقد ينتهي بك الأمر حيث لا تريد الذهاب.',
linkLabel: 'اذهب إلى البداية',
linkText: 'خذني إلى البداية',
code: '404'
}
}
}
},

Expand Down Expand Up @@ -233,6 +307,25 @@ Available in English and Spanish.
}
}
}
},
ar: {
translations: {
button: {
buttonText: 'بحث',
buttonAriaLabel: 'بحث'
},
modal: {
displayDetails: 'عرض القائمة المفصلة',
resetButtonTitle: 'مسح البحث',
backButtonTitle: 'إغلاق البحث',
noResultsText: 'لا توجد نتائج لـ',
footer: {
selectText: 'للاختيار',
navigateText: 'للتنقل',
closeText: 'للإغلاق'
}
}
}
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions docs/.vitepress/json-ld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ interface QuestionMeta {
export function buildJsonLdHeads(meta: QuestionMeta): HeadConfig[] {
const heads: HeadConfig[] = []
const isEs = meta.relativePath.startsWith('es/')
const lang = isEs ? 'es' : 'en'
const isAr = meta.relativePath.startsWith('ar/')
const lang = isEs ? 'es' : isAr ? 'ar' : 'en'
const slug = meta.relativePath.replace(/\.md$/, '').replaceAll('/', '-')
const ogImageUrl = meta.difficulty
? `${siteUrl}/og-${slug}.png`
Expand Down Expand Up @@ -47,8 +48,8 @@ export function buildJsonLdHeads(meta: QuestionMeta): HeadConfig[] {
article.dateModified = new Date(meta.lastUpdated).toISOString()
}

const homeName = isEs ? 'Inicio' : 'Home'
const homeUrl = isEs ? `${siteUrl}/es/` : `${siteUrl}/`
const homeName = isEs ? 'Inicio' : isAr ? 'الرئيسية' : 'Home'
const homeUrl = isEs ? `${siteUrl}/es/` : isAr ? `${siteUrl}/ar/` : `${siteUrl}/`
const breadcrumb = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
Expand Down
45 changes: 42 additions & 3 deletions docs/.vitepress/og-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,40 @@ const TAG_ES: Record<string, string> = {
tooling: 'herramientas'
}

const DIFFICULTY_AR: Record<string, string> = {
beginner: 'مبتدئ',
intermediate: 'متوسط',
advanced: 'متقدم'
}

const BADGE_WIDTHS_AR: Record<string, number> = {
beginner: 130,
intermediate: 150,
advanced: 140
}

const TAG_AR: Record<string, string> = {
accessibility: 'إمكانية الوصول',
animation: 'رسوم متحركة',
architecture: 'بنية معمارية',
components: 'مكونات',
core: 'أساسيات',
'data-fetching': 'جلب البيانات',
debugging: 'تصحيح الأخطاء',
directives: 'توجيهات',
'error-handling': 'معالجة الأخطاء',
errors: 'أخطاء',
forms: 'نماذج',
lifecycle: 'دورة الحياة',
migration: 'ترحيل',
performance: 'أداء',
reactivity: 'تفاعلية',
security: 'أمان',
'state-management': 'إدارة الحالة',
styling: 'تنسيق',
tooling: 'أدوات'
}

function escapeXml(str: string): string {
return str
.replace(/&/g, '&amp;')
Expand Down Expand Up @@ -97,15 +131,20 @@ export async function generateOgImage(
}

const isEs = lang === 'es'
const isAr = lang === 'ar'
const lines = wordWrap(title, 38)
const badgeColor = BADGE_COLORS[difficulty] || BADGE_COLORS.beginner
const badgeWidth = isEs
? BADGE_WIDTHS_ES[difficulty] || 180
: BADGE_WIDTHS[difficulty] || 130
: isAr
? BADGE_WIDTHS_AR[difficulty] || 130
: BADGE_WIDTHS[difficulty] || 130
const diffLabel = isEs
? DIFFICULTY_ES[difficulty] || difficulty.toUpperCase()
: difficulty.toUpperCase()
const translatedTags = isEs ? tags.map((t) => TAG_ES[t] || t) : tags
: isAr
? DIFFICULTY_AR[difficulty] || difficulty.toUpperCase()
: difficulty.toUpperCase()
const translatedTags = isEs ? tags.map((t) => TAG_ES[t] || t) : isAr ? tags.map((t) => TAG_AR[t] || t) : tags
const tagsText = translatedTags.join(' · ')

const data: Record<string, string> = {
Expand Down
46 changes: 44 additions & 2 deletions docs/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,46 @@ const TAG_LABELS_ES: Record<string, string> = {
teleport: 'Teleport'
}

const TAG_LABELS_AR: Record<string, string> = {
core: 'الأساسيات',
'composition-api': 'Composition API',
reactivity: 'التفاعلية',
directives: 'التوجيهات',
components: 'المكونات',
lifecycle: 'دورة الحياة',
'state-management': 'إدارة الحالة',
'vue-router': 'Vue Router',
composables: 'Composables',
typescript: 'TypeScript',
testing: 'الاختبار',
performance: 'الأداء',
architecture: 'البنية المعمارية',
nuxt: 'Nuxt',
ssr: 'SSR',
errors: 'الأخطاء الشائعة',
'error-handling': 'معالجة الأخطاء',
tooling: 'الأدوات',
forms: 'النماذج',
animation: 'الرسوم المتحركة',
styling: 'التنسيق',
migration: 'الترحيل',
'data-fetching': 'جلب البيانات',
debugging: 'تصحيح الأخطاء',
security: 'الأمان',
accessibility: 'إمكانية الوصول',
pinia: 'Pinia',
vite: 'Vite',
vitest: 'Vitest',
vueuse: 'VueUse',
vuex: 'Vuex',
watchers: 'المراقبون',
'v-model': 'v-model',
'provide-inject': 'Provide/Inject',
slots: 'Slots',
suspense: 'Suspense',
teleport: 'Teleport'
}

function parseFrontmatter(content: string): QuestionFrontmatter | null {
const match = content.match(/^---\n([\s\S]*?)\n---/)
if (!match) return null
Expand Down Expand Up @@ -171,7 +211,7 @@ function buildSidebarForLocale(
})
}

const urlPrefix = subdir.startsWith('es/') ? '/es' : ''
const urlPrefix = subdir.startsWith('es/') ? '/es' : subdir.startsWith('ar/') ? '/ar' : ''

return [...groups.entries()]
.sort((a, b) => (TAG_ORDER[a[0]] ?? 99) - (TAG_ORDER[b[0]] ?? 99))
Expand All @@ -190,9 +230,11 @@ function buildSidebarForLocale(
export function generateSidebar(docsDir: string): DefaultTheme.Sidebar {
const en = buildSidebarForLocale(docsDir, 'q', TAG_LABELS_EN)
const es = buildSidebarForLocale(docsDir, 'es/q', TAG_LABELS_ES)
const ar = buildSidebarForLocale(docsDir, 'ar/q', TAG_LABELS_AR)

return {
'/q/': en,
'/es/q/': es
'/es/q/': es,
'/ar/q/': ar
}
}
2 changes: 1 addition & 1 deletion docs/.vitepress/theme/QuestionMeta.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const route = useRoute()
const { isRead, markRead, toggleRead: _toggleRead } = useReadTracker()

const isQuestion = computed(() => !!frontmatter.value.difficulty)
const backUrl = computed(() => (lang.value === 'es' ? '/es/' : '/'))
const backUrl = computed(() => lang.value === 'es' ? '/es/' : lang.value === 'ar' ? '/ar/' : '/')
const questionUrl = computed(() => route.path)

onMounted(() => {
Expand Down
3 changes: 1 addition & 2 deletions docs/.vitepress/theme/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const messages: Record<string, Record<string, any>> = {
hi
}

export const completedLocales = new Set(['en', 'es'])
export const completedLocales = new Set(['en', 'es', 'ar'])

export const translationIssues: Record<string, number> = {
zh: 7,
Expand All @@ -38,7 +38,6 @@ export const translationIssues: Record<string, number> = {
fr: 11,
de: 12,
ru: 13,
ar: 14,
vi: 15,
hi: 16
}
Expand Down
4 changes: 2 additions & 2 deletions docs/.vitepress/theme/questions.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface QuestionData {
summary: string
}

export default createContentLoader('{,es/}q/*.md', {
export default createContentLoader('{,es/,ar/}q/*.md', {
transform(rawData): QuestionData[] {
return rawData
.map((page) => ({
Expand All @@ -19,7 +19,7 @@ export default createContentLoader('{,es/}q/*.md', {
difficulty: page.frontmatter.difficulty as string,
tags: (page.frontmatter.tags || []) as string[],
url: page.url,
locale: page.url.startsWith('/es/') ? 'es' : 'en',
locale: page.url.startsWith('/es/') ? 'es' : page.url.startsWith('/ar/') ? 'ar' : 'en',
summary: (page.frontmatter.summary as string) ?? ''
}))
.sort((a, b) => a.order - b.order) as QuestionData[]
Expand Down
Loading