@markuplint/i18n 4.7.0 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,10 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [4.7.1](https://github.com/markuplint/markuplint/compare/@markuplint/i18n@4.7.0...@markuplint/i18n@4.7.1) (2026-02-10)
7
+
8
+ **Note:** Version bump only for package @markuplint/i18n
9
+
6
10
  # [4.7.0](https://github.com/markuplint/markuplint/compare/@markuplint/i18n@4.6.0...@markuplint/i18n@4.7.0) (2025-08-13)
7
11
 
8
12
  ### Bug Fixes
package/SKILL.md ADDED
@@ -0,0 +1,73 @@
1
+ ---
2
+ description: Maintenance tasks for @markuplint/i18n — internationalization for markuplint
3
+ globs:
4
+ - packages/@markuplint/i18n/src/**/*.ts
5
+ - packages/@markuplint/i18n/locales/*.json
6
+ - packages/@markuplint/i18n/$schema.json
7
+ alwaysApply: false
8
+ ---
9
+
10
+ # @markuplint/i18n Maintenance
11
+
12
+ You are maintaining `@markuplint/i18n`, the internationalization package for markuplint.
13
+
14
+ ## Architecture
15
+
16
+ See [README.md](README.md) for the full API documentation including translator usage, placeholder syntax, and locale formatting.
17
+
18
+ For detailed maintenance procedures, see [docs/maintenance.md](docs/maintenance.md) ([Japanese](docs/maintenance.ja.md)).
19
+
20
+ ## Key Files
21
+
22
+ | File | Role |
23
+ | ------------------- | ---------------------------------------------------------------------------------------------- |
24
+ | `locales/ja.json` | Japanese locale dictionary (keywords + sentences + listFormat) |
25
+ | `locales/en.json` | English locale dictionary (minimal; only entries needing capitalization or special formatting) |
26
+ | `$schema.json` | JSON Schema for locale files (`additionalProperties: false`) |
27
+ | `src/translator.ts` | Core translation logic |
28
+ | `src/types.ts` | `LocaleSet`, `ListFormat`, `Translator` types |
29
+
30
+ ## Tasks
31
+
32
+ ### add-keyword
33
+
34
+ Add a new keyword used in rule messages.
35
+
36
+ 1. Add to `locales/ja.json` under `keywords` (alphabetical order, lowercase key)
37
+ - Normal keyword: `"tag name": "タグ名"`
38
+ - Complement keyword (for `:c` flag): `"c:deprecated": "は非推奨です"`
39
+ 2. Add to `locales/en.json` under `keywords` only if capitalization or special formatting is needed
40
+ 3. Add to `$schema.json` under `keywords.properties` as `{ "type": "string" }`
41
+ 4. Test: `yarn test --scope @markuplint/i18n`
42
+ 5. Build: `yarn build --scope @markuplint/i18n`
43
+
44
+ **Important**: `$schema.json` uses `additionalProperties: false`. A keyword not defined in the schema will cause validation errors. Always keep the three files in sync.
45
+
46
+ ### add-sentence
47
+
48
+ Add a new sentence template for rule messages.
49
+
50
+ 1. Design the English template as the key
51
+ - Placeholders: `{0}`, `{1}`, `{2}`...
52
+ - Complement flag: `{0:c}` (resolves to `c:` prefixed keyword in Japanese)
53
+ - No-translate mark: `{0*}` (skips translation for that placeholder)
54
+ 2. Add to `locales/ja.json` under `sentences` (placeholder order may differ for natural Japanese)
55
+ 3. Add to `$schema.json` under `sentences.properties` as `{ "type": "string" }`
56
+ 4. `en.json` does not need a `sentences` entry (the English key itself serves as the template)
57
+ 5. Test: `yarn test --scope @markuplint/i18n`
58
+
59
+ ### add-language
60
+
61
+ Add support for a new language.
62
+
63
+ 1. Create `locales/<lang>.json` using `ja.json` as a template
64
+ - `listFormat`: Define quote characters and separator for the language
65
+ - `keywords`: Translate all keywords
66
+ - `sentences`: Translate all sentence templates
67
+ 2. Add an export entry in `package.json`:
68
+ ```json
69
+ "./locales/<lang>.json": { "import": "./locales/<lang>.json", "require": "./locales/<lang>.json" }
70
+ ```
71
+ 3. `$schema.json` is shared across all languages (no changes needed)
72
+ 4. Add test cases in `src/index.spec.ts`
73
+ 5. Test: `yarn test --scope @markuplint/i18n`
@@ -1,6 +1,28 @@
1
1
  import type { LocaleSet, Primitive, Translator } from './types.js';
2
+ /**
3
+ * Creates a {@link Translator} function bound to the given locale set.
4
+ *
5
+ * The returned translator supports two call signatures:
6
+ * - **Message template**: `t("The {0} is {1}", keyword1, keyword2)` – interpolates keywords into
7
+ * a message template, looking up translations from the locale set's `sentences` and `keywords`.
8
+ * - **List formatting**: `t(["apple", "banana", "cherry"], true)` – formats an array of strings
9
+ * into a human-readable list (e.g. `"apple", "banana" and "cherry"`).
10
+ *
11
+ * @param localeSet - The locale configuration providing translations and formatting rules
12
+ * @returns A translator function for producing localized messages
13
+ */
2
14
  export declare function translator(localeSet?: LocaleSet): Translator;
3
15
  /**
16
+ * Creates a tagged template literal translator function.
17
+ *
18
+ * Allows using template literal syntax for translations:
19
+ * ```ts
20
+ * const tt = taggedTemplateTranslator(localeSet);
21
+ * const msg = tt`The ${name} is ${value}`;
22
+ * ```
23
+ *
4
24
  * @experimental
25
+ * @param localeSet - The locale configuration providing translations and formatting rules
26
+ * @returns A tagged template function that produces localized strings
5
27
  */
6
28
  export declare function taggedTemplateTranslator(localeSet?: LocaleSet): (strings: Readonly<TemplateStringsArray>, ...keys: readonly Primitive[]) => string;
package/cjs/translator.js CHANGED
@@ -8,9 +8,20 @@ const defaultListFormat = {
8
8
  separator: ', ',
9
9
  lastSeparator: ' and ',
10
10
  };
11
+ /**
12
+ * Creates a {@link Translator} function bound to the given locale set.
13
+ *
14
+ * The returned translator supports two call signatures:
15
+ * - **Message template**: `t("The {0} is {1}", keyword1, keyword2)` – interpolates keywords into
16
+ * a message template, looking up translations from the locale set's `sentences` and `keywords`.
17
+ * - **List formatting**: `t(["apple", "banana", "cherry"], true)` – formats an array of strings
18
+ * into a human-readable list (e.g. `"apple", "banana" and "cherry"`).
19
+ *
20
+ * @param localeSet - The locale configuration providing translations and formatting rules
21
+ * @returns A translator function for producing localized messages
22
+ */
11
23
  function translator(localeSet) {
12
24
  return (messageTmpl, ...keywords) => {
13
- let message = messageTmpl;
14
25
  if (typeof messageTmpl !== 'string') {
15
26
  if (messageTmpl.length === 0) {
16
27
  return '';
@@ -40,7 +51,7 @@ function translator(localeSet) {
40
51
  messageTmpl = sentence ?? key;
41
52
  messageTmpl =
42
53
  removeNoTranslateMark(input.toLowerCase()) === messageTmpl ? removeNoTranslateMark(input) : messageTmpl;
43
- message = messageTmpl.replaceAll(
54
+ const message = messageTmpl.replaceAll(
44
55
  // eslint-disable-next-line regexp/strict
45
56
  /{(\d+)(?::(c))?}/g, ($0, number, flag) => {
46
57
  const num = Number.parseInt(number);
@@ -58,7 +69,17 @@ function translator(localeSet) {
58
69
  };
59
70
  }
60
71
  /**
72
+ * Creates a tagged template literal translator function.
73
+ *
74
+ * Allows using template literal syntax for translations:
75
+ * ```ts
76
+ * const tt = taggedTemplateTranslator(localeSet);
77
+ * const msg = tt`The ${name} is ${value}`;
78
+ * ```
79
+ *
61
80
  * @experimental
81
+ * @param localeSet - The locale configuration providing translations and formatting rules
82
+ * @returns A tagged template function that produces localized strings
62
83
  */
63
84
  function taggedTemplateTranslator(localeSet) {
64
85
  const t = translator(localeSet);
package/cjs/types.d.ts CHANGED
@@ -1,21 +1,71 @@
1
+ /**
2
+ * A function that translates message templates or formats keyword lists
3
+ * according to a bound locale set.
4
+ *
5
+ * Overloads:
6
+ * 1. Translate a message template with keyword interpolation.
7
+ * 2. Format a list of keywords into a human-readable string.
8
+ * 3. Combined signature accepting either form.
9
+ */
1
10
  export interface Translator {
11
+ /**
12
+ * Translates a message template by interpolating keywords.
13
+ *
14
+ * @param messageTmpl - The message template with `{0}`, `{1}`, ... placeholders
15
+ * @param keywords - Values to substitute into the template
16
+ * @returns The translated, interpolated message string
17
+ */
2
18
  (messageTmpl: string, ...keywords: readonly Primitive[]): string;
19
+ /**
20
+ * Formats a list of keywords into a localized, human-readable string.
21
+ *
22
+ * @param messageTmpl - An array of keywords to format as a list
23
+ * @param useLastSeparator - Whether to use the "last separator" (e.g. " and ") before the final item
24
+ * @returns The formatted list string
25
+ */
3
26
  (messageTmpl: readonly string[], useLastSeparator?: boolean): string;
27
+ /**
28
+ * Combined overload accepting either a message template string or a keyword list.
29
+ *
30
+ * @param messageTmpl - A message template string or an array of keywords
31
+ * @param keywords - Values to substitute (when a string template is provided)
32
+ * @returns The translated or formatted string
33
+ */
4
34
  (messageTmpl: string | readonly string[], ...keywords: readonly Primitive[]): string;
5
35
  }
36
+ /**
37
+ * Configuration for a specific locale, including translations and formatting rules.
38
+ */
6
39
  export type LocaleSet = {
40
+ /** The locale identifier (e.g. `"en"`, `"ja"`) */
7
41
  readonly locale: string;
42
+ /** Formatting rules for rendering keyword lists */
8
43
  readonly listFormat?: ListFormat;
44
+ /** A dictionary mapping lowercase keywords to their translations */
9
45
  readonly keywords?: LocalesKeywords;
46
+ /** A dictionary mapping lowercase sentence templates to their translations */
10
47
  readonly sentences?: LocalesKeywords;
11
48
  };
49
+ /**
50
+ * Formatting rules for rendering a list of items as a human-readable string.
51
+ */
12
52
  export type ListFormat = {
53
+ /** Character(s) placed before each quoted item (e.g. `"`) */
13
54
  readonly quoteStart: string;
55
+ /** Character(s) placed after each quoted item (e.g. `"`) */
14
56
  readonly quoteEnd: string;
57
+ /** Separator between items (e.g. `", "`) */
15
58
  readonly separator: string;
59
+ /** Separator before the last item (e.g. `" and "`); falls back to `separator` if omitted */
16
60
  readonly lastSeparator?: string;
17
61
  };
62
+ /**
63
+ * A primitive value type used as a keyword in translations.
64
+ */
18
65
  export type Primitive = string | number | boolean;
66
+ /**
67
+ * A dictionary mapping message IDs (lowercase) to their translated strings.
68
+ */
19
69
  export type LocalesKeywords = {
20
70
  readonly [messageId: string]: string;
21
71
  };
@@ -0,0 +1,207 @@
1
+ # @markuplint/i18n メンテナンスガイド
2
+
3
+ ## 概要
4
+
5
+ `@markuplint/i18n` パッケージは、markuplint のルールメッセージの国際化を提供します。
6
+
7
+ - **ロケール辞書** (`locales/*.json`) — 言語ごとのキーワード、文テンプレート、リスト書式ルール
8
+ - **翻訳エンジン** (`src/translator.ts`) — テンプレートのキーワード置換、補語形式、リスト整形を処理
9
+ - **JSON Schema** (`$schema.json`) — 厳密なプロパティチェックでロケールファイルを検証
10
+
11
+ ### ファイル構成
12
+
13
+ ```
14
+ packages/@markuplint/i18n/
15
+ ├── locales/
16
+ │ ├── ja.json # 日本語辞書(完全版)
17
+ │ └── en.json # 英語辞書(最小限のオーバーライド)
18
+ ├── src/
19
+ │ ├── translator.ts # 翻訳コアロジック
20
+ │ ├── types.ts # LocaleSet, Translator 型定義
21
+ │ └── index.spec.ts # テストスイート
22
+ ├── $schema.json # ロケール JSON Schema
23
+ └── package.json
24
+ ```
25
+
26
+ ## 3ファイル同期ルール
27
+
28
+ キーワードや文テンプレートを追加する際、3つのファイルを同期する必要があります。
29
+
30
+ | ファイル | 役割 | 必須? |
31
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------- |
32
+ | `$schema.json` | 許可されるプロパティキーを定義。`additionalProperties: false` のため、ここに定義されていないキーはバリデーションエラーになる。 | **常に必須** |
33
+ | `locales/ja.json` | すべてのキーワードと文テンプレートの日本語翻訳。 | **常に必須** |
34
+ | `locales/en.json` | 英語のオーバーライド。大文字化や特殊な書式が必要な場合のみ(例: `"html elements"` → `"HTML elements"`)。 | 必要な場合のみ |
35
+
36
+ スキーマが有効なキーの定義元です。`ja.json` にキーワードを追加しても `$schema.json` に追加しなければ、ロケールファイルのスキーマ検証が失敗します。
37
+
38
+ ## 単語を追加する
39
+
40
+ キーワードは、ルールメッセージの構成要素として使われる単語または短いフレーズです。ロケールファイルの `keywords` セクションに定義します。
41
+
42
+ ### 手順
43
+
44
+ 1. **`ja.json`** の `keywords` にアルファベット順で追加:
45
+
46
+ ```json
47
+ {
48
+ "keywords": {
49
+ "focusable": "フォーカス可能"
50
+ }
51
+ }
52
+ ```
53
+
54
+ - キーは英語の小文字
55
+ - 値は日本語の翻訳
56
+
57
+ 2. **`en.json`** の `keywords` には必要な場合のみ追加:
58
+
59
+ ```json
60
+ {
61
+ "keywords": {
62
+ "html elements": "HTML elements"
63
+ }
64
+ }
65
+ ```
66
+
67
+ ほとんどの英語キーワードはエントリ不要です。キーがそのまま使用されます。
68
+
69
+ 3. **`$schema.json`** の `keywords.properties` に追加:
70
+
71
+ ```json
72
+ {
73
+ "keywords": {
74
+ "properties": {
75
+ "focusable": { "type": "string" }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ 4. **テスト**: `yarn test --scope @markuplint/i18n`
82
+ 5. **ビルド**: `yarn build --scope @markuplint/i18n`
83
+
84
+ ### 補語キーワード
85
+
86
+ 補語キーワードは `c:` プレフィックスを使い、プレースホルダーに `:c` フラグ(例: `{0:c}`)が付いた場合に解決されます。日本語では主語に続く述語として機能します。
87
+
88
+ | ロケールのキー | テンプレートでの使用 | 出力例(ja) |
89
+ | ---------------------------------------- | -------------------------------------------- | ------------------------------ |
90
+ | `"c:deprecated": "は非推奨です"` | `"{0} is {1:c}"` + キーワード `"deprecated"` | `「要素」は非推奨です` |
91
+ | `"c:disallowed": "は許可されていません"` | `"{0} is {1:c}"` + キーワード `"disallowed"` | `「属性」は許可されていません` |
92
+
93
+ 補語キーワードを追加する際:
94
+
95
+ 1. `ja.json` の keywords に `"c:<word>"` を追加
96
+ 2. `$schema.json` の keywords properties に `"c:<word>"` を追加
97
+ 3. 補語なしバージョン(`c:` なし)も別のキーワードとして必要な場合がある
98
+
99
+ ## フレーズを追加する
100
+
101
+ 文テンプレートは、プレースホルダー付きのメッセージパターンを定義します。ロケールファイルの `sentences` セクションに定義します。
102
+
103
+ ### 手順
104
+
105
+ 1. **英語テンプレート**をキーとして設計:
106
+
107
+ ```
108
+ "{0} conflicts with {1}"
109
+ ```
110
+
111
+ プレースホルダー構文:
112
+ - `{0}`, `{1}`, `{2}` — 位置パラメータ、キーワードとして翻訳される
113
+ - `{0:c}` — 補語フラグ、日本語では `c:` プレフィックスキーワードに解決
114
+ - `{0*}` — 翻訳スキップ、値がキーワード検索なしでそのまま挿入される
115
+
116
+ 2. **`ja.json`** の `sentences` に追加:
117
+
118
+ ```json
119
+ {
120
+ "sentences": {
121
+ "{0} conflicts with {1}": "{0}は{1}と競合しています"
122
+ }
123
+ }
124
+ ```
125
+
126
+ 日本語の自然な語順にするため、プレースホルダーの順序は英語と異なってもよい。
127
+
128
+ 3. **`$schema.json`** の `sentences.properties` に追加:
129
+
130
+ ```json
131
+ {
132
+ "sentences": {
133
+ "properties": {
134
+ "{0} conflicts with {1}": { "type": "string" }
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ 4. **`en.json` に sentences エントリは不要**。英語のキー自体がテンプレートとして使用されます。翻訳が見つからない場合、translator はキーをそのまま使います。
141
+
142
+ 5. **テスト**: `yarn test --scope @markuplint/i18n`
143
+
144
+ ### プレースホルダーの並べ替え
145
+
146
+ 日本語と英語では語順が異なります。文テンプレートを翻訳する際、プレースホルダーは自由に並べ替えできます:
147
+
148
+ - 英語: `"{0} is not allowed in {1}"`
149
+ - 日本語: `"{1}に{0}は許可されていません"`
150
+
151
+ プレースホルダーの番号は、translator に渡される引数の位置を指し、文字列内の位置ではありません。
152
+
153
+ ## 新しい言語を追加する
154
+
155
+ まったく新しい言語のサポートを追加する手順です。
156
+
157
+ ### 手順
158
+
159
+ 1. **`locales/<lang>.json`** を `ja.json` をテンプレートにして作成:
160
+
161
+ ```json
162
+ {
163
+ "$schema": "../$schema.json",
164
+ "listFormat": {
165
+ "quoteStart": "\"",
166
+ "quoteEnd": "\"",
167
+ "separator": ", "
168
+ },
169
+ "keywords": {
170
+ "attribute": "<翻訳>",
171
+ "element": "<翻訳>"
172
+ },
173
+ "sentences": {
174
+ "{0} is {1}": "<翻訳テンプレート>"
175
+ }
176
+ }
177
+ ```
178
+
179
+ - `listFormat`: 言語に適した引用符と区切り文字を定義
180
+ - `keywords`: `ja.json` のすべてのキーワードを翻訳
181
+ - `sentences`: `ja.json` のすべての文テンプレートを翻訳
182
+
183
+ 2. **`package.json`** にエクスポートエントリを追加:
184
+
185
+ ```json
186
+ {
187
+ "exports": {
188
+ "./locales/<lang>.json": {
189
+ "import": "./locales/<lang>.json",
190
+ "require": "./locales/<lang>.json"
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ 3. **`$schema.json` の変更は不要** — スキーマは全言語で共有されます。
197
+
198
+ 4. **`src/index.spec.ts`** に新しいロケールのテストケースを追加。
199
+
200
+ 5. **テスト**: `yarn test --scope @markuplint/i18n`
201
+
202
+ ## コマンドリファレンス
203
+
204
+ | コマンド | 説明 |
205
+ | ------------------------------------- | ------------------ |
206
+ | `yarn test --scope @markuplint/i18n` | テスト実行 |
207
+ | `yarn build --scope @markuplint/i18n` | パッケージのビルド |
@@ -0,0 +1,207 @@
1
+ # @markuplint/i18n Maintenance Guide
2
+
3
+ ## Overview
4
+
5
+ The `@markuplint/i18n` package provides internationalization for markuplint rule messages. It consists of:
6
+
7
+ - **Locale dictionaries** (`locales/*.json`) — keywords, sentence templates, and list formatting rules per language
8
+ - **Translator engine** (`src/translator.ts`) — resolves templates with keyword substitution, complement forms, and list formatting
9
+ - **JSON Schema** (`$schema.json`) — validates locale files with strict property checking
10
+
11
+ ### File Structure
12
+
13
+ ```
14
+ packages/@markuplint/i18n/
15
+ ├── locales/
16
+ │ ├── ja.json # Japanese dictionary (complete)
17
+ │ └── en.json # English dictionary (minimal overrides)
18
+ ├── src/
19
+ │ ├── translator.ts # Core translation logic
20
+ │ ├── types.ts # LocaleSet, Translator types
21
+ │ └── index.spec.ts # Test suite
22
+ ├── $schema.json # Locale JSON Schema
23
+ └── package.json
24
+ ```
25
+
26
+ ## Three-File Synchronization Rule
27
+
28
+ When adding keywords or sentences, three files must be kept in sync:
29
+
30
+ | File | Role | Required? |
31
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------- |
32
+ | `$schema.json` | Defines allowed property keys. Uses `additionalProperties: false`, so any key not listed here will cause a validation error. | **Always** |
33
+ | `locales/ja.json` | Complete Japanese translations for all keywords and sentences. | **Always** |
34
+ | `locales/en.json` | English overrides. Only needed when a keyword requires capitalization or special formatting (e.g., `"html elements"` → `"HTML elements"`). | Only if needed |
35
+
36
+ The schema is the source of truth for which keys are valid. If you add a keyword to `ja.json` without adding it to `$schema.json`, the locale file will fail schema validation.
37
+
38
+ ## Adding a Keyword
39
+
40
+ Keywords are single words or short phrases used as building blocks in rule messages. They appear in the `keywords` section of locale files.
41
+
42
+ ### Steps
43
+
44
+ 1. **Add to `ja.json`** under `keywords` in alphabetical order:
45
+
46
+ ```json
47
+ {
48
+ "keywords": {
49
+ "focusable": "フォーカス可能"
50
+ }
51
+ }
52
+ ```
53
+
54
+ - Keys must be lowercase English
55
+ - Values are the Japanese translations
56
+
57
+ 2. **Add to `en.json`** under `keywords` only if needed:
58
+
59
+ ```json
60
+ {
61
+ "keywords": {
62
+ "html elements": "HTML elements"
63
+ }
64
+ }
65
+ ```
66
+
67
+ Most English keywords do not need an entry because the key itself is used as-is.
68
+
69
+ 3. **Add to `$schema.json`** under `keywords.properties`:
70
+
71
+ ```json
72
+ {
73
+ "keywords": {
74
+ "properties": {
75
+ "focusable": { "type": "string" }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ 4. **Test**: `yarn test --scope @markuplint/i18n`
82
+ 5. **Build**: `yarn build --scope @markuplint/i18n`
83
+
84
+ ### Complement Keywords
85
+
86
+ Complement keywords use the `c:` prefix and are resolved when a placeholder has the `:c` flag (e.g., `{0:c}`). They form a predicate that attaches to the preceding subject in Japanese.
87
+
88
+ | Key in locale | Usage in template | Example output (ja) |
89
+ | ---------------------------------------- | -------------------------------------------- | ------------------------------ |
90
+ | `"c:deprecated": "は非推奨です"` | `"{0} is {1:c}"` with keyword `"deprecated"` | `「要素」は非推奨です` |
91
+ | `"c:disallowed": "は許可されていません"` | `"{0} is {1:c}"` with keyword `"disallowed"` | `「属性」は許可されていません` |
92
+
93
+ When adding a complement keyword:
94
+
95
+ 1. Add `"c:<word>"` to `ja.json` keywords
96
+ 2. Add `"c:<word>"` to `$schema.json` keywords properties
97
+ 3. The non-complement version (without `c:`) may also be needed as a separate keyword
98
+
99
+ ## Adding a Sentence Template
100
+
101
+ Sentence templates define message patterns with placeholders. They appear in the `sentences` section of locale files.
102
+
103
+ ### Steps
104
+
105
+ 1. **Design the English template** as the key:
106
+
107
+ ```
108
+ "{0} conflicts with {1}"
109
+ ```
110
+
111
+ Placeholder syntax:
112
+ - `{0}`, `{1}`, `{2}` — positional placeholders, translated as keywords
113
+ - `{0:c}` — complement flag, resolves to `c:` prefixed keyword in Japanese
114
+ - `{0*}` — no-translate mark, the value is inserted as-is without keyword lookup
115
+
116
+ 2. **Add to `ja.json`** under `sentences`:
117
+
118
+ ```json
119
+ {
120
+ "sentences": {
121
+ "{0} conflicts with {1}": "{0}は{1}と競合しています"
122
+ }
123
+ }
124
+ ```
125
+
126
+ Placeholder order may differ from English to produce natural Japanese.
127
+
128
+ 3. **Add to `$schema.json`** under `sentences.properties`:
129
+
130
+ ```json
131
+ {
132
+ "sentences": {
133
+ "properties": {
134
+ "{0} conflicts with {1}": { "type": "string" }
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ 4. **`en.json` does not need a sentences entry**. The English key itself serves as the template. The translator uses the key directly when no translation is found.
141
+
142
+ 5. **Test**: `yarn test --scope @markuplint/i18n`
143
+
144
+ ### Placeholder Reordering
145
+
146
+ Japanese word order differs from English. When translating sentence templates, you can freely reorder placeholders:
147
+
148
+ - English: `"{0} is not allowed in {1}"`
149
+ - Japanese: `"{1}に{0}は許可されていません"`
150
+
151
+ The placeholder numbers refer to the arguments passed to the translator, not their position in the string.
152
+
153
+ ## Adding a New Language
154
+
155
+ To add support for an entirely new language:
156
+
157
+ ### Steps
158
+
159
+ 1. **Create `locales/<lang>.json`** using `ja.json` as a template:
160
+
161
+ ```json
162
+ {
163
+ "$schema": "../$schema.json",
164
+ "listFormat": {
165
+ "quoteStart": "\"",
166
+ "quoteEnd": "\"",
167
+ "separator": ", "
168
+ },
169
+ "keywords": {
170
+ "attribute": "<translated>",
171
+ "element": "<translated>"
172
+ },
173
+ "sentences": {
174
+ "{0} is {1}": "<translated template>"
175
+ }
176
+ }
177
+ ```
178
+
179
+ - `listFormat`: Define the quote characters and separators appropriate for the language
180
+ - `keywords`: Translate all keywords from `ja.json`
181
+ - `sentences`: Translate all sentence templates from `ja.json`
182
+
183
+ 2. **Add export entry in `package.json`**:
184
+
185
+ ```json
186
+ {
187
+ "exports": {
188
+ "./locales/<lang>.json": {
189
+ "import": "./locales/<lang>.json",
190
+ "require": "./locales/<lang>.json"
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ 3. **`$schema.json` requires no changes** — the schema is shared across all languages.
197
+
198
+ 4. **Add test cases** in `src/index.spec.ts` to verify the new locale works correctly with the translator.
199
+
200
+ 5. **Test**: `yarn test --scope @markuplint/i18n`
201
+
202
+ ## Command Reference
203
+
204
+ | Command | Description |
205
+ | ------------------------------------- | ----------------- |
206
+ | `yarn test --scope @markuplint/i18n` | Run tests |
207
+ | `yarn build --scope @markuplint/i18n` | Build the package |
@@ -1,6 +1,28 @@
1
1
  import type { LocaleSet, Primitive, Translator } from './types.js';
2
+ /**
3
+ * Creates a {@link Translator} function bound to the given locale set.
4
+ *
5
+ * The returned translator supports two call signatures:
6
+ * - **Message template**: `t("The {0} is {1}", keyword1, keyword2)` – interpolates keywords into
7
+ * a message template, looking up translations from the locale set's `sentences` and `keywords`.
8
+ * - **List formatting**: `t(["apple", "banana", "cherry"], true)` – formats an array of strings
9
+ * into a human-readable list (e.g. `"apple", "banana" and "cherry"`).
10
+ *
11
+ * @param localeSet - The locale configuration providing translations and formatting rules
12
+ * @returns A translator function for producing localized messages
13
+ */
2
14
  export declare function translator(localeSet?: LocaleSet): Translator;
3
15
  /**
16
+ * Creates a tagged template literal translator function.
17
+ *
18
+ * Allows using template literal syntax for translations:
19
+ * ```ts
20
+ * const tt = taggedTemplateTranslator(localeSet);
21
+ * const msg = tt`The ${name} is ${value}`;
22
+ * ```
23
+ *
4
24
  * @experimental
25
+ * @param localeSet - The locale configuration providing translations and formatting rules
26
+ * @returns A tagged template function that produces localized strings
5
27
  */
6
28
  export declare function taggedTemplateTranslator(localeSet?: LocaleSet): (strings: Readonly<TemplateStringsArray>, ...keys: readonly Primitive[]) => string;
@@ -4,9 +4,20 @@ const defaultListFormat = {
4
4
  separator: ', ',
5
5
  lastSeparator: ' and ',
6
6
  };
7
+ /**
8
+ * Creates a {@link Translator} function bound to the given locale set.
9
+ *
10
+ * The returned translator supports two call signatures:
11
+ * - **Message template**: `t("The {0} is {1}", keyword1, keyword2)` – interpolates keywords into
12
+ * a message template, looking up translations from the locale set's `sentences` and `keywords`.
13
+ * - **List formatting**: `t(["apple", "banana", "cherry"], true)` – formats an array of strings
14
+ * into a human-readable list (e.g. `"apple", "banana" and "cherry"`).
15
+ *
16
+ * @param localeSet - The locale configuration providing translations and formatting rules
17
+ * @returns A translator function for producing localized messages
18
+ */
7
19
  export function translator(localeSet) {
8
20
  return (messageTmpl, ...keywords) => {
9
- let message = messageTmpl;
10
21
  if (typeof messageTmpl !== 'string') {
11
22
  if (messageTmpl.length === 0) {
12
23
  return '';
@@ -36,7 +47,7 @@ export function translator(localeSet) {
36
47
  messageTmpl = sentence ?? key;
37
48
  messageTmpl =
38
49
  removeNoTranslateMark(input.toLowerCase()) === messageTmpl ? removeNoTranslateMark(input) : messageTmpl;
39
- message = messageTmpl.replaceAll(
50
+ const message = messageTmpl.replaceAll(
40
51
  // eslint-disable-next-line regexp/strict
41
52
  /{(\d+)(?::(c))?}/g, ($0, number, flag) => {
42
53
  const num = Number.parseInt(number);
@@ -54,7 +65,17 @@ export function translator(localeSet) {
54
65
  };
55
66
  }
56
67
  /**
68
+ * Creates a tagged template literal translator function.
69
+ *
70
+ * Allows using template literal syntax for translations:
71
+ * ```ts
72
+ * const tt = taggedTemplateTranslator(localeSet);
73
+ * const msg = tt`The ${name} is ${value}`;
74
+ * ```
75
+ *
57
76
  * @experimental
77
+ * @param localeSet - The locale configuration providing translations and formatting rules
78
+ * @returns A tagged template function that produces localized strings
58
79
  */
59
80
  export function taggedTemplateTranslator(localeSet) {
60
81
  const t = translator(localeSet);
package/esm/types.d.ts CHANGED
@@ -1,21 +1,71 @@
1
+ /**
2
+ * A function that translates message templates or formats keyword lists
3
+ * according to a bound locale set.
4
+ *
5
+ * Overloads:
6
+ * 1. Translate a message template with keyword interpolation.
7
+ * 2. Format a list of keywords into a human-readable string.
8
+ * 3. Combined signature accepting either form.
9
+ */
1
10
  export interface Translator {
11
+ /**
12
+ * Translates a message template by interpolating keywords.
13
+ *
14
+ * @param messageTmpl - The message template with `{0}`, `{1}`, ... placeholders
15
+ * @param keywords - Values to substitute into the template
16
+ * @returns The translated, interpolated message string
17
+ */
2
18
  (messageTmpl: string, ...keywords: readonly Primitive[]): string;
19
+ /**
20
+ * Formats a list of keywords into a localized, human-readable string.
21
+ *
22
+ * @param messageTmpl - An array of keywords to format as a list
23
+ * @param useLastSeparator - Whether to use the "last separator" (e.g. " and ") before the final item
24
+ * @returns The formatted list string
25
+ */
3
26
  (messageTmpl: readonly string[], useLastSeparator?: boolean): string;
27
+ /**
28
+ * Combined overload accepting either a message template string or a keyword list.
29
+ *
30
+ * @param messageTmpl - A message template string or an array of keywords
31
+ * @param keywords - Values to substitute (when a string template is provided)
32
+ * @returns The translated or formatted string
33
+ */
4
34
  (messageTmpl: string | readonly string[], ...keywords: readonly Primitive[]): string;
5
35
  }
36
+ /**
37
+ * Configuration for a specific locale, including translations and formatting rules.
38
+ */
6
39
  export type LocaleSet = {
40
+ /** The locale identifier (e.g. `"en"`, `"ja"`) */
7
41
  readonly locale: string;
42
+ /** Formatting rules for rendering keyword lists */
8
43
  readonly listFormat?: ListFormat;
44
+ /** A dictionary mapping lowercase keywords to their translations */
9
45
  readonly keywords?: LocalesKeywords;
46
+ /** A dictionary mapping lowercase sentence templates to their translations */
10
47
  readonly sentences?: LocalesKeywords;
11
48
  };
49
+ /**
50
+ * Formatting rules for rendering a list of items as a human-readable string.
51
+ */
12
52
  export type ListFormat = {
53
+ /** Character(s) placed before each quoted item (e.g. `"`) */
13
54
  readonly quoteStart: string;
55
+ /** Character(s) placed after each quoted item (e.g. `"`) */
14
56
  readonly quoteEnd: string;
57
+ /** Separator between items (e.g. `", "`) */
15
58
  readonly separator: string;
59
+ /** Separator before the last item (e.g. `" and "`); falls back to `separator` if omitted */
16
60
  readonly lastSeparator?: string;
17
61
  };
62
+ /**
63
+ * A primitive value type used as a keyword in translations.
64
+ */
18
65
  export type Primitive = string | number | boolean;
66
+ /**
67
+ * A dictionary mapping message IDs (lowercase) to their translated strings.
68
+ */
19
69
  export type LocalesKeywords = {
20
70
  readonly [messageId: string]: string;
21
71
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/i18n",
3
- "version": "4.7.0",
3
+ "version": "4.7.1",
4
4
  "description": "Internationalization for markuplint",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
@@ -38,5 +38,5 @@
38
38
  "clean:esm": "tsc --build --clean tsconfig.build.json",
39
39
  "clean:cjs": "tsc --build --clean tsconfig.build-cjs.json"
40
40
  },
41
- "gitHead": "acbf53f7e30d7a59f850a0f279b617383266dab3"
41
+ "gitHead": "193ee7c1262bbed95424e38efdf1a8e56ff049f4"
42
42
  }