外观
语言
MyGo 应用以使用者的语言来呈现它的消息。 它的消息存放在消息目录中——每种语言一个 JSON 文件—— mygo dev 和 mygo build 会从中写出一个 Go 包,其中每条消息对应一个带类型的函数, 该函数以应用显示的语言说出消息,并带上 该语言的复数形式。整个应用只显示一种语言:它自己的消息、 MyGo 的控件和菜单,以及它的页面(如果有的话)。
json
{
"filesSelected": "{count, plural, =0 {No files selected} one {# file selected} other {# files selected}}"
}go
ui.Text(c, locales.FilesSelected(len(app.selected))) // "3 files selected"底层的 i18n 包使用 Unicode CLDR 的数据格式化数字和日期, 而带有前端的应用还会得到一个 TypeScript 模块,其中包含相同的消息(在页面中)。
消息目录
在配置中开启消息目录,即 mygo.json(或 mygo.config.ts):
json
{
"i18n": { "dir": "locales", "source": "en" }
}dir(默认 locales)为每种语言存放一个消息目录,文件名取自其 BCP 47 标签:en.json、 de.json、pt-BR.json、zh-Hans.json、zh-Hant.json。source (默认 en)是应用编写所用的语言:它的目录包含 每条消息,其他语言的目录回退到它。应用随附的语言就是 dir 中的文件,该目录除 Go 文件外不应存放其他内容; 没有消息目录的应用只随附英文。
消息目录是消息组成的对象,采用 ARB 格式,其键为消息命名。 源语言的目录还会描述这些消息,放在以 @ 开头的键下,供译者和生成的代码使用:
json
{
"@@locale": "en",
"filesSelected": "{count, plural, =0 {No files selected} one {# file selected} other {# files selected}}",
"@filesSelected": {
"description": "The status bar, while files are selected.",
"placeholders": {
"count": { "type": "int", "example": "3" }
}
},
"greeting": "Hello, {name}!"
}json
{
"@@locale": "de",
"filesSelected": "{count, plural, =0 {Keine Dateien ausgewählt} one {# Datei ausgewählt} other {# Dateien ausgewählt}}",
"greeting": "Hallo, {name}!"
}键以一个字母开头,其后是字母、数字和 _;某个键的函数 以它命名。占位符的 type 是 String、int、double、 num、DateTime 或 Object,它决定函数参数的类型(在 Go 中分别为 string、int、float64、float64、time.Time 和 any; 在 TypeScript 中为 string、number、Date 和 unknown)。没有指定 类型时,被 plural、selectordinal 或 number 计数的参数是 int, date 或 time 参数是 DateTime,其余都是 String。 参数的顺序先是 placeholders 中的顺序,然后是消息中出现的顺序。 @@locale 是可选的,若出现则必须与文件的语言一致。
键是扁平的:键中的点号专用于 MyGo 自身的字符串 (mygo.undo)。相关的消息应以共同 的前缀命名,例如 sidebarTitle、sidebarEmpty 和 sidebarNewNote, 这样它们在消息目录和编辑器补全中会排在一起 (locales.Sidebar…)。
消息
消息以 ICU MessageFormat 编写,即 ARB 文件、Android 和 Format.JS 的格式:
| 消息 | 含义 |
|---|---|
Hello, {name}! | 在该位置放入 name 的值 |
{count, plural, one {# file} other {# files}} | 按消息语言中 count 的复数类别选择相应的情形;# 是该数字 |
{count, plural, =0 {No files} one {# file} other {# files}} | =0 针对恰好为 0,优先于各类别 |
{n, plural, offset:1 =0 {Nobody} =1 {{host}} one {{host} and # other} other {{host} and # others}} | 使用 offset:1 时,类别和 # 按 n - 1 计数 |
{place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} | 按序数类别选择情形:1st、22nd、103rd、11th |
{gender, select, female {She} male {He} other {They}} replied. | 选取与值同名的情形,否则用 other |
{price, number}、{ratio, number, percent}、{n, number, integer} | 以该语言习惯书写的数字(1,234.5;1.234,5;12,34,567) |
{price, number, ::.00}、{n, number, ::group-off} | 恰好保留两位小数,或不带千位分隔符的数字 |
{day, date, short}、{day, time, short} | 以某种样式显示的日期或时间:short、medium、long 或 full |
{day, date, ::yMMMd} | 按 CLDR 骨架的字段、以该语言的顺序书写的日期(Jan 5, 2026;5. Jan. 2026) |
情形可以嵌套,每个 plural、selectordinal 和 select 都必须有一个 other 情形。{n} 中的数字按 {n, number} 的方式书写:最多三位小数、分组显示, 并使用该语言的数字;以小数字符串给出的数字(如 "1.50")保留其小数位数。 复数情形按 # 书写该数字后选取,与 ICU 的选择一致: 1.0004 书写为 1,在英文中对应 one。{t} 中的时间 以 {t, date, short} 和 {t, time, short} 合在一起书写。数字和 日期在 Go 中遵循 CLDR 数据,在页面中遵循 Intl,二者一致(见 格式化数字和日期)。对于 ICU 的数字骨架, Go 和页面都识别 percent、integer、小数位精度(.00、.0#) 和 group-off;页面还识别 currency/EUR,而 Go 将其作为普通数字书写。 复数类别取自 Unicode CLDR 中消息语言的定义: 英文有 one 和 other,中文和日文只有 other,俄文有 one、 few、many 和 other,阿拉伯文有六个。只写该语言拥有的类别; other 是必需的。
撇号会转义紧随其后的花括号,或复数情形中的 #:'{' 显示 {,'{name}' 显示 {name},两个撇号显示一个。其他任何 撇号都只是普通撇号,因此 don't 无需转义。
在 Go 中
mygo dev、mygo build 和 mygo generate 会把 catalog_gen.go 写入 消息目录所在的目录。该目录是一个独立的 Go 包(包名取自目录名, 或由其其他 Go 文件声明),其中嵌入了消息目录:
go
import "example.com/notes/locales"
title := locales.FilesSelected(3) // 以应用显示的语言
german := locales.In("de").FilesSelected(3) // 以另一种语言每个键对应一个函数,其参数按占位符带上类型,以应用显示的语言 说出该消息,另有 locales.In(tag) 的方法用于以另一种语言说出。 没有参数的消息只计算一次并缓存,因此再次说出只需一次 map 查找, 不分配任何内存:原生 UI 的视图每帧都会说出它的消息。
该包在初始化时向 i18n 包注册自己的目录,从而使目录中的语言成为 应用的语言:请导入它;如果 Go 代码本身不说出任何消息, 则使用 import _ "example.com/notes/locales"。应用没有这样做时, mygo dev 和 mygo build 会发出警告。
mygo dev 在启动时写出这些函数,并在消息目录每次变更后重新写出, mygo build 则在编译应用之前写出;mygo generate 专门写出它们。 这些是生成的代码:请与消息目录一起提交, 以便 go build 和 go test 在没有 CLI 的情况下也能工作。
语言变化时
原生 UI 的视图每帧重新构建,所以它显示的内容会随语言变化:
go
func (app *notes) view(c *ui.Context) {
ui.Column(c).Gap(8).Children(func() {
ui.Text(c, locales.FilesSelected(len(app.selected)))
if ui.Button(c, locales.NewNote()).Clicked() {
app.add()
}
})
}按其角色标注的菜单项("Undo"、"Quit Notes")会自动采用新语言的标签。 应用一次性设置的内容,例如窗口标题或它自己的菜单项标签, 应在听到变化后重新设置:
go
win := mygo.NewWindow(mygo.WindowOptions{Title: locales.Title(), Content: ui.View(app.view)})
mygo.App.OnLocaleChanged(func(string) {
win.SetTitle(locales.Title())
mygo.App.SetMenu(app.menu()) // 用 locales 的标签重新构建菜单
})应用显示的语言
应用只显示一种语言,由 mygo.App.Locale() 返回:在它编写所用的语言中, 选取使用者最能阅读的那一种——依据使用者偏好的语言,按系统列出的顺序匹配 (App.PreferredLanguages()),例如 zh-TW 的读者会匹配到 zh-Hant; 若无匹配,则用应用的源语言。仅用英文编写的应用对中文读者显示英文, MyGo 的控件和菜单也一样,正如 AppKit 的做法。应用编写所用的语言 就是其消息目录的语言;对于没有消息目录的应用,由 App.SetLanguages 来设定。
在 0.5 之前,无论应用编写所用的语言是什么,App.Locale() 都返回 使用者的第一偏好语言。自行翻译文本、既没有消息目录也不调用 App.SetLanguages 的应用,现在会从中得到 "en":请用 App.SetLanguages 声明应用的语言,或从 App.PreferredLanguages() 读取使用者的偏好。
App.SetLocale("de") 立即切换到另一种语言,并在后续启动中保持; App.SetLocale("") 重新跟随使用者的语言。一个语言菜单, 每种语言用其自身名称显示("Deutsch"、"简体中文"):
go
for _, tag := range mygo.App.Languages() {
items = append(items, &mygo.MenuItem{Label: i18n.Autonym(tag), Type: mygo.MenuItemRadio,
Checked: tag == mygo.App.Locale(),
Click: func(*mygo.MenuItem, *mygo.Window) { mygo.App.SetLocale(tag) }})
}macOS 会把该语言作为应用的语言保存在系统设置中(应用 defaults 里的 AppleLanguages),AppKit 和 WebKit 从下次启动起用它的显示自己的文本; 浏览器把它保存在 localStorage 中;Windows 和 Linux 保存在 App.Path(mygo.PathUserData) 下的 locale 文件里。 App.OnLocaleChanged 监听语言变化——无论是由 SetLocale 改变, 还是使用者的语言改变(macOS、Windows 和浏览器会上报变化; Linux 只在启动时读取一次环境变量 LANGUAGE、LC_ALL、LC_MESSAGES 和 LANG)。
语言变化时,原生 UI 的窗口重新绘制,按角色标注的菜单项显示新语言的标签(见 语言变化时),页面也会收到通知 (在页面中)。应用按语言的书写方向布局, 阿拉伯语和希伯来语从右到左,除非 MYGO_DIRECTION(rtl 或 ltr) 另有指定:原生 UI、页面(mygo.dir)以及 Linux 上 GTK 自身的控件(如菜单) 保持一致。
原生 UI
ui.Context.Locale() 和 ui.Context.Region() 返回应用的语言 和格式;每一帧都会跟随它们的变化。文本按应用的语言绘制, 该语言决定系统文本栈为其选用的字形,例如中日两语分别使用不同的汉字写法。 Element.Lang 设置一个元素及其内部元素文本的语言,如同 HTML 的 lang,Span.Lang 设置 RichText 中某个片段(span)的语言, Font.Lang 设置 Painter 排版文本的语言:
go
ui.Text(c, "「直」").Lang("ja") // 中文应用中的日文引号在不书写汉字的目标语言文本中出现汉字时(例如英文文本中的中文姓名), 保留系统为使用者选用的字形,与浏览器的做法一致。浏览器以页面的语言绘制 原生 UI 的文本,MyGo 使页面语言等于应用语言。
examples/gallery 有一个 Languages 页面:语言菜单、每种语言的数字和 日期、中文控件,以及伪区域设置。
区域格式
日期、时间和数字的书写方式是一项独立的设置: App.Region() 返回使用者格式所依据的区域设置,其语言可能 与应用显示的语言不同;它还包含每周的第一天,以及时间使用 12 小时 还是 24 小时制,并包含使用者在系统设置中做出的更改。 App.OnRegionChanged 监听这些变化。系统没有提供的信息—— 例如缺少数据的 libc,或不透露信息的浏览器——取自 CLDR 中该区域的 周与时间数据(i18n.FirstWeekday、i18n.HourCycle)。
Region.FormatLocale(language) 将两者组合起来,如同 Apple 的 Locale.current:以英式格式阅读的英文是 en-GB-u-fw-mon-hc-h23,以德式格式则是 en-DE-u-fw-mon-hc-h23; 用它格式化,即可按应用的语言、以使用者设定的方式书写日期:
go
formats := mygo.App.Region().FormatLocale(mygo.App.Locale())
label := i18n.FormatDate(formats, due, i18n.DateOptions{Date: i18n.MediumStyle})MyGo 的日期和时间控件就是这样做的:日历 以该区域的第一天开始一周,日期输入框 显示该区域的数字日期,时间输入框 显示其小时制周期,AM 和 PM 使用窗口的语言。
格式化数字和日期
i18n 包书写数字、日期和时间的方式与 ICU 和浏览器的 Intl 一致,对其现代覆盖范围内的区域设置使用 Unicode CLDR 的数据 (104 种语言的 430 个区域设置:en、en-GB、de-CH、 pt-BR、zh-Hant-HK、ar-EG……)。没有某区域设置的数据时, 按最接近的可用数据格式化(zh-TW 按 zh-Hant,en-AU 按 en-001), 否则按英文。
go
i18n.FormatNumber("de", 1234567.891, i18n.NumberOptions{}) // 1.234.567,891
i18n.FormatNumber("hi", 1234567, i18n.NumberOptions{}) // 12,34,567
i18n.FormatNumber("fa", 0.25, i18n.NumberOptions{Percent: true}) // ۲۵٪
i18n.FormatNumber("en", 1.5, i18n.NumberOptions{MinFractionDigits: 2}) // 1.50
i18n.FormatDate("en", t, i18n.DateOptions{Date: i18n.FullStyle}) // Monday, January 5, 2026
i18n.FormatDate("ja", t, i18n.DateOptions{Date: i18n.LongStyle, Time: i18n.ShortStyle}) // 2026年1月5日 13:07
i18n.FormatDate("ru", t, i18n.DateOptions{Skeleton: "yMMMM"}) // январь 2026 г.
i18n.FormatDate("en", t, i18n.DateOptions{Skeleton: "jm", HourCycle: 24}) // 13:07- 数字:该语言的分隔符与分组方式(印度式的 12,34,567;在从五位起分组时,四位数不分隔,西班牙语和波兰语如此)、 其减号和百分号,以及其数字体系的数字(波斯语和埃及阿拉伯语用 印度-阿拉伯数字,孟加拉语用孟加拉数字;
-u-nu-latn请求使用拉丁数字)。 小数位采用远离零方向的四舍五入,与Intl一致, 或采用四舍六入五取偶(HalfEven)。NumberFormatter会保留一个格式化器。 - 日期和时间:按样式(从
FullStyle到ShortStyle,对应dateStyle和timeStyle)或按骨架(要显示的字段,顺序不限:yMMMd、MMMd、Ed、jm):按 ICU 的DateTimePatternGenerator的方式,找出 CLDR 中最适合该区域设置的样式,因此Intl中的{year: "numeric", month: "short", day: "numeric"}与这里的yMMMd书写结果相同。j表示该区域设置小时制下的小时;HourCycle(或区域设置中的-u-hc-h23)将其设为 12 或 24 小时制。 日期使用格里高利历,时间按其time.Location。DateFormatter保留一个格式化器,Pattern给出实际使用的样式。 - 名称:
Months、Weekdays和DayPeriods按某种Width(宽度)给出, 可独立使用(日历的标题,"январь")或用于日期之中("января");Autonym是语言的自称,用于语言菜单 ("Deutsch"、"简体中文"、"British English")。 - 周与钟点:区域设置所在区域(或其语言最可能的区域) 的
FirstWeekday和HourCycle,RegionFor返回两者。
格式化只为结果分配内存;格式化器和各区域设置的数据只创建一次并保留。 数据经过压缩,首次使用时在不到一毫秒内解压。每个应用都会链接数字的格式化代码, 因为 MyGo 自身的字符串要格式化数字,连同其数据约 3 KB; 日期的格式化及其数据,连同语言名称,只有在应用用到它们时才会 链接进来:通过自带消息目录 (NewCatalog,由 mygo generate 写出)、ParseMessage、 格式化器或读取数据的函数。这是一个统一的包,而不是每种语言一个: 使用者的区域格式可能是任意区域设置的,无论应用随附哪些语言。
与 ICU 的差异(i18n/format_icu_test.go 列出了测试跳过的每个情形): 除 UTC 以外的时区按 GMT 偏移书写 (GMT+8、GMT+05:30),而 ICU 对某些时区有专名("China Standard Time"); 日历始终使用格里高利历,而 ICU 对泰语或波斯语默认使用其他历法; 时间在 CLDR 使用窄不换行空格处书写普通空格,与 V8 一致; 少数 ICU 78(CLDR 48.0)缺失或数据较旧的地区设置(海地克里奥尔语、 带多声调符号的希腊语)遵循 CLDR 48.2。
MyGo 自身的字符串
MyGo 的控件、代码编辑器、差异查看器、富文本编辑器和 Markdown 视图、终端插件、文档对话框、页面中 alert、confirm 和 prompt 的按钮、文件对话框的标题,以及它的菜单,都有自己的文案: "Undo"、"Find"、"Next month"、"Do you want to save the changes made to the document “Notes”?"、"Quit Notes",以及辅助技术读取的名称 ("hours"、"Saturation and brightness")。它们以应用显示的语言说出: 在原生 UI 中,即窗口的语言(ui.Context.Locale)。只随附英文的应用 对中文读者以英文显示这些文案,正如 AppKit 的做法。以相同语言 再次说出某条文案只需一次 map 查找,不分配任何内存, 因此控件可以每帧说出自己的文案。
英文始终会被链接。其他语言的翻译——目前为日语(ja)、 简体中文(zh-Hans)和繁体中文(zh-Hant)——是各自独立的包, 这样应用只携带它随附语言的翻译。mygo generate 为应用消息目录 写出的代码会按应用的语言导入它们:带有 zh-TW.json 的应用会获得 MyGo 的 zh-Hant。没有消息目录、用 App.SetLanguages 设置语言的应用需自行导入:
go
import _ "github.com/egoist/mygo/locales/zh-Hans"如果应用随附了 MyGo 已翻译的语言,却没有导入对应的翻译包, 应用启动时会收到提示,并以英文显示 MyGo 的字符串。 应用可以通过消息目录中的 mygo.<key> 键修改 MyGo 的字符串, 或为其提供其他语言的翻译:
json
{
"mygo.undo": "Rückgängig",
"mygo.menuQuitApp": "{app} beenden"
}这些键、它们的消息和它们的参数构成 MyGo 的消息目录, internal/l10n/en.json。 一条字符串的解析方式与应用自身的字符串相同:先按应用的语言,然后是其 回退的语言,再是应用的源语言,最后是英文。原生 UI 的 inspector 是开发者的工具,始终保持英文。
为 MyGo 添加语言
MyGo 自身的字符串位于 internal/l10n/en.json,每个翻译是 locales/<tag>/<tag>.json。要把 MyGo 翻译成一种语言,请添加 locales/<tag>/<tag>.json,包含 en.json 的每个键(其描述 说明了每条文案出现的位置),运行 go generate ./internal/l10n,它会写出 该翻译的包,并把该语言加入 mygo generate 为应用链接的语言列表, 然后运行 go test ./internal/l10n,它会像 mygo generate 检查应用消息目录那样检查:每种语言的每条消息,都带有 源语言的占位符和该语言的复数情形。 mygo.appName 和 mygo.appDescription 是应用名称和 描述在某种语言下的写法,打包时会将它们写入系统列出应用的 位置(见构建与分发)。
选取哪条消息
一条消息取自目录语言中最适合所请求语言的那一个:zh-Hant 对应 zh-TW, pt-PT 对应 pt-AO,en-GB 对应 en-AU。当该语言缺少这条消息时, 沿 CLDR 的父区域设置回退(en-AU、en-001、en;zh-Hant-HK、zh-Hant), 最后回退到源语言:缺失的翻译显示源语言的消息, 而不是什么都不显示。它的复数情形遵循消息书写所用语言的规则; 数字和日期则遵循所请求语言的约定。
检查消息目录
mygo generate 和 mygo build 会检查消息目录,mygo i18n check 也是如此。 错误会使它们停止:
- 文件没有按规范标签命名,或其
@@locale与之不符; - 无法解析的消息;
- 翻译中存在而源目录中没有的键,或 MyGo 没有对应消息的
mygo.<key>键; - 翻译中存在而源消息中没有的参数,或以其类型无法支持的方式 使用的参数(被
plural计数的String); - 两个键的函数会同名。
警告不会使它们停止:翻译缺少的消息(会显示源语言的);翻译遗漏的参数; 该语言没有的复数情形(英文中的 few)或缺少的复数情形(俄文中的 few,会以 other 代替);消息未使用的占位符。
试用某种语言
mygo dev -lang de 以德语运行应用,但不改变它保存的语言。 在翻译尚不存在时,两个伪区域设置可用来测试布局, 如同 Android 的做法:
en-XA给源语言消息的字母加上重音,使其长度增加三分之一,并加上 括号:[Šéļéçţ ƒîļéš one two]:保持原样的文本没有放进消息目录, 被截断的文本预留空间太小,由多个括号组成的句子是由无法 重新排序的片段拼成的。ar-XB将每个词从右到左显示,并把应用整体从右到左布局: 没有做镜像翻转的内容就会暴露出来。
在页面中
带前端的应用在其页面中说出相同的消息。除了 TypeScript 客户端之外,CLI 还会写一个 以消息目录所在目录命名的模块,例如 src/locales.ts,其中每个键对应一个带类型的函数;没有前端的应用不会得到它。
ts
import { languages, messages, messagesIn } from "./locales";
heading.textContent = messages.filesSelected(3); // 以页面显示的语言
messagesIn("de").greeting("Ada"); // "Hallo, Ada!"该模块包含每种语言的消息,mygo-runtime/i18n 用浏览器的 Intl 格式化它们,因此页面和 Go 以相同的复数形式说出相同的文字。 消息随前端一起打包而非远程获取:首次绘制就是正确的语言, 无需等待请求,在浏览器中打开的开发服务器页面也是如此。 messages 以当前的语言说出每条消息: 即应用的语言——MyGo 在页面脚本运行前就把它交给页面——若没有则用 浏览器的语言。
mygo.locale 和 mygo.dir 在页面脚本运行前就已存在。 语言变化时,页面会收到 mygo:locale 事件 (mygo-runtime 的 onLocaleChange):请重新绘制页面。之后加载的页面 以新语言开始。webview 也会采用该语言来显示 自己的文本:WebView2 在其环境创建时设定并从此保持(变化从下次启动起 生效),WebKitGTK 立即生效,WKWebView 从下次启动生效。
mygo-runtime/i18n 的 formatMessage(locale, message, args) 可格式化你自己的消息。
i18n 包
生成的代码构建在 i18n 包之上,应用也可以 直接使用它:NewCatalog 从任意 fs.FS 读取消息目录,ParseMessage 和 Message.Format 格式化一条消息,PluralCategory 和 OrdinalCategory 给出一个数字的 CLDR 复数类别——数字可写成 整数、浮点数,或小数位数计入的十进制字符串("1.50" 有两位),Parent 给出一个区域设置回退到的区域设置,以及 数字和日期的格式化。其数据来自 CLDR,见 i18n.CLDRVersion。
CLDR 数据是生成的:go generate ./i18n 读取 i18n/internal/gen 中固定版本的 cldr-json(-cache <dir> 从某目录读取 tarball), node scripts/plurals-golden.ts 和 node scripts/format-golden.ts 记录 ICU 的复数类别及其数字、日期、 周数据和语言名称,供测试比对。MyGo 自身字符串的函数、 其翻译列表以及 locales/ 中对应的包,由 go generate ./internal/l10n 生成。