Skip to content

Commit c30cfa8

Browse files
SnaveSutitgitbutler-client
authored andcommitted
📝 Add translation guide
1 parent fb5caa6 commit c30cfa8

3 files changed

Lines changed: 185 additions & 36 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Contributing
2+
3+
Thanks for helping out with the Animated Java website!
4+
5+
## Running the site locally
6+
7+
You'll need [Bun](https://bun.sh).
8+
9+
```bash
10+
bun install
11+
bun dev
12+
```
13+
14+
The site runs at http://localhost:5173.
15+
16+
## Translating the docs
17+
18+
Each doc page is a folder in `src/docs`, with one Markdown file per language:
19+
20+
```
21+
src/docs/nodes/cubes/
22+
├── en.md ← English (the source)
23+
└── de.md ← German translation
24+
```
25+
26+
Pages without a file for a language fall back to English, with a notice saying they aren't translated yet.
27+
28+
### Translating a page
29+
30+
1. Copy the page's `en.md` to `<lang>.md` in the same folder, e.g. `src/docs/nodes/cubes/de.md`.
31+
2. Translate the `title` and `description` in the frontmatter, then the page content.
32+
3. Leave these as they are:
33+
- Code blocks, commands, and function names.
34+
- Link paths, e.g. `[Groups](/docs/nodes/groups)`. They automatically point to the same language, so only translate the link text.
35+
- Admonition and tree syntax (`:::tip`, `:::tree`, `:::`). You can translate custom admonition titles, like `:::warning[Before you get started]`.
36+
4. Links to a section (`/docs/nodes/groups#entity-creation`) use the heading's text. If the target page is translated, update the `#anchor` to match its translated heading.
37+
5. Check it at `http://localhost:5173/<lang>/docs/<page>`, then open a pull request.
38+
39+
You don't need to translate every page at once. A pull request with a single page is welcome.
40+
41+
Currently supported languages: `en`, `de`, `nl`, `ru`, `uk`, `zh-cn`.
42+
43+
### Adding a new language
44+
45+
Want to translate into a language that isn't listed? Open an issue first so we can make sure someone can keep it up to date. Then:
46+
47+
1. Add the language code to `SUPPORTED_LANGUAGES` in `src/lib/docs/docs.ts`. Use a [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) code in lowercase, like `fr` or `pt-br`.
48+
2. Translate at least `src/docs/welcome`, so the language has a starting page.
49+
3. Open a pull request with both changes.
50+
51+
The language picker and the `/<lang>/docs` routes pick up the new language automatically.

‎src/routes/[[lang=lang]]/docs/+layout.svelte‎

Lines changed: 127 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { browser } from '$app/environment'
3-
import { afterNavigate, goto } from '$app/navigation'
3+
import { afterNavigate } from '$app/navigation'
44
import Footer from '$lib/components/footer.svelte'
55
import MinecraftIcon from '$lib/components/minecraftIcon.svelte'
66
import { getLanguageName, localizeHref, SUPPORTED_LANGUAGES } from '$lib/docs/docs'
@@ -25,9 +25,6 @@
2525
document.documentElement.lang = lang
2626
})
2727
28-
function switchLanguage(event: Event & { currentTarget: HTMLSelectElement }): void {
29-
void goto(localizeHref(data.currentPath, event.currentTarget.value) + window.location.hash)
30-
}
3128
3229
const pageTitle = $derived(
3330
data.currentDoc?.title ? `${data.currentDoc.title} | ${docsTitle}` : docsTitle
@@ -41,6 +38,8 @@
4138
let activeHeadingId = $state('')
4239
let isMobileNavOpen = $state(false)
4340
let isMobileSidebarOpen = $state(false)
41+
let isLanguageMenuOpen = $state(false)
42+
let languageMenu = $state<HTMLDivElement>()
4443
let headingObserver: IntersectionObserver | null = null
4544
let observedTargets: HTMLElement[] = []
4645
@@ -151,6 +150,26 @@
151150
isMobileNavOpen = false
152151
}
153152
153+
function toggleLanguageMenu(): void {
154+
isLanguageMenuOpen = !isLanguageMenuOpen
155+
}
156+
157+
function closeLanguageMenu(): void {
158+
isLanguageMenuOpen = false
159+
}
160+
161+
function handleWindowClick(event: MouseEvent): void {
162+
if (event.target instanceof Node && !languageMenu?.contains(event.target)) {
163+
closeLanguageMenu()
164+
}
165+
}
166+
167+
function handleWindowKeydown(event: KeyboardEvent): void {
168+
if (event.key !== 'Escape' || !isLanguageMenuOpen) return
169+
closeLanguageMenu()
170+
languageMenu?.querySelector('button')?.focus()
171+
}
172+
154173
function toggleMobileSidebar(): void {
155174
isMobileSidebarOpen = !isMobileSidebarOpen
156175
}
@@ -176,6 +195,7 @@
176195
afterNavigate(() => {
177196
closeMobileNav()
178197
closeMobileSidebar()
198+
closeLanguageMenu()
179199
void refreshToc()
180200
})
181201
@@ -213,6 +233,8 @@
213233
<meta name="twitter:image" content={socialImage} />
214234
</svelte:head>
215235

236+
<svelte:window onclick={handleWindowClick} onkeydown={handleWindowKeydown} />
237+
216238
<div class="docs-shell">
217239
<header class="docs-header minecraft-box">
218240
<a href="/" class="brand-link">
@@ -251,16 +273,43 @@
251273
{link.title}
252274
</a>
253275
{/each}
254-
<!-- A select can't show an icon, so an invisible one covers this button -->
255-
<label class="language-select minecraft-button">
256-
<MinecraftIcon path="icons/current/lang_file.svg" />
257-
{getLanguageName(lang)}
258-
<select aria-label="Language" value={lang} onchange={switchLanguage}>
276+
<div class="language-menu" bind:this={languageMenu}>
277+
<button
278+
type="button"
279+
class="language-toggle minecraft-button"
280+
aria-label="Language: {getLanguageName(lang)}"
281+
aria-controls="language-list"
282+
aria-expanded={isLanguageMenuOpen}
283+
onclick={toggleLanguageMenu}
284+
>
285+
<MinecraftIcon path="icons/current/lang_file.svg" />
286+
{getLanguageName(lang)}
287+
</button>
288+
<!-- Hidden instead of removed, so the prerendered page still links every language -->
289+
<ul
290+
id="language-list"
291+
class="language-list minecraft-box"
292+
hidden={!isLanguageMenuOpen}
293+
>
259294
{#each SUPPORTED_LANGUAGES as option}
260-
<option value={option} lang={option}>{getLanguageName(option)}</option>
295+
<li>
296+
<a
297+
class:active={option === lang}
298+
aria-current={option === lang ? 'page' : undefined}
299+
href={localizeHref(data.currentPath, option)}
300+
hreflang={option}
301+
lang={option}
302+
>
303+
{getLanguageName(option)}
304+
305+
{#if option === lang}
306+
<i class="minecraft-left-arrow"></i>
307+
{/if}
308+
</a>
309+
</li>
261310
{/each}
262-
</select>
263-
</label>
311+
</ul>
312+
</div>
264313
</nav>
265314
</header>
266315

@@ -409,26 +458,58 @@
409458
justify-content: flex-end;
410459
}
411460
412-
.language-select {
461+
.language-menu {
413462
position: relative;
463+
}
464+
465+
.language-toggle {
414466
font-family: var(--minecraft-font);
415467
font-size: var(--font-size-small);
416-
color: var(--minecraft-button-text-color);
417-
cursor: pointer;
418468
}
419469
420-
.language-select select {
470+
.language-toggle:focus-visible {
471+
outline: 2px solid #00aced;
472+
outline-offset: 6px;
473+
}
474+
475+
/* Stay pressed while the menu is open */
476+
.language-toggle[aria-expanded='true'] {
477+
/* prettier-ignore */
478+
box-shadow:
479+
0 0 0 4px var(--minecraft-button-inset-border-color) inset,
480+
0 0 0 4px var(--minecraft-border-color);
481+
padding: calc(4px + 8px) 16px calc(8px + 4px) 16px;
482+
margin-top: 12px;
483+
}
484+
485+
.language-list {
421486
position: absolute;
422-
inset: 0;
423-
width: 100%;
424-
height: 100%;
425-
opacity: 0;
426-
cursor: pointer;
487+
top: calc(100% + 16px);
488+
right: 0;
489+
z-index: 10;
490+
min-width: 100%;
491+
box-sizing: border-box;
492+
margin: 0;
493+
padding: 12px 0;
494+
gap: 2px;
427495
}
428496
429-
.language-select:focus-within {
430-
outline: 2px solid #00aced;
431-
outline-offset: 2px;
497+
.language-list[hidden] {
498+
display: none;
499+
}
500+
501+
.language-list a {
502+
display: flex;
503+
justify-content: space-between;
504+
gap: 16px;
505+
padding: 6px 20px;
506+
/* Scripts the Minecraft font lacks fall back to taller fonts, so pin the rows */
507+
line-height: 24px;
508+
}
509+
510+
.language-list a:not(.active):hover {
511+
color: var(--minecraft-text-color);
512+
background-color: #ffffff14;
432513
}
433514
434515
.docs-nav-toggle {
@@ -438,14 +519,14 @@
438519
line-height: 1;
439520
}
440521
441-
.docs-nav a {
522+
.docs-nav > a {
442523
font-family: var(--minecraft-font);
443524
font-size: var(--font-size-small);
444525
text-decoration: none;
445526
color: var(--minecraft-button-text-color);
446527
}
447528
448-
.docs-nav a.active {
529+
.docs-nav > a.active {
449530
color: var(--minecraft-text-color);
450531
background-color: #00aced;
451532
/* prettier-ignore */
@@ -456,7 +537,7 @@
456537
margin-top: 12px;
457538
}
458539
459-
.docs-nav a.active::after {
540+
.docs-nav > a.active::after {
460541
content: '';
461542
display: block;
462543
position: relative;
@@ -669,8 +750,8 @@
669750
);
670751
}
671752
672-
.docs-nav.open a,
673-
.docs-nav.open .language-select {
753+
.docs-nav.open > a,
754+
.docs-nav.open .language-toggle {
674755
display: flex;
675756
flex-direction: column;
676757
align-items: center;
@@ -681,20 +762,30 @@
681762
white-space: nowrap;
682763
}
683764
684-
.docs-nav.open a :global(i),
685-
.docs-nav.open .language-select :global(i) {
765+
.docs-nav.open > a :global(i),
766+
.docs-nav.open .language-toggle :global(i) {
686767
display: flex;
687768
align-items: center;
688769
height: 32px;
689770
margin: 0;
690771
}
691772
692773
/* The pressed button is shorter, so sink it to the row's bottom edge like the desktop header */
693-
.docs-nav.open a.active {
774+
.docs-nav.open .language-menu {
775+
display: grid;
776+
align-content: end;
777+
}
778+
779+
/* Grid cells can sit anywhere in the row, so match the button's width instead of overhanging */
780+
.docs-nav.open .language-list {
781+
left: 0;
782+
}
783+
784+
.docs-nav.open > a.active {
694785
align-self: end;
695786
}
696787
697-
.docs-nav.open a.active::after {
788+
.docs-nav.open > a.active::after {
698789
display: none;
699790
}
700791
@@ -752,16 +843,16 @@
752843
padding: 8px;
753844
}
754845
755-
.docs-nav a {
846+
.docs-nav > a {
756847
display: block;
757848
margin-top: 0;
758849
}
759850
760-
.docs-nav a.active {
851+
.docs-nav > a.active {
761852
margin-top: 0;
762853
}
763854
764-
.docs-nav a.active::after {
855+
.docs-nav > a.active::after {
765856
display: none;
766857
}
767858

‎src/routes/[[lang=lang]]/docs/[...path]/+page.svelte‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import { getLanguageName } from '$lib/docs/docs'
33
import { getAdmonitionIconUrl } from '$lib/docs/remark-admonitions'
44
5+
const TRANSLATION_GUIDE_URL =
6+
'https://github.com/Animated-Java/animated-java.github.io/blob/main/CONTRIBUTING.md#translating-the-docs'
7+
58
const { data } = $props()
69
</script>
710

@@ -16,6 +19,10 @@
1619
This page isn't available in {getLanguageName(data.lang, 'en')} yet, so it's shown in
1720
English.
1821
</p>
22+
<p>
23+
Want to help? Check out the
24+
<a href={TRANSLATION_GUIDE_URL}>translation guide</a>.
25+
</p>
1926
</div>
2027
</aside>
2128
{/if}

0 commit comments

Comments
 (0)