@mirta/i18n 0.0.1 → 0.4.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/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
package/README.md CHANGED
@@ -1,45 +1,242 @@
1
1
  # @mirta/i18n
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ [![en](https://img.shields.io/badge/lang-en-olivedrab.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-i18n/README.md)
4
+ [![ru](https://img.shields.io/badge/lang-ru-dimgray.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-i18n/README.ru.md)
5
+ [![NPM Version](https://img.shields.io/npm/v/@mirta/i18n?style=flat-square)](https://npmjs.com/package/@mirta/i18n)
6
+ [![NPM Downloads](https://img.shields.io/npm/dm/@mirta/i18n?style=flat-square&logo=npm)](https://npmjs.com/package/@mirta/i18n)
4
7
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
8
+ > Localization library for Mirta Framework CLI tools, with ICU-compatible syntax.
6
9
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
10
+ The `@mirta/i18n` package is intended exclusively for **Node.js tools** (≥ 20.6.0) and is not used in the Duktape runtime.
8
11
 
9
- ## Purpose
12
+ ## Features
10
13
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@mirta/i18n`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
14
+ - ✔️ Type-safe keys and variables — when a `LocaleShape` is provided
15
+ - ✔️ ICU-compatible syntax: `{var}`, `{count, plural, ...}`, `offset`, `=n`, `#`
16
+ - ✔️ Asynchronous loading and caching of `.json` files
17
+ - ✔️ Zero dependencies minimal footprint
18
+ - ✔️ Configurable fallback (en-US by default)
19
+ - ✔️ Unified translation contract `t(key, vars)`
15
20
 
16
- ## What is OIDC Trusted Publishing?
21
+ ## 📦 Installation
17
22
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
23
+ ```sh
24
+ pnpm add @mirta/i18n
25
+ ```
19
26
 
20
- ## Setup Instructions
27
+ ⚠️ This package is part of Mirta Framework's internal infrastructure. It is typically not used directly.
21
28
 
22
- To properly configure OIDC trusted publishing for this package:
29
+ ## Usage
23
30
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
31
+ ### 1. Organize the locale structure
28
32
 
29
- ## DO NOT USE THIS PACKAGE
33
+ Set up the localization structure for your package:
30
34
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
35
+ ```txt
36
+ <package>/
37
+ locales/
38
+ en-US.json
39
+ ru-RU.json
40
+ ```
36
41
 
37
- ## More Information
42
+ Example `en-US.json`:
38
43
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
44
+ ```json
45
+ {
46
+ "title": "Welcome",
47
+ "files.plural": "{count, plural, =0{No files} one{One file} other{# files}}",
48
+ "greeting": "Hello, {name}!"
49
+ }
50
+ ```
51
+
52
+ ### 2. Define the `LocaleShape`
53
+
54
+ To enable type safety, define an interface compatible with `GenericShape`:
55
+
56
+ ```ts
57
+ interface LocaleShape {
58
+ messages: {
59
+ 'title': string;
60
+ 'files.plural': string;
61
+ 'greeting': string;
62
+ };
63
+ variables: {
64
+ 'files.plural': { count: number };
65
+ 'greeting': { name: string };
66
+ };
67
+ }
68
+ ```
69
+
70
+ 💡 The `LocaleShape` type must be defined in the project.
71
+ Mirta Framework uses an internal script to generate it from `locales/en-US.json`.
72
+
73
+ ⚠️ If no locale shape is provided, `GenericShape` is used — keys and variables are not type-checked.
74
+
75
+ ### 3. Initialize localization
76
+
77
+ In the package to be localized:
78
+
79
+ ```ts
80
+ // src/i18n/index.ts
81
+ import { initLocalizationAsync } from '@mirta/i18n'
82
+
83
+ export const { t, setLocaleAsync } = await initLocalizationAsync<LocaleShape>()
84
+ ```
85
+
86
+ ### 4. Use translation
87
+
88
+ ```ts
89
+ console.log(t('title')) // → "Welcome"
90
+ console.log(t('greeting', { name: 'Alice' })) // → "Hello, Alice!"
91
+ console.log(t('files.plural', { count: 5 })) // → "5 files"
92
+ ```
93
+
94
+ Changing locale:
95
+
96
+ ```ts
97
+ await setLocaleAsync('ru-RU')
98
+ console.log(t('title')) // → "Добро пожаловать"
99
+ ```
100
+
101
+ ## 📚 API
102
+
103
+ ### `initLocalizationAsync<TShape>(options)`
104
+
105
+ Initializes the localization subsystem.
106
+
107
+ #### Parameters
108
+
109
+ | Field | Type | Description |
110
+ |------|-----|----------|
111
+ | `cwd` | `string` | Working directory (default: `process.cwd()`) |
112
+ | `fallbackLocale` | `string` | Fallback locale (default: `'en-US'`) |
113
+
114
+ #### Returns
115
+
116
+ `Promise<Localization<TShape>>`
117
+
118
+ #### Errors
119
+
120
+ - `fallback.LoadFailed` — if the fallback locale cannot be loaded.
42
121
 
43
122
  ---
44
123
 
45
- **Maintained for OIDC setup purposes only**
124
+ ### `Localization<TShape>`
125
+
126
+ | Method | Type | Description |
127
+ |------|-----|----------|
128
+ | `t(key, vars?)` | `(key: K, vars?: VariablesOf<TShape, K>) => string` | Type-safe translation function |
129
+ | `getLocale()` | `() => Locale` | Returns current locale |
130
+ | `setLocaleAsync(locale)` | `(locale: string) => Promise<void>` | Changes locale (normalizes and caches) |
131
+
132
+ ---
133
+
134
+ ### `Locale`
135
+
136
+ Type: `Branded<string, 'Locale'>` — branded locale type.
137
+
138
+ ### `Lang`
139
+
140
+ Type: `Branded<string, 'Lang'>` — branded language type.
141
+
142
+ ## Key Features
143
+
144
+ ### ✅ Optional type safety
145
+
146
+ The `t()` function ensures type safety **only when `LocaleShape` is provided**:
147
+
148
+ - Validates key existence
149
+ - Enforces required variables
150
+ - Prevents extra fields
151
+
152
+ ```ts
153
+ t('files.plural', { count: 2 }) // ✅
154
+ t('files.plural', {}) // ❌ Error: missing `count`
155
+ t('title', { name: 'John' }) // ❌ Error: `title` does not accept variables
156
+ ```
157
+
158
+ ### ✅ ICU-compatible syntax
159
+
160
+ Supports a **limited subset of ICU MessageFormat**, sufficient for CLI:
161
+
162
+ - Interpolation: `{name}`, `{user.name}`, `{file-count}` (allowed: `a-z`, `A-Z`, `0-9`, `_`, `.`, `-`)
163
+ - Plural: `{count, plural, one{...} few{...} other{...}}`
164
+ - Offset: `offset:1`, `=0`, `#`
165
+
166
+ ⚠️ Not supported:
167
+ - `select`, `selectordinal`, number/date formatting
168
+ - Variables with spaces: `{first name}` → not replaced
169
+ - Nested `plural` or `#` inside `=n`
170
+
171
+ Implemented without external dependencies — only essentials.
172
+
173
+ #### Example with `offset`
174
+
175
+ ```json
176
+ "sockets.active": "{count, plural,
177
+ offset:1
178
+ =0 {Only server is on}
179
+ one {One more socket connected}
180
+ other {# more sockets connected}
181
+ }"
182
+ ```
183
+
184
+ - `count = 1` → `# = 0` → "Only server is on"
185
+ - `count = 2` → `# = 1` → "One more socket connected"
186
+ - `count = 5` → `# = 4` → "4 more sockets connected"
187
+
188
+ Allows excluding persistent elements from the count.
189
+
190
+ ### ✅ Asynchronous initialization and caching
191
+
192
+ - `initLocalizationAsync` loads and caches both fallback and system locale.
193
+ - `setLocaleAsync` caches loaded locales — no redundant reloads.
194
+ - Fallback chain: `current → fallback → {{key}}` if translation is missing.
195
+
196
+ ### ✅ Locale normalization
197
+
198
+ The `setLocaleAsync` function accepts any string, but:
199
+ - Automatically normalizes format (e.g. `ru_RU` → `ru-RU`)
200
+ - Falls back to `fallbackLocale` on invalid input
201
+ - Supports only `en-US` and `ru-RU`
202
+
203
+ No manual locale validation required.
204
+
205
+ ### ✅ Language support
206
+
207
+ - `ru`: full support for `one` / `few` / `many` (per [CLDR](https://cldr.unicode.org/))
208
+ - `en` and others: `one` (if 1), otherwise `other`
209
+
210
+ > For languages with special plural forms (e.g. `pl`, `ar`), extend `getPluralForm`.
211
+
212
+ #### Language-specific behavior
213
+
214
+ For Russian (`ru-RU`), fractional numbers (e.g. `36.6`) always use the `few` plural form (e.g. `36.6 градуса`), regardless of the integer part.
215
+
216
+ This follows Russian grammatical rules: in mixed numbers, the fractional part governs the noun, requiring the genitive singular case (e.g. _"одна целая пять десятых градуса"_).
217
+
218
+ Since ICU does not define a dedicated plural category for fractional numbers, `few` is used as the closest available match.
219
+
220
+ ## When to use?
221
+
222
+ Use `@mirta/i18n` if:
223
+ - Your tool supports multiple languages,
224
+ - You need accurate plural forms (especially for Russian),
225
+ - Locales are stored in `.json` and loaded asynchronously,
226
+ - Small bundle size and zero dependencies are important.
227
+
228
+ ## Limitations
229
+
230
+ - The `LocaleShape` type must be declared before use.
231
+ - Localization instances are cached — avoid creating thousands of dynamic locales.
232
+ - No support for `select`, `selectordinal`, number/date formatting.
233
+ - Variables with spaces (`{first name}`) are not replaced.
234
+ - `#` respects `offset` but does not support formatting.
235
+
236
+ ## Testing
237
+
238
+ The package is covered with unit tests (`vitest`, `@mirta/testing`) verifying:
239
+ - Initialization
240
+ - Translation with variables and plural forms
241
+ - Locale switching
242
+ - Fallback logic
package/README.ru.md ADDED
@@ -0,0 +1,240 @@
1
+ # @mirta/i18n
2
+
3
+ [![en](https://img.shields.io/badge/lang-en-dimgray.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-i18n/README.md)
4
+ [![ru](https://img.shields.io/badge/lang-ru-olivedrab.svg?style=flat-square)](https://github.com/wb-mirta/core/blob/latest/packages/mirta-i18n/README.ru.md)
5
+ [![NPM Version](https://img.shields.io/npm/v/@mirta/i18n?style=flat-square)](https://npmjs.com/package/@mirta/i18n)
6
+ [![NPM Downloads](https://img.shields.io/npm/dm/@mirta/i18n?style=flat-square&logo=npm)](https://npmjs.com/package/@mirta/i18n)
7
+
8
+ > Библиотека локализации для CLI-инструментов Mirta Framework, с ICU-совместимым синтаксисом.
9
+
10
+ Пакет `@mirta/i18n` предназначен исключительно для **Node.js-инструментов** (≥ 20.6.0), не используется в рантайме Duktape.
11
+
12
+ ## Особенности
13
+
14
+ - ✔️ Типобезопасность ключей и переменных — при определении `LocaleShape`
15
+ - ✔️ ICU-совместимый синтаксис: `{var}`, `{count, plural, ...}`, `offset`, `=n`, `#`
16
+ - ✔️ Асинхронная загрузка и кэширование `.json`
17
+ - ✔️ Zero dependencies — только необходимый минимум
18
+ - ✔️ Настраиваемый fallback (en-US по умолчанию)
19
+ - ✔️ Контракт `t(key, vars)` — единый интерфейс перевода
20
+
21
+ ## 📦 Установка
22
+
23
+ ```sh
24
+ pnpm add @mirta/i18n
25
+ ```
26
+
27
+ ⚠️ Этот пакет — часть внутренней инфраструктуры фреймворка Mirta. Обычно не используется напрямую.
28
+
29
+ ## Использование
30
+
31
+ ### 1. Подготовьте файлы локалей
32
+
33
+ Организуйте папку `locales` следующим образом:
34
+
35
+ ```txt
36
+ <пакет>/
37
+ locales/
38
+ en-US.json
39
+ ru-RU.json
40
+ ```
41
+ Пример `en-US.json`:
42
+
43
+ ```json
44
+ {
45
+ "title": "Welcome",
46
+ "files.plural": "{count, plural, =0{No files} one{One file} other{# files}}",
47
+ "greeting": "Hello, {name}!"
48
+ }
49
+ ```
50
+
51
+ ### 2. Опишите LocaleShape
52
+
53
+ Для включения типобезопасности определите интерфейс, совместимый с `GenericShape`:
54
+
55
+ ```ts
56
+ interface LocaleShape {
57
+ messages: {
58
+ 'title': string;
59
+ 'files.plural': string;
60
+ 'greeting': string;
61
+ };
62
+ variables: {
63
+ 'files.plural': { count: number };
64
+ 'greeting': { name: string };
65
+ };
66
+ }
67
+ ```
68
+ 💡 Тип `LocaleShape` должен быть определён в проекте.
69
+ Mirta Framework использует внутренний скрипт генерации на основе `locales/en-US.json`.
70
+
71
+ ⚠️ При отсутствии `LocaleShape` используется `GenericShape` — типы ключей и переменных не проверяются.
72
+
73
+ ### 3. Инициализируйте локализацию
74
+
75
+ В пакете, который подлежит локализации:
76
+
77
+ ```ts
78
+ // src/i18n/index.ts
79
+ import { initLocalizationAsync } from '@mirta/i18n'
80
+
81
+ export const { t, setLocaleAsync } = await initLocalizationAsync<LocaleShape>()
82
+ ```
83
+
84
+ ### 4. Используйте перевод
85
+
86
+ ```ts
87
+ console.log(t('title')) // → "Welcome"
88
+ console.log(t('greeting', { name: 'Alice' })) // → "Hello, Alice!"
89
+ console.log(t('files.plural', { count: 5 })) // → "5 files"
90
+ ```
91
+ Смена локали:
92
+
93
+ ```ts
94
+ await setLocaleAsync('ru-RU')
95
+ console.log(t('title')) // → "Добро пожаловать"
96
+ ```
97
+
98
+ ## 📚 API
99
+
100
+ ### `initLocalizationAsync<TShape>(options)`
101
+
102
+ Подготавливает подсистему локализации - загружает fallback-локаль, осуществляет попытку определения и установки системной локали.
103
+
104
+ #### Параметры
105
+
106
+ | Поле | Тип | Описание |
107
+ |------|-----|----------|
108
+ | `cwd` | `string` | Рабочая директория (по умолчанию: `process.cwd()`) |
109
+ | `fallbackLocale` | `string` | Резервная локаль (по умолчанию: `'en-US'`) |
110
+
111
+ #### Возвращает
112
+
113
+ `Promise<Localization<TShape>>`
114
+
115
+ #### Ошибки
116
+
117
+ - `fallbackLoadFailed` — если не удалось загрузить fallback-локаль.
118
+
119
+ ---
120
+
121
+ ### `Localization<TShape>`
122
+
123
+ | Метод | Тип | Описание |
124
+ |------|-----|----------|
125
+ | `t(key, vars?)` | `(key: K, vars?: VariablesOf<TShape, K>) => string` | Типобезопасный перевод |
126
+ | `getLocale()` | `() => Locale` | Текущая локаль |
127
+ | `setLocaleAsync(locale)` | `(locale: string) => Promise<void>` | Смена локали (нормализует и кэширует) |
128
+
129
+ ---
130
+
131
+ ### `Locale`
132
+
133
+ Тип: `Branded<string, 'Locale'>` - брендированный тип локали.
134
+
135
+ ### `Lang`
136
+
137
+ Тип: `Branded<string, 'Lang'>` - брендированный тип языка.
138
+
139
+ ## Ключевые возможности
140
+
141
+ ### ✅ Опциональная типобезопасность
142
+
143
+ Функция `t()` обеспечивает типобезопасность **только при указании `LocaleShape`**:
144
+
145
+ - Проверка существования ключей
146
+ - Требование обязательных переменных
147
+ - Запрет лишних полей
148
+
149
+ ```ts
150
+ t('files.plural', { count: 2 }) // ✅
151
+ t('files.plural', {}) // ❌ Ошибка: нет `count`
152
+ t('title', { name: 'John' }) // ❌ Ошибка: `title` не принимает переменные
153
+ ```
154
+
155
+ ### ✅ ICU-совместимый синтаксис
156
+
157
+ Поддерживает **ограниченный набор ICU MessageFormat**, достаточный для CLI:
158
+
159
+ - Интерполяция: `{name}`, `{user.name}`, `{file-count}` (разрешены: `a-z`, `A-Z`, `0-9`, `_`, `.`, `-`)
160
+ - Plural: `{count, plural, one{...} few{...} other{...}}`
161
+ - Offset: `offset:1`, `=0`, `#`
162
+
163
+ ⚠️ Не поддерживает:
164
+ - `select`, `selectordinal`, форматирование чисел/дат
165
+ - Переменные с пробелами: `{first name}` → не заменяется
166
+ - Вложенные `plural` или `#` внутри `=n`
167
+
168
+ Реализовано без внешних зависимостей — только необходимый минимум.
169
+
170
+ #### Пример с `offset`
171
+
172
+ ```json
173
+ "sockets.active": "{count, plural,
174
+ offset:1
175
+ =0 {Только сервер включён}
176
+ one {Подключено ещё одно устройство}
177
+ other {Подключены ещё # устройства}
178
+ }"
179
+ ```
180
+
181
+ - `count = 1` → `# = 0` → "Только сервер включён"
182
+ - `count = 2` → `# = 1` → "Подключено ещё одно устройство"
183
+ - `count = 5` → `# = 4` → "Подключены ещё 4 устройства"
184
+
185
+ Позволяет исключить постоянные элементы из счётчика.
186
+
187
+ ### ✅ Асинхронная инициализация и кэширование
188
+
189
+ - `initLocalizationAsync` загружает и кэширует `fallbackLocale` и системную локаль.
190
+ - `setLocaleAsync` кэширует уже загруженные локали — повторная загрузка не выполняется.
191
+ - Fallback при отсутствии перевода: `current → fallback → {{key}}`.
192
+
193
+ ### ✅ Нормализация локалей
194
+
195
+ Функция `setLocaleAsync` принимает любую строку, но:
196
+ - Автоматически нормализует формат (например, `ru_RU` → `ru-RU`)
197
+ - При некорректном значении — использует `fallbackLocale`
198
+ - Поддерживает `en-US` и `ru-RU`
199
+
200
+ Нет необходимости вручную валидировать локаль.
201
+
202
+ ### ✅ Поддержка языков
203
+
204
+ - `ru`: полная поддержка `one` / `few` / `many` (по [CLDR](https://cldr.unicode.org/))
205
+ - `en` и другие: `one` (если 1), иначе `other`
206
+
207
+ > Для языков с особыми формами множественного числа (например, `pl`, `ar`) требуется расширение `getPluralForm`.
208
+
209
+ #### Языковые особенности
210
+
211
+ В русском языке (`ru-RU`) дробные числа всегда используют форму `few` (например, `36.6 градуса`), независимо от целой части.
212
+
213
+ > При смешанном числе существительным управляет дробь, а не целое число.<br/>
214
+ > — Д. Э. Розенталь. *Справочник по правописанию и литературной правке*, § 164, п. 8
215
+
216
+ Форма `few` используется как ближайшая доступная в ICU, поскольку стандарт не предусматривает отдельной категории для дробных числительных.
217
+
218
+ ## Когда использовать?
219
+
220
+ Используйте `@mirta/i18n`, если:
221
+ - Инструмент требует поддержки нескольких языков,
222
+ - Нужны точные формы множественного числа (особенно для русского),
223
+ - Локали хранятся в `.json` и загружаются асинхронно,
224
+ - Важны малый размер и отсутствие внешних зависимостей.
225
+
226
+ ## Ограничения
227
+
228
+ - Тип `LocaleShape` должен быть объявлен до использования.
229
+ - Наборы локалей кэшируются — избегайте создания тысяч динамических локалей.
230
+ - Нет поддержки `select`, `selectordinal`, форматирования чисел и дат.
231
+ - Переменные с пробелами (`{first name}`) — не заменяются.
232
+ - Подстановка `#` учитывает `offset`, но не поддерживает форматирование.
233
+
234
+ ## Тестирование
235
+
236
+ Пакет покрыт модульными тестами (`vitest`, `@mirta/testing`), проверяющими:
237
+ - Инициализацию,
238
+ - Перевод с переменными и plural,
239
+ - Смену локали,
240
+ - Fallback-логику.