@konce-pt/i18n 0.16.0 → 0.17.0

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://ui.konce.pt/images/koncept_ui_lib.png" alt="Koncept UI - Angular component library" width="100%" />
2
+ <img src="https://ui.konce.pt/images/koncept_ui_lib.png" alt="Koncept UI" width="100%" />
3
3
  </p>
4
4
 
5
5
  # @konce-pt/i18n
@@ -24,6 +24,9 @@ translate(KPT_MESSAGES_PL, 'paginator.range', { start: 1, end: 10, total: 42 });
24
24
 
25
25
  translate(KPT_MESSAGES_PL, 'nope.missing');
26
26
  // 'nope.missing' - brak klucza spada na EN, a potem na sam klucz
27
+
28
+ translate(KPT_MESSAGES_PL, 'dataView.count', { count: 5 }, 'pl');
29
+ // '5 elementów' - liczba mnoga ICU, formę wybiera `Intl.PluralRules` dla podanego języka
27
30
  ```
28
31
 
29
32
  ## API
@@ -33,12 +36,14 @@ translate(KPT_MESSAGES_PL, 'nope.missing');
33
36
  | `KptMessages` | `interface` | Kontrakt wszystkich etykiet UI biblioteki |
34
37
  | `KptLocale` | `type` | `'en' \| 'pl' \| (string & {})` - otwarty kod języka |
35
38
  | `KPT_MESSAGES_EN` / `KPT_MESSAGES_PL` | `KptMessages` | Słowniki bazowe |
36
- | `translate(dict, key, params?)` | `string` | Klucz kropkowy + interpolacja `{name}`, fallback -> EN -> klucz |
39
+ | `translate(dict, key, params?, locale?)` | `string` | Klucz kropkowy + interpolacja `{name}` i liczba mnoga ICU (`{count, plural, ...}`, reguły z `locale`, domyślnie `'en'`), fallback -> EN -> klucz |
37
40
  | `lookup(dict, key)` | `string \| undefined` | Surowa wartość spod klucza kropkowego |
38
41
  | `deepMerge(base, patch)` | `T` | Nakłada częściowy słownik na pełny |
39
42
  | `weekdayNames(locale)` | `string[]` | Skrócone nazwy dni, od poniedziałku |
40
43
  | `monthNames(locale)` | `string[]` | Pełne nazwy miesięcy |
41
- | `DeepPartial<T>`, `KptI18nConfig` | `type` | Typy używane przez wiązania frameworkowe |
44
+ | `formatBytes(bytes, locale, unit?, fixed?)`, `byteUnitOf(bytes)` | `string`, `KptByteUnit` | Rozmiar pliku w języku interfejsu (podstawa 1024) i jednostka, w której go zapisać |
45
+ | `KptMessageKey` | `type` | Unia kluczy kropkowych słownika bazowego - podpowiedzi w edytorze |
46
+ | `DeepPartial<T>`, `KptI18nConfig`, `KptByteUnit` | `type` | Typy używane przez wiązania frameworkowe |
42
47
 
43
48
  Nazw dni i miesięcy nie ma w słowniku - wyliczają się z `Intl` dla aktywnego locale, więc
44
49
  kalendarze działają także w języku, którego nikt nie tłumaczył ręcznie.
@@ -46,6 +51,12 @@ kalendarze działają także w języku, którego nikt nie tłumaczył ręcznie.
46
51
  Pełny opis: [`llms.txt`](https://ui.konce.pt/llms/i18n/llms.txt) (EN)
47
52
  i [`llms-pl.txt`](https://ui.konce.pt/llms/i18n/llms-pl.txt) (PL).
48
53
 
54
+ ## Testy
55
+
56
+ ```bash
57
+ pnpm --filter @konce-pt/i18n exec node --test "src/**/*.test.ts"
58
+ ```
59
+
49
60
  ## Licencja
50
61
 
51
62
  MIT © konce.pt
@@ -268,6 +268,11 @@ export interface KptMessages {
268
268
  /** Ogłoszenie po przestawieniu. Interpolacja: {position}, {total}. */
269
269
  moved: string;
270
270
  };
271
+ /** Pole tekstowe (kpt-input). */
272
+ input: {
273
+ /** Przycisk czyszczenia pola - nazwa dla czytnika ekranu. */
274
+ clear: string;
275
+ };
271
276
  inputNumber: {
272
277
  decrement: string;
273
278
  increment: string;
@@ -381,6 +386,10 @@ export interface KptMessages {
381
386
  image: string;
382
387
  /** Ogłoszenie zmiany obrazu. Interpolacja: {index}, {total}. */
383
388
  position: string;
389
+ /** Strzałka paska miniatur - przewija pasek w stronę początku. */
390
+ scrollBack: string;
391
+ /** Strzałka paska miniatur - przewija pasek w stronę końca. */
392
+ scrollForward: string;
384
393
  };
385
394
  carousel: {
386
395
  /** Interpolacja: {index}. */
package/dist/messages.js CHANGED
@@ -40,7 +40,7 @@ export const KPT_MESSAGES_EN = {
40
40
  grid: 'Grid',
41
41
  columns: 'Columns',
42
42
  empty: 'No items',
43
- count: '{count} items',
43
+ count: '{count, plural, one {# item} other {# items}}',
44
44
  },
45
45
  roadmap: {
46
46
  empty: 'No items on the roadmap',
@@ -99,9 +99,9 @@ export const KPT_MESSAGES_EN = {
99
99
  results: 'Address suggestions',
100
100
  clear: 'Clear the search',
101
101
  noResults: 'No matching places',
102
- minLength: 'Type at least {count} characters',
102
+ minLength: '{count, plural, one {Type at least # character} other {Type at least # characters}}',
103
103
  searching: 'Searching...',
104
- found: '{count} suggestions',
104
+ found: '{count, plural, one {# suggestion} other {# suggestions}}',
105
105
  },
106
106
  field: {
107
107
  invalidEmail: 'Enter a valid e-mail address',
@@ -216,6 +216,9 @@ export const KPT_MESSAGES_EN = {
216
216
  down: 'Down',
217
217
  moved: 'Moved to position {position} of {total}',
218
218
  },
219
+ input: {
220
+ clear: 'Clear the field',
221
+ },
219
222
  inputNumber: {
220
223
  decrement: 'Decrease',
221
224
  increment: 'Increase',
@@ -306,6 +309,8 @@ export const KPT_MESSAGES_EN = {
306
309
  galleria: {
307
310
  image: 'Image {index}',
308
311
  position: 'Image {index} of {total}',
312
+ scrollBack: 'Scroll thumbnails back',
313
+ scrollForward: 'Scroll thumbnails forward',
309
314
  },
310
315
  carousel: {
311
316
  slide: 'Slide {index}',
@@ -447,7 +452,7 @@ export const KPT_MESSAGES_EN = {
447
452
  today: 'Today',
448
453
  yesterday: 'Yesterday',
449
454
  typing: '{name} is typing...',
450
- typingMany: '{count} people are typing...',
455
+ typingMany: '{count, plural, one {# person is typing...} other {# people are typing...}}',
451
456
  sending: 'Sending',
452
457
  sent: 'Sent',
453
458
  delivered: 'Delivered',
@@ -457,7 +462,7 @@ export const KPT_MESSAGES_EN = {
457
462
  offline: 'No connection',
458
463
  pending: '{count} waiting to be sent',
459
464
  queue: 'Queue',
460
- newMessages: '{count} new messages',
465
+ newMessages: '{count, plural, one {# new message} other {# new messages}}',
461
466
  unread: 'Unread messages',
462
467
  empty: 'No messages yet',
463
468
  history: 'Conversation history',
@@ -475,7 +480,7 @@ export const KPT_MESSAGES_EN = {
475
480
  reactSad: 'Sad',
476
481
  open: 'Open chat',
477
482
  close: 'Close chat',
478
- unreadBadge: '{count} unread messages',
483
+ unreadBadge: '{count, plural, one {# unread message} other {# unread messages}}',
479
484
  },
480
485
  speech: {
481
486
  dictate: 'Dictate',
@@ -526,7 +531,7 @@ export const KPT_MESSAGES_PL = {
526
531
  grid: 'Siatka',
527
532
  columns: 'Kolumny',
528
533
  empty: 'Brak elementów',
529
- count: '{count} elementów',
534
+ count: '{count, plural, one {# element} few {# elementy} many {# elementów} other {# elementu}}',
530
535
  },
531
536
  roadmap: {
532
537
  empty: 'Brak elementów na roadmapie',
@@ -702,6 +707,9 @@ export const KPT_MESSAGES_PL = {
702
707
  down: 'W dół',
703
708
  moved: 'Przeniesiono na pozycję {position} z {total}',
704
709
  },
710
+ input: {
711
+ clear: 'Wyczyść pole',
712
+ },
705
713
  inputNumber: {
706
714
  decrement: 'Zmniejsz',
707
715
  increment: 'Zwiększ',
@@ -792,6 +800,8 @@ export const KPT_MESSAGES_PL = {
792
800
  galleria: {
793
801
  image: 'Obraz {index}',
794
802
  position: 'Obraz {index} z {total}',
803
+ scrollBack: 'Przewiń miniatury wstecz',
804
+ scrollForward: 'Przewiń miniatury dalej',
795
805
  },
796
806
  carousel: {
797
807
  slide: 'Slajd {index}',
@@ -933,7 +943,7 @@ export const KPT_MESSAGES_PL = {
933
943
  today: 'Dziś',
934
944
  yesterday: 'Wczoraj',
935
945
  typing: '{name} pisze...',
936
- typingMany: '{count} osoby piszą...',
946
+ typingMany: '{count, plural, one {# osoba pisze...} few {# osoby piszą...} many {# osób pisze...} other {# osoby pisze...}}',
937
947
  sending: 'Wysyłanie',
938
948
  sent: 'Wysłano',
939
949
  delivered: 'Dostarczono',
@@ -941,9 +951,9 @@ export const KPT_MESSAGES_PL = {
941
951
  failed: 'Nie wysłano',
942
952
  retry: 'Wyślij ponownie',
943
953
  offline: 'Brak połączenia',
944
- pending: '{count} czeka na wysłanie',
954
+ pending: '{count, plural, one {# czeka na wysłanie} few {# czekają na wysłanie} many {# czeka na wysłanie} other {# czeka na wysłanie}}',
945
955
  queue: 'Do kolejki',
946
- newMessages: '{count} nowych wiadomości',
956
+ newMessages: '{count, plural, one {# nowa wiadomość} few {# nowe wiadomości} many {# nowych wiadomości} other {# nowej wiadomości}}',
947
957
  unread: 'Nieprzeczytane',
948
958
  empty: 'Brak wiadomości',
949
959
  history: 'Historia rozmowy',
@@ -961,7 +971,7 @@ export const KPT_MESSAGES_PL = {
961
971
  reactSad: 'Smutek',
962
972
  open: 'Otwórz czat',
963
973
  close: 'Zamknij czat',
964
- unreadBadge: '{count} nieprzeczytanych wiadomości',
974
+ unreadBadge: '{count, plural, one {# nieprzeczytana wiadomość} few {# nieprzeczytane wiadomości} many {# nieprzeczytanych wiadomości} other {# nieprzeczytanej wiadomości}}',
965
975
  },
966
976
  speech: {
967
977
  dictate: 'Dyktuj',
@@ -7,5 +7,8 @@ export declare function lookup(dict: KptMessages, key: string): string | undefin
7
7
  /**
8
8
  * Tłumaczy klucz w notacji kropkowej (np. `'paginator.range'`), z opcjonalną
9
9
  * interpolacją `{name}`. Brakujący klucz spada na EN, a w ostateczności zwraca sam klucz.
10
+ *
11
+ * Liczbę mnogą zapisuje się blokiem ICU `{count, plural, one {# item} other {# items}}`;
12
+ * `locale` wybiera reguły odmiany (domyślnie `'en'`). Porty przekazują tu bieżący język.
10
13
  */
11
- export declare function translate(dict: KptMessages, key: string, params?: Record<string, string | number>): string;
14
+ export declare function translate(dict: KptMessages, key: string, params?: Record<string, string | number>, locale?: string): string;
package/dist/translate.js CHANGED
@@ -33,13 +33,85 @@ export function lookup(dict, key) {
33
33
  }
34
34
  return typeof node === 'string' ? node : undefined;
35
35
  }
36
+ /** Pozycja zamykającego `}` dla `{` stojącego na `open`, z liczeniem zagnieżdżeń; -1 bez pary. */
37
+ function closingBrace(text, open) {
38
+ let depth = 0;
39
+ for (let i = open; i < text.length; i++) {
40
+ if (text[i] === '{')
41
+ depth++;
42
+ else if (text[i] === '}' && --depth === 0)
43
+ return i;
44
+ }
45
+ return -1;
46
+ }
47
+ /** Gałęzie bloku liczby mnogiej: `one {...} few {...} =0 {...}` -> mapa selektor -> tekst. */
48
+ function pluralBranches(body) {
49
+ const branches = new Map();
50
+ let i = 0;
51
+ while (i < body.length) {
52
+ const match = /^\s*(=\d+|zero|one|two|few|many|other)\s*\{/.exec(body.slice(i));
53
+ if (!match)
54
+ return body.slice(i).trim() === '' ? branches : null;
55
+ const open = i + match[0].length - 1;
56
+ const close = closingBrace(body, open);
57
+ if (close < 0)
58
+ return null;
59
+ branches.set(match[1], body.slice(open + 1, close));
60
+ i = close + 1;
61
+ }
62
+ return branches;
63
+ }
64
+ /** Reguły liczby mnogiej per język - `Intl.PluralRules` jest drogi w tworzeniu, więc raz na język. */
65
+ const pluralRules = new Map();
66
+ function pluralCategory(locale, value) {
67
+ let rules = pluralRules.get(locale);
68
+ if (!rules) {
69
+ try {
70
+ rules = new Intl.PluralRules(locale);
71
+ }
72
+ catch {
73
+ // Nieznany tag języka (własny słownik pod umowną nazwą) - reguły angielskie zamiast wyjątku.
74
+ rules = new Intl.PluralRules('en');
75
+ }
76
+ pluralRules.set(locale, rules);
77
+ }
78
+ return rules.select(value);
79
+ }
80
+ /**
81
+ * Rozwija bloki `{name, plural, one {...} few {...} many {...} other {...}}` (podzbiór składni ICU).
82
+ *
83
+ * Formę wybiera `Intl.PluralRules` dla `locale`, więc słownik podaje tylko formy, a reguły
84
+ * (`2 elementy`, ale `5 elementów` i `22 elementy`) zna przeglądarka. Dokładne dopasowanie `=N`
85
+ * wygrywa z kategorią, `other` jest obowiązkowym zapasem. `#` w gałęzi to sama liczba.
86
+ * Blok z nieznanym parametrem albo bez pary nawiasów zostaje w tekście bez zmian.
87
+ */
88
+ function expandPlurals(text, params, locale) {
89
+ const head = /\{(\w+),\s*plural,/g;
90
+ let out = '';
91
+ let from = 0;
92
+ for (let match = head.exec(text); match; match = head.exec(text)) {
93
+ const close = closingBrace(text, match.index);
94
+ const value = Number(params[match[1]]);
95
+ const branches = close < 0 ? null : pluralBranches(text.slice(match.index + match[0].length, close));
96
+ if (close < 0 || !branches || params[match[1]] === undefined || Number.isNaN(value))
97
+ continue;
98
+ const chosen = branches.get(`=${value}`) ?? branches.get(pluralCategory(locale, value)) ?? branches.get('other') ?? '';
99
+ out += text.slice(from, match.index) + chosen.replace(/#/g, String(params[match[1]]));
100
+ from = close + 1;
101
+ head.lastIndex = from;
102
+ }
103
+ return out + text.slice(from);
104
+ }
36
105
  /**
37
106
  * Tłumaczy klucz w notacji kropkowej (np. `'paginator.range'`), z opcjonalną
38
107
  * interpolacją `{name}`. Brakujący klucz spada na EN, a w ostateczności zwraca sam klucz.
108
+ *
109
+ * Liczbę mnogą zapisuje się blokiem ICU `{count, plural, one {# item} other {# items}}`;
110
+ * `locale` wybiera reguły odmiany (domyślnie `'en'`). Porty przekazują tu bieżący język.
39
111
  */
40
- export function translate(dict, key, params) {
112
+ export function translate(dict, key, params, locale = 'en') {
41
113
  const raw = lookup(dict, key) ?? lookup(KPT_MESSAGES_EN, key) ?? key;
42
114
  if (!params)
43
115
  return raw;
44
- return raw.replace(/\{(\w+)\}/g, (_, name) => String(params[name] ?? `{${name}}`));
116
+ return expandPlurals(raw, params, locale).replace(/\{(\w+)\}/g, (_, name) => String(params[name] ?? `{${name}}`));
45
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konce-pt/i18n",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Translation contract and base dictionaries (EN, PL) for Koncept UI - shared by the Angular and React ports. Pure TypeScript, zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "konce.pt",