feat(app-expo): tiered large-text presets with English-friendly default

Replace the boolean large-text flag with three global typography tiers,
defaulting new installs to the smallest tier when English is in effect
while preserving legacy storage and Chinese defaults. Add a profile
sub-screen to pick the tier and unit tests for storage resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-18 16:47:29 +08:00
parent 8f6c2a6a34
commit 897f49f2ab
13 changed files with 663 additions and 344 deletions

View File

@@ -1,3 +1,4 @@
import { getLocales } from 'expo-localization';
import { Platform } from 'react-native';
import {
@@ -6,7 +7,11 @@ import {
setSecureItem,
} from '@/core/storage/secure';
import { supportedLanguages, type AppLanguage } from '@/i18n/resources';
import {
fallbackLanguage,
supportedLanguages,
type AppLanguage,
} from '@/i18n/resources';
import { THEME_NAMES, type ThemeName } from '@/constants/theme-bridge';
@@ -18,6 +23,7 @@ import {
const KEY_LANGUAGE = 'app_settings_language';
const KEY_LARGE_TEXT = 'app_settings_large_text';
const KEY_LARGE_TEXT_LEVEL = 'app_settings_large_text_level';
const KEY_DARK_MODE = 'app_settings_dark_mode';
const KEY_THEME_NAME = 'app_settings_theme_name';
const KEY_TTS_SPEAK_DEFAULT = 'app_settings_tts_speak_default';
@@ -62,14 +68,73 @@ export async function clearAppLanguage(): Promise<void> {
await deleteStored(KEY_LANGUAGE);
}
export async function getLargeText(): Promise<boolean> {
const v = await getStored(KEY_LARGE_TEXT);
if (v == null || v === '') return true;
return v === 'true';
export type LargeTextLevel = 0 | 1 | 2;
function parseStoredLargeTextLevel(raw: string | null): LargeTextLevel | null {
if (raw === '0' || raw === '1' || raw === '2') {
return Number(raw) as LargeTextLevel;
}
return null;
}
export async function setLargeText(value: boolean): Promise<void> {
await setStored(KEY_LARGE_TEXT, value ? 'true' : 'false');
function deviceLanguageFromLocales(): AppLanguage {
const locale = getLocales()[0];
const tag = locale?.languageCode ?? locale?.languageTag;
if (!tag) return fallbackLanguage;
return tag.toLowerCase().startsWith('zh') ? 'zh' : 'en';
}
async function effectiveAppLanguage(): Promise<AppLanguage> {
const override = await getAppLanguage();
return override ?? deviceLanguageFromLocales();
}
/** 无存储时的默认档位:英文关大字、中文沿用偏大默认 */
export function defaultLargeTextLevelForLanguage(lang: AppLanguage): LargeTextLevel {
return lang === 'en' ? 0 : 1;
}
/**
* 纯函数:由原始存储值与当前生效语言解析档位(供单测与实现共用)。
*/
export function computeLargeTextLevelFromStorage(
levelRaw: string | null,
legacyLargeTextRaw: string | null,
effectiveLanguage: AppLanguage,
): LargeTextLevel {
const parsed = parseStoredLargeTextLevel(levelRaw);
if (parsed !== null) return parsed;
if (legacyLargeTextRaw === 'true') return 1;
if (legacyLargeTextRaw === 'false') return 0;
return defaultLargeTextLevelForLanguage(effectiveLanguage);
}
export async function getLargeTextLevel(): Promise<LargeTextLevel> {
const [levelRaw, legacyRaw, lang] = await Promise.all([
getStored(KEY_LARGE_TEXT_LEVEL),
getStored(KEY_LARGE_TEXT),
effectiveAppLanguage(),
]);
const level = computeLargeTextLevelFromStorage(levelRaw, legacyRaw, lang);
const hasValidLevel = parseStoredLargeTextLevel(levelRaw) !== null;
if (hasValidLevel && legacyRaw != null && legacyRaw !== '') {
await deleteStored(KEY_LARGE_TEXT);
} else if (
!hasValidLevel &&
legacyRaw != null &&
legacyRaw !== ''
) {
await setStored(KEY_LARGE_TEXT_LEVEL, String(level));
await deleteStored(KEY_LARGE_TEXT);
}
return level;
}
export async function setLargeTextLevel(level: LargeTextLevel): Promise<void> {
await setStored(KEY_LARGE_TEXT_LEVEL, String(level));
await deleteStored(KEY_LARGE_TEXT);
}
export async function getDarkMode(): Promise<boolean> {