国际化(i18n)

SillyTavern 支持多种语言。本指南介绍如何添加和管理翻译。

你来到这里,很可能是因为某种语言的某段文字尚未翻译,把你折腾得够呛。首先,我会演示自己是如何修复繁体中文(Chinese (Traditional))区域设置下几处缺失翻译的。每一处缺失的原因都不同,因此你能借此很好地了解如何修复自己遇到的缺失翻译。

在第二部分,我们将了解:

  • i18n 在 SillyTavern 中是如何运作的
  • 如何编写翻译以及使用它们的代码,
  • 用于查找缺失翻译的调试函数,
  • 如何添加一种新语言
  • 以及如何提交你的修改

如果你正在开发扩展或修改核心代码,请在编写 HTML 和 JavaScript 时就把 i18n 考虑在内。这样你的成果就能方便地被他人翻译成自己的语言。

没人能独自掌握 15 种语言。我们携手合作,让 SillyTavern 惠及每一个人。

世界上的每一个人,都应当能在手机和电脑上使用自己的语言。


我们来修复一些缺失的翻译吧!

Generate Image

文本“Generate Image”在繁体中文(Chinese (Traditional))区域设置下未翻译。为什么?

generate-image-pre.png
generate-image-pre.png

右键点击该元素并检查它,你会看到如下 HTML:

<!--rendered HTML-->
<div class="list-group-item flex-container flexGap5 interactable" id="sd_gen" tabindex="0">
    <div data-i18n="[title]Trigger Stable Diffusion" title="觸發 Stable Diffusion"
         class="fa-solid fa-paintbrush extensionsMenuExtensionButton"></div>
    <span>Generate Image</span>
</div>

它的 data-i18n 属性在哪里?根本就没有!我们来加上它。在源码中找到这一段:

<!--public/scripts/extensions/stable-diffusion/button.html-->
<div id="sd_gen" class="list-group-item flex-container flexGap5">
    <div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion"
         data-i18n="[title]Trigger Stable Diffusion"></div>
    <span>Generate Image</span>
</div>
<div id="sd_stop_gen" class="list-group-item flex-container flexGap5">
    <div class="fa-solid fa-circle-stop extensionsMenuExtensionButton" title="Abort current image generation task"
         data-i18n="[title]Abort current image generation task"></div>
    <span>Stop Image Generation</span>
</div>

我们运气不错,字符串 Generate Image 在很多语言文件中都存在,包括繁体中文(Chinese (Traditional))。

generate-image-lang.png
generate-image-lang.png
{
    "Generate Image": "生成图片"
}

那为什么没显示出来?我们必须把元素正确地接上:

<!--public/scripts/extensions/stable-diffusion/button.html-->
<div id="sd_gen" class="list-group-item flex-container flexGap5">
    <div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion"
         data-i18n="[title]Trigger Stable Diffusion"></div>
    <span data-i18n="Generate Image">Generate Image</span>
</div>
<div id="sd_stop_gen" class="list-group-item flex-container flexGap5">
    <div class="fa-solid fa-circle-stop extensionsMenuExtensionButton" title="Abort current image generation task"
         data-i18n="[title]Abort current image generation task"></div>
    <span>Stop Image Generation</span>
</div>

现在它工作了!重新加载页面看看。

generate-image-post.png
generate-image-post.png

不过既然 HTML 已经打开了,它正下方的 Stop Image Generation 又是怎么回事?这段 HTML 看起来不太对劲。

如果我们生成一张图片,并在生成过程中打开魔法棒菜单(wand menu),就会看到未翻译的文本。

stop-generating-image-pre.png
stop-generating-image-pre.png

先修复 HTML:

<!--public/scripts/extensions/stable-diffusion/button.html-->
<div id="sd_gen" class="list-group-item flex-container flexGap5">
    <div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion"
         data-i18n="[title]Trigger Stable Diffusion"></div>
    <span data-i18n="Generate Image">Generate Image</span>
</div>
<div id="sd_stop_gen" class="list-group-item flex-container flexGap5">
    <div class="fa-solid fa-circle-stop extensionsMenuExtensionButton" title="Abort current image generation task"
         data-i18n="[title]Abort current image generation task"></div>
    <span data-i18n="Stop Image Generation">Stop Image Generation</span>
</div>

仅此一项还不足以解决问题。繁体中文(Chinese (Traditional))文件中没有“Stop Image Generation”的翻译。我们可以添加它!这是一种可能的译法:

{
    "Stop Image Generation": "停止生成图片"
}

……我们可以把它加在“Generate Image”翻译之后的 JSON 文件里。

{
    "Generate Image": "生成图片",
    "Stop Image Generation": "停止生成图片"
}

在与 Claude 讨论之后,我们最终采用以下译法:

  • 繁体中文(Traditional Chinese):"Stop Image Generation": "終止圖片生成"
  • 简体中文(Simplified Chinese):"Stop Image Generation": "中止图像生成"
  • 日语(Japanese):"Stop Image Generation": "画像生成を停止"
stop-generating-post-2.png
stop-generating-post-2.png

Generate Caption

“Generate Caption”在繁体中文(Chinese (Traditional))区域设置下未翻译。我们来修复它!

generate-image-post.png
generate-image-post.png

它在哪里?检查该元素。

<!--rendered HTML-->
<div id="send_picture" class="list-group-item flex-container flexGap5 interactable" tabindex="0">
    <div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
    Generate Caption
</div>

原来这段 HTML 是由 JavaScript 生成的。我们去找源码。

// public/scripts/extensions/caption/index.js
const sendButton = $(`
        <div id="send_picture" class="list-group-item flex-container flexGap5">
            <div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
            Generate Caption
        </div>`);

我们得先修复这段代码:

// public/scripts/extensions/caption/index.js
const sendButton = $(`
        <div id="send_picture" class="list-group-item flex-container flexGap5">
            <div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
            <span data-i18n="Generate Caption">Generate Caption</span>
        </div>`);

繁体中文(Chinese (Traditional))文件中也没有“Generate Caption”的翻译。我们来添加它!

{
    "Generate Caption": "生成圖片說明"
}

我们将采用以下译法:

  • 繁体中文(Traditional Chinese):"Generate Caption": "生成圖片說明"
  • 简体中文(Simplified Chinese):"Generate Caption": "生成图片说明"
  • 日语(Japanese):"Generate Caption": "画像説明を生成"
generate-caption-post.png
generate-caption-post.png

Inspect Prompts

文本“Inspect Prompts”在繁体中文(Chinese (Traditional))区域设置下未翻译。为什么?

这个稍微复杂一些。文本由 JavaScript 生成,且翻译缺失。

// Extension-PromptInspector/index.js
const enabledText = 'Stop Inspecting';
const disabledText = 'Inspect Prompts';

真没想到……这两个短语都没出现在 i18n 文件里。我们来添加它们。

{
    "Stop Inspecting": "停止檢查",
    "Inspect Prompts": "檢查提示"
}

现在我们得修复 JavaScript 代码。它必须使用 t 函数来获取翻译。

// Extension-PromptInspector/index.js
import {t} from '../../../i18n.js';

const enabledText = t`Stop Inspecting`;
const disabledText = t`Inspect Prompts`;

我们从 Claude 得到了这些建议。保留字符串、忽略代码即可。它们需要被添加到 JSON 文件中。

// 1. Simplified Chinese (zh-cn):
const enabledText = t`停止检查`;
const disabledText = t`检查提示词`;
// 2. Traditional Chinese (zh-tw):
const enabledText = t`停止檢查`;
const disabledText = t`檢查提示詞`;
// 3. Japanese (ja-jp):
const enabledText = t`検査を停止`;
const disabledText = t`プロンプトを検査`;

我们会把这些合并进 JSON 文件。

{
    "Stop Inspecting": "停止检查",
    "Inspect Prompts": "检查提示词"
}
{
    "Stop Inspecting": "停止檢查",
    "Inspect Prompts": "檢查提示詞"
}
{
    "Stop Inspecting": "検査を停止",
    "Inspect Prompts": "プロンプトを検査"
}
toggle-prompt-inspection-post-tt.png
toggle-prompt-inspection-post-tt.png

那个 tooltip(提示框)有点遗憾。问题在于代码没有使用 t 函数。

launchButton.title = 'Toggle prompt inspection';

我们需要在扩展代码中修复它。

launchButton.title = t`Toggle prompt inspection`;

我们还需要把这些翻译添加到 JSON 文件中。

{
    "Toggle prompt inspection": "切换提示检查"
}
{
    "Toggle prompt inspection": "切换提示词检查"
}
{
    "Toggle prompt inspection": "プロンプト検査の切り替え"
}

提示词检查器(Prompt Inspector)是一个独立的扩展,因此我们会在该仓库提交代码修复的 Pull Request:https://github.com/SillyTavern/Extension-PromptInspector/pull/1

翻译则会添加到主 SillyTavern 仓库。https://github.com/SillyTavern/SillyTavern/pull/3198

start-inspecting-post.png
start-inspecting-post.png

语言文件

每种语言在 public/locales/ 下都有一个以其语言代码命名的 JSON 文件(例如 ru-ru.json)。

该文件包含若干键值对,其中:

  • 键可以是原始英文文本,也可以是唯一标识符
  • 值是翻译后的文本

示例:

{
    "Save": "Сохранить",
    "Cancel": "Отмена",
    "Could not find proxy with name '${0}'": "Не удалось найти прокси с названием '${0}'"
}

翻译是如何运作的

应用中有两种使用翻译的方式:

  1. HTML 元素:使用 data-i18n 属性

    <div data-i18n="some_key">Default Text</div>

    如果有可用翻译,HTML 中的默认文本将被替换为翻译后的文本。

  2. 模板字符串:在 JavaScript 代码中使用 t 函数

    t`Some text with ${variable}`

    翻译这些字符串时,应保持 ${0}${1} 等占位符不变。

SillyTavern 使用带 data-i18n 属性的 HTML 元素来标记可翻译内容。其用法有以下几种:

1. 翻译元素文本

对于简单的文本内容:

<span data-i18n="Role:">Role:</span>
{
    "Role:": "Роль:"
}

这会把元素的文本内容替换为“Role:”的翻译。

2. 翻译属性

要翻译诸如 title(标题)或 placeholder(占位符)之类的属性:

<a class="menu_button fa-chain fa-solid fa-fw"
   title="Insert prompt"
   data-i18n="[title]Insert prompt"></a>
{
    "Insert prompt": "Вставить промпт"
}

[title] 前缀指明了要翻译哪个属性。属性值的其余部分将作为在 JSON 文件中查找的键。程序员通常用英文文本作为键,但这并非必须,键可以是任意唯一标识符。

不过,原始英文文本必须出现在相应属性中(title="Insert prompt")。它会在翻译缺失时作为回退使用。尤其要注意的是,并不存在英语的翻译文件。

下面是使用唯一标识符 no_items_text 作为键(而非英文文本)的示例:

<!--suppress HtmlUnknownAttribute -->
<div class="openai_logit_bias_list" no_items_text="No items"
     data-i18n="[no_items_text]openai_logit_bias_no_items"></div>
{
    "openai_logit_bias_no_items": "没有相关产品"
}

3. 一个元素的多重翻译

有些元素同时需要内容和属性的翻译,二者以分号分隔。最常见的模式是同时翻译元素的文本内容及其 title 属性:

<div data-source="openrouter" class="menu_button menu_button_icon openrouter_authorize"
     title="Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai"
     data-i18n="Authorize;[title]Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai">
    Authorize
</div>
{
    "Authorize": "Авторизоваться",
    "Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai": "Получите свой OpenRouter API токен используя OAuth. У вас будет открыта вкладка openrouter.ai"
}

这里翻译了:

  • 元素的文本内容,使用键“Authorize”
  • title 属性,使用键“Get your OpenRouter API token using OAuth flow. You will be redirected to openrouter.ai”

注意,title 属性和元素的文本内容都以英文提供,作为回退。

你也可以翻译多个属性:

<!--suppress HtmlUnknownAttribute -->
<textarea id="send_textarea" name="text" class="mdHotkeys"
          data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help"
          placeholder="Not connected to API!"
          no_connection_text="Not connected to API!"
          connected_text="Type a message, or /? for help"></textarea>

你的语言文件中对应的翻译会是这样:

{
    "Not connected to API!": "Нет соединения с API!",
    "Type a message, or /? for help": "Введите сообщение или /? для помощи"
}

当页面加载时,系统会:

  1. 找到所有带 data-i18n 属性的元素
  2. 解析类似 [title][placeholder] 的属性前缀
  3. 在你的语言 JSON 文件中查找每个键
  4. 用翻译后的文本替换元素的内容或属性

动态文本

对于 JavaScript 代码中的动态文本,翻译使用以下两种方式之一:

  1. 配合 t 函数的模板字面量:
    toastr.warn(t`Tag ${tagName} not found.`);
  2. 直接调用翻译函数:
    translate("Some text", "optional_key")

变量占位符

有些字符串包含使用 ${0}${1} 等形式的动态值占位符:

toastr.error(t`Could not find proxy with name '${presetName}'`);
{
    "Could not find proxy with name '${0}'": "Не удалось найти прокси с названием '${0}'"
}

键和翻译中的占位符要保持一致。系统会用 presetName 等变量的值替换 ${0}

查找缺失的翻译

假设你不止想修复某一条烦人的缺失翻译,而是想把它们全都找出来。

这是个远大的志向!即便只修复一条翻译也很有价值。但若你想把它们一网打尽,就需要借助工具。

SillyTavern-i18n

https://github.com/SillyTavern/SillyTavern-i18n

用于处理前端本地化文件的工具。

功能:

  • 自动从 HTML 文件中提取并添加待翻译的新键。
  • 从本地化文件中清理(prune)已失效的键。
  • 使用 Google 自动翻译自动填充缺失的值。
  • 按键对 JSON 文件排序。

内置调试函数

它们位于 用户设置(User Settings) > 调试菜单(Debug Menu) 下。

获取缺失的翻译(Get missing translations)

检测当前区域设置中缺失的本地化数据,并将其转储到浏览器控制台。如果当前区域设置是英语,则会搜索所有其他区域设置。

控制台会显示一张缺失翻译的表格,包含:

  • key:需要翻译的文本或标识符
  • language:你当前的语言代码
  • value:待翻译的英文文本

应用区域设置(Apply locale)

将当前选定的区域设置重新应用到页面。

添加新语言

要添加对一种新语言的支持:

  1. 将你的语言添加到 public/locales/lang.json

    {
      "lang": "xx-xx",
      "display": "Language Name (English Name)"
    }
  2. 创建 public/locales/xx-xx.json 并填入你的翻译

贡献

当你的翻译准备就绪后:

  1. 验证你的 JSON 文件格式正确
  2. 在应用中进行充分测试
  3. 通过 GitHub pull request 提交