Skip to content

Commit 8cdf15a

Browse files
authored
App router 404 localization (#57230)
1 parent 6997049 commit 8cdf15a

21 files changed

+576
-395
lines changed

data/ui.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,3 +343,9 @@ cookbook_landing:
343343
search_articles: Search articles
344344
category: Category
345345
complexity: Complexity
346+
347+
not_found:
348+
title: Ooops!
349+
message: It looks like this page doesn't exist.
350+
contact: We track these errors automatically, but if the problem persists please feel free to contact us.
351+
contact_cta: Contact support

src/app/404/page.tsx

Lines changed: 7 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { getAppRouterContext } from '@/app/lib/app-router-context'
2-
import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext'
3-
import { translate } from '@/languages/lib/translation-utils'
4-
import { CommentDiscussionIcon, MarkGithubIcon } from '@primer/octicons-react'
1+
import { Client404Wrapper } from '@/app/components/Client404Wrapper'
2+
import { createServerAppRouterContext } from '@/app/lib/server-context-utils'
3+
import { headers } from 'next/headers'
54
import type { Metadata } from 'next'
65

76
export const dynamic = 'force-dynamic'
@@ -12,94 +11,10 @@ export const metadata: Metadata = {
1211
}
1312

1413
export default async function Page404() {
15-
// Get context with UI data
16-
const appContext = await getAppRouterContext()
14+
const headersList = await headers()
15+
const pathname = headersList.get('x-pathname') || '/404'
1716

18-
const siteTitle = translate(appContext.site.data.ui, 'header.github_docs', 'GitHub Docs')
19-
const oopsTitle = translate(appContext.site.data.ui, 'meta.oops', 'Ooops!')
17+
const appContext = createServerAppRouterContext(pathname)
2018

21-
return (
22-
<AppRouterMainContextProvider context={appContext}>
23-
<div className="min-h-screen d-flex flex-column">
24-
{/* Simple Header */}
25-
<div className="border-bottom color-border-muted no-print">
26-
<header className="container-xl p-responsive py-3 position-relative d-flex width-full">
27-
<div className="d-flex flex-1 flex-items-center">
28-
<a
29-
href={`/${appContext.currentLanguage}`}
30-
className="color-fg-default no-underline d-flex flex-items-center"
31-
>
32-
<MarkGithubIcon size={32} className="mr-2" />
33-
<span className="f4 text-bold">{siteTitle}</span>
34-
</a>
35-
</div>
36-
</header>
37-
</div>
38-
39-
{/* Main Content */}
40-
<div className="container-xl p-responsive py-6 width-full flex-1">
41-
<article className="col-md-10 col-lg-7 mx-auto">
42-
<h1>{oopsTitle}</h1>
43-
<div className="f2 color-fg-muted mb-3" data-container="lead">
44-
It looks like this page doesn't exist.
45-
</div>
46-
<p className="f3">
47-
We track these errors automatically, but if the problem persists please feel free to
48-
contact us.
49-
</p>
50-
<a id="support" href="https://support.github.com" className="btn btn-outline mt-2">
51-
<CommentDiscussionIcon size="small" className="octicon mr-1" />
52-
Contact support
53-
</a>
54-
</article>
55-
</div>
56-
57-
<footer className="py-6">
58-
<div className="container-xl px-3 px-md-6">
59-
<ul className="d-flex flex-wrap list-style-none">
60-
<li className="d-flex mr-xl-3 color-fg-muted">
61-
<span>© 2025 GitHub, Inc.</span>
62-
</li>
63-
<li className="ml-3">
64-
<a
65-
className="text-underline"
66-
href="/site-policy/github-terms/github-terms-of-service"
67-
>
68-
Terms
69-
</a>
70-
</li>
71-
<li className="ml-3">
72-
<a
73-
className="text-underline"
74-
href="/site-policy/privacy-policies/github-privacy-statement"
75-
>
76-
Privacy
77-
</a>
78-
</li>
79-
<li className="ml-3">
80-
<a className="text-underline" href="https://www.githubstatus.com/">
81-
Status
82-
</a>
83-
</li>
84-
<li className="ml-3">
85-
<a className="text-underline" href="https://github.com/pricing">
86-
Pricing
87-
</a>
88-
</li>
89-
<li className="ml-3">
90-
<a className="text-underline" href="https://services.github.com/">
91-
Expert services
92-
</a>
93-
</li>
94-
<li className="ml-3">
95-
<a className="text-underline" href="https://github.blog/">
96-
Blog
97-
</a>
98-
</li>
99-
</ul>
100-
</div>
101-
</footer>
102-
</div>
103-
</AppRouterMainContextProvider>
104-
)
19+
return <Client404Wrapper appContext={appContext} />
10520
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use client'
2+
3+
import { useAppRouterMainContext } from '@/app/components/AppRouterMainContext'
4+
import { createTranslationFunctions } from '@/languages/lib/translation-utils'
5+
import { LinkExternalIcon } from '@primer/octicons-react'
6+
7+
export function AppRouterFooter() {
8+
const context = useAppRouterMainContext()
9+
10+
const { t } = createTranslationFunctions(context.site.data.ui, 'footer')
11+
12+
return (
13+
<section className="container-xl px-3 mt-6 pb-8 px-md-6 color-fg-muted">
14+
{context.currentLanguage !== 'en' && <h2 className="f4 mb-2 col-12">{t('legal_heading')}</h2>}
15+
16+
{/* Machine translation notice for non-English languages */}
17+
{context.currentLanguage !== 'en' && <p>{t('machine')}</p>}
18+
19+
<ul className="d-flex flex-wrap list-style-none">
20+
<li className="mr-3">&copy; {new Date().getFullYear()} GitHub, Inc.</li>
21+
22+
{/* German-specific Impressum link (legally required) */}
23+
{context.currentLanguage === 'de' && (
24+
<li className="mr-3">
25+
<a
26+
className="text-underline"
27+
href="https://aka.ms/impressum_de"
28+
target="_blank"
29+
rel="noopener"
30+
>
31+
{t('imprint')}
32+
</a>
33+
<LinkExternalIcon aria-label="(external site)" size={12} />
34+
</li>
35+
)}
36+
37+
<li className="mr-3">
38+
<a
39+
className="text-underline"
40+
href={`/${context.currentLanguage}/site-policy/github-terms/github-terms-of-service`}
41+
>
42+
{t('terms')}
43+
</a>
44+
</li>
45+
46+
<li className="mr-3">
47+
<a
48+
className={`text-underline ${
49+
context.currentLanguage === 'ko' ? 'color-fg-attention text-bold' : ''
50+
}`}
51+
href={`/${context.currentLanguage}/site-policy/privacy-policies/github-privacy-statement`}
52+
>
53+
{t('privacy')}
54+
</a>
55+
</li>
56+
57+
<li className="mr-3">
58+
<a className="text-underline" href="https://www.githubstatus.com/">
59+
{t('status')}
60+
</a>
61+
</li>
62+
63+
<li className="mr-3">
64+
<a className="text-underline" href="https://github.com/pricing">
65+
{t('pricing')}
66+
</a>
67+
</li>
68+
69+
<li className="mr-3">
70+
<a className="text-underline" href="https://services.github.com">
71+
{t('expert_services')}
72+
</a>
73+
</li>
74+
75+
<li className="mr-3">
76+
<a className="text-underline" href="https://github.blog">
77+
{t('blog')}
78+
</a>
79+
</li>
80+
</ul>
81+
</section>
82+
)
83+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use client'
2+
3+
import { MarkGithubIcon } from '@primer/octicons-react'
4+
import { useAppRouterMainContext } from '@/app/components/AppRouterMainContext'
5+
import { createTranslationFunctions } from '@/languages/lib/translation-utils'
6+
7+
export function AppRouterHeader() {
8+
const context = useAppRouterMainContext()
9+
10+
const { t } = createTranslationFunctions(context.site.data.ui, 'header')
11+
12+
const siteTitle = t('github_docs')
13+
14+
return (
15+
<div className="border-bottom color-border-muted no-print">
16+
<header className="container-xl p-responsive py-3 position-relative d-flex width-full">
17+
<div className="d-flex flex-1 flex-items-center">
18+
<a
19+
href={`/${context.currentLanguage}`}
20+
className="color-fg-default no-underline d-flex flex-items-center"
21+
>
22+
<MarkGithubIcon size={32} className="mr-2" />
23+
<span className="f4 text-bold">{siteTitle}</span>
24+
</a>
25+
</div>
26+
</header>
27+
</div>
28+
)
29+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use client'
2+
3+
import { createContext, useContext } from 'react'
4+
import { clientLanguages, type ClientLanguageCode } from '@/languages/lib/client-languages'
5+
6+
export type AppRouterLanguageItem = {
7+
name: string
8+
nativeName?: string
9+
code: string
10+
hreflang?: string
11+
}
12+
13+
export type AppRouterLanguagesContextT = {
14+
languages: Record<string, AppRouterLanguageItem>
15+
currentLanguage?: ClientLanguageCode
16+
}
17+
18+
export const AppRouterLanguagesContext = createContext<AppRouterLanguagesContextT | null>(null)
19+
20+
export const useAppRouterLanguages = (): AppRouterLanguagesContextT => {
21+
const context = useContext(AppRouterLanguagesContext)
22+
23+
if (!context) {
24+
throw new Error(
25+
'"useAppRouterLanguages" may only be used inside "AppRouterLanguagesContext.Provider"',
26+
)
27+
}
28+
29+
return context
30+
}
31+
32+
/**
33+
* Provider component for App Router language context
34+
*/
35+
interface AppRouterLanguagesProviderProps {
36+
children: React.ReactNode
37+
currentLanguage?: ClientLanguageCode
38+
}
39+
40+
export function AppRouterLanguagesProvider({
41+
children,
42+
currentLanguage,
43+
}: AppRouterLanguagesProviderProps) {
44+
const value: AppRouterLanguagesContextT = {
45+
languages: clientLanguages,
46+
currentLanguage,
47+
}
48+
49+
return (
50+
<AppRouterLanguagesContext.Provider value={value}>
51+
{children}
52+
</AppRouterLanguagesContext.Provider>
53+
)
54+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client'
2+
3+
import { AppRouterFooter } from '@/app/components/AppRouterFooter'
4+
import { AppRouterHeader } from '@/app/components/AppRouterHeader'
5+
import { AppRouterLanguagesProvider } from '@/app/components/AppRouterLanguagesContext'
6+
import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext'
7+
import type { ServerAppRouterContext } from '@/app/lib/server-context-utils'
8+
import { createTranslationFunctions } from '@/languages/lib/translation-utils'
9+
import { CommentDiscussionIcon } from '@primer/octicons-react'
10+
11+
interface Client404WrapperProps {
12+
appContext: ServerAppRouterContext
13+
}
14+
15+
export function Client404Wrapper({ appContext }: Client404WrapperProps) {
16+
const { t } = createTranslationFunctions(appContext.site.data.ui, 'not_found')
17+
return (
18+
<AppRouterLanguagesProvider currentLanguage={appContext.currentLanguage}>
19+
<AppRouterMainContextProvider context={appContext}>
20+
<div className="min-h-screen d-flex flex-column">
21+
<AppRouterHeader />
22+
{/* Main Content */}
23+
<div className="container-xl p-responsive py-6 width-full flex-1">
24+
<article className="col-md-10 col-lg-7 mx-auto">
25+
<h1>{t('title')}</h1>
26+
<div className="f2 color-fg-muted mb-3" data-container="lead">
27+
{t('message')}
28+
</div>
29+
<p className="f3">{t('contact')}</p>
30+
<a id="support" href="https://support.github.com" className="btn btn-outline mt-2">
31+
<CommentDiscussionIcon size="small" className="octicon mr-1" />
32+
{t('contact_cta')}
33+
</a>
34+
</article>
35+
</div>
36+
<AppRouterFooter />
37+
</div>
38+
</AppRouterMainContextProvider>
39+
</AppRouterLanguagesProvider>
40+
)
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client'
2+
3+
import { AppRouterMainContextProvider } from '@/app/components/AppRouterMainContext'
4+
import { AppRouterLanguagesProvider } from '@/app/components/AppRouterLanguagesContext'
5+
import { NotFoundContent } from '@/app/components/NotFoundContent'
6+
import type { ServerAppRouterContext } from '@/app/lib/server-context-utils'
7+
8+
interface ClientNotFoundWrapperProps {
9+
appContext: ServerAppRouterContext
10+
}
11+
12+
export function ClientNotFoundWrapper({ appContext }: ClientNotFoundWrapperProps) {
13+
return (
14+
<AppRouterLanguagesProvider currentLanguage={appContext.currentLanguage}>
15+
<AppRouterMainContextProvider context={appContext}>
16+
<NotFoundContent />
17+
</AppRouterMainContextProvider>
18+
</AppRouterLanguagesProvider>
19+
)
20+
}

0 commit comments

Comments
 (0)