Skip to content

Commit 18390b4

Browse files
committed
🌐 Fix doc localization routing, links, and SEO tags
- Doc links, header, redirects, and pagination dropped the language or 404'd - Render markdown links through a lang-aware component so prerendered HTML is correct - Match only supported languages; drop the duplicate /en prefix - Untranslated pages fall back to English with a notice - Add a language switcher so translations are reachable - Set html lang per language and remove duplicate canonical/OG tags
1 parent d66a4e4 commit 18390b4

24 files changed

Lines changed: 208 additions & 154 deletions

‎src/app.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!doctype html>
2-
<html lang="en">
2+
<html lang="%lang%">
33
<head>
44
<meta charset="utf-8" />
55
<link rel="icon" href="%sveltekit.assets%/images/animated_java_icon.svg" />

‎src/hooks.server.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { DEFAULT_LANGUAGE } from '$lib/docs/docs'
2+
import type { Handle } from '@sveltejs/kit'
3+
4+
export const handle: Handle = ({ event, resolve }) => {
5+
const lang = event.params.lang ?? DEFAULT_LANGUAGE
6+
return resolve(event, { transformPageChunk: ({ html }) => html.replace('%lang%', lang) })
7+
}

‎src/lib/docs/docLayout.svelte‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!--
2+
@component
3+
mdsvex layout for every doc page. Its module exports replace the matching
4+
markdown elements, so `a` makes doc links follow the current language.
5+
-->
6+
<script module lang="ts">
7+
export { default as a } from './docLink.svelte'
8+
</script>
9+
10+
<script lang="ts">
11+
import type { Snippet } from 'svelte'
12+
13+
const { children }: { children?: Snippet } = $props()
14+
</script>
15+
16+
{@render children?.()}

‎src/lib/docs/docLink.svelte‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
import { page } from '$app/state'
3+
import { localizeHref } from '$lib/docs/docs'
4+
import type { Snippet } from 'svelte'
5+
import type { HTMLAnchorAttributes } from 'svelte/elements'
6+
7+
interface Props extends HTMLAnchorAttributes {
8+
children?: Snippet
9+
}
10+
11+
const { href, children, ...rest }: Props = $props()
12+
</script>
13+
14+
<a href={localizeHref(href ?? '', page.params.lang)} {...rest}>{@render children?.()}</a>

‎src/lib/docs/docs.ts‎

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,53 @@
1-
import { resolve } from '$app/paths'
21
import type { Component } from 'svelte'
32

43
const MODULES = import.meta.glob('/src/docs/**/*.md')
54

65
export const DEFAULT_LANGUAGE = 'en'
7-
export const SUPPORTED_LANGUAGES = ['de', 'en', 'nl', 'ru', 'uk', 'zh-cn']
6+
export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'ru', 'uk', 'zh-cn']
7+
/** Languages served under a `/<lang>` prefix. The default language has no prefix. */
8+
export const PREFIXED_LANGUAGES = SUPPORTED_LANGUAGES.filter(lang => lang !== DEFAULT_LANGUAGE)
89

9-
export function getDocModule(lang = 'en', path: string) {
10-
const normalizedLang = SUPPORTED_LANGUAGES.includes(lang) ? lang : DEFAULT_LANGUAGE
11-
const key = `/src/docs/${path}/${normalizedLang}.md`
10+
type DocModule = () => Promise<{ default: Component; metadata?: Record<string, any> }>
1211

13-
const mod = MODULES[key] ?? MODULES[`/src/docs/${path}/${DEFAULT_LANGUAGE}.md`]
12+
/** Every doc page path relative to `/docs` (e.g. `nodes/cubes`) that has at least one language file. */
13+
export const DOC_PATHS = [
14+
...new Set(
15+
Object.keys(MODULES).map(key =>
16+
key.replace(/^\/src\/docs\//, '').replace(/\/[^/]+\.md$/, '')
17+
)
18+
),
19+
]
1420

15-
return mod as () => Promise<{ default: Component; metadata: Record<string, any> }>
21+
/**
22+
* Finds the doc module for `path` in `lang`, falling back to the default language.
23+
* `translated` is false when the fallback was used.
24+
*/
25+
export function getDocModule(path: string, lang: string) {
26+
const translatedModule = MODULES[`/src/docs/${path}/${lang}.md`] as DocModule | undefined
27+
if (translatedModule) return { load: translatedModule, translated: true }
28+
29+
const fallback = MODULES[`/src/docs/${path}/${DEFAULT_LANGUAGE}.md`] as DocModule | undefined
30+
if (fallback) return { load: fallback, translated: false }
31+
}
32+
33+
/** Prefixes a site-absolute `/docs` path with `lang`. Every other href is returned unchanged. */
34+
export function localizeHref(href: string, lang: string | undefined): string {
35+
if (!lang || lang === DEFAULT_LANGUAGE || !/^\/docs(?=[/#?]|$)/.test(href)) return href
36+
return `/${lang}${href}`
37+
}
38+
39+
/** Removes a leading `/<lang>` prefix and any trailing slash from a pathname. */
40+
export function stripLanguage(pathname: string, lang: string | undefined): string {
41+
let path = pathname
42+
if (lang && lang !== DEFAULT_LANGUAGE && path.startsWith(`/${lang}/`)) {
43+
path = path.slice(lang.length + 1)
44+
}
45+
if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1)
46+
return path
1647
}
1748

18-
export function docHref(path: string, lang: string): string {
19-
const prefix = lang === DEFAULT_LANGUAGE ? '' : `/${lang}`
20-
return resolve(`${prefix}/${path}`)
49+
/** The name of `lang` written in `inLang`, e.g. `Deutsch` for `de` by default, or `German` in `en`. */
50+
export function getLanguageName(lang: string, inLang = lang): string {
51+
const name = new Intl.DisplayNames([inLang], { type: 'language' }).of(lang) ?? lang
52+
return name.charAt(0).toLocaleUpperCase(inLang) + name.slice(1)
2153
}

‎src/lib/docs/rehypeDocLinks.ts‎

Lines changed: 0 additions & 16 deletions
This file was deleted.

‎src/params/lang.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { PREFIXED_LANGUAGES } from '$lib/docs/docs'
2+
import type { ParamMatcher } from '@sveltejs/kit'
3+
4+
export const match: ParamMatcher = param => PREFIXED_LANGUAGES.includes(param)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
import '$lib/styles/global.css'
3+
import '$lib/styles/minecraftUI.css'
4+
5+
const { children } = $props()
6+
</script>
7+
8+
{@render children?.()}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,37 @@
88
import { backOut } from 'svelte/easing'
99
import { fade, fly } from 'svelte/transition'
1010
11+
const siteUrl = 'https://animated-java.dev'
12+
const pageTitle = 'Animated Java'
13+
const pageDescription = 'Effortlessly craft complex animations for Minecraft: Java Edition'
14+
const socialImage = `${siteUrl}/images/animated_java_icon.svg`
15+
1116
let startTransition = $state(false)
1217
1318
onMount(() => {
1419
startTransition = true
1520
})
1621
</script>
1722

23+
<svelte:head>
24+
<title>{pageTitle}</title>
25+
<meta name="description" content={pageDescription} />
26+
<link rel="canonical" href={siteUrl} />
27+
28+
<meta property="og:type" content="website" />
29+
<meta property="og:site_name" content="Animated Java" />
30+
<meta property="og:title" content={pageTitle} />
31+
<meta property="og:description" content={pageDescription} />
32+
<meta property="og:url" content={siteUrl} />
33+
<meta property="og:image" content={socialImage} />
34+
<meta content="#00ACED" data-react-helmet="true" name="theme-color" />
35+
36+
<meta name="twitter:card" content="summary_large_image" />
37+
<meta name="twitter:title" content={pageTitle} />
38+
<meta name="twitter:description" content={pageDescription} />
39+
<meta name="twitter:image" content={socialImage} />
40+
</svelte:head>
41+
1842
<div class="home-page-shell">
1943
<div class="bg-layer bg-base" aria-hidden="true"></div>
2044
<div class="bg-layer bg-aurora bg-aurora-a" aria-hidden="true"></div>

0 commit comments

Comments
 (0)