PAYATHON 2026

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。
  • 明确默认语言、缺失键的回退顺序,以及所有回退都失败时的显示方式。

构建检查可以比较各语言的键集合和占位符,发现漏译或参数不一致。这只能检查资源结构,翻译是否准确仍需要人工审校。

切换语言时,先加载资源再提交状态

语言切换可以按以下顺序进行:

  1. 校验目标 locale 是否受支持。
  2. 加载目标语言的公共资源和当前页面资源。
  3. 资源就绪后,更新 i18n 当前语言和应用状态。
  4. 更新页面的 lang,并在需要时更新 dir。
  5. 保存用户语言偏好。

如果用户连续切换语言,应忽略过期请求的结果,防止较早的请求晚返回后覆盖最终选择。缓存资源时,至少要区分 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 配置。

主题和语言应保持独立,避免维护诸如“中文深色版”“英文深色版”这样的组合配置。组件读取主题变量和当前翻译资源即可。

发布前需要检查每个主题是否覆盖了完整变量,以及按钮、输入框、焦点、禁用状态和图表是否仍然清晰可用。多语言还要检查长文案、缺失翻译和文字方向。主题数量代表配置数量,实际可用程度需要通过这些检查确认。

备注:内容仅供参考。