Skip to content

Commit 54f5d6c

Browse files
committed
Update
1 parent 85190f4 commit 54f5d6c

12 files changed

Lines changed: 144 additions & 12 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ English uses the root path; Simplified Chinese uses `/zh-Hans/`. Each locale has
4343

4444
Edit `public/i18n/en.json` and `public/i18n/zh-Hans.json` to update copy. Keep the same keys and section structure in every locale. These resources contain data only; components handle presentation and behavior.
4545

46-
To add a language, create its JSON resource, register it in `src/i18n/index.ts`, and update the locale and sitemap settings in `astro.config.ts`. Add the localized text for every existing key, including accessibility labels and metadata. Language names come from each resource's `languageName` field.
46+
Visitors to unprefixed URLs are directed to the first supported language in their browser preferences. Chinese language tags resolve to Simplified Chinese, and unsupported languages fall back to English. Explicit `/zh-Hans/` URLs retain their language. A manual selection in the language menu is saved and takes priority over browser preferences on subsequent visits. Language changes preserve query parameters and section anchors. If local storage is unavailable, manual selections use a `lang` query parameter instead.
47+
48+
To add a language, create its JSON resource, register it in `src/i18n/index.ts`, and add its identifier to `src/i18n/routing.mjs`. Astro and sitemap configuration share this locale list. Add the localized text for every existing key, including accessibility labels and metadata. Language names come from each resource's `languageName` field.
4749

4850
## Announcements
4951

‎astro.config.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ import sitemap from '@astrojs/sitemap';
99
import tailwindcss from '@tailwindcss/vite';
1010
import icon from 'astro-icon';
1111
import astrowind from './vendor/integration';
12+
import { defaultLocale, locales } from './src/i18n/routing.mjs';
1213

1314
export default defineConfig({
1415
site: 'https://diffscope.org',
1516
output: 'static',
1617
i18n: {
17-
defaultLocale: 'en',
18-
locales: ['en', 'zh-Hans'],
18+
defaultLocale,
19+
locales: [...locales],
1920
routing: { prefixDefaultLocale: false },
2021
},
2122
integrations: [
22-
sitemap({ i18n: { defaultLocale: 'en', locales: { en: 'en', 'zh-Hans': 'zh-Hans' } } }),
23+
sitemap({ i18n: { defaultLocale, locales: Object.fromEntries(locales.map((locale) => [locale, locale])) } }),
2324
icon({ iconDir: 'src/assets/icons', include: { tabler: ['*'] } }),
2425
astrowind({ config: './src/config.yaml' }),
2526
],

‎public/announcement.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"enabled": false
2+
"enabled": true
33
}

‎public/i18n/en.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
},
8585
"languageName": "English",
8686
"announcement": {
87-
"text": ""
87+
"text": "This project is currently under development. Downloads will be available soon."
8888
},
8989
"placeholder": {
9090
"body": "TODO"

‎public/i18n/zh-Hans.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"footer": {
7575
"rights": "© Team OpenVPI",
7676
"licenseBefore": "除非另有声明,网站内容以 ",
77-
"licenseAfter": "许可。",
77+
"licenseAfter": " 许可。",
7878
"licenseName": "CC BY-SA 4.0"
7979
},
8080
"notFound": {
@@ -84,7 +84,7 @@
8484
},
8585
"languageName": "简体中文",
8686
"announcement": {
87-
"text": ""
87+
"text": "本项目当前正在开发中。下载将在不久后可用。"
8888
},
8989
"placeholder": {
9090
"body": "TODO"

‎src/components/widgets/Header.astro‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const links = Object.entries(t.nav).map(([key, label]) => ({
4444
href={localizePath(page || '/', language)}
4545
lang={language}
4646
hreflang={language}
47+
data-language={language}
4748
aria-current={locale === language ? 'true' : undefined}
4849
>
4950
{localeNames[language]}

‎src/i18n/index.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55

66
import en from '../../public/i18n/en.json';
77
import zh from '../../public/i18n/zh-Hans.json';
8+
import { locales } from './routing.mjs';
9+
export { locales, localizePath } from './routing.mjs';
810

911
export type Dictionary = typeof en;
10-
export const locales = ['en', 'zh-Hans'] as const;
1112
export type Locale = (typeof locales)[number];
1213
export const dictionaries: Record<Locale, Dictionary> = { en, 'zh-Hans': zh };
1314
export const localeNames: Record<Locale, string> = Object.fromEntries(
1415
locales.map((locale) => [locale, dictionaries[locale].languageName])
1516
) as Record<Locale, string>;
16-
export const localizePath = (path: string, locale: Locale) =>
17-
`${locale === 'en' ? '' : `/${locale}`}${path === '/' ? '/' : `/${path.replace(/^\/+|\/+$/g, '')}/`}`;

‎src/i18n/routing.mjs‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SPDX-FileCopyrightText: Team OpenVPI
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
/** @type {readonly ['en', 'zh-Hans']} */
7+
export const locales = ['en', 'zh-Hans'];
8+
export const defaultLocale = 'en';
9+
export const pages = ['', 'downloads', 'features', 'contribute', 'community'];
10+
export const localePreferenceKey = 'diffscope-locale';
11+
12+
/**
13+
* @param {string} path
14+
* @param {typeof locales[number]} locale
15+
*/
16+
export const localizePath = (path, locale) =>
17+
`${locale === defaultLocale ? '' : `/${locale}`}${path === '/' ? '/' : `/${path.replace(/^\/+|\/+$/g, '')}/`}`;
18+
19+
/** @param {readonly string[]} languages */
20+
export function matchBrowserLocale(languages) {
21+
for (const language of languages) {
22+
const normalized = language.toLowerCase().replaceAll('_', '-');
23+
const exact = locales.find((locale) => locale.toLowerCase() === normalized);
24+
if (exact) return exact;
25+
const base = normalized.split('-')[0];
26+
const related = locales.find((locale) => locale.toLowerCase().split('-')[0] === base);
27+
if (related) return related;
28+
}
29+
return defaultLocale;
30+
}
31+
32+
/**
33+
* Only negotiate unprefixed page URLs; explicit locale URLs remain authoritative.
34+
* @param {string} pathname
35+
* @param {readonly string[]} languages
36+
* @param {string | null} preference
37+
*/
38+
export function getLanguageRedirect(pathname, languages, preference) {
39+
const page = pathname.replace(/^\//, '').replace(/\/$/, '');
40+
if (!pages.includes(page)) return null;
41+
const locale = locales.find((locale) => locale === preference) ?? matchBrowserLocale(languages);
42+
return locale === defaultLocale ? null : localizePath(page || '/', locale);
43+
}

‎src/layouts/PageLayout.astro‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ const canonical = new URL(localizePath(page || '/', locale), Astro.site);
5050
<meta property="og:image:height" content="630" />
5151
<meta name="twitter:card" content="summary_large_image" />
5252
<CustomStyles />
53+
<script>
54+
import '~/scripts/language';
55+
</script>
5356
<script is:inline>
5457
(() => {
5558
let preference = null;

‎src/pages/[...path].astro‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import PageLayout from '~/layouts/PageLayout.astro';
77
import Home from '~/components/widgets/Home.astro';
88
import { dictionaries, locales, type Locale } from '~/i18n';
9+
import { pages } from '~/i18n/routing.mjs';
910
1011
export function getStaticPaths() {
1112
return locales.flatMap((locale) =>
12-
['', 'downloads', 'features', 'contribute', 'community'].map((page) => ({
13+
pages.map((page) => ({
1314
params: { path: [locale === 'en' ? '' : locale, page].filter(Boolean).join('/') || undefined },
1415
props: { locale, page },
1516
}))

0 commit comments

Comments
 (0)