@widgetic/design-system 0.5.15 → 0.5.17
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 +167 -0
- package/dist/components/CopyableHeader.svelte +25 -11
- package/dist/components/CopyableHeader.svelte.d.ts +4 -0
- package/dist/components/EmptyState.svelte +1 -1
- package/dist/components/PasswordInput.svelte +2 -2
- package/dist/components/WScrollArea.svelte +2 -2
- package/dist/components/ic/browser/BrowserInputController.svelte +2 -2
- package/dist/components/index.d.ts +1 -0
- package/dist/components/modals/ActionModal.svelte +3 -0
- package/dist/components/modals/ActionModal.svelte.d.ts +1 -0
- package/dist/components/modals/BaseModal.svelte +4 -2
- package/dist/components/modals/BaseModal.svelte.d.ts +1 -0
- package/dist/components/modals/NewItemDialog.svelte +48 -30
- package/dist/components/modals/NewItemDialog.svelte.d.ts +3 -0
- package/dist/components/modals/Portal.svelte +9 -1
- package/dist/components/modals/README.md +34 -1
- package/dist/components/modals/RenameDialog.svelte +29 -9
- package/dist/components/modals/RenameDialog.svelte.d.ts +3 -0
- package/dist/components/modals/messages.d.ts +39 -0
- package/dist/components/modals/messages.js +10 -0
- package/dist/components/navigation/Breadcrumbs.svelte +30 -17
- package/dist/components/navigation/Breadcrumbs.svelte.d.ts +4 -0
- package/dist/components/navigation/types.d.ts +8 -0
- package/dist/components/pdf/PDFViewer.svelte +17 -8
- package/dist/components/pdf/PDFViewer.svelte.d.ts +5 -0
- package/dist/components/search-input/SearchInput.svelte +3 -3
- package/dist/components/shad-editor/colorpicker.svelte +4 -1
- package/dist/components/shad-editor/custom/Extentions/ImageExtention.d.ts +20 -1
- package/dist/components/shad-editor/custom/Extentions/ImageExtention.js +4 -1
- package/dist/components/shad-editor/custom/code-extended.svelte +4 -2
- package/dist/components/shad-editor/custom/image-extended-component.svelte +54 -32
- package/dist/components/shad-editor/editor.css +5 -5
- package/dist/components/shad-editor/i18n/context.d.ts +6 -0
- package/dist/components/shad-editor/i18n/context.js +11 -0
- package/dist/components/shad-editor/i18n/index.d.ts +8 -0
- package/dist/components/shad-editor/i18n/index.js +11 -0
- package/dist/components/shad-editor/i18n/messages.d.ts +109 -0
- package/dist/components/shad-editor/i18n/messages.js +116 -0
- package/dist/components/shad-editor/icons/block-quote.svelte +3 -1
- package/dist/components/shad-editor/icons/bold.svelte +3 -1
- package/dist/components/shad-editor/icons/buttle-list.svelte +3 -1
- package/dist/components/shad-editor/icons/code.svelte +3 -1
- package/dist/components/shad-editor/icons/highlighter.svelte +5 -3
- package/dist/components/shad-editor/icons/image.svelte +7 -5
- package/dist/components/shad-editor/icons/italic.svelte +3 -1
- package/dist/components/shad-editor/icons/link.svelte +8 -6
- package/dist/components/shad-editor/icons/ordered-list.svelte +3 -1
- package/dist/components/shad-editor/icons/quickcolor.svelte +24 -22
- package/dist/components/shad-editor/icons/redo.svelte +3 -1
- package/dist/components/shad-editor/icons/search-replace.svelte +16 -14
- package/dist/components/shad-editor/icons/strikethrough.svelte +3 -1
- package/dist/components/shad-editor/icons/subscript.svelte +3 -1
- package/dist/components/shad-editor/icons/superscript.svelte +3 -1
- package/dist/components/shad-editor/icons/table.svelte +21 -19
- package/dist/components/shad-editor/icons/task-list.svelte +3 -1
- package/dist/components/shad-editor/icons/text.svelte +13 -11
- package/dist/components/shad-editor/icons/textalign.svelte +11 -9
- package/dist/components/shad-editor/icons/textcolor.svelte +5 -3
- package/dist/components/shad-editor/icons/underline.svelte +3 -1
- package/dist/components/shad-editor/icons/undo.svelte +3 -1
- package/dist/components/shad-editor/index.d.ts +1 -0
- package/dist/components/shad-editor/index.js +2 -0
- package/dist/components/shad-editor/shad-editor.svelte +32 -3
- package/dist/components/shad-editor/shad-editor.svelte.d.ts +12 -0
- package/dist/components/textDirection.d.ts +91 -0
- package/dist/components/textDirection.js +99 -0
- package/dist/components/toast/ToasterModal.svelte +49 -14
- package/dist/components/toast/ToasterModal.svelte.d.ts +9 -2
- package/dist/components/toast/toasterRoots.d.ts +23 -0
- package/dist/components/toast/toasterRoots.js +36 -0
- package/dist/components/ui/alert/index.d.ts +2 -2
- package/dist/components/ui/alert/index.js +1 -1
- package/dist/components/ui/breadcrumb/breadcrumb-separator.svelte +1 -1
- package/dist/components/ui/calendar/calendar-next-button.svelte +21 -4
- package/dist/components/ui/calendar/calendar-next-button.svelte.d.ts +9 -2
- package/dist/components/ui/calendar/calendar-prev-button.svelte +21 -4
- package/dist/components/ui/calendar/calendar-prev-button.svelte.d.ts +9 -2
- package/dist/components/ui/calendar/calendar.svelte +18 -6
- package/dist/components/ui/calendar/calendar.svelte.d.ts +6 -1
- package/dist/components/ui/command/command-input.svelte +1 -1
- package/dist/components/ui/command/command-shortcut.svelte +1 -1
- package/dist/components/ui/dialog/dialog-content.svelte +5 -2
- package/dist/components/ui/dialog/dialog-content.svelte.d.ts +1 -0
- package/dist/components/ui/dialog/dialog-header.svelte +1 -1
- package/dist/components/ui/drawer/drawer-header.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +2 -2
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +6 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +2 -2
- package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +7 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +2 -2
- package/dist/components/ui/hover-card/hover-card-content.svelte +6 -0
- package/dist/components/ui/input-otp/input-otp-slot.svelte +1 -1
- package/dist/components/ui/popover/popover-content.svelte +6 -0
- package/dist/components/ui/progress/progress.svelte +2 -2
- package/dist/components/ui/range-calendar/range-calendar-cell.svelte +1 -1
- package/dist/components/ui/range-calendar/range-calendar-next-button.svelte +21 -4
- package/dist/components/ui/range-calendar/range-calendar-next-button.svelte.d.ts +9 -2
- package/dist/components/ui/range-calendar/range-calendar-prev-button.svelte +21 -4
- package/dist/components/ui/range-calendar/range-calendar-prev-button.svelte.d.ts +9 -2
- package/dist/components/ui/range-calendar/range-calendar.svelte +18 -6
- package/dist/components/ui/range-calendar/range-calendar.svelte.d.ts +7 -2
- package/dist/components/ui/scroll-area/scroll-area-scrollbar.svelte +1 -1
- package/dist/components/ui/scroll-area/scroll-area.svelte +6 -1
- package/dist/components/ui/select/select-content.svelte +6 -0
- package/dist/components/ui/select/select-group-heading.svelte +1 -1
- package/dist/components/ui/select/select-item.svelte +2 -2
- package/dist/components/ui/slider/slider.svelte +6 -0
- package/dist/components/ui/switch/switch.svelte +1 -1
- package/dist/components/ui/table/table-cell.svelte +1 -1
- package/dist/components/ui/table/table-head.svelte +1 -1
- package/dist/components/ui/tooltip/tooltip-content.svelte +6 -0
- package/dist/i18n/audit/catalogUsage.d.ts +36 -0
- package/dist/i18n/audit/catalogUsage.js +58 -0
- package/dist/i18n/audit/designSystemWords.d.ts +47 -0
- package/dist/i18n/audit/designSystemWords.js +137 -0
- package/dist/i18n/audit/hardCodedText.d.ts +40 -0
- package/dist/i18n/audit/hardCodedText.js +27 -0
- package/dist/i18n/audit/index.d.ts +19 -0
- package/dist/i18n/audit/index.js +19 -0
- package/dist/i18n/audit/messageCalls.d.ts +34 -0
- package/dist/i18n/audit/messageCalls.js +89 -0
- package/dist/i18n/audit/syntaxTree.d.ts +33 -0
- package/dist/i18n/audit/syntaxTree.js +90 -0
- package/dist/i18n/audit/visibleText.d.ts +57 -0
- package/dist/i18n/audit/visibleText.js +252 -0
- package/dist/i18n/createI18n.d.ts +63 -0
- package/dist/i18n/createI18n.js +72 -0
- package/dist/i18n/direction.d.ts +27 -0
- package/dist/i18n/direction.js +89 -0
- package/dist/i18n/index.d.ts +17 -0
- package/dist/i18n/index.js +16 -0
- package/dist/i18n/localeDate.d.ts +34 -0
- package/dist/i18n/localeDate.js +104 -0
- package/dist/i18n/splitMessages.d.ts +21 -0
- package/dist/i18n/splitMessages.js +33 -0
- package/dist/i18n/translator.d.ts +61 -0
- package/dist/i18n/translator.js +337 -0
- package/dist/rtl/audit/auditPhysicalPlacement.d.ts +36 -0
- package/dist/rtl/audit/auditPhysicalPlacement.js +58 -0
- package/dist/rtl/audit/index.d.ts +14 -0
- package/dist/rtl/audit/index.js +14 -0
- package/dist/rtl/audit/physicalPlacement.d.ts +64 -0
- package/dist/rtl/audit/physicalPlacement.js +444 -0
- package/dist/shad-editor/index.d.ts +1 -1
- package/dist/shad-editor/index.js +1 -1
- package/dist/styles/global.css +4 -4
- package/dist/utils/fillMessage.d.ts +8 -0
- package/dist/utils/fillMessage.js +10 -0
- package/dist/utils/itemName.d.ts +17 -0
- package/dist/utils/itemName.js +41 -0
- package/package.json +18 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds where the code asks the catalog for a message: `$t('upload.cancel')`, `$t('key', { name })`,
|
|
3
|
+
* `messageForVisitor(error, $t, 'key')`, `{ messageKey: 'key', messageValues: { limit } }`. A test then checks that each
|
|
4
|
+
* key is in the catalog and that the values given are the values the message asks for (a value that is never given shows
|
|
5
|
+
* up in the page as a bare `{name}`).
|
|
6
|
+
*
|
|
7
|
+
* It reads the syntax tree, like `visibleText.ts`. A `.ts` file is read as the body of a module script, which Svelte's
|
|
8
|
+
* parser reads with the same TypeScript support.
|
|
9
|
+
*/
|
|
10
|
+
export interface MessageCall {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
/** The key, when it is a plain string; the part before the `${...}` when it is built (`filterName.${filter}`). */
|
|
14
|
+
key: string;
|
|
15
|
+
/** True when the key is built from a value (`filterName.${filter}`): only the start of it is known. */
|
|
16
|
+
keyIsPrefix: boolean;
|
|
17
|
+
/** The names of the values handed over, or `null` when they are not written out as an object (`$t('key', values)`). */
|
|
18
|
+
valueNames: string[] | null;
|
|
19
|
+
/** Which call it is, for the failure message. */
|
|
20
|
+
via: string;
|
|
21
|
+
}
|
|
22
|
+
/** A function that looks a message up: which of its arguments is the key, and which holds the values (`null` when none does). */
|
|
23
|
+
export interface MessageLookup {
|
|
24
|
+
key: number;
|
|
25
|
+
values: number | null;
|
|
26
|
+
}
|
|
27
|
+
/** The functions every package uses to look a message up. */
|
|
28
|
+
export declare const DEFAULT_LOOKUPS: Readonly<Record<string, MessageLookup>>;
|
|
29
|
+
export interface MessageCallOptions {
|
|
30
|
+
/** More functions that look a message up (`messageForVisitor: { key: 2, values: null }`). Added to the defaults. */
|
|
31
|
+
lookups?: Readonly<Record<string, MessageLookup>>;
|
|
32
|
+
}
|
|
33
|
+
/** Every message the code in one file asks for. */
|
|
34
|
+
export declare function findMessageCalls(file: string, source: string, options?: MessageCallOptions): MessageCall[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { lineOf, parseFile, walkTree } from "./syntaxTree.js";
|
|
2
|
+
/** The functions every package uses to look a message up. */
|
|
3
|
+
export const DEFAULT_LOOKUPS = {
|
|
4
|
+
$t: { key: 0, values: 1 },
|
|
5
|
+
t: { key: 0, values: 1 },
|
|
6
|
+
translate: { key: 0, values: 1 },
|
|
7
|
+
translateEnglish: { key: 0, values: 1 }
|
|
8
|
+
};
|
|
9
|
+
/** The name of a function called by its own name (`t`); an empty text for a method call or anything else. */
|
|
10
|
+
function plainCalleeName(call) {
|
|
11
|
+
const callee = call.callee;
|
|
12
|
+
if (!callee)
|
|
13
|
+
return '';
|
|
14
|
+
if (callee.type === 'Identifier')
|
|
15
|
+
return callee.name;
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
/** The key written at a node: a string, or the fixed start of a template literal. `null` for anything else. */
|
|
19
|
+
function keyOf(node) {
|
|
20
|
+
if (!node)
|
|
21
|
+
return null;
|
|
22
|
+
if (node.type === 'Literal' && typeof node.value === 'string')
|
|
23
|
+
return { key: node.value, keyIsPrefix: false };
|
|
24
|
+
if (node.type === 'TemplateLiteral') {
|
|
25
|
+
const start = node.quasis[0]?.value?.cooked ?? '';
|
|
26
|
+
return node.expressions.length === 0 ? { key: start, keyIsPrefix: false } : { key: start, keyIsPrefix: true };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** The property names of an object written out in the code, or null when the node is anything else. */
|
|
31
|
+
function propertyNamesOf(node) {
|
|
32
|
+
if (!node || node.type !== 'ObjectExpression')
|
|
33
|
+
return null;
|
|
34
|
+
const names = [];
|
|
35
|
+
for (const property of node.properties) {
|
|
36
|
+
// A spread (`{ ...values }`) or a computed name (`{ [name]: 1 }`) hides names: give up on this call rather than guess.
|
|
37
|
+
if (property.type !== 'Property' || property.computed)
|
|
38
|
+
return null;
|
|
39
|
+
const name = property.key?.name ?? property.key?.value;
|
|
40
|
+
if (typeof name !== 'string')
|
|
41
|
+
return null;
|
|
42
|
+
names.push(name);
|
|
43
|
+
}
|
|
44
|
+
return names;
|
|
45
|
+
}
|
|
46
|
+
/** Every message the code in one file asks for. */
|
|
47
|
+
export function findMessageCalls(file, source, options = {}) {
|
|
48
|
+
const lookups = { ...DEFAULT_LOOKUPS, ...(options.lookups ?? {}) };
|
|
49
|
+
const { tree, text, lineShift } = parseFile(file, source);
|
|
50
|
+
const calls = [];
|
|
51
|
+
for (const { node, parents } of walkTree(tree)) {
|
|
52
|
+
if (node.type === 'CallExpression') {
|
|
53
|
+
const name = plainCalleeName(node);
|
|
54
|
+
// An own-property check: a function called `constructor` or `toString` is not a lookup.
|
|
55
|
+
const lookup = Object.prototype.hasOwnProperty.call(lookups, name) ? lookups[name] : undefined;
|
|
56
|
+
if (lookup === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
const key = keyOf(node.arguments?.[lookup.key]);
|
|
59
|
+
if (key === null)
|
|
60
|
+
continue;
|
|
61
|
+
const valuesNode = lookup.values === null ? undefined : node.arguments?.[lookup.values];
|
|
62
|
+
calls.push({
|
|
63
|
+
file,
|
|
64
|
+
line: lineOf(text, node.start ?? 0) - lineShift,
|
|
65
|
+
...key,
|
|
66
|
+
valueNames: lookup.values === null ? [] : valuesNode === undefined ? [] : propertyNamesOf(valuesNode),
|
|
67
|
+
via: `${name}()`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else if (node.type === 'Property' && (node.key?.name === 'messageKey' || node.key?.value === 'messageKey')) {
|
|
71
|
+
const key = keyOf(node.value);
|
|
72
|
+
if (key === null)
|
|
73
|
+
continue;
|
|
74
|
+
// The values sit next to the key, in the same object: `{ messageKey: 'key', messageValues: { limit } }`.
|
|
75
|
+
const parent = parents[parents.length - 1];
|
|
76
|
+
const valuesProperty = parent?.type === 'ObjectExpression'
|
|
77
|
+
? parent.properties.find((sibling) => sibling.type === 'Property' && (sibling.key?.name === 'messageValues' || sibling.key?.value === 'messageValues'))
|
|
78
|
+
: undefined;
|
|
79
|
+
calls.push({
|
|
80
|
+
file,
|
|
81
|
+
line: lineOf(text, node.start ?? 0) - lineShift,
|
|
82
|
+
...key,
|
|
83
|
+
valueNames: valuesProperty === undefined ? [] : propertyNamesOf(valuesProperty.value),
|
|
84
|
+
via: 'messageKey'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return calls;
|
|
89
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the audits of the words of a package share: reading a file into Svelte's syntax tree, walking it, and listing
|
|
3
|
+
* the files of a folder. Test and build tooling only: it reads the source of a package, so a page never loads it.
|
|
4
|
+
*/
|
|
5
|
+
export type SyntaxNode = any;
|
|
6
|
+
/** A file read into a syntax tree. */
|
|
7
|
+
export interface ParsedFile {
|
|
8
|
+
/** The tree of the Svelte file (a `.ts` file is read as the body of a module script, which has the same TypeScript support). */
|
|
9
|
+
tree: SyntaxNode;
|
|
10
|
+
/** The text the tree was made from: the file itself, or the file inside the module script that wraps it. */
|
|
11
|
+
text: string;
|
|
12
|
+
/** How many lines lower than in the file the text puts the code: 1 for a `.ts` file (the wrapper), 0 for a Svelte file. */
|
|
13
|
+
lineShift: number;
|
|
14
|
+
}
|
|
15
|
+
/** Reads a Svelte file, or a `.ts` file (as the body of a module script), into Svelte's modern syntax tree. */
|
|
16
|
+
export declare function parseFile(file: string, source: string): ParsedFile;
|
|
17
|
+
/** The line (from 1) of a character offset in a text. */
|
|
18
|
+
export declare function lineOf(source: string, offset: number): number;
|
|
19
|
+
/** Every node of a tree, with the nodes above it (the closest last). Fields that hold positions or types, not code, are skipped. */
|
|
20
|
+
export declare function walkTree(node: SyntaxNode, parents?: SyntaxNode[]): Generator<{
|
|
21
|
+
node: SyntaxNode;
|
|
22
|
+
parents: SyntaxNode[];
|
|
23
|
+
}>;
|
|
24
|
+
/** The name of the function a call node calls: `t`, or `toast.error` for a member call; an empty text for anything else. */
|
|
25
|
+
export declare function calleeName(call: SyntaxNode): string;
|
|
26
|
+
/** The text of a string or template literal, with `{}` where a value goes; null for any other node. */
|
|
27
|
+
export declare function literalText(node: SyntaxNode): string | null;
|
|
28
|
+
/** Every file under `folder` whose name `wanted` accepts, as sorted paths. */
|
|
29
|
+
export declare function listFiles(folder: string, wanted: (name: string) => boolean): string[];
|
|
30
|
+
/** Every Svelte file under `folder`, as paths. */
|
|
31
|
+
export declare function listSvelteFiles(folder: string): string[];
|
|
32
|
+
/** Every Svelte and TypeScript file under `folder` (declaration files left out), as paths. */
|
|
33
|
+
export declare function listSourceFiles(folder: string): string[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from 'svelte/compiler';
|
|
4
|
+
/** Reads a Svelte file, or a `.ts` file (as the body of a module script), into Svelte's modern syntax tree. */
|
|
5
|
+
export function parseFile(file, source) {
|
|
6
|
+
const isSvelte = file.endsWith('.svelte');
|
|
7
|
+
// A `.ts` file is read inside a script, so a closing script tag in one of its strings (the code of an embed has one)
|
|
8
|
+
// would end the script early (Svelte ends a script at `</script>`, with spaces allowed before the `>`). `<\/script` is
|
|
9
|
+
// the same text inside a string or a template, and no line moves.
|
|
10
|
+
const text = isSvelte ? source : `<script lang="ts" module>\n${source.replace(/<\/script/g, '<\\/script')}\n</script>`;
|
|
11
|
+
return { tree: parse(text, { modern: true, filename: file }), text, lineShift: isSvelte ? 0 : 1 };
|
|
12
|
+
}
|
|
13
|
+
/** The line (from 1) of a character offset in a text. */
|
|
14
|
+
export function lineOf(source, offset) {
|
|
15
|
+
let line = 1;
|
|
16
|
+
for (let index = 0; index < offset && index < source.length; index += 1) {
|
|
17
|
+
if (source.charCodeAt(index) === 10)
|
|
18
|
+
line += 1;
|
|
19
|
+
}
|
|
20
|
+
return line;
|
|
21
|
+
}
|
|
22
|
+
/** Fields that hold positions, or the types of TypeScript: neither is code that runs, so no word a visitor reads is in them. */
|
|
23
|
+
const SKIPPED_FIELDS = new Set([
|
|
24
|
+
'loc',
|
|
25
|
+
'start',
|
|
26
|
+
'end',
|
|
27
|
+
'type',
|
|
28
|
+
'metadata',
|
|
29
|
+
'parent',
|
|
30
|
+
'typeAnnotation',
|
|
31
|
+
'typeArguments'
|
|
32
|
+
]);
|
|
33
|
+
/** Every node of a tree, with the nodes above it (the closest last). Fields that hold positions or types, not code, are skipped. */
|
|
34
|
+
export function* walkTree(node, parents = []) {
|
|
35
|
+
if (!node || typeof node !== 'object')
|
|
36
|
+
return;
|
|
37
|
+
if (Array.isArray(node)) {
|
|
38
|
+
for (const child of node)
|
|
39
|
+
yield* walkTree(child, parents);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (typeof node.type === 'string')
|
|
43
|
+
yield { node, parents };
|
|
44
|
+
for (const key of Object.keys(node)) {
|
|
45
|
+
if (SKIPPED_FIELDS.has(key))
|
|
46
|
+
continue;
|
|
47
|
+
const value = node[key];
|
|
48
|
+
if (value && typeof value === 'object')
|
|
49
|
+
yield* walkTree(value, typeof node.type === 'string' ? [...parents, node] : parents);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** The name of the function a call node calls: `t`, or `toast.error` for a member call; an empty text for anything else. */
|
|
53
|
+
export function calleeName(call) {
|
|
54
|
+
const callee = call.callee;
|
|
55
|
+
if (!callee)
|
|
56
|
+
return '';
|
|
57
|
+
if (callee.type === 'Identifier')
|
|
58
|
+
return callee.name;
|
|
59
|
+
if (callee.type === 'MemberExpression')
|
|
60
|
+
return `${calleeName({ callee: callee.object })}.${callee.property?.name ?? ''}`;
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
/** The text of a string or template literal, with `{}` where a value goes; null for any other node. */
|
|
64
|
+
export function literalText(node) {
|
|
65
|
+
if (node.type === 'Literal' && typeof node.value === 'string')
|
|
66
|
+
return node.value;
|
|
67
|
+
if (node.type === 'TemplateLiteral')
|
|
68
|
+
return node.quasis.map((quasi) => quasi.value.cooked).join('{}');
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/** Every file under `folder` whose name `wanted` accepts, as sorted paths. */
|
|
72
|
+
export function listFiles(folder, wanted) {
|
|
73
|
+
const files = [];
|
|
74
|
+
for (const entry of fs.readdirSync(folder, { withFileTypes: true })) {
|
|
75
|
+
const full = path.join(folder, entry.name);
|
|
76
|
+
if (entry.isDirectory())
|
|
77
|
+
files.push(...listFiles(full, wanted));
|
|
78
|
+
else if (wanted(entry.name))
|
|
79
|
+
files.push(full);
|
|
80
|
+
}
|
|
81
|
+
return files.sort();
|
|
82
|
+
}
|
|
83
|
+
/** Every Svelte file under `folder`, as paths. */
|
|
84
|
+
export function listSvelteFiles(folder) {
|
|
85
|
+
return listFiles(folder, (name) => name.endsWith('.svelte'));
|
|
86
|
+
}
|
|
87
|
+
/** Every Svelte and TypeScript file under `folder` (declaration files left out), as paths. */
|
|
88
|
+
export function listSourceFiles(folder) {
|
|
89
|
+
return listFiles(folder, (name) => (name.endsWith('.svelte') || name.endsWith('.ts')) && !name.endsWith('.d.ts'));
|
|
90
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds the English a visitor can read in the Svelte files (and the TypeScript files) of a package, so a test can insist
|
|
3
|
+
* that all of it comes from the message catalog. It reads the syntax tree (never a regular expression over the text), so
|
|
4
|
+
* it sees what Svelte sees:
|
|
5
|
+
*
|
|
6
|
+
* - text between tags that contains a letter,
|
|
7
|
+
* - the text-like attributes (`title`, `aria-label`, `alt`, `placeholder`, and the props a design-system component
|
|
8
|
+
* shows: `label`, `message`, `description`, `confirmLabel`, `cancelLabel` ...) when they are a literal or hold a literal,
|
|
9
|
+
* - string literals inside a `{...}` in the template,
|
|
10
|
+
* - the strings handed to `showToast(...)` / `toast.*(...)`, or to a property called `description`, `title`,
|
|
11
|
+
* `message`, `text` or `label` of such a call or of an object assigned in the script, or a variable whose name ends in
|
|
12
|
+
* `Label`, `Message`, `Title`, `Description` or `Text`,
|
|
13
|
+
* - the strings given as VALUES to a message lookup (`t('x.y', { name: item.name || 'Item' })`): they go into the
|
|
14
|
+
* sentence, so a word written there is a word of the sentence that no catalog can say in another language.
|
|
15
|
+
*
|
|
16
|
+
* A package adds the names it uses for words (`emptyText`, `previewError` ...) with the options.
|
|
17
|
+
*/
|
|
18
|
+
export interface VisibleText {
|
|
19
|
+
file: string;
|
|
20
|
+
line: number;
|
|
21
|
+
kind: 'text' | 'attribute' | 'expression' | 'script';
|
|
22
|
+
/** The attribute, property or call the text sits in. */
|
|
23
|
+
where: string;
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
/** Attributes (of an element or of a component) whose value a visitor reads or hears. */
|
|
27
|
+
export declare const DEFAULT_TEXT_ATTRIBUTES: readonly string[];
|
|
28
|
+
/** Property names that hold a sentence when they are written in a script. */
|
|
29
|
+
export declare const DEFAULT_TEXT_PROPERTIES: readonly string[];
|
|
30
|
+
/** The functions that look a message up, with the position of the argument that is the key (code, not words). */
|
|
31
|
+
export declare const DEFAULT_MESSAGE_LOOKUPS: Readonly<Record<string, number>>;
|
|
32
|
+
/** A variable with a name like this holds words (`const emptyText = '...'`, `let message = '...'`). */
|
|
33
|
+
export declare const DEFAULT_TEXT_VARIABLE_PATTERN: RegExp;
|
|
34
|
+
/** What a package can add to what the scan looks for. Everything is added to the defaults above; nothing replaces them. */
|
|
35
|
+
export interface VisibleTextOptions {
|
|
36
|
+
/** More attribute names whose value a visitor reads (the props of the package's own components that show words). */
|
|
37
|
+
textAttributes?: readonly string[];
|
|
38
|
+
/** More property names that hold a sentence in a script (`previewError`, `hint` ...). */
|
|
39
|
+
textProperties?: readonly string[];
|
|
40
|
+
/** Replaces the pattern of the names of variables that hold words. */
|
|
41
|
+
textVariablePattern?: RegExp;
|
|
42
|
+
/** Functions whose strings are words a visitor reads (a function that returns the sentence of a failure). */
|
|
43
|
+
textFunctions?: readonly string[];
|
|
44
|
+
/** More functions that look a message up, with the position of the key in their arguments (`{ messageForVisitor: 2 }`). */
|
|
45
|
+
messageLookups?: Readonly<Record<string, number>>;
|
|
46
|
+
/**
|
|
47
|
+
* The keys of the package's catalog. A string that is exactly one of them is the NAME of a message, not words: a table
|
|
48
|
+
* of keys written as `{ text: 'chat.contextType.text' }` (a property that holds words when it holds a sentence) is read
|
|
49
|
+
* by the code that looks the message up. A string that is not a key of the catalog is read as usual.
|
|
50
|
+
*/
|
|
51
|
+
messageKeys?: Iterable<string>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The visible English in one file: a Svelte file (its markup and its scripts) or a `.ts` file (its code, as a script).
|
|
55
|
+
* Lines are the lines of the file.
|
|
56
|
+
*/
|
|
57
|
+
export declare function findVisibleText(file: string, source: string, options?: VisibleTextOptions): VisibleText[];
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { calleeName, lineOf, literalText, parseFile, walkTree } from "./syntaxTree.js";
|
|
2
|
+
/** Attributes (of an element or of a component) whose value a visitor reads or hears. */
|
|
3
|
+
export const DEFAULT_TEXT_ATTRIBUTES = [
|
|
4
|
+
'title',
|
|
5
|
+
'aria-label',
|
|
6
|
+
'aria-description',
|
|
7
|
+
'aria-valuetext',
|
|
8
|
+
'alt',
|
|
9
|
+
'placeholder',
|
|
10
|
+
'label',
|
|
11
|
+
'message',
|
|
12
|
+
'description',
|
|
13
|
+
'confirmLabel',
|
|
14
|
+
'cancelLabel',
|
|
15
|
+
'closeLabel',
|
|
16
|
+
'text',
|
|
17
|
+
'heading',
|
|
18
|
+
'tooltip',
|
|
19
|
+
'caption'
|
|
20
|
+
];
|
|
21
|
+
/** Property names that hold a sentence when they are written in a script. */
|
|
22
|
+
export const DEFAULT_TEXT_PROPERTIES = ['text', 'label', 'title', 'message', 'description', 'placeholder', 'errorMessage'];
|
|
23
|
+
/** The functions that look a message up, with the position of the argument that is the key (code, not words). */
|
|
24
|
+
export const DEFAULT_MESSAGE_LOOKUPS = { $t: 0, t: 0, translate: 0, translateEnglish: 0 };
|
|
25
|
+
/** A variable with a name like this holds words (`const emptyText = '...'`, `let message = '...'`). */
|
|
26
|
+
export const DEFAULT_TEXT_VARIABLE_PATTERN = /^(label|message|title|description|text)$|(Label|Message|Title|Description|Text)$/;
|
|
27
|
+
function settingsOf(options) {
|
|
28
|
+
return {
|
|
29
|
+
textAttributes: new Set([...DEFAULT_TEXT_ATTRIBUTES, ...(options.textAttributes ?? [])]),
|
|
30
|
+
textProperties: new Set([...DEFAULT_TEXT_PROPERTIES, ...(options.textProperties ?? [])]),
|
|
31
|
+
textVariablePattern: options.textVariablePattern ?? DEFAULT_TEXT_VARIABLE_PATTERN,
|
|
32
|
+
textFunctions: new Set(options.textFunctions ?? []),
|
|
33
|
+
messageLookups: { ...DEFAULT_MESSAGE_LOOKUPS, ...(options.messageLookups ?? {}) },
|
|
34
|
+
messageKeys: new Set(options.messageKeys ?? [])
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const HAS_LETTER = /\p{L}/u;
|
|
38
|
+
/** True for a literal that is only compared (`status === 'failed'`) or used as a key (`map['key']`): code, not words. */
|
|
39
|
+
function isOnlyCompared(parents) {
|
|
40
|
+
const parent = parents[parents.length - 1];
|
|
41
|
+
if (!parent)
|
|
42
|
+
return false;
|
|
43
|
+
if (parent.type === 'BinaryExpression' && ['==', '===', '!=', '!=='].includes(parent.operator))
|
|
44
|
+
return true;
|
|
45
|
+
if (parent.type === 'MemberExpression' && parent.computed)
|
|
46
|
+
return true;
|
|
47
|
+
if (parent.type === 'SwitchCase')
|
|
48
|
+
return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/** True for the key in `$t('upload.cancel')` or in `messageForVisitor(error, $t, 'upload.unknownError')`. */
|
|
52
|
+
function isMessageKey(node, parents, settings) {
|
|
53
|
+
const parent = parents[parents.length - 1];
|
|
54
|
+
if (parent?.type !== 'CallExpression')
|
|
55
|
+
return false;
|
|
56
|
+
const keyPosition = settings.messageLookups[calleeName(parent)];
|
|
57
|
+
return keyPosition !== undefined && parent.arguments?.[keyPosition] === node;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The call that looks a message up when the literal is one of the values it is given, not its key:
|
|
61
|
+
* `t('editor.preview.frameTitleNamed', { name: widget?.name || 'Widget' })`. A value goes into the sentence, so a word
|
|
62
|
+
* written there is a word of the sentence that no catalog can say in another language. The search stops at a function (a
|
|
63
|
+
* string inside a callback is not what the sentence is made of).
|
|
64
|
+
*/
|
|
65
|
+
function lookupGivenTheValue(literal, parents, settings) {
|
|
66
|
+
let inner = literal;
|
|
67
|
+
for (let depth = parents.length - 1; depth >= 0; depth -= 1) {
|
|
68
|
+
const ancestor = parents[depth];
|
|
69
|
+
if (/Function/.test(ancestor.type))
|
|
70
|
+
return null;
|
|
71
|
+
if (ancestor.type === 'CallExpression') {
|
|
72
|
+
const keyPosition = settings.messageLookups[calleeName(ancestor)];
|
|
73
|
+
if (keyPosition !== undefined && (ancestor.arguments?.indexOf(inner) ?? -1) > keyPosition)
|
|
74
|
+
return ancestor;
|
|
75
|
+
}
|
|
76
|
+
inner = ancestor;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/** The kinds of expression a string passes through on its way out of a function: `return ok ? 'Done' : 'Failed'`. */
|
|
81
|
+
const PASSES_A_STRING_ON = new Set([
|
|
82
|
+
'ConditionalExpression',
|
|
83
|
+
'LogicalExpression',
|
|
84
|
+
'TemplateLiteral',
|
|
85
|
+
'TSAsExpression',
|
|
86
|
+
'TSNonNullExpression',
|
|
87
|
+
'TSSatisfiesExpression'
|
|
88
|
+
]);
|
|
89
|
+
/** The function a string literal is returned from (with the node that holds that function), when the literal is part of what it returns. */
|
|
90
|
+
function functionReturning(literal, parents) {
|
|
91
|
+
let inner = literal;
|
|
92
|
+
for (let depth = parents.length - 1; depth >= 0; depth -= 1) {
|
|
93
|
+
const ancestor = parents[depth];
|
|
94
|
+
if (PASSES_A_STRING_ON.has(ancestor.type) || (ancestor.type === 'BinaryExpression' && ancestor.operator === '+')) {
|
|
95
|
+
inner = ancestor;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ancestor.type === 'ArrowFunctionExpression' && ancestor.body === inner)
|
|
99
|
+
return { fn: ancestor, container: parents[depth - 1] };
|
|
100
|
+
if (ancestor.type !== 'ReturnStatement')
|
|
101
|
+
return null;
|
|
102
|
+
for (let up = depth - 1; up >= 0; up -= 1) {
|
|
103
|
+
if (/Function/.test(parents[up].type))
|
|
104
|
+
return { fn: parents[up], container: parents[up - 1] };
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/** The name a function goes by: its own, or the variable, property or method it is written into. */
|
|
111
|
+
function nameOfFunction(fn, container) {
|
|
112
|
+
if (fn.id?.name)
|
|
113
|
+
return fn.id.name;
|
|
114
|
+
if (container?.type === 'VariableDeclarator' && container.init === fn)
|
|
115
|
+
return container.id?.name;
|
|
116
|
+
if ((container?.type === 'Property' || container?.type === 'MethodDefinition' || container?.type === 'PropertyDefinition') && container.value === fn) {
|
|
117
|
+
return container.key?.name ?? container.key?.value;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const TOAST_CALL = /^(showToast|toast(\.\w+)?)$/;
|
|
122
|
+
/** The kinds of toast: the first argument of `showToast`, which picks the colour and not a sentence. */
|
|
123
|
+
const TOAST_KINDS = new Set(['success', 'error', 'warning', 'info', 'message', 'loading']);
|
|
124
|
+
function clean(text) {
|
|
125
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
126
|
+
}
|
|
127
|
+
function scanScript(file, source, lineShift, program, settings, found) {
|
|
128
|
+
for (const { node, parents } of walkTree(program)) {
|
|
129
|
+
const text = literalText(node);
|
|
130
|
+
if (text === null || !HAS_LETTER.test(text) || isOnlyCompared(parents) || isMessageKey(node, parents, settings) || settings.messageKeys.has(text))
|
|
131
|
+
continue;
|
|
132
|
+
const parent = parents[parents.length - 1];
|
|
133
|
+
const lookup = lookupGivenTheValue(node, parents, settings);
|
|
134
|
+
let where = lookup ? `${calleeName(lookup)}() value` : '';
|
|
135
|
+
for (let depth = parents.length - 1; where === '' && depth >= 0; depth -= 1) {
|
|
136
|
+
const ancestor = parents[depth];
|
|
137
|
+
if (ancestor.type === 'CallExpression' && TOAST_CALL.test(calleeName(ancestor))) {
|
|
138
|
+
// `showToast('error', ...)`: the kind of toast is not a sentence (`toast.error('...')` has no kind to give).
|
|
139
|
+
if (calleeName(ancestor) === 'showToast' && ancestor.arguments?.[0] === node && TOAST_KINDS.has(text))
|
|
140
|
+
break;
|
|
141
|
+
where = `${calleeName(ancestor)}()`;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (ancestor.type === 'Property' && ancestor.key) {
|
|
145
|
+
// A property that is not one of the text properties (`position`, `id`, `value`, `className` ...) is not a sentence.
|
|
146
|
+
const propertyName = ancestor.key.name ?? ancestor.key.value;
|
|
147
|
+
if (settings.textProperties.has(propertyName))
|
|
148
|
+
where = `${propertyName}:`;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
if (ancestor.type === 'VariableDeclarator' && settings.textVariablePattern.test(ancestor.id?.name ?? '')) {
|
|
152
|
+
where = `${ancestor.id.name} =`;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (ancestor.type === 'AssignmentExpression' && settings.textVariablePattern.test(ancestor.left?.name ?? '')) {
|
|
156
|
+
where = `${ancestor.left.name} =`;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
// A statement or a function ends the search: the literal is not what is being shown.
|
|
160
|
+
if (/Statement$|Function|Declaration$/.test(ancestor.type) && ancestor.type !== 'VariableDeclarator')
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (where === '' && settings.textFunctions.size > 0) {
|
|
164
|
+
// A function the package says returns words: the strings it returns are read by a visitor.
|
|
165
|
+
const returning = functionReturning(node, parents);
|
|
166
|
+
const name = returning ? nameOfFunction(returning.fn, returning.container) : undefined;
|
|
167
|
+
if (name !== undefined && settings.textFunctions.has(name))
|
|
168
|
+
where = `${name}()`;
|
|
169
|
+
}
|
|
170
|
+
if (where === '' || parent?.type === 'ImportDeclaration')
|
|
171
|
+
continue;
|
|
172
|
+
found.push({ file, line: lineOf(source, node.start ?? 0) - lineShift, kind: 'script', where, text: clean(text) });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function scanTemplate(file, source, lineShift, node, settings, found) {
|
|
176
|
+
if (!node || typeof node !== 'object')
|
|
177
|
+
return;
|
|
178
|
+
if (Array.isArray(node)) {
|
|
179
|
+
for (const child of node)
|
|
180
|
+
scanTemplate(file, source, lineShift, child, settings, found);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// A <style> or <script> inside the markup holds code, not words.
|
|
184
|
+
if ((node.type === 'RegularElement' || node.type === 'SvelteElement') && (node.name === 'style' || node.name === 'script'))
|
|
185
|
+
return;
|
|
186
|
+
switch (node.type) {
|
|
187
|
+
case 'Text': {
|
|
188
|
+
const data = node.data ?? '';
|
|
189
|
+
if (HAS_LETTER.test(data))
|
|
190
|
+
found.push({ file, line: lineOf(source, node.start ?? 0) - lineShift, kind: 'text', where: '', text: clean(data) });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
case 'ExpressionTag':
|
|
194
|
+
case 'RenderTag':
|
|
195
|
+
case 'ConstTag':
|
|
196
|
+
case 'HtmlTag': {
|
|
197
|
+
// `{@const x = 'words'}` keeps its code in `declaration`, the other tags in `expression`
|
|
198
|
+
for (const { node: inner, parents } of walkTree(node.expression ?? node.declaration)) {
|
|
199
|
+
const text = literalText(inner);
|
|
200
|
+
if (text !== null && HAS_LETTER.test(text) && !isOnlyCompared(parents) && !isMessageKey(inner, parents, settings) && !settings.messageKeys.has(text)) {
|
|
201
|
+
found.push({ file, line: lineOf(source, node.start ?? 0) - lineShift, kind: 'expression', where: '{…}', text: clean(text) });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
case 'Attribute': {
|
|
207
|
+
if (!settings.textAttributes.has(node.name))
|
|
208
|
+
return;
|
|
209
|
+
const parts = Array.isArray(node.value) ? node.value : [node.value];
|
|
210
|
+
for (const part of parts) {
|
|
211
|
+
if (!part || part === true)
|
|
212
|
+
continue;
|
|
213
|
+
if (part.type === 'Text' && HAS_LETTER.test(part.data)) {
|
|
214
|
+
found.push({ file, line: lineOf(source, node.start ?? 0) - lineShift, kind: 'attribute', where: node.name, text: clean(part.data) });
|
|
215
|
+
}
|
|
216
|
+
else if (part.type === 'ExpressionTag') {
|
|
217
|
+
for (const { node: inner, parents } of walkTree(part.expression)) {
|
|
218
|
+
const text = literalText(inner);
|
|
219
|
+
if (text !== null && HAS_LETTER.test(text) && !isOnlyCompared(parents) && !isMessageKey(inner, parents, settings) && !settings.messageKeys.has(text)) {
|
|
220
|
+
found.push({ file, line: lineOf(source, node.start ?? 0) - lineShift, kind: 'attribute', where: node.name, text: clean(text) });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
for (const key of Object.keys(node)) {
|
|
231
|
+
if (['type', 'start', 'end', 'metadata', 'name_loc', 'loc', 'parent', 'expression'].includes(key))
|
|
232
|
+
continue;
|
|
233
|
+
const value = node[key];
|
|
234
|
+
if (value && typeof value === 'object')
|
|
235
|
+
scanTemplate(file, source, lineShift, value, settings, found);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The visible English in one file: a Svelte file (its markup and its scripts) or a `.ts` file (its code, as a script).
|
|
240
|
+
* Lines are the lines of the file.
|
|
241
|
+
*/
|
|
242
|
+
export function findVisibleText(file, source, options = {}) {
|
|
243
|
+
const settings = settingsOf(options);
|
|
244
|
+
const found = [];
|
|
245
|
+
const { tree, text, lineShift } = parseFile(file, source);
|
|
246
|
+
scanTemplate(file, text, lineShift, tree.fragment, settings, found);
|
|
247
|
+
if (tree.instance?.content)
|
|
248
|
+
scanScript(file, text, lineShift, tree.instance.content, settings, found);
|
|
249
|
+
if (tree.module?.content)
|
|
250
|
+
scanScript(file, text, lineShift, tree.module.content, settings, found);
|
|
251
|
+
return found;
|
|
252
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
import { type MessageOverrides, type MessageValues } from './translator.ts';
|
|
3
|
+
/** The function a screen calls to get its words: `t('upload.clear')`, `t('toast.deleted', { name })`. */
|
|
4
|
+
export type Translate<Key extends string = string> = (key: Key, values?: MessageValues) => string;
|
|
5
|
+
/**
|
|
6
|
+
* What the parts of a package need to speak the page's language, shared with them through Svelte's component context.
|
|
7
|
+
*
|
|
8
|
+
* Both are stores, so a screen that is already open changes its words when the page sets the language again.
|
|
9
|
+
*/
|
|
10
|
+
export interface I18nContext<Key extends string = string> {
|
|
11
|
+
/** Turns a key into text. */
|
|
12
|
+
t: Readable<Translate<Key>>;
|
|
13
|
+
/** The language tag in use ("en" unless the page asked for another one that this browser knows). */
|
|
14
|
+
locale: Readable<string>;
|
|
15
|
+
}
|
|
16
|
+
/** What the page chose. Both are optional; a missing language is English. */
|
|
17
|
+
export interface I18nSettings {
|
|
18
|
+
/** A BCP 47 tag ("de", "pt-BR"): sets the plural rules and how numbers, dates and lists are written. */
|
|
19
|
+
locale?: string | null;
|
|
20
|
+
/** The page's own words: any of the catalog's keys. A key it leaves out keeps the English text. */
|
|
21
|
+
messages?: MessageOverrides | null;
|
|
22
|
+
}
|
|
23
|
+
/** What {@link I18n.provideI18n} gives back to the component that called it. */
|
|
24
|
+
export interface I18nHandle<Key extends string = string> extends I18nContext<Key> {
|
|
25
|
+
/** Changes the language the parts below speak. Cheap to call on every change of the props. */
|
|
26
|
+
update(settings: I18nSettings): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The two functions of Svelte that the context is made of. They only work while a component is being created, so a test
|
|
30
|
+
* hands in stand-ins; the packages use the real ones by leaving this out.
|
|
31
|
+
*/
|
|
32
|
+
export interface SvelteContextApi {
|
|
33
|
+
getContext<Value>(key: unknown): Value | undefined;
|
|
34
|
+
setContext<Value>(key: unknown, value: Value): Value;
|
|
35
|
+
}
|
|
36
|
+
export interface I18nOptions<Key extends string> {
|
|
37
|
+
/** The English messages of the package: every key its screens ask for. */
|
|
38
|
+
catalog: Readonly<Record<Key, string>>;
|
|
39
|
+
/** The package's name in the console: `FileBrowser` makes `[FileBrowser] There is no message called "x".` */
|
|
40
|
+
label: string;
|
|
41
|
+
/** Stand-ins for Svelte's `getContext` and `setContext`; tests only. */
|
|
42
|
+
contextApi?: SvelteContextApi;
|
|
43
|
+
}
|
|
44
|
+
/** What {@link createI18n} makes: the three things a package's code uses. */
|
|
45
|
+
export interface I18n<Key extends string = string> {
|
|
46
|
+
/**
|
|
47
|
+
* Called by the root component of a package while it is created, so everything inside it speaks the page's language.
|
|
48
|
+
* The context belongs to one root component, so two of them on a page can speak two languages.
|
|
49
|
+
*/
|
|
50
|
+
provideI18n(initial: I18nSettings): I18nHandle<Key>;
|
|
51
|
+
/** Called by a part of the package while it is created. Never throws; outside a root component it speaks English. */
|
|
52
|
+
useI18n(): I18nContext<Key>;
|
|
53
|
+
/** The English `t`, for code that has no component around it (a helper called without a translator). */
|
|
54
|
+
translateEnglish: Translate<Key>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Makes the words of one package: the context its root component provides, the hook its parts use, and the English
|
|
58
|
+
* translator for code with no component around it.
|
|
59
|
+
*
|
|
60
|
+
* Every package makes its own (its catalog, its console label, its own context key), so the File Browser inside the
|
|
61
|
+
* editor and the editor around it can each have their own words, and each is told only about its own messages.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createI18n<Key extends string>(options: I18nOptions<Key>): I18n<Key>;
|