feat: release v0.26 portable i18n and presentation adapters

This commit is contained in:
2026-09-29 13:31:42 +08:00
parent 6a95cad184
commit d1a747c36f
75 changed files with 5681 additions and 52 deletions

180
i18n/README.md Normal file
View File

@@ -0,0 +1,180 @@
# 通用国际化
本功能从 `v0.26.0-preview` 提供。`i18n` 可以单独用于 CLI、后台任务、通知或 Web 服务,不依赖 App、HTTP、错误目录、数据库或前端框架。业务文案、启用语言和用户偏好由接入项目维护。
模块没有全局当前语言、全局可变词典或自动文件扫描。词典构建后只读,支持多个实例、多个命名空间和并发调用。完整可运行示例见 [examples/i18n](../examples/i18n/README.md),生成契约见 [i18n.api](../examples/contracts/i18n.api)。
## 独立使用
```go
bundle, err := i18n.New(i18n.Config{
SourceLocale: "en", DefaultLocale: "en",
Locales: []string{"en", "zh-Hans"},
},
i18n.Document{Version: 1, Namespace: "common", Locale: "en",
Messages: map[string]i18n.Message{
"greeting": {
Params: map[string]i18n.Kind{"name": i18n.String},
Value: i18n.Pattern{Text: "Hello, {name}"},
},
}},
i18n.Document{Version: 1, Namespace: "common", Locale: "zh-Hans",
Messages: map[string]i18n.Message{
"greeting": {Value: i18n.Pattern{Text: "你好,{name}"}},
}},
)
if err != nil { return err }
result, err := bundle.Render("zh-CN", i18n.Ref{
Namespace: "common", Key: "greeting",
}, map[string]any{"name": "Alex"})
// result.Text == "你好,Alex"; result.Locale == "zh-Hans"
```
`Render` 显式接受语言。也可以用 `WithLocale(ctx, locale)` 和 `RenderContext`,语言只属于该上下文。任务消费者需由项目显式取得接收者语言;Go context 不会自动跨进程或写入队列。
`Result` 提供文本、匹配后的请求语言、实际词典语言和是否发生回退。缺键、缺参、类型错误、渲染限额返回 `*Diagnostic`;核心不决定 HTTP 状态,也不把任意错误自动转换成业务错误。诊断没有参数值、内部原因或原始文案。
## 语言选择与回退
- `SourceLocale` 是定义消息和参数的语言,可以只用作最终回退而不对用户开放。
- `DefaultLocale` 是无合适偏好时的选择,必须在 `Locales` 中。
- `Locales` 是有序的启用语言。`Match` 使用 `x/text/language` 的 BCP 47 匹配,返回真实配置的词典标签;不会用匹配器生成的带扩展标签直接索引文件。
- `Fallbacks` 是显式、有序、无环的关系。先查选中语言,再深度优先查配置的回退;然后查默认语言及其回退,最后查源语言,过程中去重。不会从文件名或地域字符串猜一条词典继承链。
- 非法请求偏好忽略并使用可用偏好或默认语言;非法配置、重复规范化语言、回退循环在 `New` 时失败。
运行时允许部分语言缺词,`Check` 单独列出各启用语言缺少的直接翻译。CI 可因此失败,在线请求仍使用回退。存在但格式损坏的译文在构建时拒绝;替换词典应先构建新实例,成功后由项目替换实例。
## 文案资源 v1
```json
{
"version": 1,
"namespace": "notifications",
"locale": "en",
"messages": {
"unread": {
"params": {"count": "integer"},
"value": {
"plural": "count",
"cases": {
"=0": {"text": "No unread messages"},
"one": {"text": "{count} unread message"},
"other": {"text": "{count} unread messages"}
}
}
}
}
}
```
同一命名空间的同一键只有一份源定义。译文省略 `params` 时继承源定义;重复声明时必须完全一致。参数是消息级契约:各语言使用相同参数集合,分支可以不使用其中某个参数,但整条消息的所有分支合起来必须使用全部参数。
`value` 支持:
- `text`:命名插值 `{name}`,`{{`、`}}` 表示字面花括号;空字符串是合法译文。
- `literal: true`:文本原样保留,适用于已有固定消息。插入参数不会再次解析,也不会自动查词典。
- `select`:根据 string/boolean 参数选择命名分支,必须有 `other`。
- `plural`:根据 integer/number 参数选择基数复数分支,必须有 `other`;先匹配 `=0` 等精确 int64 分支,再匹配 `zero/one/two/few/many/other`。可与 select 嵌套。
复数规则固定为 [Unicode CLDR JSON 46.1.0](https://github.com/unicode-org/cldr-json/blob/46.1.0/cldr-json/cldr-core/supplemental/plurals.json)。Go 和浏览器使用同一份规则,导出包携带实际语言及回退语言的规则,避免浏览器自带规则版本不同。数据来源和许可见 [cldr](cldr/README.md)。规则版本与文案格式版本分别记录。
参数类型:
| 类型 | Go 调用 | 浏览器调用 |
| --- | --- | --- |
| `string` | 字符串 | string |
| `boolean` | bool | boolean |
| `integer` | int/int32/int64 等整数,或精确的 json.Number;范围为 int64 | 安全整数 number、bigint 或十进制整数字符串;超范围拒绝 |
| `number` | 有限 float32/float64 或 json.Number | 有限 number |
Number 按规范化数值处理:`1.0` 与 `1` 等价,负零显示为 `0`,不保留输入小数位数。整数不会经过 float64;不接受任意对象、Stringer、nil、自动字符串转数值。日期、货币、精度敏感的小数和字段标签可由项目显式格式化为字符串参数,时区与币种不会从语言推断。
v1 的范围是普通文本、命名参数、select 和基数复数,不宣称兼容完整 ICU MessageFormat;不含 ordinal、紧凑数字格式、HTML 执行或任意表达式。复数计算使用普通数值,CLDR 的 compact-number `c/e` 操作数为 0。字段文案可以先显式 Render 后再作为参数;用户输入和业务数据保持原样。
## 加载、覆盖与资源限制
`LoadFS(fs.FS, paths...)` 读取明确列出的 JSON 文件,支持 `embed.FS` 和 `os.DirFS`。`DecodeJSON` 可用于业务自己取得的内存数据,拒绝重复键、未知字段、无效 UTF-8、超深结构。它们不展开 `${...}`,不执行项目代码,不自动拉远程词典。文件系统实际访问范围由传入的 FS 决定。
项目已有配置加载器可加载 `i18n.Config` 的语言设置,词典正文用本包加载;避免配置加载器把词典里的 `${...}` 解释成环境变量。数据库或远程词典由项目读取为 Document,再调用 New。
同一语言、命名空间可拆成多个文件;重复键默认报错。`Overlay(base, override)` 明确替换完整消息并返回独立副本,再传给 New 校验;不隐式合并分支或参数。项目决定覆盖的先后顺序。
主要限额:每个 JSON 文件 1 MiB、LoadFS 总量 4 MiB、最多 1024 个文档、64 个资源语言、128 个命名空间、10000 条消息、每条 64 个参数、单文本/字符串参数 64 KiB、模式深度 24、渲染结果 1 MiB。导出的单语言包最多 4 MiB,可按命名空间拆分。
`Config()`、`Documents()` 和 `Schema()` 返回独立副本;`Config.Validate()` 可预检语言配置。运行时没有修改词典的方法。
## 接入 go-omni 错误与 HTTP
```go
// 项目自己的 Catalog,既有 status/code/message/params 继续维护在这里。
catalog = errori18n.Bind(catalog, "accountErrors")
sourceDocuments, err := errori18n.Source(catalog, "en")
// 把 sourceDocuments 与项目加载的译文一起传入 i18n.New。
// Bind 不修改输入;已有显式 translation 引用保持。
languagePolicy, err := httpi18n.New(bundle)
localizer := errori18n.New(bundle, nil) // 第二参数可提供并发安全的诊断回调。
if err := localizer.Validate(catalog); err != nil { return err }
app := omni.New(
omni.WithErrors(catalog),
omni.WithLanguagePolicy(languagePolicy),
omni.WithErrorLocalizer(localizer),
// 继续传入项目自己的 WithResponse / WithResponseEncoder。
)
```
也可在单项 Definition 或 `.api errorCatalog` 条目明确声明:
```json
"translation": {"namespace":"accountErrors", "key":"username.invalid"}
```
翻译引用与逻辑错误标识、对外业务码分别独立。不同目录可以为同名错误配置不同引用;多个错误引用同一源消息时,Source 会拒绝不一致的源文案或参数。Go 源目录通过 Source 导出,保留原固定文案的字面花括号,不包含状态、业务码、example 或 cause。
HTTP 策略在应用中间件之前选语言,默认读取有界 `Accept-Language`,保留请求上下文中显式设置的语言优先级。`httpi18n.WithResolver` 可替换语言来源,并显式声明缓存 Vary 所需的头;无头可声明的个性化来源自动标记 private。HTTP 策略不会读取用户数据库、JWT 或 Cookie 中的业务字段,也不自动设置可能与逐词回退不符的 Content-Language。独立标准 HTTP 可以用 `Policy.Wrap`。
自定义 Resolver panic 时按无偏好处理,继续使用显式上下文语言或默认语言,避免偏好查询故障中断业务请求。项目可在自己的 Resolver 中记录诊断。
错误适配器先使用原有规则解析业务错误,只替换展示文案;缺资源、适配失败保持原文,参数调用错误仍遵循原有 INTERNAL_ERROR 处理。没有 translation 引用的条目保持原行为。标准响应的平铺字段、嵌套 `${message}`、RawJSON 错误、旧 Messages 校验、路由独立目录、认证提前返回和缓冲克隆均保留原语义。
`response.Result.Message` 继续是契约原文;新增 `MessageOverride` 和 `PublicMessage()`。内置 Formatter 使用最终公开文案。自定义 Encoder 需要翻译时显式使用 `PublicMessage()`;固定回执继续由 Encoder 决定。`c.JSON/Text/XML/Bytes` 等直接输出不自动重写。
失败快照保存当次展示文案。幂等重放保留首次响应和失败元数据,不重新翻译,不把语言加入业务幂等键。`Localizer.Localize(ctx, registry, resolvedError)` 也可供非 HTTP 入口使用;返回值只包含文本和是否应用,不修改传入错误。
生成器识别 translation 元数据并写入 Catalog、路由目录和 OpenAPI;已有无 translation 的契约不变。新 metadata 的远程 runtime 下限为 `v0.26.0-preview`,CLI 和框架使用同版;开发框架时可以显式使用本地 checkout。生成到不具备能力的旧依赖会在写入前失败。
## CLI 提取、检查与导出
下面的命令在框架根目录可以直接运行;安装后的 CLI 把 `go run ./cmd/go-omni` 换成 `go-omni`。
```sh
go run ./cmd/go-omni i18n check --config examples/i18n/config.json \
--resource examples/i18n/locales/ui.en.json \
--resource examples/i18n/locales/ui.zh-Hans.json
go run ./cmd/go-omni i18n export --config examples/i18n/config.json \
--resource examples/i18n/locales/ui.en.json \
--resource examples/i18n/locales/ui.zh-Hans.json \
--locale zh-Hans --namespace ui
go run ./cmd/go-omni i18n types --config examples/i18n/config.json \
--resource examples/i18n/locales/ui.en.json \
--resource examples/i18n/locales/ui.zh-Hans.json --namespace ui
go run ./cmd/go-omni i18n extract --go examples/i18n/main.go
```
- `check` 检查资源和直接翻译遗漏,非空诊断退出非零;可重复提供 `--go` 检查静态引用。
- `extract --go` 识别 Go 中 `i18n.Ref` 的字符串字面量及 import 别名;动态引用、非字面量 Render 调用和 dot import 会报告,不能声称完成任意 Go/JS/模板的静态分析。提取命令输出发现结果,check 把未解决动态引用视为诊断。
- `extract --catalog exported-errors.json --namespace errors --locale en` 读取已有错误目录导出物,生成单个源语言 Document,不执行项目 Go。目录可以由已有 workspace Go Catalog 导出流程产生。
- `export` 显式选择命名空间,输出一个选中语言及全部回退资源的 Pack,不输出业务错误元数据。可重复 `--namespace`。
- `types` 输出按 namespace/key 分层的 TypeScript 参数类型及 Translate 类型,不强制 Go 命名风格,不把 int64 简单降成 number。
这些命令只读输入并输出 stdout,没有自动覆盖业务文件、扫描其他项目、翻译服务调用或安装前端依赖。`Bundle.Export`、`TypeScript` 提供相同的库入口。
## 前端
见 [browser](browser/README.md)。运行时是普通 ES module,带 TypeScript 声明,可按项目需要复制进前端或通过 `browseri18n.Files()` 提供静态文件。语言包按需加载、先验证再提交;连续切换只有最新选择能生效,失败保留已提交语言。没有 DOM、localStorage、表单或业务请求的隐式操作。
升级本模块时必须一起验证资源格式、规则版本、前后端一致性、错误兼容和生成消费者。现有项目不启用这两个适配选项时,响应与错误行为保持原样。

39
i18n/browser/README.md Normal file
View File

@@ -0,0 +1,39 @@
# 浏览器运行时
`runtime.mjs` 是可独立导入的 ES module,`runtime.d.mts` 提供类型。需要支持 ES modules、BigInt、TextEncoder 和 structuredClone 的现代浏览器;Node.js 22 用于本仓库验证。复数规则来自导出包,与 Go 共用固定 CLDR 数据,不调用浏览器自己的 Intl.PluralRules。语言选择使用包中 `locales` 的精确标识,不由浏览器再次改写别名或推断回退链。
```js
import { createLanguageController, fetchPack } from './runtime.mjs';
const urls = { en: '/locales/en.json', 'zh-Hans': '/locales/zh-Hans.json' };
const language = createLanguageController(
await fetchPack(urls.en),
locale => fetchPack(urls[locale])
);
await language.setLanguage('zh-Hans');
const text = language.current.t('ui', 'greeting', { name: 'Alex' });
```
`current.render` 额外返回实际语言和 fallback 标记。`setLanguage` 成功提交返回 true;被更新选择取代返回 false;最新加载失败抛错并保留 current。甚至“切回当前语言”也会取消旧请求的提交资格。未知语言、错误语言包、参数 schema 或启用语言集合变化都不能提交。
`fetchPack` 检查 HTTP 状态和 4 MiB 限额,支持 AbortSignal 和自定义 fetch。加载资源、鉴权、缓存策略与 URL 映射由调用者决定。它只返回 JSON 数据,`createLocalizer` / controller 负责完整资源检查。
`overlayPack(pack, documents)` 显式覆盖已导出键的完整消息并重新校验,不修改输入,不允许改变参数契约。例如替换品牌标题;覆写源消息时保留原 params。业务可在 loader 返回之前应用 overrides。
```ts
import type { Translate } from './messages.generated';
const t: Translate = language.current.t;
// t('ui', 'greeting', { name: 'Alex' })
```
应用需要在切换成功后重新取得 current.t。视图使用 textContent 或框架的文本绑定,动态参数不递归解析、不作为 HTML 执行。金额格式化、RTL 布局、表单、路由、业务提交和偏好持久化属于应用;运行时不重建页面或重新发业务请求。
完整表单示例:[examples/i18n](../../examples/i18n/README.md)。验证:
```sh
node --test i18n/browser/runtime.test.mjs
go test ./i18n -run TestBrowserConformance
# PATH 中提供 tsc 时,验证导出类型与实际 runtime 声明及错误参数诊断。
go test ./i18n -run TestTypeScriptConsumer -v
```

13
i18n/browser/assets.go Normal file
View File

@@ -0,0 +1,13 @@
// Package browseri18n embeds the optional framework-neutral browser module.
// Frontend projects can also copy/import runtime.mjs directly.
package browseri18n
import (
"embed"
"io/fs"
)
//go:embed runtime.mjs runtime.d.mts
var files embed.FS
func Files() fs.FS { return files }

View File

@@ -0,0 +1,6 @@
import { readFileSync } from "node:fs";
import assert from "node:assert/strict";
import { createLocalizer } from "./runtime.mjs";
const { pack, cases } = JSON.parse(readFileSync(0, "utf8"));
const localizer = createLocalizer(pack);
for (const sample of cases) assert.equal(localizer.t("common", sample.key, sample.params), sample.want);

View File

@@ -0,0 +1,29 @@
export type Kind = "string" | "boolean" | "integer" | "number";
export interface Pattern { text?: string; literal?: boolean; select?: string; plural?: string; cases?: Record<string, Pattern>; }
export interface Message { params?: Record<string, Kind>; value: Pattern; }
export interface Document { version: 1; namespace: string; locale: string; messages: Record<string, Message>; }
export interface Configuration { sourceLocale: string; defaultLocale: string; locales: string[]; fallbacks?: Record<string, string[]>; }
export interface Pack {
version: 1;
locale: string;
config: Configuration;
documents: Document[];
ruleVersion: "cldr-46.1";
pluralRules: Record<string, Partial<Record<"zero" | "one" | "two" | "few" | "many", string>>>;
license?: string;
}
export interface Rendered { text: string; locale: string; requested: string; fallback: boolean; }
export interface Localizer {
readonly locale: string;
readonly locales: readonly string[];
render(namespace: string, key: string, params?: Record<string, unknown>): Rendered;
t(namespace: string, key: string, params?: Record<string, unknown>): string;
}
export class MessageError extends Error { readonly kind: string; constructor(kind: string, problem: string); }
export function createLocalizer(pack: Pack): Localizer;
export function overlayPack(pack: Pack, overrides: Document[]): Pack;
export function createLanguageController(initialPack: Pack, loadPack: (locale: string) => Promise<Pack>): {
readonly current: Localizer;
setLanguage(locale: string): Promise<boolean>;
};
export function fetchPack(url: string | URL, options?: { signal?: AbortSignal; fetch?: typeof globalThis.fetch }): Promise<Pack>;

305
i18n/browser/runtime.mjs Normal file
View File

@@ -0,0 +1,305 @@
// Framework-neutral runtime for go-omni message resource version 1.
// No globals, DOM changes, persistence, HTML insertion, or business requests.
const owns = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
const kinds = new Set(["string", "boolean", "integer", "number"]);
const pluralCases = new Set(["zero", "one", "two", "few", "many", "other"]);
const integer = /^-?(0|[1-9][0-9]*)$/;
const paramName = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
const encoder = new TextEncoder();
const contractKey = Symbol("messageContract");
const bytes = text => encoder.encode(text).length;
const object = v => v !== null && typeof v === "object" && !Array.isArray(v);
const id = v => typeof v === "string" && v.length > 0 && bytes(v) <= 256 && v.trim() === v && !/[\u0000-\u001f\u007f-\u009f]/u.test(v);
const fail = (kind, problem) => { throw new MessageError(kind, problem); };
const fields = (value, allowed) => {
if (!object(value) || Object.keys(value).some(key => !allowed.includes(key))) fail("resource", "invalid resource fields");
};
const localeTag = value => {
// Tags are canonicalized by the Go exporter. Keep their identity verbatim:
// host Intl versions can remap aliases (e.g. cmn) or reject private-use tags.
if (typeof value !== "string" || bytes(value) > 128 || value === "und" ||
!/^(?:[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*|[xX](?:-[A-Za-z0-9]{1,8})+)$/.test(value)) fail("locale", "invalid exported locale identifier");
return value;
};
export class MessageError extends Error {
constructor(kind, problem) { super(`i18n: ${problem}`); this.name = "MessageError"; this.kind = kind; }
}
function scalar(kind, value) {
if (kind === "string" && typeof value === "string" && bytes(value) <= 65536) return value;
if (kind === "boolean" && typeof value === "boolean") return String(value);
if (kind === "number" && typeof value === "number" && Number.isFinite(value)) return String(value);
if (kind === "integer") {
let n;
if (typeof value === "bigint") n = value;
else if (typeof value === "number" && Number.isSafeInteger(value)) n = BigInt(value);
else if (typeof value === "string" && value.length <= 20 && integer.test(value)) n = BigInt(value);
else fail("parameter", "expected an exact integer");
if (n >= -(1n << 63n) && n < (1n << 63n)) return String(n);
}
fail("parameter", "wrong argument type, range or size");
}
function compile(pattern, schema, used, budget, depth = 0) {
fields(pattern, ["text", "literal", "select", "plural", "cases"]);
const text = pattern.text ?? "";
if (typeof text !== "string" || (pattern.literal !== undefined && typeof pattern.literal !== "boolean")) fail("pattern", "invalid text or literal flag");
budget.nodes++; budget.bytes += bytes(text);
if (depth > 24 || budget.nodes > 100000 || budget.bytes > 4194304 || bytes(text) > 65536) fail("resource", "pattern exceeds limits");
const select = pattern.select ?? "", plural = pattern.plural ?? "";
if (typeof select !== "string" || typeof plural !== "string") fail("pattern", "invalid branch parameter");
if (select || plural) {
if ((select && plural) || text || pattern.literal || !object(pattern.cases)) fail("pattern", "invalid branch");
const entries = Object.entries(pattern.cases), key = select || plural;
if (!owns(pattern.cases, "other") || entries.length > 128) fail("pattern", "branch requires other and at most 128 cases");
const kind = schema.get(key);
if (plural ? kind !== "integer" && kind !== "number" : kind !== "string" && kind !== "boolean") fail("parameter", "branch parameter type mismatch");
used.add(key);
const cases = new Map();
for (const [name, child] of entries) {
if (!id(name)) fail("pattern", "invalid case name");
if (plural && !pluralCases.has(name)) {
if (!name.startsWith("=") || !integer.test(name.slice(1)) || scalar("integer", name.slice(1)) !== name.slice(1)) fail("pattern", "invalid plural case");
}
cases.set(name, compile(child, schema, used, budget, depth + 1));
}
return { select, plural, cases };
}
if (pattern.cases !== undefined) fail("pattern", "cases require a branch");
if (pattern.literal) return { parts: [{ text }] };
const parts = [];
let literal = "";
const flush = () => { if (literal) parts.push({ text: literal }); literal = ""; };
for (let i = 0; i < text.length;) {
const ch = text[i];
if (ch !== "{" && ch !== "}") { literal += ch; i++; continue; }
if (text[i + 1] === ch) { literal += ch; i += 2; continue; }
if (ch === "}") fail("pattern", "unescaped closing brace");
const end = text.indexOf("}", i + 1);
if (end < 0) fail("pattern", "unclosed placeholder");
const key = text.slice(i + 1, end);
if (!schema.has(key)) fail("parameter", "undeclared placeholder");
used.add(key); flush(); parts.push({ param: key }); i = end + 1;
}
flush(); return { parts };
}
function pluralOperands(text) {
const [mantissa, exponent = "0"] = text.replace(/^-/, "").split("e");
let [whole, fraction = ""] = mantissa.split(".");
const digits = whole + fraction, point = whole.length + Number(exponent);
if (point <= 0) { whole = "0"; fraction = "0".repeat(-point) + digits; }
else if (point >= digits.length) { whole = digits + "0".repeat(point - digits.length); fraction = ""; }
else { whole = digits.slice(0, point); fraction = digits.slice(point); }
const tail = fraction.replace(/0+$/, ""), i = BigInt(whole || "0"), f = BigInt(fraction || "0");
return { n: i, i, v: BigInt(fraction.length), w: BigInt(tail.length), f, t: BigInt(tail || "0"), c: 0n, e: 0n, fractional: f !== 0n };
}
function compilePluralRules(rules) {
if (!object(rules) || Object.keys(rules).some(k => !["zero", "one", "two", "few", "many"].includes(k))) fail("resource", "invalid plural rules");
const compiled = new Map();
for (const [category, text] of Object.entries(rules)) {
if (typeof text !== "string" || text.length > 2048) fail("resource", "invalid plural rule size");
compiled.set(category, text.split(" or ").map(or => or.split(" and ").map(and => {
const match = /^([nivwftec])(?: % ([1-9][0-9]*))? (!=|=) ([0-9]+(?:\.\.[0-9]+)?(?:,\s*[0-9]+(?:\.\.[0-9]+)?)*)$/.exec(and.trim());
if (!match) fail("resource", "invalid CLDR plural condition");
const ranges = match[4].split(",").map(raw => { const [lo, hi = lo] = raw.trim().split(".."); const range = [BigInt(lo), BigInt(hi)]; if (range[0] > range[1]) fail("resource", "reversed plural range"); return range; });
return { operand: match[1], mod: match[2] ? BigInt(match[2]) : undefined, negate: match[3] === "!=", ranges };
})));
}
return text => {
const values = pluralOperands(text);
for (const category of ["zero", "one", "two", "few", "many"]) {
const rule = compiled.get(category);
if (rule?.some(group => group.every(condition => {
let value = values[condition.operand]; if (condition.mod) value %= condition.mod;
let found = (condition.operand !== "n" || !values.fractional) && condition.ranges.some(([lo, hi]) => value >= lo && value <= hi);
return condition.negate ? !found : found;
}))) return category;
}
return "other";
};
}
function render(pattern, locale, values, pluralRules) {
if (pattern.cases) {
let key = values.get(pattern.select);
if (pattern.plural) {
const value = values.get(pattern.plural);
key = `=${value}`;
if (!pattern.cases.has(key)) key = pluralRules.get(locale)(value);
}
return render(pattern.cases.get(key) ?? pattern.cases.get("other"), locale, values, pluralRules);
}
let text = "", size = 0;
for (const part of pattern.parts) {
const value = part.param === undefined ? part.text : values.get(part.param);
size += bytes(value); if (size > 1048576) fail("resource", "rendered message exceeds 1 MiB");
text += value;
}
return text;
}
// Compiles a detached pack for one locale. Arguments are never recursively
// interpreted as messages or HTML. Applications insert output as text.
export function createLocalizer(pack) {
fields(pack, ["version", "locale", "config", "documents", "ruleVersion", "pluralRules", "license"]);
if (pack.version !== 1 || !Array.isArray(pack.documents) || pack.documents.length > 1024) fail("resource", "unsupported pack version or size");
const c = pack.config;
fields(c, ["sourceLocale", "defaultLocale", "locales", "fallbacks"]);
const source = localeTag(c.sourceLocale), defaultLocale = localeTag(c.defaultLocale), locale = localeTag(pack.locale);
if (!Array.isArray(c.locales) || c.locales.length < 1 || c.locales.length > 64) fail("locale", "invalid enabled locales");
const locales = c.locales.map(localeTag), enabled = new Set(locales);
if (enabled.size !== locales.length || ![defaultLocale, locale].every(l => enabled.has(l))) fail("locale", "duplicate or disabled locale");
const fallback = new Map();
if (c.fallbacks !== undefined && !object(c.fallbacks)) fail("locale", "invalid fallback configuration");
for (const [raw, list] of Object.entries(c.fallbacks ?? {})) {
const tag = localeTag(raw);
if (!enabled.has(tag) || fallback.has(tag) || !Array.isArray(list) || list.length > 64) fail("locale", "invalid fallback configuration");
const targets = list.map(localeTag);
if (new Set(targets).size !== targets.length || targets.some(l => !enabled.has(l))) fail("locale", "invalid fallback targets");
fallback.set(tag, targets);
}
const state = new Map();
const visit = tag => { if (state.get(tag) === 1) fail("locale", "fallback cycle"); if (state.get(tag) === 2) return; state.set(tag, 1); for (const to of fallback.get(tag) ?? []) visit(to); state.set(tag, 2); };
for (const l of locales) visit(l);
const chain = [], seen = new Set();
const add = l => { if (seen.has(l)) return; seen.add(l); chain.push(l); for (const to of fallback.get(l) ?? []) add(to); };
add(locale); add(defaultLocale); add(source);
if (pack.ruleVersion !== "cldr-46.1" || !object(pack.pluralRules) || Object.keys(pack.pluralRules).length > 65) fail("resource", "unsupported plural rule version");
const pluralRules = new Map();
for (const tag of chain) {
if (!owns(pack.pluralRules, tag)) fail("resource", "missing plural rules for fallback language");
pluralRules.set(tag, compilePluralRules(pack.pluralRules[tag]));
}
const namespaces = new Map(), documents = [], all = new Map();
let count = 0;
for (const doc of pack.documents) {
fields(doc, ["version", "namespace", "locale", "messages"]);
if (doc.version !== 1 || !id(doc.namespace) || !object(doc.messages)) fail("resource", "invalid document");
const tag = localeTag(doc.locale);
if (!enabled.has(tag) && tag !== source) fail("locale", "pack contains a disabled locale");
if (!all.has(tag)) all.set(tag, new Map());
const byNamespace = all.get(tag);
if (!byNamespace.has(doc.namespace)) byNamespace.set(doc.namespace, new Set());
for (const key of Object.keys(doc.messages)) {
if (!id(key) || byNamespace.get(doc.namespace).has(key)) fail("resource", "invalid or duplicate message");
byNamespace.get(doc.namespace).add(key);
if (++count > 10000) fail("resource", "too many messages");
}
documents.push({ namespace: doc.namespace, tag, messages: doc.messages });
}
for (const doc of documents.filter(d => d.tag === source)) {
if (!namespaces.has(doc.namespace)) namespaces.set(doc.namespace, new Map());
const messages = namespaces.get(doc.namespace);
for (const [key, message] of Object.entries(doc.messages)) {
fields(message, ["params", "value"]);
if (!object(message.value) || message.params !== undefined && !object(message.params)) fail("resource", "message requires value and valid params");
const schema = new Map(Object.entries(message.params ?? {}));
if (schema.size > 64 || [...schema].some(([k, v]) => !paramName.test(k) || bytes(k) > 128 || !kinds.has(v))) fail("parameter", "invalid parameter schema");
messages.set(key, { schema, patterns: new Map() });
}
}
if (namespaces.size > 128) fail("resource", "too many namespaces");
const budget = { nodes: 0, bytes: 0 };
for (const doc of documents) {
for (const [key, message] of Object.entries(doc.messages)) {
fields(message, ["params", "value"]);
const entry = namespaces.get(doc.namespace)?.get(key);
if (!entry) fail("missing", "translation has no source message");
if (message.params !== undefined && (!object(message.params) || Object.keys(message.params).length !== entry.schema.size || Object.entries(message.params).some(([k, v]) => entry.schema.get(k) !== v))) fail("parameter", "translation schema differs from source");
const used = new Set();
entry.patterns.set(doc.tag, compile(message.value, entry.schema, used, budget));
if ([...entry.schema.keys()].some(k => !used.has(k))) fail("parameter", "declared parameter is unused");
}
}
const translate = (namespace, key, params = {}) => {
const entry = namespaces.get(namespace)?.get(key);
if (!entry) fail("missing", "unknown message");
if (!object(params)) fail("parameter", "expected parameter object");
const values = new Map();
for (const [name, kind] of entry.schema) {
if (!owns(params, name)) fail("parameter", "missing argument");
values.set(name, scalar(kind, params[name]));
}
if (Object.keys(params).some(k => !entry.schema.has(k))) fail("parameter", "undeclared argument");
for (const tag of chain) {
const pattern = entry.patterns.get(tag);
if (pattern) return { text: render(pattern, tag, values, pluralRules), locale: tag, requested: locale, fallback: tag !== locale };
}
fail("missing", "no message in fallback chain");
};
const contract = JSON.stringify([pack.ruleVersion, source, defaultLocale, locales, [...fallback].sort(), [...namespaces].sort().map(([ns, messages]) => [ns, [...messages].sort().map(([key, entry]) => [key, [...entry.schema].sort()])])]);
return Object.freeze({ [contractKey]: contract, locale, locales: Object.freeze([...locales]), render: translate, t: (ns, key, params) => translate(ns, key, params).text });
}
// Only a fully loaded and validated pack can replace the current language.
// Last selection wins. A failed latest request keeps the committed language;
// older requests cannot commit after it, even when they later succeed.
export function createLanguageController(initialPack, loadPack) {
if (typeof loadPack !== "function") fail("resource", "loader required");
let current = createLocalizer(initialPack), sequence = 0;
const enabled = [...current.locales];
return Object.freeze({
get current() { return current; },
async setLanguage(raw) {
const ticket = ++sequence, locale = localeTag(raw);
if (!enabled.includes(locale)) fail("locale", "locale is not enabled");
if (locale === current.locale) return true;
let pack;
try { pack = await loadPack(locale); }
catch (error) { if (ticket !== sequence) return false; throw error; }
if (ticket !== sequence) return false;
const next = createLocalizer(pack);
if (next.locale !== locale || next[contractKey] !== current[contractKey]) fail("locale", "loaded pack does not match requested language/configuration");
current = next;
return true;
}
});
}
// Explicit whole-message overrides. Originals stay untouched; unknown keys and
// changed parameter schemas are rejected by validation before the pack is used.
export function overlayPack(pack, overrides) {
createLocalizer(pack);
if (!Array.isArray(overrides) || overrides.length > 1024) fail("resource", "invalid overrides");
const result = structuredClone(pack), seen = new Set();
for (const doc of overrides) {
fields(doc, ["version", "namespace", "locale", "messages"]);
if (doc.version !== 1 || !object(doc.messages)) fail("resource", "invalid override document");
const tag = localeTag(doc.locale);
const target = result.documents.find(d => d.namespace === doc.namespace && localeTag(d.locale) === tag);
if (!target) fail("resource", "override must target an exported namespace and locale");
for (const [key, message] of Object.entries(doc.messages)) {
const identity = JSON.stringify([tag, doc.namespace, key]);
if (seen.has(identity) || !owns(target.messages, key)) fail("resource", "unknown or duplicate override key");
seen.add(identity);
Object.defineProperty(target.messages, key, { value: structuredClone(message), enumerable: true, writable: true, configurable: true });
}
}
const checked = createLocalizer(result), original = createLocalizer(pack);
if (checked[contractKey] !== original[contractKey]) fail("parameter", "overrides cannot change the message contract");
return result;
}
// Optional bounded fetch loader. The caller supplies the URL mapping; locale is
// never treated as an arbitrary path or endpoint by this runtime.
export async function fetchPack(url, { signal, fetch: fetcher = globalThis.fetch } = {}) {
const response = await fetcher(url, { signal, credentials: "same-origin" });
if (!response.ok) fail("load", "language pack request failed");
if (!response.body) fail("load", "language pack response has no body");
const reader = response.body.getReader(), chunks = []; let size = 0;
try {
for (;;) {
const { done, value } = await reader.read(); if (done) break;
size += value.byteLength;
if (size > 4194304) { await reader.cancel(); fail("resource", "language pack exceeds 4 MiB"); }
chunks.push(value);
}
} finally { reader.releaseLock(); }
const raw = new Uint8Array(size); let offset = 0;
for (const chunk of chunks) { raw.set(chunk, offset); offset += chunk.length; }
try { return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw)); }
catch { fail("load", "invalid language pack JSON"); }
}

View File

@@ -0,0 +1,50 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { createLocalizer, createLanguageController, overlayPack, fetchPack } from "./runtime.mjs";
const pack = locale => ({ version: 1, locale, ruleVersion: "cldr-46.1", pluralRules: { en: { one: "i = 1 and v = 0" }, fr: { one: "i = 0,1" }, de: { one: "i = 1 and v = 0" } }, config: { sourceLocale: "en", defaultLocale: "en", locales: ["en", "fr", "de"] }, documents: [
{ version: 1, locale: "en", namespace: "ui", messages: { title: { value: { text: "English" } }, greet: { params: { name: "string" }, value: { text: "Hello {name}" } } } },
... (locale === "en" ? [] : [{ version: 1, locale, namespace: "ui", messages: { title: { value: { text: locale } } } }])
] });
test("detached resources, explicit overrides, text arguments and fallback", () => {
const data = pack("fr"), localizer = createLocalizer(data);
data.documents[0].messages.greet.value.text = "changed";
assert.equal(localizer.t("ui", "greet", { name: "<b>{name}</b>" }), "Hello <b>{name}</b>");
assert.equal(localizer.render("ui", "greet", { name: "x" }).locale, "en");
const original = pack("en");
const overridden = overlayPack(original, [{ version: 1, namespace: "ui", locale: "en", messages: { title: { value: { text: "Custom" } } } }]);
assert.equal(createLocalizer(overridden).t("ui", "title"), "Custom");
assert.equal(createLocalizer(original).t("ui", "title"), "English");
assert.throws(() => overlayPack(original, [{ version: 1, namespace: "ui", locale: "en", messages: { unknown: { value: {} } } }]));
});
test("latest selection wins and failures preserve committed state", async () => {
const pending = new Map();
const controller = createLanguageController(pack("en"), locale => new Promise((resolve, reject) => pending.set(locale, { resolve, reject })));
const french = controller.setLanguage("fr"), german = controller.setLanguage("de");
pending.get("de").resolve(pack("de")); assert.equal(await german, true);
pending.get("fr").resolve(pack("fr")); assert.equal(await french, false);
assert.equal(controller.current.locale, "de");
const failed = controller.setLanguage("fr"); pending.get("fr").reject(Error("offline"));
await assert.rejects(failed); assert.equal(controller.current.locale, "de");
const stale = controller.setLanguage("fr"); await controller.setLanguage("de");
pending.get("fr").reject(Error("stale failure")); assert.equal(await stale, false);
assert.equal(controller.current.t("ui", "title"), "de");
});
test("bad packs cannot commit or change enabled languages/schemas", async () => {
const controller = createLanguageController(pack("en"), async () => { const data = pack("fr"); delete data.documents[0].messages.greet; return data; });
await assert.rejects(controller.setLanguage("fr")); assert.equal(controller.current.locale, "en");
await assert.rejects(controller.setLanguage("zz"));
const cycle = pack("en"); cycle.config.fallbacks = { en: ["fr"], fr: ["en"] }; assert.throws(() => createLocalizer(cycle));
const duplicate = pack("en"); duplicate.documents.push(duplicate.documents[0]); assert.throws(() => createLocalizer(duplicate));
});
test("bounded fetching and validation before switching", async () => {
const data = await fetchPack("/en.json", { fetch: async () => new Response(JSON.stringify(pack("en"))) });
assert.equal(createLocalizer(data).locale, "en");
await assert.rejects(fetchPack("/bad", { fetch: async () => new Response("bad", { status: 500 }) }));
await assert.rejects(fetchPack("/bad", { fetch: async () => new Response("bad") }));
await assert.rejects(fetchPack("/huge", { fetch: async () => new Response(new Uint8Array(4194305)) }));
});

26
i18n/browser/testdata/consumer.mts vendored Normal file
View File

@@ -0,0 +1,26 @@
import {createLocalizer, createLanguageController, fetchPack, overlayPack} from './runtime.mjs';
import type {Pack} from './runtime.mjs';
import type {Translate} from './messages.generated.mjs';
declare const pack: Pack;
const messages = createLocalizer(pack);
const t: Translate = messages.t;
t('common', 'greeting', {first_name: 'Alex'});
t('common', 'count', {count: 9223372036854775807n});
t('common', 'count', {count: '9223372036854775807'});
t('common', 'literal', {});
// @ts-expect-error a message without parameters accepts only an empty object
t('common', 'literal', {extra: 1});
// @ts-expect-error missing parameter
t('common', 'greeting', {});
// @ts-expect-error invalid parameter type
t('common', 'greeting', {first_name: false});
// @ts-expect-error undeclared parameter
t('common', 'greeting', {first_name: 'Alex', secret: 'extra'});
// @ts-expect-error unknown key
t('common', 'unknown', {});
// @ts-expect-error unknown namespace
t('missing', 'literal', {});
const controller = createLanguageController(pack, locale => fetchPack('/' + locale + '.json'));
void controller.setLanguage('en');
overlayPack(pack, [{version: 1, locale: 'en', namespace: 'common', messages: {literal: {value: {text: 'Custom'}}}}]);

434
i18n/bundle.go Normal file
View File

@@ -0,0 +1,434 @@
package i18n
import (
"context"
"fmt"
"reflect"
"sort"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/text/language"
)
type entry struct {
schema map[string]Kind
patterns map[string]*compiled
}
// Bundle owns detached, compiled resources and is safe for concurrent reads.
// Reconfiguration constructs another Bundle; no global current language exists.
type Bundle struct {
config Config
matcher language.Matcher
locales []string
tags map[string]language.Tag
entries map[Ref]*entry
documents []Document
}
func sortedKeys[V any](m map[string]V) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func validID(s string) bool {
return s != "" && len(s) <= 256 && utf8.ValidString(s) && strings.TrimSpace(s) == s && strings.IndexFunc(s, unicode.IsControl) < 0
}
func canonical(s string) (string, error) {
if s == "" || len(s) > 128 {
return "", fmt.Errorf("i18n: language tag required (maximum 128 bytes)")
}
t, err := language.Parse(s)
if err != nil || t == language.Und {
return "", fmt.Errorf("i18n: invalid language tag")
}
return t.String(), nil
}
func normalizeConfig(c Config) (Config, error) {
var err error
if c.SourceLocale, err = canonical(c.SourceLocale); err != nil {
return Config{}, err
}
if c.DefaultLocale, err = canonical(c.DefaultLocale); err != nil {
return Config{}, err
}
if len(c.Locales) == 0 || len(c.Locales) > 64 {
return Config{}, fmt.Errorf("i18n: enable between 1 and 64 locales")
}
seen := map[string]bool{}
locales := make([]string, 0, len(c.Locales))
for _, raw := range c.Locales {
l, e := canonical(raw)
if e != nil {
return Config{}, e
}
if seen[l] {
return Config{}, fmt.Errorf("i18n: duplicate normalized locale %s", l)
}
seen[l] = true
locales = append(locales, l)
}
if !seen[c.DefaultLocale] {
return Config{}, fmt.Errorf("i18n: default locale must be enabled")
}
c.Locales = locales
fallbacks := map[string][]string{}
for _, raw := range sortedKeys(c.Fallbacks) {
l, e := canonical(raw)
if e != nil || !seen[l] {
return Config{}, fmt.Errorf("i18n: fallback locale must be enabled")
}
if _, ok := fallbacks[l]; ok {
return Config{}, fmt.Errorf("i18n: duplicate normalized fallback locale")
}
if len(c.Fallbacks[raw]) > 64 {
return Config{}, fmt.Errorf("i18n: too many fallback locales")
}
list := []string{}
unique := map[string]bool{}
for _, v := range c.Fallbacks[raw] {
to, e := canonical(v)
if e != nil || !seen[to] || unique[to] {
return Config{}, fmt.Errorf("i18n: fallback targets must be distinct enabled locales")
}
unique[to] = true
list = append(list, to)
}
fallbacks[l] = list
}
state := map[string]uint8{}
var visit func(string) error
visit = func(l string) error {
if state[l] == 1 {
return fmt.Errorf("i18n: fallback cycle")
}
if state[l] == 2 {
return nil
}
state[l] = 1
for _, to := range fallbacks[l] {
if err := visit(to); err != nil {
return err
}
}
state[l] = 2
return nil
}
for _, l := range locales {
if err := visit(l); err != nil {
return Config{}, err
}
}
c.Fallbacks = fallbacks
return c, nil
}
func (c Config) Validate() error { _, err := normalizeConfig(c); return err }
// New accepts split documents; duplicate (locale, namespace, key) definitions
// are rejected. Use Overlay explicitly when replacement is intended.
func New(config Config, documents ...Document) (*Bundle, error) {
c, err := normalizeConfig(config)
if err != nil {
return nil, err
}
b := &Bundle{config: c, tags: map[string]language.Tag{}, entries: map[Ref]*entry{}}
b.locales = append([]string{c.DefaultLocale}, c.Locales...)
b.locales = unique(b.locales)
tags := make([]language.Tag, 0, len(b.locales))
for _, l := range b.locales {
t := language.MustParse(l)
tags = append(tags, t)
b.tags[l] = t
}
b.matcher = language.NewMatcher(tags)
if len(documents) > 1024 {
return nil, fmt.Errorf("i18n: too many resource documents")
}
groups := map[string]map[string]map[string]Message{}
total := 0
names := map[string]bool{}
languages := map[string]bool{}
for _, doc := range documents {
l, e := canonical(doc.Locale)
if e != nil {
return nil, e
}
if doc.Version != Version || !validID(doc.Namespace) {
return nil, fmt.Errorf("i18n: version 1 and a valid namespace are required")
}
names[doc.Namespace] = true
languages[l] = true
if len(names) > 128 || len(languages) > 64 {
return nil, fmt.Errorf("i18n: namespace or locale resource limit exceeded")
}
if groups[l] == nil {
groups[l] = map[string]map[string]Message{}
}
if groups[l][doc.Namespace] == nil {
groups[l][doc.Namespace] = map[string]Message{}
}
for _, k := range sortedKeys(doc.Messages) {
if !validID(k) {
return nil, fmt.Errorf("i18n: invalid message key")
}
if _, ok := groups[l][doc.Namespace][k]; ok {
return nil, &Diagnostic{Kind: "duplicate", Namespace: doc.Namespace, Key: k, Locale: l, Problem: "use an explicit overlay to replace a message"}
}
total++
if total > 10000 {
return nil, fmt.Errorf("i18n: resource exceeds 10000 messages")
}
groups[l][doc.Namespace][k] = doc.Messages[k]
}
}
for _, ns := range sortedKeys(groups[c.SourceLocale]) {
for _, key := range sortedKeys(groups[c.SourceLocale][ns]) {
m := groups[c.SourceLocale][ns][key]
if len(m.Params) > 64 {
return nil, fmt.Errorf("i18n: message exceeds 64 parameters")
}
schema := map[string]Kind{}
for name, kind := range m.Params {
if !paramName.MatchString(name) || len(name) > 128 {
return nil, &Diagnostic{Kind: "parameter", Namespace: ns, Key: key, Param: name, Problem: "invalid parameter name"}
}
switch kind {
case String, Boolean, Integer, Number:
default:
return nil, &Diagnostic{Kind: "parameter", Namespace: ns, Key: key, Param: name, Problem: "unsupported type"}
}
schema[name] = kind
}
b.entries[Ref{ns, key}] = &entry{schema: schema, patterns: map[string]*compiled{}}
}
}
limits := &budget{}
for _, l := range sortedKeys(groups) {
for _, ns := range sortedKeys(groups[l]) {
doc := Document{Version: Version, Locale: l, Namespace: ns, Messages: map[string]Message{}}
for _, key := range sortedKeys(groups[l][ns]) {
m := groups[l][ns][key]
e := b.entries[Ref{ns, key}]
if e == nil {
return nil, &Diagnostic{Kind: "unknown", Namespace: ns, Key: key, Locale: l, Problem: "message has no source definition"}
}
if m.Params != nil && !sameSchema(m.Params, e.schema) {
return nil, &Diagnostic{Kind: "parameter", Namespace: ns, Key: key, Locale: l, Problem: "translation parameter schema differs from source"}
}
used := map[string]bool{}
p, err := compile(m.Value, e.schema, used, limits, 0)
if err == nil {
for _, name := range sortedKeys(e.schema) {
if !used[name] {
err = &Diagnostic{Kind: "parameter", Param: name, Problem: "declared parameter is unused in this message"}
break
}
}
}
if err != nil {
d := *err.(*Diagnostic)
d.Namespace, d.Key, d.Locale = ns, key, l
return nil, &d
}
e.patterns[l] = p
doc.Messages[key] = Message{Params: cloneSchema(e.schema), Value: clonePattern(m.Value)}
}
b.documents = append(b.documents, doc)
}
}
return b, nil
}
func unique(in []string) []string {
out := []string{}
seen := map[string]bool{}
for _, s := range in {
if !seen[s] {
seen[s] = true
out = append(out, s)
}
}
return out
}
func sameSchema(a, b map[string]Kind) bool {
return len(a) == len(b) && reflect.DeepEqual(cloneSchema(a), cloneSchema(b))
}
func cloneSchema(m map[string]Kind) map[string]Kind {
out := map[string]Kind{}
for k, v := range m {
out[k] = v
}
return out
}
func clonePattern(p Pattern) Pattern {
if p.Cases != nil {
m := make(map[string]Pattern, len(p.Cases))
for k, v := range p.Cases {
m[k] = clonePattern(v)
}
p.Cases = m
}
return p
}
func (b *Bundle) Config() Config {
if b == nil {
return Config{}
}
c := b.config
c.Locales = append([]string(nil), c.Locales...)
c.Fallbacks = map[string][]string{}
for k, v := range b.config.Fallbacks {
c.Fallbacks[k] = append([]string{}, v...)
}
return c
}
// Match accepts ordered language tags, not an HTTP header. It returns an enabled
// dictionary tag (not a synthetic matcher tag with user extensions).
func (b *Bundle) Match(preferences ...string) string {
if b == nil {
return ""
}
tags := make([]language.Tag, 0, len(preferences))
for _, s := range preferences {
if len(tags) >= 64 {
break
}
if len(s) > 128 {
continue
}
if t, e := language.Parse(s); s != "" && e == nil {
tags = append(tags, t)
}
}
_, index, _ := b.matcher.Match(tags...)
return b.locales[index]
}
func (b *Bundle) chain(locale string) []string {
out := []string{}
seen := map[string]bool{}
var add func(string)
add = func(l string) {
if seen[l] {
return
}
seen[l] = true
out = append(out, l)
for _, to := range b.config.Fallbacks[l] {
add(to)
}
}
add(locale)
add(b.config.DefaultLocale)
add(b.config.SourceLocale)
return out
}
func (b *Bundle) Render(locale string, ref Ref, params map[string]any) (Result, error) {
if b == nil {
return Result{}, &Diagnostic{Kind: "missing", Namespace: ref.Namespace, Key: ref.Key, Problem: "no bundle"}
}
requested := b.Match(locale)
e := b.entries[ref]
if e == nil {
return Result{}, &Diagnostic{Kind: "missing", Namespace: ref.Namespace, Key: ref.Key, Locale: requested, Problem: "unknown message"}
}
values := make(map[string]string, len(e.schema))
for _, k := range sortedKeys(e.schema) {
v, ok := params[k]
if !ok {
return Result{}, &Diagnostic{Kind: "parameter", Namespace: ref.Namespace, Key: ref.Key, Param: k, Problem: "missing argument"}
}
s, err := scalar(e.schema[k], v)
if err != nil {
d := *err.(*Diagnostic)
d.Namespace, d.Key, d.Param = ref.Namespace, ref.Key, k
return Result{}, &d
}
values[k] = s
}
for _, k := range sortedKeys(params) {
if _, ok := e.schema[k]; !ok {
return Result{}, &Diagnostic{Kind: "parameter", Namespace: ref.Namespace, Key: ref.Key, Param: k, Problem: "undeclared argument"}
}
}
for _, l := range b.chain(requested) {
if p := e.patterns[l]; p != nil {
text, err := p.render(language.MustParse(l), values)
return Result{Text: text, Requested: requested, Locale: l, Fallback: l != requested}, err
}
}
return Result{}, &Diagnostic{Kind: "missing", Namespace: ref.Namespace, Key: ref.Key, Locale: requested, Problem: "no message in fallback chain"}
}
func (b *Bundle) RenderContext(ctx context.Context, ref Ref, params map[string]any) (Result, error) {
return b.Render(Locale(ctx), ref, params)
}
// Schema returns a detached source schema for adapters and static tools.
func (b *Bundle) Schema(ref Ref) (map[string]Kind, bool) {
if b == nil {
return nil, false
}
e := b.entries[ref]
if e == nil {
return nil, false
}
return cloneSchema(e.schema), true
}
// Check reports missing direct translations. Runtime fallback remains usable;
// build tooling can reject these diagnostics without changing runtime behavior.
func (b *Bundle) Check() []Diagnostic {
out := []Diagnostic{}
if b == nil {
return out
}
for _, d := range b.documents {
if d.Locale != b.config.SourceLocale {
continue
}
for _, key := range sortedKeys(d.Messages) {
e := b.entries[Ref{d.Namespace, key}]
for _, l := range b.config.Locales {
if e.patterns[l] == nil {
out = append(out, Diagnostic{Kind: "missing", Namespace: d.Namespace, Key: key, Locale: l, Problem: "direct translation missing"})
}
}
}
}
return out
}
// Documents returns detached resources. An empty namespace filter includes all;
// exporting for a browser should normally supply explicit namespaces.
func (b *Bundle) Documents(locale string, namespaces ...string) []Document {
out := []Document{}
if b == nil {
return out
}
wanted := map[string]bool{}
for _, ns := range namespaces {
wanted[ns] = true
}
for _, d := range b.documents {
if locale != "" && d.Locale != locale || len(wanted) > 0 && !wanted[d.Namespace] {
continue
}
copy := d
copy.Messages = map[string]Message{}
for k, m := range d.Messages {
copy.Messages[k] = Message{Params: cloneSchema(m.Params), Value: clonePattern(m.Value)}
}
out = append(out, copy)
}
return out
}

353
i18n/bundle_test.go Normal file
View File

@@ -0,0 +1,353 @@
package i18n_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"math"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"testing/fstest"
"git.yuntuo.hk/k2safe/go-omni/i18n"
)
func settings() i18n.Config {
return i18n.Config{SourceLocale: "en", DefaultLocale: "en", Locales: []string{"en", "zh-Hans", "ru", "ar", "fr"}, Fallbacks: map[string][]string{"zh-Hans": {"en"}}}
}
func docs() []i18n.Document {
plural := i18n.Pattern{Plural: "count", Cases: map[string]i18n.Pattern{}}
for _, category := range []string{"zero", "one", "two", "few", "many", "other"} {
plural.Cases[category] = i18n.Pattern{Text: category + ":{count}"}
}
out := []i18n.Document{}
for _, locale := range settings().Locales {
out = append(out, i18n.Document{Version: 1, Namespace: "common", Locale: locale, Messages: map[string]i18n.Message{
"count": {Params: map[string]i18n.Kind{"count": i18n.Integer}, Value: plural},
"decimal": {Params: map[string]i18n.Kind{"count": i18n.Number}, Value: plural},
"choice": {Params: map[string]i18n.Kind{"type": i18n.String, "count": i18n.Integer}, Value: i18n.Pattern{Select: "type", Cases: map[string]i18n.Pattern{"admin": {Text: "admin:{count}"}, "other": plural}}},
"literal": {Value: i18n.Pattern{Text: "${HOME} {notAParameter}", Literal: true}},
"empty": {Value: i18n.Pattern{}},
}})
}
out[0].Messages["greeting"] = i18n.Message{Params: map[string]i18n.Kind{"first_name": i18n.String}, Value: i18n.Pattern{Text: "Hello {first_name} {{literal}}"}}
return out
}
func makeBundle(t testing.TB) *i18n.Bundle {
t.Helper()
b, err := i18n.New(settings(), docs()...)
if err != nil {
t.Fatal(err)
}
return b
}
func TestIndependentImmutableBundlesAndFallback(t *testing.T) {
resources := docs()
cfg := settings()
b, err := i18n.New(cfg, resources...)
if err != nil {
t.Fatal(err)
}
resources[0].Messages["greeting"] = i18n.Message{Value: i18n.Pattern{Text: "changed"}}
cfg.Locales[0] = "de"
cfg.Fallbacks["zh-Hans"][0] = "de"
snapshot := b.Config()
snapshot.Fallbacks["zh-Hans"][0] = "fr"
exported := b.Documents("")
exported[0].Messages["count"] = i18n.Message{}
result, err := b.RenderContext(i18n.WithLocale(context.Background(), "zh-CN"), i18n.Ref{Namespace: "common", Key: "greeting"}, map[string]any{"first_name": "{count}"})
if err != nil || result.Text != "Hello {count} {literal}" || result.Locale != "en" || !result.Fallback || result.Requested != "zh-Hans" {
t.Fatal(result, err)
}
if len(b.Check()) != 4 {
t.Fatal(b.Check())
}
if got := b.Match("ru-RU"); got != "ru" {
t.Fatal(got)
}
for _, key := range []string{"literal", "empty"} {
got, err := b.Render("en", i18n.Ref{Namespace: "common", Key: key}, nil)
if err != nil {
t.Fatal(err)
}
if key == "literal" && got.Text != "${HOME} {notAParameter}" || key == "empty" && got.Text != "" {
t.Fatal(got)
}
}
var wg sync.WaitGroup
for n := 0; n < 80; n++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
l, want := "en", "other:22"
if n%2 == 0 {
l, want = "ru", "few:22"
}
got, err := b.Render(l, i18n.Ref{Namespace: "common", Key: "count"}, map[string]any{"count": int64(22)})
if err != nil || got.Text != want {
t.Errorf("%+v %v", got, err)
}
}(n)
}
wg.Wait()
other := docs()
other[0].Messages["greeting"] = i18n.Message{Value: i18n.Pattern{Text: "different application"}}
b2, err := i18n.New(settings(), other...)
if err != nil {
t.Fatal(err)
}
got, _ := b2.Render("en", i18n.Ref{Namespace: "common", Key: "greeting"}, nil)
if got.Text != "different application" {
t.Fatal(got)
}
}
func TestResourceAndParameterDiagnostics(t *testing.T) {
b := makeBundle(t)
for _, params := range []map[string]any{nil, {"count": "21"}, {"count": 21.0}, {"count": 21, "extra": "secret"}, {"count": uint64(math.MaxUint64)}, {"count": []string{"secret"}}} {
_, err := b.Render("en", i18n.Ref{Namespace: "common", Key: "count"}, params)
var d *i18n.Diagnostic
if !errors.As(err, &d) || d.Kind != "parameter" || strings.Contains(err.Error(), "secret") {
t.Fatal(err)
}
}
_, err := b.Render("en", i18n.Ref{Namespace: "common", Key: "missing"}, nil)
var d *i18n.Diagnostic
if !errors.As(err, &d) || d.Kind != "missing" {
t.Fatal(err)
}
_, err = b.Render("en", i18n.Ref{Namespace: "common", Key: "decimal"}, map[string]any{"count": 1e-16})
if err != nil {
t.Fatal(err)
}
for _, change := range []func([]i18n.Document){
func(d []i18n.Document) { d[1].Messages["unknown"] = i18n.Message{} },
func(d []i18n.Document) {
m := d[1].Messages["count"]
m.Params = map[string]i18n.Kind{"count": i18n.String}
d[1].Messages["count"] = m
},
func(d []i18n.Document) {
m := d[1].Messages["count"]
m.Value = i18n.Pattern{Text: "{missing}"}
d[1].Messages["count"] = m
},
func(d []i18n.Document) {
m := d[1].Messages["count"]
m.Value = i18n.Pattern{Text: "unused"}
d[1].Messages["count"] = m
},
func(d []i18n.Document) {
m := d[1].Messages["count"]
delete(m.Value.Cases, "other")
d[1].Messages["count"] = m
},
func(d []i18n.Document) { d[1].Version = 2 },
} {
d := docs()
change(d)
if _, err := i18n.New(settings(), d...); err == nil {
t.Fatal("invalid document accepted")
}
}
for _, change := range []func(*i18n.Config){func(c *i18n.Config) { c.Fallbacks["en"] = []string{"zh-Hans"} }, func(c *i18n.Config) { c.DefaultLocale = "de" }, func(c *i18n.Config) { c.Locales = append(c.Locales, "EN") }} {
c := settings()
change(&c)
if _, err := i18n.New(c, docs()...); err == nil {
t.Fatal("invalid config accepted")
}
}
}
func TestSourceLocaleCanBeFallbackOnly(t *testing.T) {
// These tags also exercise browser portability: Intl can alias cmn to zh,
// and rejects private-use tags despite Go's BCP 47 matcher accepting them.
for _, locale := range []string{"fr", "cmn", "x-example"} {
t.Run(locale, func(t *testing.T) {
b, err := i18n.New(i18n.Config{SourceLocale: "en", DefaultLocale: locale, Locales: []string{locale}},
i18n.Document{Version: 1, Namespace: "common", Locale: "en", Messages: map[string]i18n.Message{"fallback": {Value: i18n.Pattern{Text: "Source"}}}})
if err != nil {
t.Fatal(err)
}
got, err := b.Render(locale, i18n.Ref{Namespace: "common", Key: "fallback"}, nil)
if err != nil || got.Text != "Source" || !got.Fallback || got.Locale != "en" || got.Requested != locale || len(b.Check()) != 1 {
t.Fatal(got, err, b.Check())
}
var pack bytes.Buffer
if err := b.Export(&pack, locale, "common"); err != nil {
t.Fatal(err)
}
if !bytes.Contains(pack.Bytes(), []byte("UNICODE LICENSE")) {
t.Fatal("exported rule data must carry its license")
}
if node, err := exec.LookPath("node"); err == nil {
input, _ := json.Marshal(struct {
Pack json.RawMessage `json:"pack"`
Cases []any `json:"cases"`
}{pack.Bytes(), []any{map[string]any{"key": "fallback", "params": map[string]any{}, "want": "Source"}}})
cmd := exec.Command(node, "browser/conformance.mjs")
cmd.Stdin = bytes.NewReader(input)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("%s: %v", out, err)
}
}
})
}
}
func TestStrictLoadingAndExplicitOverlay(t *testing.T) {
raw := `{"version":1,"locale":"en","namespace":"common","messages":{"x":{"value":{"literal":true,"text":"${HOME}"}}}}`
fs := fstest.MapFS{"en.json": {Data: []byte(raw)}}
loaded, err := i18n.LoadFS(fs, "en.json")
if err != nil {
t.Fatal(err)
}
if loaded[0].Messages["x"].Value.Text != "${HOME}" {
t.Fatal("environment interpolation")
}
for _, bad := range []string{strings.Replace(raw, `"version":1`, `"version":1,"version":1`, 1), strings.Replace(raw, `"version":1`, `"unknown":1`, 1), strings.Replace(raw, `{"value":{"literal":true,"text":"${HOME}"}}`, `{}`, 1), strings.Replace(raw, `"literal":true`, `"literal":true,"unknown":true`, 1), raw + ` {}`, strings.Repeat("[", 34) + "0" + strings.Repeat("]", 34)} {
var d i18n.Document
if err := i18n.DecodeJSON([]byte(bad), &d); err == nil {
t.Fatal("invalid JSON accepted", bad)
}
}
c := i18n.Config{SourceLocale: "en", DefaultLocale: "en", Locales: []string{"en"}}
if _, err := i18n.New(c, loaded[0], loaded[0]); err == nil {
t.Fatal("duplicate accepted")
}
override := i18n.Document{Version: 1, Locale: "EN", Namespace: "common", Messages: map[string]i18n.Message{"x": {Value: i18n.Pattern{Text: "replacement"}}}}
merged, err := i18n.Overlay(loaded[0], override)
if err != nil {
t.Fatal(err)
}
b, err := i18n.New(c, merged)
if err != nil {
t.Fatal(err)
}
got, _ := b.Render("en", i18n.Ref{Namespace: "common", Key: "x"}, nil)
if got.Text != "replacement" || loaded[0].Messages["x"].Value.Text != "${HOME}" {
t.Fatal(got)
}
}
func TestBrowserConformance(t *testing.T) {
node, err := exec.LookPath("node")
if err != nil {
t.Skip("Node.js is required for cross-runtime conformance")
}
b := makeBundle(t)
type sample struct {
Key string `json:"key"`
Params map[string]any `json:"params"`
Want string `json:"want"`
}
cases := map[string][]sample{
"en": {{"count", map[string]any{"count": 1}, "one:1"}, {"count", map[string]any{"count": 2}, "other:2"}, {"count", map[string]any{"count": "9223372036854775807"}, "other:9223372036854775807"}, {"decimal", map[string]any{"count": 1.5}, "other:1.5"}, {"decimal", map[string]any{"count": 1e-16}, "other:1e-16"}, {"choice", map[string]any{"type": "admin", "count": 2}, "admin:2"}},
"ru": {{"count", map[string]any{"count": 21}, "one:21"}, {"count", map[string]any{"count": 22}, "few:22"}, {"count", map[string]any{"count": 11}, "many:11"}, {"count", map[string]any{"count": 10000021}, "one:10000021"}, {"decimal", map[string]any{"count": 1.25}, "other:1.25"}},
"ar": {{"count", map[string]any{"count": 0}, "zero:0"}, {"count", map[string]any{"count": 2}, "two:2"}, {"count", map[string]any{"count": 3}, "few:3"}, {"count", map[string]any{"count": 11}, "many:11"}, {"count", map[string]any{"count": 100}, "other:100"}},
"zh-Hans": {{"count", map[string]any{"count": 1}, "other:1"}, {"greeting", map[string]any{"first_name": "{count}"}, "Hello {count} {literal}"}},
"fr": {{"count", map[string]any{"count": 0}, "one:0"}, {"count", map[string]any{"count": 2}, "other:2"}, {"count", map[string]any{"count": 1000000}, "many:1000000"}},
}
for locale, samples := range cases {
for _, s := range samples {
params := map[string]any{}
for k, v := range s.Params {
params[k] = v
}
if v, ok := params["count"].(string); ok {
params["count"] = json.Number(v)
}
got, err := b.Render(locale, i18n.Ref{Namespace: "common", Key: s.Key}, params)
if err != nil || got.Text != s.Want {
t.Fatalf("Go %s %s: %+v %v", locale, s.Key, got, err)
}
}
var pack bytes.Buffer
if err := b.Export(&pack, locale, "common"); err != nil {
t.Fatal(err)
}
input, _ := json.Marshal(struct {
Pack json.RawMessage `json:"pack"`
Cases []sample `json:"cases"`
}{pack.Bytes(), samples})
cmd := exec.Command(node, "browser/conformance.mjs")
cmd.Stdin = bytes.NewReader(input)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("browser %s: %s %v", locale, output, err)
}
}
var ts bytes.Buffer
if err := b.TypeScript(&ts, "common"); err != nil {
t.Fatal(err)
}
if !strings.Contains(ts.String(), "bigint") || !strings.Contains(ts.String(), `"first_name": string`) {
t.Fatal(ts.String())
}
if err := b.Export(&bytes.Buffer{}, "en"); err == nil {
t.Fatal("export requires explicit namespaces")
}
}
func TestTypeScriptConsumer(t *testing.T) {
tsc, err := exec.LookPath("tsc")
if err != nil {
t.Skip("TypeScript compiler is required for generated type conformance")
}
dir := t.TempDir()
var types bytes.Buffer
if err := makeBundle(t).TypeScript(&types, "common"); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "messages.generated.d.mts"), types.Bytes(), 0600); err != nil {
t.Fatal(err)
}
for target, source := range map[string]string{"runtime.d.mts": "browser/runtime.d.mts", "consumer.mts": "browser/testdata/consumer.mts"} {
data, err := os.ReadFile(source)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, target), data, 0600); err != nil {
t.Fatal(err)
}
}
cmd := exec.Command(tsc, "--noEmit", "--strict", "--target", "ES2022", "--module", "NodeNext", "--moduleResolution", "NodeNext", "consumer.mts")
cmd.Dir = dir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("TypeScript consumer: %s %v", output, err)
}
}
func FuzzDecodeJSON(f *testing.F) {
f.Add([]byte(`{"version":1,"locale":"en","namespace":"x","messages":{"x":{"value":{"text":"hello"}}}}`))
f.Fuzz(func(t *testing.T, data []byte) {
var d i18n.Document
if i18n.DecodeJSON(data, &d) == nil {
b, err := i18n.New(i18n.Config{SourceLocale: "en", DefaultLocale: "en", Locales: []string{"en"}}, d)
if err == nil {
for _, doc := range b.Documents("") {
if !reflect.DeepEqual(doc, b.Documents(doc.Locale)[0]) {
t.Fatal("unstable resource")
}
}
}
}
})
}
func BenchmarkRender(b *testing.B) {
catalog := makeBundle(b)
params := map[string]any{"count": 22}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := catalog.Render("ru", i18n.Ref{Namespace: "common", Key: "count"}, params); err != nil {
b.Fatal(err)
}
}
}

41
i18n/cldr/LICENSE Normal file
View File

@@ -0,0 +1,41 @@
UNICODE LICENSE V3
COPYRIGHT AND PERMISSION NOTICE
Copyright © 2015-2024 Unicode, Inc.
NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
Permission is hereby granted, free of charge, to any person obtaining a
copy of data files and any associated documentation (the "Data Files") or
software and any associated documentation (the "Software") to deal in the
Data Files or Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, and/or sell
copies of the Data Files or Software, and to permit persons to whom the
Data Files or Software are furnished to do so, provided that either (a)
this copyright and permission notice appear with all copies of the Data
Files or Software, or (b) this copyright and permission notice appear in
associated Documentation.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or other
dealings in these Data Files or Software without prior written
authorization of the copyright holder.
SPDX-License-Identifier: Unicode-3.0

19
i18n/cldr/README.md Normal file
View File

@@ -0,0 +1,19 @@
# Pinned cardinal plural data
`plurals.json` is copied without modification from Unicode CLDR JSON **46.1.0**:
https://github.com/unicode-org/cldr-json/blob/46.1.0/cldr-json/cldr-core/supplemental/plurals.json
The upstream data identifies CLDR version 46 / Unicode 16.0.0. See the accompanying
Unicode license. No data is downloaded at application startup or during generation.
Both Go and the browser evaluate these same rules. Browser packs include only the
rules for the exported language and its fallback chain, together with the Unicode
license. They do not depend on the
browser's built-in `Intl.PluralRules` data version. Language-tag matching is a
separate responsibility provided by `golang.org/x/text/language`.
When updating this data, update `PluralRulesVersion`, preserve the license, and run
the upstream-sample and Go/browser conformance tests. This version supports cardinal
rules for ordinary numbers; compact-number operands `c`/`e` are zero. Ordinal and
compact-number formatting are not part of the v1 message format.

933
i18n/cldr/plurals.json Normal file
View File

@@ -0,0 +1,933 @@
{
"supplemental": {
"version": {
"_unicodeVersion": "16.0.0",
"_cldrVersion": "46"
},
"plurals-type-cardinal": {
"af": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ak": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"am": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"an": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ar": {
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-few": "n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …",
"pluralRule-count-many": "n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …",
"pluralRule-count-other": " @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ars": {
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-few": "n % 100 = 3..10 @integer 3~10, 103~110, 1003, … @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …",
"pluralRule-count-many": "n % 100 = 11..99 @integer 11~26, 111, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …",
"pluralRule-count-other": " @integer 100~102, 200~202, 300~302, 400~402, 500~502, 600, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"as": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"asa": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ast": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"az": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"bal": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"be": {
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …",
"pluralRule-count-few": "n % 10 = 2..4 and n % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 2.0, 3.0, 4.0, 22.0, 23.0, 24.0, 32.0, 33.0, 102.0, 1002.0, …",
"pluralRule-count-many": "n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …"
},
"bem": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"bez": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"bg": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"bho": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"blo": {
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"bm": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"bn": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"bo": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"br": {
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11,71,91 @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, …",
"pluralRule-count-two": "n % 10 = 2 and n % 100 != 12,72,92 @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, … @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, …",
"pluralRule-count-few": "n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99 @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, … @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, …",
"pluralRule-count-many": "n != 0 and n % 1000000 = 0 @integer 1000000, … @decimal 1000000.0, 1000000.00, 1000000.000, 1000000.0000, …",
"pluralRule-count-other": " @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, …"
},
"brx": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"bs": {
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …",
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ca": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-many": "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …"
},
"ce": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ceb": {
"pluralRule-count-one": "v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …"
},
"cgg": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"chr": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ckb": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"cs": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-few": "i = 2..4 and v = 0 @integer 2~4",
"pluralRule-count-many": "v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
},
"csw": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"cy": {
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-few": "n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000",
"pluralRule-count-many": "n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000",
"pluralRule-count-other": " @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"da": {
"pluralRule-count-one": "n = 1 or t != 0 and i = 0,1 @integer 1 @decimal 0.1~1.6",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 2.0~3.4, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"de": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"doi": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"dsb": {
"pluralRule-count-one": "v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-two": "v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, …",
"pluralRule-count-few": "v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, …",
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"dv": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"dz": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ee": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"el": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"en": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"eo": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"es": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-many": "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …"
},
"et": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"eu": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"fa": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ff": {
"pluralRule-count-one": "i = 0,1 @integer 0, 1 @decimal 0.0~1.5",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"fi": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"fil": {
"pluralRule-count-one": "v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …"
},
"fo": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"fr": {
"pluralRule-count-one": "i = 0,1 @integer 0, 1 @decimal 0.0~1.5",
"pluralRule-count-many": "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …"
},
"fur": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"fy": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ga": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-few": "n = 3..6 @integer 3~6 @decimal 3.0, 4.0, 5.0, 6.0, 3.00, 4.00, 5.00, 6.00, 3.000, 4.000, 5.000, 6.000, 3.0000, 4.0000, 5.0000, 6.0000",
"pluralRule-count-many": "n = 7..10 @integer 7~10 @decimal 7.0, 8.0, 9.0, 10.0, 7.00, 8.00, 9.00, 10.00, 7.000, 8.000, 9.000, 10.000, 7.0000, 8.0000, 9.0000, 10.0000",
"pluralRule-count-other": " @integer 0, 11~25, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"gd": {
"pluralRule-count-one": "n = 1,11 @integer 1, 11 @decimal 1.0, 11.0, 1.00, 11.00, 1.000, 11.000, 1.0000",
"pluralRule-count-two": "n = 2,12 @integer 2, 12 @decimal 2.0, 12.0, 2.00, 12.00, 2.000, 12.000, 2.0000",
"pluralRule-count-few": "n = 3..10,13..19 @integer 3~10, 13~19 @decimal 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 3.00",
"pluralRule-count-other": " @integer 0, 20~34, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"gl": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"gsw": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"gu": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"guw": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"gv": {
"pluralRule-count-one": "v = 0 and i % 10 = 1 @integer 1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, …",
"pluralRule-count-two": "v = 0 and i % 10 = 2 @integer 2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, …",
"pluralRule-count-few": "v = 0 and i % 100 = 0,20,40,60,80 @integer 0, 20, 40, 60, 80, 100, 120, 140, 1000, 10000, 100000, 1000000, …",
"pluralRule-count-many": "v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @integer 3~10, 13~19, 23, 103, 1003, …"
},
"ha": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"haw": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"he": {
"pluralRule-count-one": "i = 1 and v = 0 or i = 0 and v != 0 @integer 1 @decimal 0.0~0.9, 0.00~0.05",
"pluralRule-count-two": "i = 2 and v = 0 @integer 2",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.0~2.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"hi": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"hnj": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"hr": {
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …",
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"hsb": {
"pluralRule-count-one": "v = 0 and i % 100 = 1 or f % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-two": "v = 0 and i % 100 = 2 or f % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, … @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2, …",
"pluralRule-count-few": "v = 0 and i % 100 = 3..4 or f % 100 = 3..4 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.3, 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4, 10.3, 100.3, 1000.3, …",
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"hu": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"hy": {
"pluralRule-count-one": "i = 0,1 @integer 0, 1 @decimal 0.0~1.5",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ia": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"id": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ig": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ii": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"io": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"is": {
"pluralRule-count-one": "t = 0 and i % 10 = 1 and i % 100 != 11 or t % 10 = 1 and t % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~0.9, 1.2~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"it": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-many": "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …"
},
"iu": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ja": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"jbo": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"jgo": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"jmc": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"jv": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"jw": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ka": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kab": {
"pluralRule-count-one": "i = 0,1 @integer 0, 1 @decimal 0.0~1.5",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kaj": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kcg": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kde": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kea": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kk": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kkj": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kl": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"km": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kn": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ko": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ks": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ksb": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ksh": {
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ku": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"kw": {
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000 @integer 2, 22, 42, 62, 82, 102, 122, 142, 1000, 10000, 100000, … @decimal 2.0, 22.0, 42.0, 62.0, 82.0, 102.0, 122.0, 142.0, 1000.0, 10000.0, 100000.0, …",
"pluralRule-count-few": "n % 100 = 3,23,43,63,83 @integer 3, 23, 43, 63, 83, 103, 123, 143, 1003, … @decimal 3.0, 23.0, 43.0, 63.0, 83.0, 103.0, 123.0, 143.0, 1003.0, …",
"pluralRule-count-many": "n != 1 and n % 100 = 1,21,41,61,81 @integer 21, 41, 61, 81, 101, 121, 141, 161, 1001, … @decimal 21.0, 41.0, 61.0, 81.0, 101.0, 121.0, 141.0, 161.0, 1001.0, …",
"pluralRule-count-other": " @integer 4~19, 100, 1004, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.1, 1000000.0, …"
},
"ky": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"lag": {
"pluralRule-count-zero": "n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000",
"pluralRule-count-one": "i = 0,1 and n != 0 @integer 1 @decimal 0.1~1.6",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"lb": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"lg": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"lij": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"lkt": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"lld": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-many": "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …"
},
"ln": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"lo": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"lt": {
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, …",
"pluralRule-count-few": "n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, …",
"pluralRule-count-many": "f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, …",
"pluralRule-count-other": " @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"lv": {
"pluralRule-count-zero": "n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-other": " @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, …"
},
"mas": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"mg": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"mgo": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"mk": {
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.2~1.0, 1.2~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ml": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"mn": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"mo": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-few": "v != 0 or n = 0 or n != 1 and n % 100 = 1..19 @integer 0, 2~16, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @integer 20~35, 100, 1000, 10000, 100000, 1000000, …"
},
"mr": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ms": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"mt": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-few": "n = 0 or n % 100 = 3..10 @integer 0, 3~10, 103~109, 1003, … @decimal 0.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 103.0, 1003.0, …",
"pluralRule-count-many": "n % 100 = 11..19 @integer 11~19, 111~117, 1011, … @decimal 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 111.0, 1011.0, …",
"pluralRule-count-other": " @integer 20~35, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"my": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nah": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"naq": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nb": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nd": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ne": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nl": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nn": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nnh": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"no": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nqo": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nr": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nso": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ny": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"nyn": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"om": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"or": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"os": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"osa": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"pa": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"pap": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"pcm": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"pl": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
"pluralRule-count-many": "v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
"pluralRule-count-other": " @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"prg": {
"pluralRule-count-zero": "n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19 @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-one": "n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.0, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-other": " @integer 2~9, 22~29, 102, 1002, … @decimal 0.2~0.9, 1.2~1.9, 10.2, 100.2, 1000.2, …"
},
"ps": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"pt": {
"pluralRule-count-one": "i = 0..1 @integer 0, 1 @decimal 0.0~1.5",
"pluralRule-count-many": "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …"
},
"pt-PT": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-many": "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …"
},
"rm": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ro": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-few": "v != 0 or n = 0 or n != 1 and n % 100 = 1..19 @integer 0, 2~16, 101, 1001, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @integer 20~35, 100, 1000, 10000, 100000, 1000000, …"
},
"rof": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ru": {
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
"pluralRule-count-many": "v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
"pluralRule-count-other": " @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"rwk": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sah": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"saq": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sat": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sc": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"scn": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-many": "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …"
},
"sd": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sdh": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"se": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"seh": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ses": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sg": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sh": {
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …",
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"shi": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-few": "n = 2..10 @integer 2~10 @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 2.00, 3.00, 4.00, 5.00, 6.00, 7.00, 8.00",
"pluralRule-count-other": " @integer 11~26, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~1.9, 2.1~2.7, 10.1, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"si": {
"pluralRule-count-one": "n = 0,1 or i = 0 and f = 1 @integer 0, 1 @decimal 0.0, 0.1, 1.0, 0.00, 0.01, 1.00, 0.000, 0.001, 1.000, 0.0000, 0.0001, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.2~0.9, 1.1~1.8, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sk": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-few": "i = 2..4 and v = 0 @integer 2~4",
"pluralRule-count-many": "v != 0 @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
},
"sl": {
"pluralRule-count-one": "v = 0 and i % 100 = 1 @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, …",
"pluralRule-count-two": "v = 0 and i % 100 = 2 @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, …",
"pluralRule-count-few": "v = 0 and i % 100 = 3..4 or v != 0 @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603, 604, 703, 704, 1003, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …"
},
"sma": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"smi": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"smj": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"smn": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sms": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-two": "n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000",
"pluralRule-count-other": " @integer 0, 3~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sn": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"so": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sq": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sr": {
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1, …",
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, … @decimal 0.2~0.4, 1.2~1.4, 2.2~2.4, 3.2~3.4, 4.2~4.4, 5.2, 10.2, 100.2, 1000.2, …",
"pluralRule-count-other": " @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ss": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ssy": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"st": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"su": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sv": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"sw": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"syr": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ta": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"te": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"teo": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"th": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ti": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"tig": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"tk": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"tl": {
"pluralRule-count-one": "v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9 @integer 0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.3, 0.5, 0.7, 0.8, 1.0~1.3, 1.5, 1.7, 1.8, 2.0, 2.1, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …",
"pluralRule-count-other": " @integer 4, 6, 9, 14, 16, 19, 24, 26, 104, 1004, … @decimal 0.4, 0.6, 0.9, 1.4, 1.6, 1.9, 2.4, 2.6, 10.4, 100.4, 1000.4, …"
},
"tn": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"to": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"tpi": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"tr": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ts": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"tzm": {
"pluralRule-count-one": "n = 0..1 or n = 11..99 @integer 0, 1, 11~24 @decimal 0.0, 1.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0",
"pluralRule-count-other": " @integer 2~10, 100~106, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ug": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"uk": {
"pluralRule-count-one": "v = 0 and i % 10 = 1 and i % 100 != 11 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …",
"pluralRule-count-few": "v = 0 and i % 10 = 2..4 and i % 100 != 12..14 @integer 2~4, 22~24, 32~34, 42~44, 52~54, 62, 102, 1002, …",
"pluralRule-count-many": "v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14 @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …",
"pluralRule-count-other": " @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"und": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ur": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"uz": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"ve": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"vec": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-many": "e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5 @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, … @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6, …",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3, 6c3, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …"
},
"vi": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"vo": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"vun": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"wa": {
"pluralRule-count-one": "n = 0..1 @integer 0, 1 @decimal 0.0, 1.0, 0.00, 1.00, 0.000, 1.000, 0.0000, 1.0000",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"wae": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"wo": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"xh": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"xog": {
"pluralRule-count-one": "n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"yi": {
"pluralRule-count-one": "i = 1 and v = 0 @integer 1",
"pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"yo": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"yue": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"zh": {
"pluralRule-count-other": " @integer 0~15, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
},
"zu": {
"pluralRule-count-one": "i = 0 or n = 1 @integer 0, 1 @decimal 0.0~1.0, 0.00~0.04",
"pluralRule-count-other": " @integer 2~17, 100, 1000, 10000, 100000, 1000000, … @decimal 1.1~2.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …"
}
}
}
}

151
i18n/errori18n/errors.go Normal file
View File

@@ -0,0 +1,151 @@
// Package errori18n adapts public errors to independent i18n dictionaries.
package errori18n
import (
"context"
"fmt"
"reflect"
"sort"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"git.yuntuo.hk/k2safe/go-omni/i18n"
)
type Localizer struct {
bundle *i18n.Bundle
report func(error)
}
// New borrows an immutable Bundle. report is optional, must be concurrency-safe,
// and receives diagnostics without argument values or internal error causes.
func New(bundle *i18n.Bundle, report func(error)) *Localizer {
return &Localizer{bundle: bundle, report: report}
}
func kind(t string) i18n.Kind {
switch t {
case "string":
return i18n.String
case "bool":
return i18n.Boolean
case "int", "int32", "int64":
return i18n.Integer
case "float64":
return i18n.Number
}
return ""
}
// Bind returns a detached catalog, assigning references only to entries which
// do not already have one. Different catalogs can bind to different namespaces.
func Bind(c apierrors.Catalog, namespace string) apierrors.Catalog {
c = c.Clone()
for key, d := range c.Definitions {
if d.Translation == nil {
d.Translation = &apierrors.Translation{Namespace: namespace, Key: key}
c.Definitions[key] = d
}
}
return c
}
// Source exports only explicitly referenced public messages. Codes, statuses,
// examples and causes are not included. Fixed legacy braces remain literal.
func Source(c apierrors.Catalog, locale string) ([]i18n.Document, error) {
r, err := apierrors.NewRegistry(c)
if err != nil {
return nil, err
}
groups := map[string]i18n.Document{}
for _, d := range r.Catalog().Definitions {
if d.Translation == nil {
continue
}
ref := d.Translation
doc, ok := groups[ref.Namespace]
if !ok {
doc = i18n.Document{Version: i18n.Version, Locale: locale, Namespace: ref.Namespace, Messages: map[string]i18n.Message{}}
}
m := i18n.Message{Params: map[string]i18n.Kind{}, Value: i18n.Pattern{Text: d.Message, Literal: d.Params == nil}}
for _, p := range d.Params {
m.Params[p.Name] = kind(p.Type)
}
if prior, exists := doc.Messages[ref.Key]; exists && !reflect.DeepEqual(prior, m) {
return nil, fmt.Errorf("errori18n: conflicting source messages for one translation reference")
}
doc.Messages[ref.Key] = m
groups[ref.Namespace] = doc
}
names := []string{}
for ns := range groups {
names = append(names, ns)
}
sort.Strings(names)
docs := []i18n.Document{}
for _, ns := range names {
docs = append(docs, groups[ns])
}
_, err = i18n.New(i18n.Config{SourceLocale: locale, DefaultLocale: locale, Locales: []string{locale}}, docs...)
return docs, err
}
// Validate can be used during startup or generation to check explicit references
// and type compatibility. A runtime mismatch always preserves the original error.
func (l *Localizer) Validate(c apierrors.Catalog) error {
r, err := apierrors.NewRegistry(c)
if err != nil {
return err
}
for _, d := range r.Catalog().Definitions {
if d.Translation != nil {
if err := l.check(d); err != nil {
return err
}
}
}
return nil
}
func (l *Localizer) check(d apierrors.Definition) error {
ref := d.Translation
if l == nil || l.bundle == nil {
return &i18n.Diagnostic{Kind: "missing", Problem: "error localizer has no bundle"}
}
schema, ok := l.bundle.Schema(i18n.Ref{Namespace: ref.Namespace, Key: ref.Key})
if !ok {
return &i18n.Diagnostic{Kind: "missing", Namespace: ref.Namespace, Key: ref.Key, Problem: "error translation reference is not defined"}
}
expected := map[string]i18n.Kind{}
for _, p := range d.Params {
expected[p.Name] = kind(p.Type)
}
if !reflect.DeepEqual(schema, expected) {
return &i18n.Diagnostic{Kind: "parameter", Namespace: ref.Namespace, Key: ref.Key, Problem: "error parameter schema differs from message schema"}
}
return nil
}
func (l *Localizer) Localize(ctx context.Context, registry *apierrors.Registry, failure *apierrors.APIError) (string, bool) {
if l == nil || failure == nil {
return "", false
}
d, ok := registry.Definition(failure.Code)
if !ok || d.Translation == nil {
return "", false
}
if err := l.check(d); err != nil {
if l.report != nil {
l.report(err)
}
return "", false
}
result, err := l.bundle.RenderContext(ctx, i18n.Ref{Namespace: d.Translation.Namespace, Key: d.Translation.Key}, failure.Parameters())
if err != nil {
if l.report != nil {
l.report(err)
}
return "", false
}
if result.Fallback && l.report != nil {
l.report(&i18n.Diagnostic{Kind: "fallback", Namespace: d.Translation.Namespace, Key: d.Translation.Key, Locale: result.Requested, Problem: "used a fallback dictionary"})
}
return result.Text, true
}

View File

@@ -0,0 +1,54 @@
package errori18n_test
import (
"context"
"errors"
"testing"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"git.yuntuo.hk/k2safe/go-omni/i18n"
"git.yuntuo.hk/k2safe/go-omni/i18n/errori18n"
)
func TestCatalogAdapterIsExplicitAndDetached(t *testing.T) {
c := apierrors.Catalog{Definitions: map[string]apierrors.Definition{"A": {Status: 409, Code: 7, Message: "{value}", Params: []apierrors.Parameter{{Name: "value", Type: "int64"}}}, "LITERAL": {Code: 8, Message: "${HOME}"}}}
bound := errori18n.Bind(c, "errors")
if c.Definitions["A"].Translation != nil {
t.Fatal("mutated source")
}
source, err := errori18n.Source(bound, "en")
if err != nil {
t.Fatal(err)
}
b, err := i18n.New(i18n.Config{SourceLocale: "en", DefaultLocale: "en", Locales: []string{"en"}}, source...)
if err != nil {
t.Fatal(err)
}
l := errori18n.New(b, nil)
if err := l.Validate(bound); err != nil {
t.Fatal(err)
}
r := apierrors.MustRegistry(bound)
bound.Definitions["A"].Translation.Namespace = "changed"
metadata, _ := r.Definition("A")
metadata.Translation.Namespace = "changed"
metadata.Params[0].Type = "bool"
private := errors.New("private")
failure := r.Resolve(apierrors.ByCode("A").WithParams(map[string]any{"value": int64(9223372036854775807)}).WithCause(private))
message, ok := l.Localize(context.Background(), r, failure)
if !ok || message != "9223372036854775807" || failure.Status != 409 || !errors.Is(failure, private) {
t.Fatal(message, ok, failure)
}
if _, ok := l.Localize(context.Background(), apierrors.MustRegistry(c), failure); ok {
t.Fatal("implicit translation")
}
missing := errori18n.New(nil, nil)
if _, ok := missing.Localize(context.Background(), r, failure); ok {
t.Fatal("missing bundle translated")
}
args := failure.Parameters()
args["value"] = int64(0)
if failure.Parameters()["value"] != int64(9223372036854775807) {
t.Fatal("mutable parameters")
}
}

123
i18n/export.go Normal file
View File

@@ -0,0 +1,123 @@
package i18n
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"golang.org/x/text/language"
)
// Pack is a self-contained browser artifact for one selected language. Its
// documents include the entire configured fallback chain and parameter schemas.
type Pack struct {
Version int `json:"version"`
Locale string `json:"locale"`
Config Config `json:"config"`
Documents []Document `json:"documents"`
RuleVersion string `json:"ruleVersion"`
PluralRules map[string]map[string]string `json:"pluralRules"`
License string `json:"license"`
}
func (b *Bundle) namespaces(names []string) error {
if b == nil {
return fmt.Errorf("i18n: bundle required")
}
if len(names) == 0 {
return fmt.Errorf("i18n: export requires explicit namespaces")
}
known := map[string]bool{}
for ref := range b.entries {
known[ref.Namespace] = true
}
seen := map[string]bool{}
for _, ns := range names {
if !known[ns] || seen[ns] {
return fmt.Errorf("i18n: export namespace is unknown or duplicated: %s", ns)
}
seen[ns] = true
}
return nil
}
func (b *Bundle) Export(w io.Writer, locale string, namespaces ...string) error {
if w == nil {
return fmt.Errorf("i18n: writer required")
}
if err := b.namespaces(namespaces); err != nil {
return err
}
l, err := canonical(locale)
if err != nil {
return err
}
if _, ok := b.tags[l]; !ok {
return fmt.Errorf("i18n: export locale must be enabled")
}
docs := []Document{}
for _, candidate := range b.chain(l) {
docs = append(docs, b.Documents(candidate, namespaces...)...)
}
// Source is always needed by clients even if absent from an explicit chain.
rules := map[string]map[string]string{}
for _, locale := range b.chain(l) {
rules[locale] = rulesFor(language.MustParse(locale)).text
}
pack := Pack{Version: Version, Locale: l, Config: b.Config(), Documents: docs, RuleVersion: PluralRulesVersion, PluralRules: rules, License: unicodeLicense}
var buffer bytes.Buffer
enc := json.NewEncoder(&buffer)
enc.SetIndent("", " ")
if err := enc.Encode(pack); err != nil {
return err
}
if buffer.Len() > 4<<20 {
return fmt.Errorf("i18n: exported pack exceeds 4 MiB; split namespaces")
}
_, err = io.Copy(w, &buffer)
return err
}
// TypeScript emits nested namespace/key types, so keys containing punctuation
// never collide through concatenation or Go-specific identifier conversion.
func (b *Bundle) TypeScript(w io.Writer, namespaces ...string) error {
if w == nil {
return fmt.Errorf("i18n: writer required")
}
if err := b.namespaces(namespaces); err != nil {
return err
}
var out strings.Builder
out.WriteString("// Generated by go-omni i18n types. Do not edit.\nexport interface MessageParams {\n")
for _, doc := range b.Documents(b.config.SourceLocale, namespaces...) {
fmt.Fprintf(&out, " %s: {\n", strconv.Quote(doc.Namespace))
for _, key := range sortedKeys(doc.Messages) {
m := doc.Messages[key]
if len(m.Params) == 0 {
fmt.Fprintf(&out, " %s: Record<string, never>;\n", strconv.Quote(key))
continue
}
fmt.Fprintf(&out, " %s: {", strconv.Quote(key))
for _, name := range sortedKeys(m.Params) {
typ := "string"
switch m.Params[name] {
case Boolean:
typ = "boolean"
case Integer:
typ = "number | bigint | `${bigint}`"
case Number:
typ = "number"
}
fmt.Fprintf(&out, " %s: %s;", strconv.Quote(name), typ)
}
out.WriteString(" };\n")
}
out.WriteString(" };\n")
}
out.WriteString("}\nexport type Namespace = keyof MessageParams;\nexport type Translate = <N extends Namespace, K extends Extract<keyof MessageParams[N], string>>(namespace: N, key: K, params: Extract<MessageParams[N][K], Record<string, unknown>>) => string;\n")
_, err := io.WriteString(w, out.String())
return err
}

131
i18n/httpi18n/http.go Normal file
View File

@@ -0,0 +1,131 @@
// Package httpi18n negotiates an i18n language for standard HTTP requests.
package httpi18n
import (
"fmt"
"net/http"
"strings"
"git.yuntuo.hk/k2safe/go-omni/i18n"
"golang.org/x/text/language"
)
// Resolver supplies ordered language tags, e.g. from an authenticated preference.
// It must be safe for concurrent requests. It does not receive a mutable Bundle.
// A panic is treated as no preference, preserving request execution.
type Resolver func(*http.Request) []string
type Option func(*Policy) error
type Policy struct {
bundle *i18n.Bundle
resolver Resolver
vary []string
private bool
}
// WithResolver overrides the default Accept-Language resolver. Cache variation
// is explicit. With no varying headers, responses are marked private.
func WithResolver(resolver Resolver, varyHeaders ...string) Option {
return func(p *Policy) error {
if resolver == nil {
return fmt.Errorf("httpi18n: nil resolver")
}
p.resolver = resolver
p.vary = nil
p.private = len(varyHeaders) == 0
for _, h := range varyHeaders {
if !headerName(h) {
return fmt.Errorf("httpi18n: invalid Vary header")
}
p.vary = append(p.vary, http.CanonicalHeaderKey(h))
}
return nil
}
}
func headerName(s string) bool {
if s == "" {
return false
}
for _, c := range s {
if c > 127 || !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.ContainsRune("!#$%&'*+-.^_`|~", c)) {
return false
}
}
return true
}
func New(bundle *i18n.Bundle, options ...Option) (*Policy, error) {
if bundle == nil {
return nil, fmt.Errorf("httpi18n: bundle required")
}
p := &Policy{bundle: bundle, vary: []string{"Accept-Language"}, resolver: func(r *http.Request) []string {
header := r.Header.Get("Accept-Language")
if len(header) > 8192 {
return nil
}
tags, _, err := language.ParseAcceptLanguage(header)
if err != nil {
return nil
}
out := []string{}
for _, tag := range tags {
if len(out) == 64 {
break
}
out = append(out, tag.String())
}
return out
}}
for _, o := range options {
if o == nil {
return nil, fmt.Errorf("httpi18n: nil option")
}
if err := o(p); err != nil {
return nil, err
}
}
return p, nil
}
// Apply installs the negotiated language before application middleware. An
// explicit context preference takes priority and makes the response private.
// It does not claim Content-Language: individual messages may use fallbacks.
func (p *Policy) Apply(w http.ResponseWriter, r *http.Request) *http.Request {
if p == nil {
return r
}
preferences := []string{}
explicit := i18n.Locale(r.Context())
if explicit != "" {
preferences = append(preferences, explicit)
}
preferences = append(preferences, p.resolve(r)...)
for _, h := range p.vary {
addVary(w.Header(), h)
}
if p.private || explicit != "" {
w.Header().Add("Cache-Control", "private")
}
return r.WithContext(i18n.WithLocale(r.Context(), p.bundle.Match(preferences...)))
}
func (p *Policy) resolve(r *http.Request) (preferences []string) {
defer func() {
if recover() != nil {
preferences = nil
}
}()
return p.resolver(r)
}
func addVary(h http.Header, value string) {
for _, line := range h.Values("Vary") {
for _, v := range strings.Split(line, ",") {
if strings.EqualFold(strings.TrimSpace(v), value) || strings.TrimSpace(v) == "*" {
return
}
}
}
h.Add("Vary", value)
}
func (p *Policy) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, p.Apply(w, r)) })
}

View File

@@ -0,0 +1,59 @@
package httpi18n_test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.yuntuo.hk/k2safe/go-omni/i18n"
"git.yuntuo.hk/k2safe/go-omni/i18n/httpi18n"
)
func TestNegotiationAndExplicitResolvers(t *testing.T) {
b, err := i18n.New(i18n.Config{SourceLocale: "en", DefaultLocale: "en", Locales: []string{"en", "fr", "zh-Hans", "zh-Hant"}})
if err != nil {
t.Fatal(err)
}
p, err := httpi18n.New(b)
if err != nil {
t.Fatal(err)
}
for _, test := range []struct{ header, explicit, want string }{{"fr;q=0.8,en;q=0.1", "", "fr"}, {"fr;q=0,en;q=1", "", "en"}, {"zh-TW", "", "zh-Hant"}, {"zh-CN", "", "zh-Hans"}, {"invalid;garbage", "", "en"}, {strings.Repeat("x", 8193), "", "en"}, {"fr", "en", "en"}} {
r := httptest.NewRequest("GET", "/", nil)
r.Header.Set("Accept-Language", test.header)
if test.explicit != "" {
r = r.WithContext(i18n.WithLocale(r.Context(), test.explicit))
}
w := httptest.NewRecorder()
w.Header().Set("Vary", "Origin")
p.Wrap(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
if got := i18n.Locale(r.Context()); got != test.want {
t.Errorf("got %s want %s", got, test.want)
}
})).ServeHTTP(w, r)
if strings.Join(w.Header().Values("Vary"), ",") != "Origin,Accept-Language" {
t.Fatal(w.Header())
}
if test.explicit != "" && !strings.Contains(w.Header().Get("Cache-Control"), "private") {
t.Fatal(w.Header())
}
}
custom, err := httpi18n.New(b, httpi18n.WithResolver(func(*http.Request) []string { return []string{"fr"} }))
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
r := custom.Apply(w, httptest.NewRequest("GET", "/", nil))
if i18n.Locale(r.Context()) != "fr" || w.Header().Get("Cache-Control") != "private" {
t.Fatal(r, w.Header())
}
broken, err := httpi18n.New(b, httpi18n.WithResolver(func(*http.Request) []string { panic("preference lookup failed") }))
if err != nil {
t.Fatal(err)
}
r = broken.Apply(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))
if i18n.Locale(r.Context()) != "en" {
t.Fatal("resolver panic must preserve default selection")
}
}

122
i18n/i18ntool/extract.go Normal file
View File

@@ -0,0 +1,122 @@
// Package i18ntool provides conservative, syntax-only Go key extraction.
package i18ntool
import (
"go/ast"
"go/parser"
"go/token"
"strconv"
)
type Reference struct {
Namespace string `json:"namespace"`
Key string `json:"key"`
File string `json:"file"`
Line int `json:"line"`
}
type Finding struct {
File string `json:"file"`
Line int `json:"line"`
Problem string `json:"problem"`
}
type Extraction struct {
References []Reference `json:"references"`
Findings []Finding `json:"findings"`
}
// ExtractGo reads literal i18n.Ref expressions, including import aliases. It
// neither executes project code nor claims to resolve dynamic keys or wrappers.
// Render/RenderContext calls without a literal reference are reported for review.
func ExtractGo(filename string, source []byte) (Extraction, error) {
out := Extraction{References: []Reference{}, Findings: []Finding{}}
fs := token.NewFileSet()
file, err := parser.ParseFile(fs, filename, source, 0)
if err != nil {
return out, err
}
aliases := map[string]bool{}
for _, imp := range file.Imports {
path, _ := strconv.Unquote(imp.Path.Value)
if path == "git.yuntuo.hk/k2safe/go-omni/i18n" {
name := "i18n"
if imp.Name != nil {
name = imp.Name.Name
}
if name == "." {
out.Findings = append(out.Findings, Finding{filename, fs.Position(imp.Pos()).Line, "dot import requires manual extraction"})
} else {
aliases[name] = true
}
}
}
if len(aliases) == 0 {
return out, nil
}
ast.Inspect(file, func(n ast.Node) bool {
lit, ok := n.(*ast.CompositeLit)
if !ok {
return true
}
sel, ok := lit.Type.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Ref" {
return true
}
pkg, ok := sel.X.(*ast.Ident)
if !ok || !aliases[pkg.Name] {
return true
}
values := map[string]string{}
valid := true
for i, el := range lit.Elts {
key := ""
value := el
if kv, ok := el.(*ast.KeyValueExpr); ok {
id, ok := kv.Key.(*ast.Ident)
if !ok {
valid = false
continue
}
key = id.Name
value = kv.Value
} else if i == 0 {
key = "Namespace"
} else if i == 1 {
key = "Key"
} else {
valid = false
}
s, ok := value.(*ast.BasicLit)
if !ok || s.Kind != token.STRING {
valid = false
continue
}
v, e := strconv.Unquote(s.Value)
if e != nil {
valid = false
}
values[key] = v
}
line := fs.Position(lit.Pos()).Line
if valid && values["Namespace"] != "" && values["Key"] != "" {
out.References = append(out.References, Reference{values["Namespace"], values["Key"], filename, line})
} else {
out.Findings = append(out.Findings, Finding{filename, line, "dynamic or incomplete message reference"})
}
return true
})
ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Render" && sel.Sel.Name != "RenderContext" || len(call.Args) != 3 {
return true
}
if _, ok := call.Args[1].(*ast.CompositeLit); !ok {
out.Findings = append(out.Findings, Finding{filename, fs.Position(call.Pos()).Line, "nonliteral Render reference; verify manually (syntax-only extraction)"})
}
return true
})
return out, nil
}

View File

@@ -0,0 +1,14 @@
package i18ntool_test
import (
"git.yuntuo.hk/k2safe/go-omni/i18n/i18ntool"
"testing"
)
func TestAliasesLiteralsAndDynamicDiagnostics(t *testing.T) {
source := []byte("package demo\nimport tr \"git.yuntuo.hk/k2safe/go-omni/i18n\"\nfunc f(){ b.Render(\"en\",tr.Ref{Namespace:\"forms\",Key:\"button\"},nil); b.Render(\"en\",dynamic,nil); _=tr.Ref{Namespace:\"forms\",Key:name} }\n")
result, err := i18ntool.ExtractGo("demo.go", source)
if err != nil || len(result.References) != 1 || len(result.Findings) != 2 || result.References[0].Namespace != "forms" {
t.Fatal(result, err)
}
}

143
i18n/load.go Normal file
View File

@@ -0,0 +1,143 @@
package i18n
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/fs"
"unicode/utf8"
)
// DecodeJSON rejects unknown fields, duplicate keys and excessive nesting.
// It never expands environment references or evaluates message text.
func DecodeJSON(data []byte, dst any) error {
if len(data) > 1<<20 || !utf8.Valid(data) {
return fmt.Errorf("i18n: document exceeds 1 MiB or is not UTF-8")
}
scan := json.NewDecoder(bytes.NewReader(data))
scan.UseNumber()
nodes := 0
var walk func(int) error
walk = func(depth int) error {
nodes++
if depth > 32 || nodes > 100000 {
return fmt.Errorf("i18n: JSON nesting or node limit exceeded")
}
t, err := scan.Token()
if err != nil {
return fmt.Errorf("i18n: invalid JSON")
}
d, ok := t.(json.Delim)
if !ok {
return nil
}
switch d {
case '{':
seen := map[string]bool{}
for scan.More() {
k, e := scan.Token()
if e != nil {
return fmt.Errorf("i18n: invalid JSON object")
}
key, ok := k.(string)
if !ok || seen[key] {
return fmt.Errorf("i18n: duplicate JSON key")
}
seen[key] = true
if err := walk(depth + 1); err != nil {
return err
}
}
case '[':
for scan.More() {
if err := walk(depth + 1); err != nil {
return err
}
}
default:
return fmt.Errorf("i18n: invalid JSON delimiter")
}
_, err = scan.Token()
return err
}
if err := walk(0); err != nil {
return err
}
if _, err := scan.Token(); err != io.EOF {
return fmt.Errorf("i18n: expected one JSON value")
}
d := json.NewDecoder(bytes.NewReader(data))
d.UseNumber()
d.DisallowUnknownFields()
if err := d.Decode(dst); err != nil {
return fmt.Errorf("i18n: invalid document schema")
}
return nil
}
// LoadFS reads exactly the explicitly listed files; it never scans directories.
func LoadFS(source fs.FS, paths ...string) ([]Document, error) {
if source == nil || len(paths) > 1024 {
return nil, fmt.Errorf("i18n: filesystem required; at most 1024 files")
}
docs := make([]Document, 0, len(paths))
total := 0
for _, path := range paths {
if !fs.ValidPath(path) {
return nil, fmt.Errorf("i18n: invalid resource path")
}
f, err := source.Open(path)
if err != nil {
return nil, fmt.Errorf("i18n: cannot open %s: %w", path, err)
}
info, err := f.Stat()
if err != nil || !info.Mode().IsRegular() {
f.Close()
return nil, fmt.Errorf("i18n: resource must be a regular file: %s", path)
}
data, err := io.ReadAll(io.LimitReader(f, (1<<20)+1))
f.Close()
if err != nil {
return nil, err
}
total += len(data)
if total > 4<<20 {
return nil, fmt.Errorf("i18n: resources exceed 4 MiB")
}
var doc Document
if err := DecodeJSON(data, &doc); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
docs = append(docs, doc)
}
return docs, nil
}
// Overlay replaces whole messages, never individual branches or parameter
// schemas. The returned document is detached and must still pass New validation.
func Overlay(base, override Document) (Document, error) {
l1, e1 := canonical(base.Locale)
l2, e2 := canonical(override.Locale)
if e1 != nil || e2 != nil || l1 != l2 || base.Namespace != override.Namespace || base.Version != Version || override.Version != Version {
return Document{}, fmt.Errorf("i18n: overlay requires matching version, locale and namespace")
}
// A bounded JSON round trip also rejects cyclic Go values before cloning.
data, err := json.Marshal([]Document{base, override})
if err != nil || len(data) > 1<<20 {
return Document{}, fmt.Errorf("i18n: invalid or oversized overlay")
}
var docs []Document
if err := DecodeJSON(data, &docs); err != nil {
return Document{}, err
}
out := docs[0]
out.Locale = l1
if out.Messages == nil {
out.Messages = map[string]Message{}
}
for key, m := range docs[1].Messages {
out.Messages[key] = m
}
return out, nil
}

221
i18n/pattern.go Normal file
View File

@@ -0,0 +1,221 @@
package i18n
import (
"encoding/json"
"math"
"reflect"
"regexp"
"strconv"
"strings"
"unicode/utf8"
"golang.org/x/text/language"
)
var paramName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.-]*$`)
var integerText = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)
type part struct{ text, param string }
type compiled struct {
parts []part
selectKey, pluralKey string
cases map[string]*compiled
}
type budget struct{ nodes, bytes int }
func compile(p Pattern, schema map[string]Kind, used map[string]bool, b *budget, depth int) (*compiled, error) {
b.nodes++
b.bytes += len(p.Text)
if depth > 24 || b.nodes > 100000 || b.bytes > 4<<20 || len(p.Text) > 64<<10 || !utf8.ValidString(p.Text) {
return nil, &Diagnostic{Kind: "resource", Problem: "pattern exceeds limits or contains invalid UTF-8"}
}
c := &compiled{selectKey: p.Select, pluralKey: p.Plural}
if p.Select != "" || p.Plural != "" {
if p.Select != "" && p.Plural != "" || p.Text != "" || p.Literal || len(p.Cases) == 0 || len(p.Cases) > 128 {
return nil, &Diagnostic{Kind: "pattern", Problem: "expected one select/plural branch with cases"}
}
key := p.Select
if key == "" {
key = p.Plural
}
kind, ok := schema[key]
if !ok || p.Plural != "" && kind != Integer && kind != Number || p.Select != "" && kind != String && kind != Boolean {
return nil, &Diagnostic{Kind: "parameter", Param: key, Problem: "branch requires a declared parameter of the appropriate type"}
}
used[key] = true
if _, ok := p.Cases["other"]; !ok {
return nil, &Diagnostic{Kind: "pattern", Problem: "branch requires other"}
}
c.cases = make(map[string]*compiled, len(p.Cases))
for _, name := range sortedKeys(p.Cases) {
if !validID(name) {
return nil, &Diagnostic{Kind: "pattern", Problem: "invalid case name"}
}
if p.Plural != "" && !pluralCase(name) {
return nil, &Diagnostic{Kind: "pattern", Problem: "invalid plural category or exact integer case"}
}
child, err := compile(p.Cases[name], schema, used, b, depth+1)
if err != nil {
return nil, err
}
c.cases[name] = child
}
return c, nil
}
if p.Cases != nil {
return nil, &Diagnostic{Kind: "pattern", Problem: "cases require select or plural"}
}
if p.Literal {
c.parts = []part{{text: p.Text}}
return c, nil
}
var literal strings.Builder
flush := func() {
if literal.Len() > 0 {
c.parts = append(c.parts, part{text: literal.String()})
literal.Reset()
}
}
for i := 0; i < len(p.Text); {
ch := p.Text[i]
if ch != '{' && ch != '}' {
literal.WriteByte(ch)
i++
continue
}
if i+1 < len(p.Text) && p.Text[i+1] == ch {
literal.WriteByte(ch)
i += 2
continue
}
if ch == '}' {
return nil, &Diagnostic{Kind: "pattern", Problem: "unescaped closing brace"}
}
end := strings.IndexByte(p.Text[i+1:], '}')
if end < 0 {
return nil, &Diagnostic{Kind: "pattern", Problem: "unclosed placeholder"}
}
name := p.Text[i+1 : i+1+end]
if _, ok := schema[name]; !ok {
return nil, &Diagnostic{Kind: "parameter", Param: name, Problem: "undeclared placeholder"}
}
used[name] = true
flush()
c.parts = append(c.parts, part{param: name})
i += end + 2
}
flush()
return c, nil
}
func pluralCase(s string) bool {
switch s {
case "zero", "one", "two", "few", "many", "other":
return true
}
if !strings.HasPrefix(s, "=") {
return false
}
n, err := strconv.ParseInt(s[1:], 10, 64)
return err == nil && strconv.FormatInt(n, 10) == s[1:]
}
func scalar(kind Kind, value any) (string, error) {
bad := func() (string, error) {
return "", &Diagnostic{Kind: "parameter", Problem: "wrong type, range, or non-finite value"}
}
if value == nil {
return bad()
}
v := reflect.ValueOf(value)
switch kind {
case String:
if v.Kind() == reflect.String && v.Type() != reflect.TypeOf(json.Number("")) && utf8.ValidString(v.String()) && len(v.String()) <= 64<<10 {
return v.String(), nil
}
case Boolean:
if v.Kind() == reflect.Bool {
return strconv.FormatBool(v.Bool()), nil
}
case Integer:
if n, ok := value.(json.Number); ok {
x, e := n.Int64()
if e == nil {
return strconv.FormatInt(x, 10), nil
}
return bad()
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if v.Uint() <= math.MaxInt64 {
return strconv.FormatUint(v.Uint(), 10), nil
}
}
case Number:
var n float64
if j, ok := value.(json.Number); ok {
var e error
n, e = j.Float64()
if e != nil {
return bad()
}
} else {
switch v.Kind() {
case reflect.Float32, reflect.Float64:
n = v.Float()
default:
return bad()
}
}
if !math.IsNaN(n) && !math.IsInf(n, 0) {
if n == 0 {
return "0", nil
}
// ECMAScript's plain/scientific thresholds, shared with the browser runtime.
format := byte('f')
if math.Abs(n) < 1e-6 || math.Abs(n) >= 1e21 {
format = 'e'
}
s := strconv.FormatFloat(n, format, -1, 64)
s = strings.ReplaceAll(strings.ReplaceAll(s, "e-0", "e-"), "e+0", "e+")
return s, nil
}
}
return bad()
}
func (c *compiled) render(tag language.Tag, values map[string]string) (string, error) {
if c.cases != nil {
key := values[c.selectKey]
if c.pluralKey != "" {
n := values[c.pluralKey]
key = "=" + n
if _, ok := c.cases[key]; !ok {
var err error
key, err = category(tag, n)
if err != nil {
return "", err
}
}
}
next := c.cases[key]
if next == nil {
next = c.cases["other"]
}
return next.render(tag, values)
}
var out strings.Builder
for _, p := range c.parts {
value := p.text
if p.param != "" {
value = values[p.param]
}
if out.Len()+len(value) > 1<<20 {
return "", &Diagnostic{Kind: "resource", Problem: "rendered message exceeds 1 MiB"}
}
out.WriteString(value)
}
return out.String(), nil
}

196
i18n/plural.go Normal file
View File

@@ -0,0 +1,196 @@
package i18n
import (
_ "embed"
"encoding/json"
"fmt"
"math/big"
"regexp"
"strconv"
"strings"
"golang.org/x/text/language"
)
// PluralRulesVersion pins both runtimes to the same Unicode data. Changing it
// is a presentation compatibility change, independent of message format Version.
const PluralRulesVersion = "cldr-46.1"
//go:embed cldr/plurals.json
var pluralData []byte
//go:embed cldr/LICENSE
var unicodeLicense string
type pluralRange struct{ lo, hi *big.Int }
type pluralCondition struct {
operand byte
modulus *big.Int
negate bool
ranges []pluralRange
}
type pluralRule [][]pluralCondition // OR of AND groups, per the CLDR grammar.
type pluralSet struct {
text map[string]string
rules map[string]pluralRule
}
var pluralConditionSyntax = regexp.MustCompile(`^([nivwftec])(?: % ([1-9][0-9]*))? (!=|=) ([0-9]+(?:\.\.[0-9]+)?(?:,\s*[0-9]+(?:\.\.[0-9]+)?)*)$`)
var cardinalRules = loadCardinalRules()
func bigDecimal(s string) *big.Int {
if s == "" {
return new(big.Int)
}
n, ok := new(big.Int).SetString(s, 10)
if !ok {
panic("i18n: invalid embedded plural integer")
}
return n
}
func parsePluralRule(text string) (pluralRule, error) {
var result pluralRule
if len(text) > 2048 {
return nil, fmt.Errorf("plural rule too large")
}
for _, or := range strings.Split(text, " or ") {
var group []pluralCondition
for _, and := range strings.Split(or, " and ") {
parts := pluralConditionSyntax.FindStringSubmatch(strings.TrimSpace(and))
if parts == nil {
return nil, fmt.Errorf("invalid CLDR plural condition")
}
c := pluralCondition{operand: parts[1][0], negate: parts[3] == "!="}
if parts[2] != "" {
c.modulus = bigDecimal(parts[2])
}
for _, r := range strings.Split(parts[4], ",") {
lo, hi, has := strings.Cut(strings.TrimSpace(r), "..")
if !has {
hi = lo
}
low, high := bigDecimal(lo), bigDecimal(hi)
if low.Cmp(high) > 0 {
return nil, fmt.Errorf("reversed plural range")
}
c.ranges = append(c.ranges, pluralRange{low, high})
}
group = append(group, c)
}
result = append(result, group)
}
return result, nil
}
func loadCardinalRules() map[string]pluralSet {
var data struct {
Supplemental struct {
Rules map[string]map[string]string `json:"plurals-type-cardinal"`
} `json:"supplemental"`
}
if err := json.Unmarshal(pluralData, &data); err != nil {
panic("i18n: invalid embedded CLDR data")
}
out := map[string]pluralSet{}
for locale, entries := range data.Supplemental.Rules {
set := pluralSet{text: map[string]string{}, rules: map[string]pluralRule{}}
for name, raw := range entries {
category := strings.TrimPrefix(name, "pluralRule-count-")
text, _, _ := strings.Cut(raw, "@")
text = strings.TrimSpace(text)
if category == "other" {
continue
}
rule, err := parsePluralRule(text)
if err != nil {
panic("i18n: unsupported embedded CLDR rule for " + locale)
}
set.text[category] = text
set.rules[category] = rule
}
out[strings.ReplaceAll(locale, "_", "-")] = set
}
return out
}
func rulesFor(tag language.Tag) pluralSet {
for {
if rules, ok := cardinalRules[tag.String()]; ok {
return rules
}
if tag == language.Und {
break
}
tag = tag.Parent()
}
return pluralSet{text: map[string]string{}, rules: map[string]pluralRule{}}
}
// operands expands the canonical numeric string exactly, avoiding any float
// conversion for int64 and any browser rounding of tiny fractional operands.
func operands(text string) (map[byte]*big.Int, bool) {
text = strings.TrimPrefix(text, "-")
mantissa, exponent, has := strings.Cut(text, "e")
shift := 0
if has {
shift, _ = strconv.Atoi(exponent)
}
whole, fraction, _ := strings.Cut(mantissa, ".")
digits := whole + fraction
point := len(whole) + shift
switch {
case point <= 0:
whole = "0"
fraction = strings.Repeat("0", -point) + digits
case point >= len(digits):
whole = digits + strings.Repeat("0", point-len(digits))
fraction = ""
default:
whole = digits[:point]
fraction = digits[point:]
}
tail := strings.TrimRight(fraction, "0")
i, f := bigDecimal(whole), bigDecimal(fraction)
return map[byte]*big.Int{'n': i, 'i': i, 'v': big.NewInt(int64(len(fraction))), 'w': big.NewInt(int64(len(tail))), 'f': f, 't': bigDecimal(tail), 'c': new(big.Int), 'e': new(big.Int)}, f.Sign() != 0
}
func (rule pluralRule) matches(values map[byte]*big.Int, fractional bool) bool {
for _, group := range rule {
all := true
for _, condition := range group {
value := values[condition.operand]
if condition.modulus != nil {
value = new(big.Int).Mod(value, condition.modulus)
}
found := false
if condition.operand != 'n' || !fractional {
for _, r := range condition.ranges {
if value.Cmp(r.lo) >= 0 && value.Cmp(r.hi) <= 0 {
found = true
break
}
}
}
if condition.negate {
found = !found
}
if !found {
all = false
break
}
}
if all {
return true
}
}
return false
}
func category(tag language.Tag, text string) (string, error) {
values, fractional := operands(text)
rules := rulesFor(tag).rules
for _, category := range []string{"zero", "one", "two", "few", "many"} {
if rule, ok := rules[category]; ok && rule.matches(values, fractional) {
return category, nil
}
}
return "other", nil
}

61
i18n/plural_test.go Normal file
View File

@@ -0,0 +1,61 @@
package i18n
import (
"encoding/json"
"strings"
"testing"
)
func TestPinnedCLDRSamples(t *testing.T) {
var data struct {
Supplemental struct {
Rules map[string]map[string]string `json:"plurals-type-cardinal"`
} `json:"supplemental"`
}
if err := json.Unmarshal(pluralData, &data); err != nil {
t.Fatal(err)
}
count := 0
for locale, entries := range data.Supplemental.Rules {
for name, raw := range entries {
want := strings.TrimPrefix(name, "pluralRule-count-")
for _, section := range strings.Split(raw, "@")[1:] {
_, samples, ok := strings.Cut(section, " ")
if !ok {
continue
}
for _, sample := range strings.Split(samples, ",") {
for _, number := range strings.Split(strings.TrimSpace(sample), "~") {
if number == "" || strings.ContainsAny(number, "…ce") {
continue
}
values, fractional := operands(number)
got := "other"
for _, cat := range []string{"zero", "one", "two", "few", "many"} {
if rule, ok := cardinalRules[locale].rules[cat]; ok && rule.matches(values, fractional) {
got = cat
break
}
}
count++
if got != want {
t.Errorf("%s %s: got %s want %s", locale, number, got, want)
}
}
}
}
}
}
if count < 5000 {
t.Fatalf("too few CLDR samples: %d", count)
}
t.Logf("validated %d upstream cardinal samples", count)
}
func TestRuleParserRejectsUnsupportedSyntax(t *testing.T) {
for _, text := range []string{"n = 1; run()", "n % 0 = 1", "n = 5..1", "unknown = 1"} {
if _, err := parsePluralRule(text); err == nil {
t.Fatal(text)
}
}
}

120
i18n/types.go Normal file
View File

@@ -0,0 +1,120 @@
// Package i18n provides immutable, instance-scoped message catalogs. It has no
// dependency on HTTP, application configuration, business errors or storage.
package i18n
import (
"context"
"encoding/json"
"fmt"
)
const Version = 1
// Kind describes portable message arguments, independently of Go identifiers.
type Kind string
const (
String Kind = "string"
Boolean Kind = "boolean"
Integer Kind = "integer" // Signed int64; browser callers may use bigint/string.
Number Kind = "number" // Finite IEEE-754 float64.
)
// Pattern is a text leaf or a select/plural branch. Branches require "other".
// Text uses {name}; {{ and }} escape braces. Literal disables interpolation.
// Plural is cardinal, with optional exact integer cases such as "=0".
type Pattern struct {
Text string `json:"text,omitempty"`
Literal bool `json:"literal,omitempty"`
Select string `json:"select,omitempty"`
Plural string `json:"plural,omitempty"`
Cases map[string]Pattern `json:"cases,omitempty"`
}
type Message struct {
Params map[string]Kind `json:"params,omitempty"`
Value Pattern `json:"value"`
}
func (m *Message) UnmarshalJSON(data []byte) error {
type plain Message
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
if value, ok := raw["value"]; !ok || string(value) == "null" {
return fmt.Errorf("i18n: message requires a value")
}
for key := range raw {
if key != "value" && key != "params" {
return fmt.Errorf("i18n: unknown message field")
}
}
var value plain
// Pattern's fields are checked by the strict decoder, including nested cases.
if err := DecodeJSON(data, &value); err != nil {
return err
}
*m = Message(value)
return nil
}
// Document is the versioned interchange format. Source-locale documents own
// parameter schemas; translations inherit them and may repeat identical schemas.
type Document struct {
Version int `json:"version"`
Namespace string `json:"namespace"`
Locale string `json:"locale"`
Messages map[string]Message `json:"messages"`
}
// Config distinguishes source text from the default selection and enabled locales.
// Fallbacks are explicit, ordered edges; cycles are rejected. The default locale
// must be enabled. The source locale can remain a fallback-only language.
type Config struct {
SourceLocale string `json:"sourceLocale"`
DefaultLocale string `json:"defaultLocale"`
Locales []string `json:"locales"`
Fallbacks map[string][]string `json:"fallbacks,omitempty"`
}
type Ref struct{ Namespace, Key string }
// Result reports the actual dictionary language, which may differ from Requested.
type Result struct {
Text string
Requested string
Locale string
Fallback bool
}
// Diagnostic deliberately excludes argument values and message contents.
type Diagnostic struct {
Kind string `json:"kind"`
Namespace string `json:"namespace,omitempty"`
Key string `json:"key,omitempty"`
Locale string `json:"locale,omitempty"`
Param string `json:"param,omitempty"`
Problem string `json:"problem"`
}
func (d *Diagnostic) Error() string {
return fmt.Sprintf("i18n: %s namespace=%q key=%q locale=%q param=%q: %s", d.Kind, d.Namespace, d.Key, d.Locale, d.Param, d.Problem)
}
type localeKey struct{}
// WithLocale records a preference; rendering still matches it against the bundle.
func WithLocale(ctx context.Context, locale string) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, localeKey{}, locale)
}
func Locale(ctx context.Context) string {
if ctx == nil {
return ""
}
value, _ := ctx.Value(localeKey{}).(string)
return value
}