Skip to content
Merged
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
3 changes: 2 additions & 1 deletion content/faq/de/editions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ order: 2

Unser Server unterstützt sowohl die Java Edition als auch die Bedrock Edition.
Die genauen Adressen findest du im Abschnitt „Mit dem Server verbinden" auf
dieser Seite.
dieser Seite. Eine Schritt-für-Schritt-Anleitung für beide Editionen findest du auf
[der Seite Minecraft beitreten](/de/join).
3 changes: 2 additions & 1 deletion content/faq/en/editions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ order: 2
---

Our server supports both the Java Edition and Bedrock Edition. The exact
addresses are listed in the "Connect to the server" section on this page.
addresses are listed in the "Connect to the server" section on this page. A step-by-step guide for both
editions is on the [Join Minecraft page](/en/join).
30 changes: 29 additions & 1 deletion i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"more": "Mehr",
"status": "Status",
"builds": "Bauwerke",
"join": "Beitreten",
"home_link": "OneLiteFeather – Startseite",
"community_overview": "Übersicht",
"events": "Events",
Expand Down Expand Up @@ -66,7 +67,8 @@
"copy_port": "Port kopieren",
"copy_port_aria": "Port kopieren: {port}",
"port_label": "Port",
"clipboard_note": "Dein Browser unterstützt das Kopieren in die Zwischenablage nicht. Bitte kopiere die Adresse manuell."
"clipboard_note": "Dein Browser unterstützt das Kopieren in die Zwischenablage nicht. Bitte kopiere die Adresse manuell.",
"more_link": "Mehr zum Beitreten"
}
},
"blog": {
Expand Down Expand Up @@ -596,6 +598,32 @@
"intro": "Dieser Bau zeigt die folgenden Projekte im Einsatz."
}
},
"join": {
"title": "Server beitreten: Java & Bedrock",
"description": "So trittst du dem deutschsprachigen OneLiteFeather Minecraft-Netzwerk bei: Adresse für Java oder Bedrock kopieren und in wenigen Schritten loslegen.",
"heading": "Minecraft-Server beitreten",
"intro": "OneLiteFeather ist ein deutschsprachiges Minecraft-Netzwerk, gegründet 2019. Du kannst mit der Java Edition oder der Bedrock Edition beitreten. Kopiere die passende Adresse und folge den Schritten für deine Edition.",
"java": {
"heading": "Mit der Java Edition beitreten",
"step_1": "Starte Minecraft Java Edition und öffne den Mehrspielermodus.",
"step_2": "Klicke auf „Server hinzufügen“.",
"step_3": "Trage als Serveradresse {address} ein. Den Servernamen wählst du selbst.",
"step_4": "Klicke auf „Fertig“, wähle den Server in der Liste aus und klicke auf „Server beitreten“."
},
"bedrock": {
"heading": "Mit der Bedrock Edition beitreten",
"step_1": "Starte Minecraft Bedrock Edition und öffne den Reiter „Spielen“.",
"step_2": "Wechsle zum Reiter „Server“ und wähle „Server hinzufügen“.",
"step_3": "Trage einen Servernamen deiner Wahl ein, als Serveradresse {host} und als Port {port}.",
"step_4": "Speichere den Eintrag und verbinde dich mit dem Server."
},
"help": {
"heading": "Hilfe und Fragen",
"text": "Klappt die Verbindung nicht? Unser Discord hilft dir weiter. Antworten auf häufige Fragen findest du auf der Startseite.",
"discord": "Discord",
"faq": "Häufige Fragen"
}
},
"about": {
"title": "Über uns",
"description": "OneLiteFeather ist ein Minecraft-Netzwerk, das von seiner Community lebt: Gemeinschaftsbauten, offene Software, eigene Events und transparente Finanzierung.",
Expand Down
30 changes: 29 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"more": "More",
"status": "Status",
"builds": "Builds",
"join": "Join",
"home_link": "OneLiteFeather – Home",
"community_overview": "Overview",
"events": "Events",
Expand Down Expand Up @@ -66,7 +67,8 @@
"copy_port": "Copy port",
"copy_port_aria": "Copy port: {port}",
"port_label": "Port",
"clipboard_note": "Your browser does not support copying to the clipboard. Please copy the address manually."
"clipboard_note": "Your browser does not support copying to the clipboard. Please copy the address manually.",
"more_link": "More on joining"
}
},
"index": {
Expand Down Expand Up @@ -596,6 +598,32 @@
"intro": "This build shows the following projects in use."
}
},
"join": {
"title": "Join the server: Java & Bedrock",
"description": "Join the German-speaking OneLiteFeather Minecraft network: copy the Java or Bedrock address, add the server in your game and start playing in a few steps.",
"heading": "Join the Minecraft server",
"intro": "OneLiteFeather is a German-speaking Minecraft network, founded in 2019. You can join with the Java Edition or the Bedrock Edition. Copy the matching address and follow the steps for your edition.",
"java": {
"heading": "Join with Java Edition",
"step_1": "Start Minecraft Java Edition and open Multiplayer.",
"step_2": "Click Add Server.",
"step_3": "Enter {address} as the server address. You can choose the server name yourself.",
"step_4": "Click Done, select the server in the list and click Join Server."
},
"bedrock": {
"heading": "Join with Bedrock Edition",
"step_1": "Start Minecraft Bedrock Edition and open the Play tab.",
"step_2": "Switch to the Servers tab and choose Add Server.",
"step_3": "Enter any server name, {host} as the server address and {port} as the port.",
"step_4": "Save the entry and connect to the server."
},
"help": {
"heading": "Help and questions",
"text": "Can't connect? Our Discord can help. Answers to common questions are on the home page.",
"discord": "Discord",
"faq": "Common questions"
}
},
"about": {
"title": "About us",
"description": "OneLiteFeather is a Minecraft network that lives on its community: community builds, open software, events of our own and transparent funding.",
Expand Down
8 changes: 7 additions & 1 deletion layers/home/components/ServerAddresses.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const props = withDefaults(defineProps<Props>(), {
bedrockPort: undefined
})

const { t } = useI18n()
const { t, locale } = useI18n()

// Prefer props if provided, otherwise fall back to i18n
const displayTitle = computed(() => props.title ?? t('server.connect.title'))
Expand Down Expand Up @@ -128,6 +128,12 @@ const panelClass
/>
</div>

<div class="mt-6 text-center">
<M3Button :to="`/${locale}/join`" variant="text" :icon="['fas', 'arrow-right']">
{{ t('server.connect.more_link') }}
</M3Button>
</div>

<ClientOnly>
<p v-if="!isSupported" class="mt-4 text-center text-body-medium text-on-surface-variant">
{{ t('server.connect.clipboard_note') }}
Expand Down
20 changes: 20 additions & 0 deletions layers/home/composables/useServerConnect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Locale } from '#layers/content-core'
import type { ServerConnectDocument } from '../types'

/**
* The server connect document (Java and Bedrock addresses) for the active
* locale. Shares its cache key with `useHomeContent`, so both read one payload.
*/
export async function useServerConnect() {
const { locale } = useI18n()
const repo = useContentRepository()
const activeLocale = computed<Locale>(() => (locale?.value || 'de') as Locale)

const { data: connect } = await useAsyncData<ServerConnectDocument | null>(
() => `server-connect-${activeLocale.value}`,
() => repo.getServerConnect(activeLocale.value),
{ watch: [activeLocale] }
)

return { connect }
}
1 change: 1 addition & 0 deletions layers/navigation/navItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function buildNavConfig({ eventsTopLevel }: { eventsTopLevel: boolean }):
icon: ['fas', 'handshake'],
children: [
{ type: 'link', textKey: 'navigation.community_overview', routeName: 'community', icon: ['fas', 'handshake'] },
{ type: 'link', textKey: 'navigation.join', routeName: 'join', icon: ['fas', 'play'] },
{ type: 'link', textKey: 'navigation.builds', routeName: 'community-poi', icon: ['fas', 'location-dot'] },
{ type: 'link', textKey: 'navigation.projects', routeName: 'projects', icon: ['fas', 'code'] },
...(eventsTopLevel ? [] : [eventsLink])
Expand Down
111 changes: 111 additions & 0 deletions pages/join.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<script setup lang="ts">
import { definePageMeta } from '#imports'

const { t, locale } = useI18n()
const { discordUrl } = useRuntimeConfig().public
const { connect } = await useServerConnect()

const stepsClass = 'mt-4 max-w-3xl list-decimal space-y-3 pl-6 text-body-large text-on-surface-variant'
const javaParams = computed(() => ({ address: connect.value?.javaAddress ?? '' }))
const bedrockParams = computed(() => ({
host: connect.value?.bedrockHost ?? '',
port: connect.value?.bedrockPort ?? ''
}))

definePageMeta({
layout: 'default'
})

usePageSeo({
title: t('join.title'),
description: t('join.description'),
schemaType: 'WebPage',
keywords: [
'Minecraft Server',
'Minecraft-Server beitreten',
'Java Edition',
'Bedrock Edition',
'Join Minecraft'
]
})

useBreadcrumbs(() => [
{ name: t('navigation.home'), url: `/${locale.value}` }, { name: t('navigation.join') }
])
</script>

<template>
<div class="mx-auto max-w-6xl px-4 py-10 md:py-14">
<header class="max-w-3xl">
<h1 class="text-display-small font-bold text-on-surface">
{{ t('join.heading') }}
</h1>
<p class="mt-4 text-body-large text-on-surface-variant">
{{ t('join.intro') }}
</p>
</header>

<div class="mt-10">
<ServerAddresses
v-if="connect"
:java-address="connect.javaAddress"
:bedrock-host="connect.bedrockHost"
:bedrock-port="connect.bedrockPort"
/>
</div>

<section class="mt-12" aria-labelledby="join-java">
<h2 id="join-java" class="text-headline-medium font-bold text-on-surface">
{{ t('join.java.heading') }}
</h2>
<ol :class="stepsClass">
<li>{{ t('join.java.step_1') }}</li>
<li>{{ t('join.java.step_2') }}</li>
<li>{{ t('join.java.step_3', javaParams) }}</li>
<li>{{ t('join.java.step_4') }}</li>
</ol>
</section>

<section class="mt-12" aria-labelledby="join-bedrock">
<h2 id="join-bedrock" class="text-headline-medium font-bold text-on-surface">
{{ t('join.bedrock.heading') }}
</h2>
<ol :class="stepsClass">
<li>{{ t('join.bedrock.step_1') }}</li>
<li>{{ t('join.bedrock.step_2') }}</li>
<li>{{ t('join.bedrock.step_3', bedrockParams) }}</li>
<li>{{ t('join.bedrock.step_4') }}</li>
</ol>
</section>

<section class="mt-12" aria-labelledby="join-help">
<h2 id="join-help" class="text-headline-medium font-bold text-on-surface">
{{ t('join.help.heading') }}
</h2>
<p class="mt-3 max-w-3xl text-body-large text-on-surface-variant">
{{ t('join.help.text') }}
</p>
<ul class="mt-5 flex flex-wrap gap-3">
<li v-if="discordUrl">
<M3Button
:href="discordUrl"
target="_blank"
variant="tonal"
:icon="['fab', 'discord']"
>
{{ t('join.help.discord') }}
</M3Button>
</li>
<li>
<M3Button
:to="`/${locale}#faq-heading`"
variant="outlined"
:icon="['fas', 'circle-info']"
>
{{ t('join.help.faq') }}
</M3Button>
</li>
</ul>
</section>
</div>
</template>
117 changes: 117 additions & 0 deletions tests/join/content.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { repoRoot } from '../helpers/sources'

type Tree = Record<string, unknown>

const messages = (locale: string): Tree => JSON.parse(readFileSync(`${repoRoot}/i18n/locales/${locale}.json`, 'utf8'))
const read = (file: string) => readFileSync(`${repoRoot}/${file}`, 'utf8')
function lookup(tree: Tree, dotted: string): unknown {
return dotted.split('.').reduce<unknown>((node, part) => (node as Tree | undefined)?.[part], tree)
}

const LOCALES = ['de', 'en'] as const

const KEYS = [
'join.title',
'join.description',
'join.heading',
'join.intro',
'join.java.heading',
'join.java.step_1',
'join.java.step_2',
'join.java.step_3',
'join.java.step_4',
'join.bedrock.heading',
'join.bedrock.step_1',
'join.bedrock.step_2',
'join.bedrock.step_3',
'join.bedrock.step_4',
'join.help.heading',
'join.help.text',
'join.help.discord',
'join.help.faq',
'navigation.join',
'server.connect.more_link'
]

describe('join page messages', () => {
it.each(LOCALES)('has every join key filled in %s', (locale) => {
const tree = messages(locale)
for (const key of KEYS) {
const value = lookup(tree, key)
expect(typeof value, `${locale}: ${key}`).toBe('string')
expect((value as string).trim(), `${locale}: ${key}`).not.toBe('')
}
})

it.each(LOCALES)('keeps the address placeholders in the steps in %s', (locale) => {
const tree = messages(locale)
expect(lookup(tree, 'join.java.step_3')).toContain('{address}')
expect(lookup(tree, 'join.bedrock.step_3')).toContain('{host}')
expect(lookup(tree, 'join.bedrock.step_3')).toContain('{port}')
})

it.each(LOCALES)('keeps the title short enough for a search result in %s', (locale) => {
const title = lookup(messages(locale), 'join.title') as string
expect(title.length).toBeLessThanOrEqual(39)
})

it.each(LOCALES)('writes a meta description of 120 to 155 characters in %s', (locale) => {
const description = lookup(messages(locale), 'join.description') as string
expect(description.length).toBeGreaterThanOrEqual(120)
expect(description.length).toBeLessThanOrEqual(155)
})

it('uses umlauts and the du-form in the German copy', () => {
const copy = JSON.stringify(messages('de').join)
expect(copy).toMatch(/[äöüß]/)
expect(copy).toMatch(/\b(du|dir|deine|deinen|dein)\b/)
})
})

describe('name and language of the network', () => {
const LANGUAGE = {
de: { intro: 'deutschsprachiges Minecraft-Netzwerk', description: 'deutschsprachigen' },
en: { intro: 'German-speaking Minecraft network', description: 'German-speaking' }
} as const

it.each(LOCALES)('names the server in the heading and the title in %s', (locale) => {
const tree = messages(locale)
expect(lookup(tree, 'join.heading') as string).toMatch(/server/i)
expect(lookup(tree, 'join.title') as string).toMatch(/server/i)
})

it.each(LOCALES)('says the network is German-speaking in the intro in %s', (locale) => {
expect(lookup(messages(locale), 'join.intro') as string).toContain(LANGUAGE[locale].intro)
})

it.each(LOCALES)('says the network is German-speaking in the meta description in %s', (locale) => {
expect(lookup(messages(locale), 'join.description') as string).toContain(LANGUAGE[locale].description)
})

it.each(LOCALES)('states the founding year in the intro in %s', (locale) => {
expect(lookup(messages(locale), 'join.intro') as string).toContain('2019')
})
})

describe('entry points to the join page', () => {
it('links the German editions FAQ answer to /de/join', () => {
expect(read('content/faq/de/editions.md')).toContain('](/de/join)')
})

it('links the English editions FAQ answer to /en/join', () => {
expect(read('content/faq/en/editions.md')).toContain('](/en/join)')
})

it('links the home server addresses section to the join page in the current locale', () => {
const addresses = read('layers/home/components/ServerAddresses.vue')
expect(addresses).toMatch(/:to="`\/\$\{locale\}\/join`"/)
expect(addresses).toContain("t('server.connect.more_link')")
})

it('points the join page FAQ link at the heading the FAQ section renders', () => {
expect(read('layers/home/components/FaqSection.vue')).toContain('id="faq-heading"')
expect(read('pages/join.vue')).toContain('#faq-heading')
})
})
Loading
Loading