From e162da6dbe4af8d56ef71a42192e354cdc548a38 Mon Sep 17 00:00:00 2001 From: jubnl Date: Sat, 25 Jul 2026 16:19:15 +0200 Subject: [PATCH] fix(server): resolve zh-TW default language, close lint gap on bracket env access --- server/eslint.config.mjs | 7 +++++++ server/src/app-config/derive.ts | 6 +++++- server/src/app-config/env.schema.ts | 3 ++- server/src/nest/app-config/tokens.ts | 7 +++++++ server/tests/unit/app-config/derive.test.ts | 6 +++++- server/tests/unit/app-config/validate.test.ts | 1 + 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs index 5b76c7f8f..fa253e46a 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -72,6 +72,13 @@ export default tseslint.config( message: 'Read configuration via src/app-config (readEnv()/derive/tokens), not process.env. Exemptions: eslint.config.mjs + src/app-config/README.md.', }, + { + // Bracket-notation variant (process['env']) — property is a Literal + // node (.value), so the selector above can't match it. + selector: "MemberExpression[object.name='process'][property.value='env']", + message: + 'Read configuration via src/app-config (readEnv()/derive/tokens), not process.env. Exemptions: eslint.config.mjs + src/app-config/README.md.', + }, ], }, }, diff --git a/server/src/app-config/derive.ts b/server/src/app-config/derive.ts index 2c6bae182..753dfc562 100644 --- a/server/src/app-config/derive.ts +++ b/server/src/app-config/derive.ts @@ -54,7 +54,11 @@ export function deriveApp(raw: RawEnv) { function resolveDefaultLanguage(raw: string | undefined): string { const lang = raw?.toLowerCase() || 'en'; - return SUPPORTED_LANGUAGE_CODES.includes(lang) ? lang : 'en'; + // Case-insensitive match resolved to the CANONICAL code (zh-TW, not zh-tw). + // The legacy lowercase-only `includes` could never match zh-TW and silently + // fell back to English for a supported language — deliberate fix. + const canonical = SUPPORTED_LANGUAGE_CODES.find((c) => c.toLowerCase() === lang); + return canonical ?? 'en'; } export function deriveHttp(raw: RawEnv) { diff --git a/server/src/app-config/env.schema.ts b/server/src/app-config/env.schema.ts index fd458dd86..51c8569b6 100644 --- a/server/src/app-config/env.schema.ts +++ b/server/src/app-config/env.schema.ts @@ -62,7 +62,8 @@ export const envSchema = z.object({ APP_VERSION: anyString, APP_URL: url, ALLOWED_ORIGINS: anyString, - DEFAULT_LANGUAGE: oneOf([...SUPPORTED_LANGUAGE_CODES]), + // Candidates lowercased so mixed-case codes (zh-TW) validate case-insensitively. + DEFAULT_LANGUAGE: oneOf(SUPPORTED_LANGUAGE_CODES.map((c) => c.toLowerCase())), // Security / session ENCRYPTION_KEY: anyString, diff --git a/server/src/nest/app-config/tokens.ts b/server/src/nest/app-config/tokens.ts index cf93991e6..2d1adda9e 100644 --- a/server/src/nest/app-config/tokens.ts +++ b/server/src/nest/app-config/tokens.ts @@ -10,6 +10,13 @@ * * Consumption pattern: * constructor(@Inject(httpConfig.KEY) private readonly http: ConfigType) {} + * + * NOTE: today only httpConfig has a DI consumer (bootstrap.ts threads it into + * the pre-init middleware); the migrated call sites otherwise read through + * readEnv() because they live in classes whose unit tests instantiate them + * directly. The full token set is provisioned deliberately — it is the + * sanctioned pattern for NEW Nest consumers of boot-stable config, not dead + * scaffolding to prune. */ import { registerAs } from '@nestjs/config'; import { diff --git a/server/tests/unit/app-config/derive.test.ts b/server/tests/unit/app-config/derive.test.ts index 66371031b..c669de46e 100644 --- a/server/tests/unit/app-config/derive.test.ts +++ b/server/tests/unit/app-config/derive.test.ts @@ -39,11 +39,15 @@ describe('deriveApp', () => { expect(deriveApp({ APP_URL: 'https://x.example/' }).appUrl).toBe('https://x.example/'); }); - it('DEFAULT_LANGUAGE resolves: lowercased, validated, en fallback (config.ts semantics)', () => { + it('DEFAULT_LANGUAGE resolves: case-insensitive, canonical code, en fallback', () => { expect(deriveApp({ DEFAULT_LANGUAGE: 'EN' }).defaultLanguage).toBe('en'); expect(deriveApp({ DEFAULT_LANGUAGE: 'de' }).defaultLanguage).toBe('de'); expect(deriveApp({ DEFAULT_LANGUAGE: 'klingon' }).defaultLanguage).toBe('en'); expect(deriveApp({}).defaultLanguage).toBe('en'); + // Mixed-case canonical codes resolve to their canonical form (legacy could + // never match zh-TW and silently served English). + expect(deriveApp({ DEFAULT_LANGUAGE: 'zh-TW' }).defaultLanguage).toBe('zh-TW'); + expect(deriveApp({ DEFAULT_LANGUAGE: 'zh-tw' }).defaultLanguage).toBe('zh-TW'); }); }); diff --git a/server/tests/unit/app-config/validate.test.ts b/server/tests/unit/app-config/validate.test.ts index fb169fb6d..11ffa90c4 100644 --- a/server/tests/unit/app-config/validate.test.ts +++ b/server/tests/unit/app-config/validate.test.ts @@ -89,6 +89,7 @@ describe('validateEnvAtBoot', () => { ['TREK_API_DOCS_ENABLED', ' TRUE '], ['TREK_PLUGINS_ENABLED', ' OFF '], ['DEFAULT_LANGUAGE', 'EN'], + ['DEFAULT_LANGUAGE', 'zh-TW'], ['LOG_LEVEL', 'ERROR'], ['SESSION_DURATION', ' 12H '], ])('accepts differently-cased/padded valid %s=%s', (key, value) => {