mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
fix(site): choose first-visit language from browser
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@ Sidebar grouping comes from the `sections` array in the generator — register a
|
||||
|
||||
`src/styles/base.css` holds shared tokens. The landing page uses the user-provided Wandor direction throughout: warm paper illustrations, a white-faded ambient video, Geist typography, Special Elite wordmark, liquid glass surfaces and black capsule controls. Keep the entire landing journey consistent, including features, onboarding, FAQ and download. Documentation uses the same paper, illustration, typography and terracotta palette, scoped through `src/docs/doc-wandor.css` so its reading surfaces remain legible in both themes. The public display name is `cc-haha`; preserve legacy executable paths and release filenames when they must match actual artifacts. Rules:
|
||||
|
||||
English is the default language for the landing page and documentation. `/en` and `/en/start` are the English entries; `/` and `/start` remain explicit Chinese routes. A saved manual language choice takes precedence when opening `/`.
|
||||
On a first visit to `/`, show Chinese when the browser's preferred language is Chinese; use English for every other language. `/en` and `/en/start` are the English entries; `/` and `/start` remain Chinese routes. A saved manual language choice takes precedence when opening `/`.
|
||||
|
||||
- Use tokens (`--surface-*`, `--text-*`, `--border*`, `--brand*`, `--sp-*`, `--fs-*`, `--r-*`) rather than literal values. A raw hex in a component is a bug.
|
||||
- Keep screenshots flat and legible. Never stack, tilt, blur, or auto-rotate product UI as the main evidence.
|
||||
|
||||
+4
-3
@@ -20,15 +20,16 @@
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<title>cc-haha — Less busywork. More room for life.</title>
|
||||
<script>
|
||||
// 首帧就把语言定下来:没有保存的选择时默认英文。只认根路径,带语言前缀
|
||||
// 首帧就把语言定下来:没有保存的选择时按浏览器首选语言。只认根路径,带语言前缀
|
||||
// 的地址是用户的明确意图。规则与 src/lib/locale.js 保持一致。
|
||||
(function () {
|
||||
if (window.location.pathname.replace(/\/+$/, '') !== '') return
|
||||
var locale = 'en'
|
||||
var preferredLanguage = (navigator.languages && navigator.languages[0]) || navigator.language || ''
|
||||
var locale = /^zh(?:[-_]|$)/i.test(preferredLanguage) ? 'zh' : 'en'
|
||||
try {
|
||||
var stored = localStorage.getItem('cch-locale')
|
||||
if (stored === 'en' || stored === 'zh') locale = stored
|
||||
} catch (error) { /* 私密模式下仍默认英文 */ }
|
||||
} catch (error) { /* 私密模式下仍按浏览器语言 */ }
|
||||
if (locale === 'en') window.location.replace('/en' + window.location.search + window.location.hash)
|
||||
else document.documentElement.lang = 'zh-CN'
|
||||
})()
|
||||
|
||||
@@ -234,7 +234,7 @@ async function checkAppScreenshotFiles() {
|
||||
|
||||
/**
|
||||
* 语言分流的判定规则在两处各有一份:src/lib/locale.js(可测的模块)和 index.html 里的内联
|
||||
* 副本(首帧就要跳,等不到模块加载)。锁住 storage key、默认语言及根路径边界。
|
||||
* 副本(首帧就要跳,等不到模块加载)。锁住 storage key、浏览器语言读取及根路径边界。
|
||||
*/
|
||||
async function checkLocaleRedirect() {
|
||||
const problems = []
|
||||
@@ -253,8 +253,12 @@ async function checkLocaleRedirect() {
|
||||
problems.push(`index.html: 内联语言脚本没有用 '${storageKey}',与 src/lib/locale.js 不一致`)
|
||||
}
|
||||
|
||||
if (!shellSource.includes(`var locale = '${defaultLocale}'`)) {
|
||||
problems.push(`index.html: 内联语言脚本的默认语言与 src/lib/locale.js 的 ${defaultLocale} 不一致`)
|
||||
if (!shellSource.includes('navigator.languages && navigator.languages[0]') || !shellSource.includes('navigator.language')) {
|
||||
problems.push('index.html: 内联语言脚本缺少浏览器首选语言读取')
|
||||
}
|
||||
|
||||
if (!shellSource.includes(`? 'zh' : '${defaultLocale}'`)) {
|
||||
problems.push(`index.html: 内联语言脚本的非中文默认语言与 src/lib/locale.js 的 ${defaultLocale} 不一致`)
|
||||
}
|
||||
|
||||
// 少了这道判断,/en/start 这类地址也会被卷进分流。
|
||||
|
||||
+10
-4
@@ -1,19 +1,25 @@
|
||||
/**
|
||||
* 站点语言分流。
|
||||
*
|
||||
* 只有根路径 `/` 会按保存的选择分流:没有选择时默认跳 `/en`。
|
||||
* 只有根路径 `/` 会按保存的选择分流:没有选择时,浏览器首选语言是中文就留在中文页,
|
||||
* 其余语言跳 `/en`。
|
||||
* 带语言前缀的地址(`/en`、`/start`、`/en/start`)都是用户点进来的明确意图,一概不动 ——
|
||||
* 否则英文用户点一条中文文档链接会被莫名踢走。
|
||||
*
|
||||
* 手动切过语言之后,选择记进 localStorage,之后回到 `/` 就按记住的来。
|
||||
*
|
||||
* 注意:同一套判断在 index.html 里有一份内联副本 —— 首帧就得跳完,等不到这个模块加载。
|
||||
* 改 STORAGE_KEY 或默认语言时两处要一起改,check-docs.mjs 会盯着它们不漂移。
|
||||
* 改 STORAGE_KEY 或浏览器语言判断时两处要一起改,check-docs.mjs 会盯着它们不漂移。
|
||||
*/
|
||||
|
||||
export const LOCALE_STORAGE_KEY = 'cch-locale'
|
||||
export const DEFAULT_LOCALE = 'en'
|
||||
|
||||
export function resolveBrowserLocale({ language, languages } = {}) {
|
||||
const preferredLanguage = languages?.[0] || language || ''
|
||||
return /^zh(?:[-_]|$)/i.test(preferredLanguage) ? 'zh' : DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
export function normalizeStoredLocale(value) {
|
||||
return value === 'en' || value === 'zh' ? value : null
|
||||
}
|
||||
@@ -21,10 +27,10 @@ export function normalizeStoredLocale(value) {
|
||||
/**
|
||||
* 根路径该跳去哪;返回 null 表示留在原地(用户选择了中文站)。
|
||||
*/
|
||||
export function resolveRootRedirect({ pathname, stored }) {
|
||||
export function resolveRootRedirect({ pathname, stored, language, languages }) {
|
||||
if (String(pathname ?? '/').replace(/\/+$/, '') !== '') return null
|
||||
|
||||
const locale = normalizeStoredLocale(stored) || DEFAULT_LOCALE
|
||||
const locale = normalizeStoredLocale(stored) || resolveBrowserLocale({ language, languages })
|
||||
return locale === 'en' ? '/en' : null
|
||||
}
|
||||
|
||||
|
||||
+63
-10
@@ -1,7 +1,26 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, it } from 'node:test'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
|
||||
import { DEFAULT_LOCALE, normalizeStoredLocale, resolveRootRedirect } from './locale.js'
|
||||
import { DEFAULT_LOCALE, normalizeStoredLocale, resolveBrowserLocale, resolveRootRedirect } from './locale.js'
|
||||
|
||||
const html = readFileSync(new URL('../../index.html', import.meta.url), 'utf8')
|
||||
const bootstrap = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)]
|
||||
.map((match) => match[1])
|
||||
.find((script) => script.includes('window.location.pathname'))
|
||||
|
||||
function runBootstrap({ pathname = '/', stored = null, languages, language = '' }) {
|
||||
let redirectedTo = null
|
||||
const document = { documentElement: { lang: 'en' } }
|
||||
runInNewContext(bootstrap, {
|
||||
document,
|
||||
localStorage: { getItem: () => stored },
|
||||
navigator: { languages, language },
|
||||
window: { location: { pathname, search: '?from=homepage', hash: '#discover', replace: (url) => { redirectedTo = url } } }
|
||||
})
|
||||
return { redirectedTo, lang: document.documentElement.lang }
|
||||
}
|
||||
|
||||
describe('normalizeStoredLocale', () => {
|
||||
it('只接受 zh / en', () => {
|
||||
@@ -13,12 +32,33 @@ describe('normalizeStoredLocale', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveRootRedirect', () => {
|
||||
it('没有保存偏好时始终默认英文,不受浏览器语言影响', () => {
|
||||
assert.equal(DEFAULT_LOCALE, 'en')
|
||||
for (const languages of [['zh-CN'], ['en-US'], ['ja-JP'], []]) {
|
||||
assert.equal(resolveRootRedirect({ languages, pathname: '/' }), '/en')
|
||||
describe('resolveBrowserLocale', () => {
|
||||
it('浏览器首选语言是中文时选择中文,包括地区与繁简体标记', () => {
|
||||
for (const language of ['zh', 'zh-CN', 'zh-TW', 'zh-Hant', 'ZH-hk']) {
|
||||
assert.equal(resolveBrowserLocale({ languages: [language, 'en-US'] }), 'zh', language)
|
||||
}
|
||||
})
|
||||
|
||||
it('其他语言及缺失的浏览器语言都选择英文', () => {
|
||||
assert.equal(DEFAULT_LOCALE, 'en')
|
||||
for (const language of ['en-US', 'ja-JP', 'fr-FR', '', 'zho']) {
|
||||
assert.equal(resolveBrowserLocale({ languages: [language] }), 'en', language)
|
||||
}
|
||||
assert.equal(resolveBrowserLocale(), 'en')
|
||||
})
|
||||
|
||||
it('优先使用浏览器的语言列表,并在没有列表时回退到 language', () => {
|
||||
assert.equal(resolveBrowserLocale({ languages: ['ja-JP', 'zh-CN'], language: 'zh-CN' }), 'en')
|
||||
assert.equal(resolveBrowserLocale({ languages: [], language: 'zh-CN' }), 'zh')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveRootRedirect', () => {
|
||||
it('没有保存偏好时按浏览器首选语言分流', () => {
|
||||
assert.equal(resolveRootRedirect({ languages: ['zh-CN'], pathname: '/' }), null)
|
||||
assert.equal(resolveRootRedirect({ languages: ['zh-TW'], pathname: '/' }), null)
|
||||
assert.equal(resolveRootRedirect({ languages: ['en-US', 'zh-CN'], pathname: '/' }), '/en')
|
||||
assert.equal(resolveRootRedirect({ languages: ['ja-JP'], pathname: '/' }), '/en')
|
||||
assert.equal(resolveRootRedirect({ pathname: '/' }), '/en')
|
||||
})
|
||||
|
||||
@@ -35,12 +75,25 @@ describe('resolveRootRedirect', () => {
|
||||
})
|
||||
|
||||
it('手动选择中文后保留中文首页,选择英文后进入英文首页', () => {
|
||||
assert.equal(resolveRootRedirect({ pathname: '/', stored: 'zh' }), null)
|
||||
assert.equal(resolveRootRedirect({ pathname: '/', stored: 'en' }), '/en')
|
||||
assert.equal(resolveRootRedirect({ pathname: '/', stored: 'zh', languages: ['ja-JP'] }), null)
|
||||
assert.equal(resolveRootRedirect({ pathname: '/', stored: 'en', languages: ['zh-CN'] }), '/en')
|
||||
})
|
||||
|
||||
it('保存值无效时按英文默认值处理', () => {
|
||||
assert.equal(resolveRootRedirect({ pathname: '/', stored: 'garbage' }), '/en')
|
||||
it('保存值无效时仍按浏览器语言处理', () => {
|
||||
assert.equal(resolveRootRedirect({ pathname: '/', stored: 'garbage', languages: ['zh-CN'] }), null)
|
||||
assert.equal(resolveRootRedirect({ pathname: '/', stored: '' }), '/en')
|
||||
})
|
||||
})
|
||||
|
||||
describe('index.html 首帧语言分流', () => {
|
||||
it('中文浏览器留在中文首页,其他语言带查询和锚点进入英文首页', () => {
|
||||
assert.deepEqual(runBootstrap({ languages: ['zh-CN'] }), { redirectedTo: null, lang: 'zh-CN' })
|
||||
assert.deepEqual(runBootstrap({ languages: ['ja-JP'] }), { redirectedTo: '/en?from=homepage#discover', lang: 'en' })
|
||||
})
|
||||
|
||||
it('手动选择优先于浏览器语言,明确路径不重定向', () => {
|
||||
assert.deepEqual(runBootstrap({ languages: ['zh-CN'], stored: 'en' }), { redirectedTo: '/en?from=homepage#discover', lang: 'en' })
|
||||
assert.deepEqual(runBootstrap({ languages: ['ja-JP'], stored: 'zh' }), { redirectedTo: null, lang: 'zh-CN' })
|
||||
assert.deepEqual(runBootstrap({ pathname: '/start', languages: ['ja-JP'] }), { redirectedTo: null, lang: 'en' })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user