@krizaka/i18n 0.1.0-beta.1 → 0.1.0-beta.2
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 +8 -2
- package/dist/rich.d.ts +9 -1
- package/dist/rich.js +15 -4
- package/dist/rich.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Node ≥ 20, ESM only. React (≥ 18) only for `./rich` and `./react`; TypeScrip
|
|
|
25
25
|
| Entry | What | React |
|
|
26
26
|
| :-- | :-- | :-- |
|
|
27
27
|
| `@krizaka/i18n` | `createI18n`, `format`, `placeholdersOf`, `isPluralMessage`, the types | no |
|
|
28
|
-
| `@krizaka/i18n/rich` | `<Rich text href? renderLink?>` | no hook — Server Components too |
|
|
28
|
+
| `@krizaka/i18n/rich` | `<Rich text href? renderLink? renderBold? slots?>` | no hook — Server Components too |
|
|
29
29
|
| `@krizaka/i18n/react` | `createI18nReact(i18n)` → `I18nProvider`, `useI18n` | client (`"use client"`) |
|
|
30
30
|
| `@krizaka/i18n/check` | `checkMessages`, `findUnused`, `scanHardcoded`, `main` (the CLI, programmatic) | no |
|
|
31
31
|
| `krizaka-i18n` (bin) | `check` · `scan` | no |
|
|
@@ -93,8 +93,14 @@ import { Rich } from "@krizaka/i18n/rich";
|
|
|
93
93
|
<Rich text={t("legal.read")} href={["/terms", "/privacy"]} renderLink={({ href, children }) => <Link href={href!}>{children}</Link>} />
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
```tsx
|
|
97
|
+
<Rich text={t("auth.sent")} slots={{ email: <strong className="break-all">{email}</strong> }} /> // "{email}" → the node
|
|
98
|
+
<Rich text={t("home.lead")} renderBold={(children) => <strong className="font-semibold text-fg">{children}</strong>} />
|
|
99
|
+
```
|
|
100
|
+
|
|
96
101
|
Messages are text, never HTML: an unknown or unbalanced tag is shown as written. Each `<a>` takes the next `href`; a
|
|
97
|
-
link without one renders its words only. `<b>` and `<a>` may hold each other.
|
|
102
|
+
link without one renders its words only. `<b>` and `<a>` may hold each other. `slots` puts a node in place of a
|
|
103
|
+
`{name}` placeholder (one without a slot stays as written); `renderBold` styles the `<b>`.
|
|
98
104
|
|
|
99
105
|
## The CLI
|
|
100
106
|
|
package/dist/rich.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ interface RichProps {
|
|
|
15
15
|
href?: string | readonly string[];
|
|
16
16
|
/** Renders a link (e.g. with `next/link`). Default: `<a href>`, or the plain words when the link has no href. */
|
|
17
17
|
renderLink?: (link: RichLink) => ReactNode;
|
|
18
|
+
/** Renders a `<b>…</b>` (e.g. with the app's classes). Default: `<strong>`. */
|
|
19
|
+
renderBold?: (children: ReactNode) => ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Nodes put in place of `{name}` placeholders (a link, an emphasised value): `slots={{ email: <strong>{email}</strong> }}`.
|
|
22
|
+
* A placeholder without a slot stays as written. Plain values go through `format` / `t` before, not here.
|
|
23
|
+
*/
|
|
24
|
+
slots?: Readonly<Record<string, ReactNode>>;
|
|
18
25
|
}
|
|
19
26
|
/**
|
|
20
27
|
* Renders a message with its markup — `<b>` → `<strong>`, `<a>` → a link — and everything else as text (never HTML:
|
|
@@ -24,8 +31,9 @@ interface RichProps {
|
|
|
24
31
|
* ```tsx
|
|
25
32
|
* <Rich text={t.home.lead} />
|
|
26
33
|
* <Rich text="Read <a>the docs</a>, <b>now</b>." href="/docs" renderLink={({ href, children }) => <Link href={href!}>{children}</Link>} />
|
|
34
|
+
* <Rich text="We sent a link to {email}." slots={{ email: <strong>{email}</strong> }} />
|
|
27
35
|
* ```
|
|
28
36
|
*/
|
|
29
|
-
declare function Rich({ text, href, renderLink }: RichProps): ReactNode;
|
|
37
|
+
declare function Rich({ text, href, renderLink, renderBold, slots }: RichProps): ReactNode;
|
|
30
38
|
|
|
31
39
|
export { Rich, type RichLink, type RichProps, Rich as default };
|
package/dist/rich.js
CHANGED
|
@@ -2,12 +2,23 @@
|
|
|
2
2
|
import { Fragment } from "react";
|
|
3
3
|
import { Fragment as Fragment2, jsx } from "react/jsx-runtime";
|
|
4
4
|
var TAG = /<(b|a)>([\s\S]*?)<\/\1>/g;
|
|
5
|
+
var SLOT = /\{(\w+)\}/;
|
|
5
6
|
function defaultLink({ href, children }) {
|
|
6
7
|
return href === void 0 ? children : /* @__PURE__ */ jsx("a", { href, children });
|
|
7
8
|
}
|
|
8
|
-
function
|
|
9
|
+
function defaultBold(children) {
|
|
10
|
+
return /* @__PURE__ */ jsx("strong", { children });
|
|
11
|
+
}
|
|
12
|
+
function Rich({ text, href, renderLink = defaultLink, renderBold = defaultBold, slots }) {
|
|
9
13
|
const hrefs = typeof href === "string" ? [href] : href ?? [];
|
|
10
14
|
let links = 0;
|
|
15
|
+
const plain = (source, key) => {
|
|
16
|
+
if (!slots) return /* @__PURE__ */ jsx(Fragment, { children: source }, key);
|
|
17
|
+
const parts = source.split(SLOT);
|
|
18
|
+
return /* @__PURE__ */ jsx(Fragment, { children: parts.map(
|
|
19
|
+
(part, i) => i % 2 === 0 ? part : /* @__PURE__ */ jsx(Fragment, { children: Object.hasOwn(slots, part) ? slots[part] : `{${part}}` }, i)
|
|
20
|
+
) }, key);
|
|
21
|
+
};
|
|
11
22
|
const render = (source, prefix) => {
|
|
12
23
|
const out = [];
|
|
13
24
|
let last = 0;
|
|
@@ -15,9 +26,9 @@ function Rich({ text, href, renderLink = defaultLink }) {
|
|
|
15
26
|
const [whole, tag, inner] = match;
|
|
16
27
|
const at = match.index;
|
|
17
28
|
const key = `${prefix}${out.length}`;
|
|
18
|
-
if (at > last) out.push(
|
|
29
|
+
if (at > last) out.push(plain(source.slice(last, at), `${key}t`));
|
|
19
30
|
if (tag === "b") {
|
|
20
|
-
out.push(/* @__PURE__ */ jsx(
|
|
31
|
+
out.push(/* @__PURE__ */ jsx(Fragment, { children: renderBold(render(inner, `${key}.`)) }, key));
|
|
21
32
|
} else {
|
|
22
33
|
const index = links++;
|
|
23
34
|
out.push(
|
|
@@ -26,7 +37,7 @@ function Rich({ text, href, renderLink = defaultLink }) {
|
|
|
26
37
|
}
|
|
27
38
|
last = at + whole.length;
|
|
28
39
|
}
|
|
29
|
-
if (last < source.length) out.push(
|
|
40
|
+
if (last < source.length) out.push(plain(source.slice(last), `${prefix}end`));
|
|
30
41
|
return out;
|
|
31
42
|
};
|
|
32
43
|
return /* @__PURE__ */ jsx(Fragment2, { children: render(text, "") });
|
package/dist/rich.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rich.tsx"],"sourcesContent":["import { Fragment, type ReactNode } from \"react\";\n\n/** What `renderLink` receives for each `<a>…</a>` of a message, in order of appearance. */\nexport interface RichLink {\n /** The href given for this link (`href[index]`, or `href` itself when it is a string), if any. */\n href: string | undefined;\n /** The position of the link in the message: 0 for the first `<a>`. */\n index: number;\n children: ReactNode;\n}\n\nexport interface RichProps {\n /** The message: plain text with `<b>…</b>` emphasis and `<a>…</a>` links, the only markup messages may carry. */\n text: string;\n /** The target of each `<a>`, in order (a string for a single link). The words stay in the message, the URL in code. */\n href?: string | readonly string[];\n /** Renders a link (e.g. with `next/link`). Default: `<a href>`, or the plain words when the link has no href. */\n renderLink?: (link: RichLink) => ReactNode;\n}\n\nconst TAG = /<(b|a)>([\\s\\S]*?)<\\/\\1>/g;\n\nfunction defaultLink({ href, children }: RichLink): ReactNode {\n return href === undefined ? children : <a href={href}>{children}</a>;\n}\n\n/**\n * Renders a message with its markup — `<b>` → `<strong>`, `<a>` → a link — and everything else as text (never HTML:\n * no `dangerouslySetInnerHTML`, an unknown or unbalanced tag is shown as written). `<b>` and `<a>` may hold each other.\n * No hook, no context: usable in a Server Component.\n *\n * ```tsx\n * <Rich text={t.home.lead} />\n * <Rich text=\"Read <a>the docs</a>, <b>now</b>.\" href=\"/docs\" renderLink={({ href, children }) => <Link href={href!}>{children}</Link>} />\n * ```\n */\nexport function Rich({ text, href, renderLink = defaultLink }: RichProps): ReactNode {\n const hrefs = typeof href === \"string\" ? [href] : (href ?? []);\n let links = 0;\n\n // The match is lazy: inside <b>…</b> there is no </b>, so only an <a> can match there (and the other way round).\n const render = (source: string, prefix: string): ReactNode[] => {\n const out: ReactNode[] = [];\n let last = 0;\n for (const match of source.matchAll(TAG)) {\n const [whole, tag, inner] = match as unknown as [string, \"a\" | \"b\", string];\n const at = match.index as number;\n const key = `${prefix}${out.length}`;\n if (at > last) out.push(
|
|
1
|
+
{"version":3,"sources":["../src/rich.tsx"],"sourcesContent":["import { Fragment, type ReactNode } from \"react\";\n\n/** What `renderLink` receives for each `<a>…</a>` of a message, in order of appearance. */\nexport interface RichLink {\n /** The href given for this link (`href[index]`, or `href` itself when it is a string), if any. */\n href: string | undefined;\n /** The position of the link in the message: 0 for the first `<a>`. */\n index: number;\n children: ReactNode;\n}\n\nexport interface RichProps {\n /** The message: plain text with `<b>…</b>` emphasis and `<a>…</a>` links, the only markup messages may carry. */\n text: string;\n /** The target of each `<a>`, in order (a string for a single link). The words stay in the message, the URL in code. */\n href?: string | readonly string[];\n /** Renders a link (e.g. with `next/link`). Default: `<a href>`, or the plain words when the link has no href. */\n renderLink?: (link: RichLink) => ReactNode;\n /** Renders a `<b>…</b>` (e.g. with the app's classes). Default: `<strong>`. */\n renderBold?: (children: ReactNode) => ReactNode;\n /**\n * Nodes put in place of `{name}` placeholders (a link, an emphasised value): `slots={{ email: <strong>{email}</strong> }}`.\n * A placeholder without a slot stays as written. Plain values go through `format` / `t` before, not here.\n */\n slots?: Readonly<Record<string, ReactNode>>;\n}\n\nconst TAG = /<(b|a)>([\\s\\S]*?)<\\/\\1>/g;\n\nconst SLOT = /\\{(\\w+)\\}/;\n\nfunction defaultLink({ href, children }: RichLink): ReactNode {\n return href === undefined ? children : <a href={href}>{children}</a>;\n}\n\nfunction defaultBold(children: ReactNode): ReactNode {\n return <strong>{children}</strong>;\n}\n\n/**\n * Renders a message with its markup — `<b>` → `<strong>`, `<a>` → a link — and everything else as text (never HTML:\n * no `dangerouslySetInnerHTML`, an unknown or unbalanced tag is shown as written). `<b>` and `<a>` may hold each other.\n * No hook, no context: usable in a Server Component.\n *\n * ```tsx\n * <Rich text={t.home.lead} />\n * <Rich text=\"Read <a>the docs</a>, <b>now</b>.\" href=\"/docs\" renderLink={({ href, children }) => <Link href={href!}>{children}</Link>} />\n * <Rich text=\"We sent a link to {email}.\" slots={{ email: <strong>{email}</strong> }} />\n * ```\n */\nexport function Rich({ text, href, renderLink = defaultLink, renderBold = defaultBold, slots }: RichProps): ReactNode {\n const hrefs = typeof href === \"string\" ? [href] : (href ?? []);\n let links = 0;\n\n /** Plain text, with its `{slot}` placeholders replaced by their nodes. */\n const plain = (source: string, key: string): ReactNode => {\n if (!slots) return <Fragment key={key}>{source}</Fragment>;\n const parts = source.split(SLOT);\n return (\n <Fragment key={key}>\n {parts.map((part, i) =>\n i % 2 === 0 ? part : <Fragment key={i}>{Object.hasOwn(slots, part) ? slots[part] : `{${part}}`}</Fragment>,\n )}\n </Fragment>\n );\n };\n\n // The match is lazy: inside <b>…</b> there is no </b>, so only an <a> can match there (and the other way round).\n const render = (source: string, prefix: string): ReactNode[] => {\n const out: ReactNode[] = [];\n let last = 0;\n for (const match of source.matchAll(TAG)) {\n const [whole, tag, inner] = match as unknown as [string, \"a\" | \"b\", string];\n const at = match.index as number;\n const key = `${prefix}${out.length}`;\n if (at > last) out.push(plain(source.slice(last, at), `${key}t`));\n if (tag === \"b\") {\n out.push(<Fragment key={key}>{renderBold(render(inner, `${key}.`))}</Fragment>);\n } else {\n const index = links++;\n out.push(\n <Fragment key={key}>{renderLink({ href: hrefs[index], index, children: render(inner, `${key}.`) })}</Fragment>,\n );\n }\n last = at + whole.length;\n }\n if (last < source.length) out.push(plain(source.slice(last), `${prefix}end`));\n return out;\n };\n\n return <>{render(text, \"\")}</>;\n}\n\nexport default Rich;\n"],"mappings":";AAAA,SAAS,gBAAgC;AAgCA,SA0DhC,YAAAA,WA1DgC;AALzC,IAAM,MAAM;AAEZ,IAAM,OAAO;AAEb,SAAS,YAAY,EAAE,MAAM,SAAS,GAAwB;AAC5D,SAAO,SAAS,SAAY,WAAW,oBAAC,OAAE,MAAa,UAAS;AAClE;AAEA,SAAS,YAAY,UAAgC;AACnD,SAAO,oBAAC,YAAQ,UAAS;AAC3B;AAaO,SAAS,KAAK,EAAE,MAAM,MAAM,aAAa,aAAa,aAAa,aAAa,MAAM,GAAyB;AACpH,QAAM,QAAQ,OAAO,SAAS,WAAW,CAAC,IAAI,IAAK,QAAQ,CAAC;AAC5D,MAAI,QAAQ;AAGZ,QAAM,QAAQ,CAAC,QAAgB,QAA2B;AACxD,QAAI,CAAC,MAAO,QAAO,oBAAC,YAAoB,oBAAN,GAAa;AAC/C,UAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,WACE,oBAAC,YACE,gBAAM;AAAA,MAAI,CAAC,MAAM,MAChB,IAAI,MAAM,IAAI,OAAO,oBAAC,YAAkB,iBAAO,OAAO,OAAO,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,IAAI,OAAvD,CAA2D;AAAA,IACjG,KAHa,GAIf;AAAA,EAEJ;AAGA,QAAM,SAAS,CAAC,QAAgB,WAAgC;AAC9D,UAAM,MAAmB,CAAC;AAC1B,QAAI,OAAO;AACX,eAAW,SAAS,OAAO,SAAS,GAAG,GAAG;AACxC,YAAM,CAAC,OAAO,KAAK,KAAK,IAAI;AAC5B,YAAM,KAAK,MAAM;AACjB,YAAM,MAAM,GAAG,MAAM,GAAG,IAAI,MAAM;AAClC,UAAI,KAAK,KAAM,KAAI,KAAK,MAAM,OAAO,MAAM,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC;AAChE,UAAI,QAAQ,KAAK;AACf,YAAI,KAAK,oBAAC,YAAoB,qBAAW,OAAO,OAAO,GAAG,GAAG,GAAG,CAAC,KAAzC,GAA2C,CAAW;AAAA,MAChF,OAAO;AACL,cAAM,QAAQ;AACd,YAAI;AAAA,UACF,oBAAC,YAAoB,qBAAW,EAAE,MAAM,MAAM,KAAK,GAAG,OAAO,UAAU,OAAO,OAAO,GAAG,GAAG,GAAG,EAAE,CAAC,KAAlF,GAAoF;AAAA,QACrG;AAAA,MACF;AACA,aAAO,KAAK,MAAM;AAAA,IACpB;AACA,QAAI,OAAO,OAAO,OAAQ,KAAI,KAAK,MAAM,OAAO,MAAM,IAAI,GAAG,GAAG,MAAM,KAAK,CAAC;AAC5E,WAAO;AAAA,EACT;AAEA,SAAO,oBAAAA,WAAA,EAAG,iBAAO,MAAM,EAAE,GAAE;AAC7B;AAEA,IAAO,eAAQ;","names":["Fragment"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/i18n",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "The i18n engine of every Krizaka app: typed message keys and placeholders from en.json, format(), plurals through @krizaka/intl, <Rich> (<b>, <a>), a React provider, and the krizaka-i18n CLI that checks catalogues and finds hard-coded strings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"krizaka",
|