SaaS 应用如何实现多语言支持和 33 种主题的动态切换?
主题和语言可以分开管理:用统一的 CSS variables 控制主题,由 i18n 层加载对应的翻译资源。组件使用语义化颜色和翻译键,切换配置时更新应用状态,无需重新加载页面。
只要各主题遵循相同的变量约定,33 种主题就可以复用同一套组件。某个 boilerplate 是否完整支持这些能力,还需要检查其实现和资源覆盖情况,不能仅凭描述确认。
用 CSS variables 实现主题切换
先定义组件需要的语义变量,例如页面背景、正文颜色和主按钮颜色。尽量避免让组件直接依赖某个主题的具体色值。
:root,
:root[data-theme="light"] {
color-scheme: light;
--color-bg: #ffffff;
--color-text: #172033;
--color-primary: #1d4ed8;
--color-on-primary: #ffffff;
}
:root[data-theme="dark"] {
color-scheme: dark;
--color-bg: #111827;
--color-text: #f9fafb;
--color-primary: #93c5fd;
--color-on-primary: #111827;
}
:root[data-theme="forest"] {
color-scheme: light;
--color-bg: #f0fdf4;
--color-text: #14532d;
--color-primary: #166534;
--color-on-primary: #ffffff;
}
body {
background: var(--color-bg);
color: var(--color-text);
}
.primary-button {
background: var(--color-primary);
color: var(--color-on-primary);
}
修改根元素的 data-theme 后,使用这些变量的组件就会更新样式。示例展示了三个主题,其余主题可以按相同约定补充。
如果所有主题的 CSS 已随页面加载,切换时就不需要额外请求。如果主题样式按需加载,应先等待对应样式可用,再更新 data-theme,避免短暂出现缺失样式。
保存主题偏好,并校验读取结果
主题 ID 通常属于低敏感度的偏好数据,可以保存在 localStorage 中。浏览器存储可能不可用,其中的数据也可以被修改,因此需要处理读取异常,并用允许列表校验主题 ID。
以下示例在浏览器环境中运行:
const allowedThemes = new Set(['light', 'dark', 'forest']);
const themeStorageKey = 'app.theme';
const defaultTheme = 'light';
function readTheme() {
try {
const stored = localStorage.getItem(themeStorageKey);
return allowedThemes.has(stored) ? stored : defaultTheme;
} catch {
return defaultTheme;
}
}
function applyTheme(theme) {
if (!allowedThemes.has(theme)) {
return false;
}
document.documentElement.dataset.theme = theme;
return true;
}
function selectTheme(theme) {
if (!applyTheme(theme)) {
return false;
}
try {
localStorage.setItem(themeStorageKey, theme);
} catch {
// 无法持久化时,本次页面中的主题切换仍然有效。
}
return true;
}
applyTheme(readTheme());
// 在主题选择控件的事件处理函数中调用:
// selectTheme('dark');
在 React/Vue 中,可以将主题状态放在应用级 Context、composable 或 store 中,让主题选择控件调用统一的更新函数。尽量保持单一状态来源,避免组件状态和 DOM 属性分别更新后不同步。
如果用户需要跨设备同步,可以通过已认证的接口将主题 ID 保存到账号设置中。服务端同样要校验允许的主题值,并依据登录身份确定修改对象。localStorage 适合保存主题偏好,不应用于保存密码等敏感信息;给主题值加密也不能替代接口权限校验。
按语言和业务模块组织翻译文件
随着页面增加,可以按 locale 和 namespace 拆分资源:
locales/
en/
common.json
dashboard.json
billing.json
zh-CN/
common.json
dashboard.json
billing.json
例如,两个语言版本的 common.json 使用相同的键:
{
"actions": {
"save": "Save",
"cancel": "Cancel"
},
"preferences": {
"theme": "Theme",
"language": "Language"
}
}
{
"actions": {
"save": "保存",
"cancel": "取消"
},
"preferences": {
"theme": "主题",
"language": "语言"
}
}
组件通过翻译键访问文案。具体调用形式取决于项目使用的 i18n 实现,应在接入层统一约定。
组织翻译资源时可以注意以下几点:
common只放跨页面复用的文案,业务文案放在对应模块,避免公共文件不断膨胀。- 使用含义稳定的键,例如
actions.save,避免把容易变动的整段文案作为键。 - 使用完整句子和占位符,避免拼接多个翻译片段;不同语言的词序可能不同。
- 数量变化交给支持对应语言复数规则的 i18n 实现处理,不要只判断
count === 1。 - 明确默认语言、缺失键的回退顺序,以及所有回退都失败时的显示方式。
构建检查可以比较各语言的键集合和占位符,发现漏译或参数不一致。这只能检查资源结构,翻译是否准确仍需要人工审校。
切换语言时,先加载资源再提交状态
语言切换可以按以下顺序进行:
- 校验目标 locale 是否受支持。
- 加载目标语言的公共资源和当前页面资源。
- 资源就绪后,更新 i18n 当前语言和应用状态。
- 更新页面的
lang,并在需要时更新dir。 - 保存用户语言偏好。
如果用户连续切换语言,应忽略过期请求的结果,防止较早的请求晚返回后覆盖最终选择。缓存资源时,至少要区分 locale 和 namespace;资源发布后还需要相应的缓存更新策略。
日期、数字和货币格式可以使用 Intl,并传入当前 locale:
const locale = 'zh-CN';
const formattedNumber = new Intl.NumberFormat(locale).format(12345.67);
const formattedAmount = new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'CNY'
}).format(12345.67);
语言和业务货币应分别配置。用户选择中文界面,账单货币不一定就是 CNY。
处理首次渲染和主题覆盖范围
纯客户端应用可以在主应用渲染前读取主题偏好,减少默认主题闪现。使用 SSR 时,服务端无法读取 localStorage,可以通过账号设置或 cookie 提供首次渲染所需的偏好,并让客户端初始状态保持一致。如果使用提前执行的初始化脚本,也需要符合站点的 CSP 配置。
主题和语言应保持独立,避免维护诸如“中文深色版”“英文深色版”这样的组合配置。组件读取主题变量和当前翻译资源即可。
发布前需要检查每个主题是否覆盖了完整变量,以及按钮、输入框、焦点、禁用状态和图表是否仍然清晰可用。多语言还要检查长文案、缺失翻译和文字方向。主题数量代表配置数量,实际可用程度需要通过这些检查确认。
备注:内容仅供参考。
