@nebulahome/package-sdk 1.0.1 → 1.1.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/CHANGELOG.md +15 -0
- package/README.md +39 -9
- package/dist/chunks/host.js +21 -7
- package/dist/chunks/node-build.js +184 -12
- package/dist/chunks/version.js +1 -1
- package/dist/ui.d.ts +35 -1
- package/dist/ui.js +56 -2
- package/dist/vite.d.ts +2 -2
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
The SDK's version is the version of the package API the platform implements: a minor version adds
|
|
4
4
|
to it, a major one breaks it, and a patch leaves it as it is.
|
|
5
5
|
|
|
6
|
+
## 1.1.0
|
|
7
|
+
|
|
8
|
+
- `useTranslation()` in `@nebulahome/package-sdk/ui`: a package's page, cards and entity views
|
|
9
|
+
say their strings with `t('clients.title', 'Clients')`, in the viewer's language. The English
|
|
10
|
+
is the fallback, and a plural takes `{ one, other }` with `count`; outside the interface - a
|
|
11
|
+
component's unit test - every string is its English.
|
|
12
|
+
- Catalogues: `locales/<language>.json` beside `nebula.config.ts` (`it.json`, `pt-BR.json`), JSON
|
|
13
|
+
objects of strings nested as the keys are dotted, with i18next's plural suffixes and
|
|
14
|
+
`{{placeholders}}`. `nebula()` bundles each as a chunk of the interface, loaded only for a viewer
|
|
15
|
+
who reads that language; `validate` checks them (a language for a name, strings only, plurals
|
|
16
|
+
with their `_other`, at most 256 KB); `build` warns which strings a catalogue says in English
|
|
17
|
+
and which of its keys no string uses.
|
|
18
|
+
- What `nebula.config.ts` declares - names, settings, entity types - stays English, as the
|
|
19
|
+
platform shows it everywhere.
|
|
20
|
+
|
|
6
21
|
## 1.0.1
|
|
7
22
|
|
|
8
23
|
- `nebulahome-package --help`, and a mistyped command, work where Vite is not installed -
|
package/README.md
CHANGED
|
@@ -86,17 +86,47 @@ import { nebula } from '@nebulahome/package-sdk/vite';
|
|
|
86
86
|
export default defineConfig({ plugins: [react(), tailwindcss(), nebula()] });
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
## Translating the interface
|
|
90
|
+
|
|
91
|
+
The page, the cards and the entity views say their strings through `useTranslation()`, with the
|
|
92
|
+
English beside each key; a catalogue per language goes in `locales/` next to `nebula.config.ts`:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// src/ui/forecast-card.tsx
|
|
96
|
+
import { useTranslation } from '@nebulahome/package-sdk/ui';
|
|
97
|
+
|
|
98
|
+
const { t } = useTranslation();
|
|
99
|
+
|
|
100
|
+
t('forecast.empty', 'No forecast yet');
|
|
101
|
+
t('forecast.days', { one: '{{count}} day', other: '{{count}} days' }, { count: days.length });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
// locales/it.json
|
|
106
|
+
{
|
|
107
|
+
"forecast": {
|
|
108
|
+
"empty": "Ancora nessuna previsione",
|
|
109
|
+
"days_one": "{{count}} giorno",
|
|
110
|
+
"days_other": "{{count}} giorni"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A string a catalogue lacks is said in English, and `nebulahome-package build` lists them. What
|
|
116
|
+
`nebula.config.ts` declares (the package's name, its cards' and settings' names, its entity types)
|
|
117
|
+
stays English: the platform shows it the same way to everyone.
|
|
118
|
+
|
|
89
119
|
## Entry points
|
|
90
120
|
|
|
91
|
-
| Import | What
|
|
92
|
-
| ------------------------- |
|
|
93
|
-
| `@nebulahome/package-sdk` | `definePackage`, `defineSettings`, `defineCard`, `defineEntityType`, `definePage`, `defineContainer`, `defineCompose`, `field`, entity id helpers, `SDK_VERSION`, the value types.
|
|
94
|
-
| `/server` | `defineServer`, and the context `start` is given: settings (live or restart), devices and entities, typed custom entities, the rest of the house, methods for the interface, services between packages.
|
|
95
|
-
| `/ui` | The props of a page, a card and an entity view; hooks for the package, its methods, its settings, the house, the viewer, the theme; navigation inside the package's page and
|
|
96
|
-
| `/components` | The part of the interface's design system a package may draw with.
|
|
97
|
-
| `/vite` | `nebula()`, the Vite plugin that builds both halves: the interface in chunks with its CSS scoped to the package, the server bundled whole.
|
|
98
|
-
| `/testing` | `createTestContext()`: run `start` in a unit test, every effect recorded.
|
|
99
|
-
| `/styles.css` | The interface's Tailwind tokens, to `@reference`.
|
|
121
|
+
| Import | What |
|
|
122
|
+
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `@nebulahome/package-sdk` | `definePackage`, `defineSettings`, `defineCard`, `defineEntityType`, `definePage`, `defineContainer`, `defineCompose`, `field`, entity id helpers, `SDK_VERSION`, the value types. |
|
|
124
|
+
| `/server` | `defineServer`, and the context `start` is given: settings (live or restart), devices and entities, typed custom entities, the rest of the house, methods for the interface, services between packages. |
|
|
125
|
+
| `/ui` | The props of a page, a card and an entity view; hooks for the package, its methods, its settings, the house, the viewer, the theme, its strings; navigation inside the package's page and the platform's. |
|
|
126
|
+
| `/components` | The part of the interface's design system a package may draw with. |
|
|
127
|
+
| `/vite` | `nebula()`, the Vite plugin that builds both halves: the interface in chunks with its CSS scoped to the package, the server bundled whole. |
|
|
128
|
+
| `/testing` | `createTestContext()`: run `start` in a unit test, every effect recorded. |
|
|
129
|
+
| `/styles.css` | The interface's Tailwind tokens, to `@reference`. |
|
|
100
130
|
|
|
101
131
|
The `nebulahome-package` command checks, builds and packs a package:
|
|
102
132
|
|
package/dist/chunks/host.js
CHANGED
|
@@ -23,15 +23,16 @@ function hostSdkVersion() {
|
|
|
23
23
|
return typeof version === "string" ? version : void 0;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* One member of a module the interface lends.
|
|
26
|
+
* One member of a module the interface lends, when it lends it.
|
|
27
|
+
*
|
|
28
|
+
* For a member with a way of its own to do without the interface - `useTranslation` says the
|
|
29
|
+
* English - where {@link hostMember} would refuse.
|
|
27
30
|
*
|
|
28
31
|
* @param module - The module's key.
|
|
29
32
|
* @param name - The member.
|
|
30
|
-
* @
|
|
31
|
-
* @returns The interface's implementation of it.
|
|
32
|
-
* @throws {Error} When there is no interface, or it does not implement the member.
|
|
33
|
+
* @returns The interface's implementation of it, or undefined when there is none.
|
|
33
34
|
*/
|
|
34
|
-
function
|
|
35
|
+
function lentMember(module, name) {
|
|
35
36
|
const nebula = Reflect.get(globalThis, "__nebula");
|
|
36
37
|
const modules = isRecord(nebula) ? nebula["modules"] : void 0;
|
|
37
38
|
const lent = isRecord(modules) ? modules[module] : void 0;
|
|
@@ -45,9 +46,22 @@ function hostMember(module, name, since) {
|
|
|
45
46
|
* @returns True when it can be called or drawn.
|
|
46
47
|
*/
|
|
47
48
|
const isLent = (value) => typeof value === "function" || isRecord(value) && "$$typeof" in value;
|
|
48
|
-
|
|
49
|
+
return isLent(member) ? member : void 0;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One member of a module the interface lends.
|
|
53
|
+
*
|
|
54
|
+
* @param module - The module's key.
|
|
55
|
+
* @param name - The member.
|
|
56
|
+
* @param since - The SDK version that added it.
|
|
57
|
+
* @returns The interface's implementation of it.
|
|
58
|
+
* @throws {Error} When there is no interface, or it does not implement the member.
|
|
59
|
+
*/
|
|
60
|
+
function hostMember(module, name, since) {
|
|
61
|
+
const member = lentMember(module, name);
|
|
62
|
+
if (member !== void 0) return member;
|
|
49
63
|
const version = hostSdkVersion();
|
|
50
64
|
throw new Error(version === void 0 ? `${name} is lent by the NebulaHome interface, and there is none here` : `${name} needs package SDK ${since}; this interface implements ${version}`);
|
|
51
65
|
}
|
|
52
66
|
//#endregion
|
|
53
|
-
export { UI_MODULE as n, hostMember as r, COMPONENTS_MODULE as t };
|
|
67
|
+
export { lentMember as i, UI_MODULE as n, hostMember as r, COMPONENTS_MODULE as t };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as PackageConfigError } from "./node-problems.js";
|
|
2
2
|
import { a as isRecord$1, b as strictObject, i as generatePackage, l as packageIdSchema, n as keepingNodeEnv, r as RELEASE_PATHS, u as DEPENDENCY_ID_PATTERN, x as string, y as record } from "./node-config.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { build, resolveConfig, runnerImport } from "vite";
|
|
7
7
|
//#region \0rolldown/runtime.js
|
|
@@ -6850,7 +6850,143 @@ function composeFileProblems(text, settingKeys) {
|
|
|
6850
6850
|
* the two are compared before a package is installed or started (PACKAGES_DOCS.md 11).
|
|
6851
6851
|
*/
|
|
6852
6852
|
/** The SDK's own version, `1.0.0`. */
|
|
6853
|
-
const SDK_VERSION = "1.0
|
|
6853
|
+
const SDK_VERSION = "1.1.0";
|
|
6854
|
+
//#endregion
|
|
6855
|
+
//#region src/vite/catalogues.ts
|
|
6856
|
+
/**
|
|
6857
|
+
* A package's translation catalogues: `locales/<language>.json` beside `nebula.config.ts`
|
|
6858
|
+
* (PACKAGES_DOCS.md 6.5).
|
|
6859
|
+
*
|
|
6860
|
+
* Each is a JSON object of strings, nested as the keys are dotted - `{ "clients": { "title":
|
|
6861
|
+
* "Client" } }` for `t('clients.title', 'Clients')` - with i18next's plural suffixes (`_one`,
|
|
6862
|
+
* `_many`, `_other`) and `{{placeholders}}`. The build bundles each as a chunk of the interface,
|
|
6863
|
+
* loaded only for a viewer who reads that language; English needs no file, being the strings'
|
|
6864
|
+
* own fallback.
|
|
6865
|
+
*/
|
|
6866
|
+
/** The folder the catalogues are in, beside the config. */
|
|
6867
|
+
const LOCALES_FOLDER = "locales";
|
|
6868
|
+
/** A language tag a catalogue may be named by: `it`, `pt-BR`, `zh-Hant`. */
|
|
6869
|
+
const LANGUAGE_TAG = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|\d{3}))?$/;
|
|
6870
|
+
/** The plural suffixes i18next reads. */
|
|
6871
|
+
const PLURAL_SUFFIX = /_(?:zero|one|two|few|many|other)$/;
|
|
6872
|
+
/**
|
|
6873
|
+
* The catalogues beside a config, by file name: whether they hold together is
|
|
6874
|
+
* {@link catalogueProblems}'s to say.
|
|
6875
|
+
*
|
|
6876
|
+
* @param configDirectory - The folder of `nebula.config.ts`.
|
|
6877
|
+
* @returns Them, ordered by language; none when there is no `locales` folder.
|
|
6878
|
+
*/
|
|
6879
|
+
async function findCatalogues(configDirectory) {
|
|
6880
|
+
const folder = path.join(configDirectory, LOCALES_FOLDER);
|
|
6881
|
+
let names;
|
|
6882
|
+
try {
|
|
6883
|
+
names = await readdir(folder);
|
|
6884
|
+
} catch {
|
|
6885
|
+
return [];
|
|
6886
|
+
}
|
|
6887
|
+
return names.filter((name) => name.endsWith(".json")).map((name) => ({
|
|
6888
|
+
language: name.slice(0, -5),
|
|
6889
|
+
file: path.join(folder, name)
|
|
6890
|
+
})).sort((left, right) => left.language.localeCompare(right.language));
|
|
6891
|
+
}
|
|
6892
|
+
/**
|
|
6893
|
+
* The catalogues whose name is a language: the ones the interface is given.
|
|
6894
|
+
*
|
|
6895
|
+
* @param catalogues - The catalogues found.
|
|
6896
|
+
* @returns The ones it can load.
|
|
6897
|
+
*/
|
|
6898
|
+
function namedCatalogues(catalogues) {
|
|
6899
|
+
return catalogues.filter((catalogue) => LANGUAGE_TAG.test(catalogue.language));
|
|
6900
|
+
}
|
|
6901
|
+
/**
|
|
6902
|
+
* The keys of a catalogue, dotted, plural suffixes left on.
|
|
6903
|
+
*
|
|
6904
|
+
* @param value - The catalogue, or a part of it.
|
|
6905
|
+
* @param prefix - The key of the part.
|
|
6906
|
+
* @param problems - Where what does not hold is said.
|
|
6907
|
+
* @returns The keys.
|
|
6908
|
+
*/
|
|
6909
|
+
function keysOf(value, prefix, problems) {
|
|
6910
|
+
if (typeof value === "string") return [prefix];
|
|
6911
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
6912
|
+
problems.push(`${prefix === "" ? "the catalogue" : prefix}: is not a string or an object of strings`);
|
|
6913
|
+
return [];
|
|
6914
|
+
}
|
|
6915
|
+
return Object.entries(value).flatMap(([name, child]) => {
|
|
6916
|
+
if (name === "" || name.includes(".")) {
|
|
6917
|
+
problems.push(`${prefix === "" ? "" : `${prefix}.`}${name}: a key has no dots of its own - nest it instead`);
|
|
6918
|
+
return [];
|
|
6919
|
+
}
|
|
6920
|
+
return keysOf(child, prefix === "" ? name : `${prefix}.${name}`, problems);
|
|
6921
|
+
});
|
|
6922
|
+
}
|
|
6923
|
+
/**
|
|
6924
|
+
* Reads a catalogue's keys.
|
|
6925
|
+
*
|
|
6926
|
+
* @param file - The catalogue.
|
|
6927
|
+
* @returns Its keys, and what is wrong with it.
|
|
6928
|
+
*/
|
|
6929
|
+
async function readCatalogue(file) {
|
|
6930
|
+
const problems = [];
|
|
6931
|
+
if ((await stat(file)).size > 262144) return {
|
|
6932
|
+
keys: [],
|
|
6933
|
+
problems: ["a catalogue is at most 256 KB"]
|
|
6934
|
+
};
|
|
6935
|
+
let parsed;
|
|
6936
|
+
try {
|
|
6937
|
+
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
6938
|
+
} catch {
|
|
6939
|
+
return {
|
|
6940
|
+
keys: [],
|
|
6941
|
+
problems: ["is not JSON"]
|
|
6942
|
+
};
|
|
6943
|
+
}
|
|
6944
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {
|
|
6945
|
+
keys: [],
|
|
6946
|
+
problems: ["is not an object of strings"]
|
|
6947
|
+
};
|
|
6948
|
+
const keys = keysOf(parsed, "", problems);
|
|
6949
|
+
const plurals = new Set(keys.filter((key) => PLURAL_SUFFIX.test(key)).map((key) => key.replace(PLURAL_SUFFIX, "")));
|
|
6950
|
+
for (const base of plurals) if (!keys.includes(`${base}_other`)) problems.push(`${base}: a plural needs its _other form`);
|
|
6951
|
+
return {
|
|
6952
|
+
keys,
|
|
6953
|
+
problems
|
|
6954
|
+
};
|
|
6955
|
+
}
|
|
6956
|
+
/**
|
|
6957
|
+
* What is wrong with a package's catalogues, file by file.
|
|
6958
|
+
*
|
|
6959
|
+
* @param catalogues - The catalogues found.
|
|
6960
|
+
* @param configDirectory - The folder of `nebula.config.ts`, for the names said.
|
|
6961
|
+
* @returns The problems; none when they hold together.
|
|
6962
|
+
*/
|
|
6963
|
+
async function catalogueProblems(catalogues, configDirectory) {
|
|
6964
|
+
const problems = [];
|
|
6965
|
+
for (const catalogue of catalogues) {
|
|
6966
|
+
const name = path.relative(configDirectory, catalogue.file).replaceAll("\\", "/");
|
|
6967
|
+
if (!LANGUAGE_TAG.test(catalogue.language)) {
|
|
6968
|
+
problems.push(`${name}: is not named by a language, as it.json or pt-BR.json`);
|
|
6969
|
+
continue;
|
|
6970
|
+
}
|
|
6971
|
+
const { problems: found } = await readCatalogue(catalogue.file);
|
|
6972
|
+
problems.push(...found.map((problem) => `${name}: ${problem}`));
|
|
6973
|
+
}
|
|
6974
|
+
return problems;
|
|
6975
|
+
}
|
|
6976
|
+
/**
|
|
6977
|
+
* The strings a catalogue lacks and the keys no string uses.
|
|
6978
|
+
*
|
|
6979
|
+
* @param used - The keys the interface's code says, as `t('key', …)`.
|
|
6980
|
+
* @param keys - The catalogue's keys, plural suffixes left on.
|
|
6981
|
+
* @returns What is missing and what is left over, each ordered.
|
|
6982
|
+
*/
|
|
6983
|
+
function catalogueCoverage(used, keys) {
|
|
6984
|
+
const present = new Set(keys.map((key) => key.replace(PLURAL_SUFFIX, "")));
|
|
6985
|
+
return {
|
|
6986
|
+
missing: [...used].filter((key) => !present.has(key)).sort(),
|
|
6987
|
+
unknown: [...present].filter((key) => !used.has(key)).sort()
|
|
6988
|
+
};
|
|
6989
|
+
}
|
|
6854
6990
|
//#endregion
|
|
6855
6991
|
//#region src/vite/load.ts
|
|
6856
6992
|
/**
|
|
@@ -12735,8 +12871,9 @@ function sharedModuleCode(source) {
|
|
|
12735
12871
|
* `nebula()`: the Vite plugin that builds a package's two halves (PACKAGES_DOCS.md 8.1).
|
|
12736
12872
|
*
|
|
12737
12873
|
* - **The interface** (`vite build`): one entry generated from the components bound in
|
|
12738
|
-
* `nebula.config.ts`, one chunk per component, React read from
|
|
12739
|
-
* scoped to the package's own elements; into `<outDir>/ui/`.
|
|
12874
|
+
* `nebula.config.ts`, one chunk per component and per translation catalogue, React read from
|
|
12875
|
+
* the interface, the stylesheet scoped to the package's own elements; into `<outDir>/ui/`. What
|
|
12876
|
+
* each catalogue lacks of the strings the code says is reported, never refused.
|
|
12740
12877
|
* - **The server** (`vite build --ssr`): the config's `server` module with every dependency inside
|
|
12741
12878
|
* it, for Node 22, into one `<outDir>/server/index.js`. The config's loaders are emptied there,
|
|
12742
12879
|
* and an import of `/ui` or `/components` refused: the interface is never part of the server.
|
|
@@ -12749,16 +12886,24 @@ const PLUGIN_NAME = "nebulahome:package";
|
|
|
12749
12886
|
const UI_ENTRY = "virtual:nebulahome/ui";
|
|
12750
12887
|
/** An import of a module, as the config's loaders are written: `import('./card.js')`. */
|
|
12751
12888
|
const DYNAMIC_IMPORT = /\bimport\(\s*(['"`])[^'"`]+\1\s*\)/g;
|
|
12889
|
+
/**
|
|
12890
|
+
* A string the interface's code says, as `t('clients.title', …)`: a literal key, the way the
|
|
12891
|
+
* catalogues can be checked against it.
|
|
12892
|
+
*/
|
|
12893
|
+
const SAID_KEY = /\bt\(\s*(['"`])([\w.-]+)\1\s*,/g;
|
|
12894
|
+
/** The package's own modules, which say its strings: not its dependencies. */
|
|
12895
|
+
const OWN_MODULE = /^(?!.*[\\/]node_modules[\\/]).*\.[cm]?[jt]sx?$/;
|
|
12752
12896
|
/** The SDK's entry points that belong to the interface. */
|
|
12753
12897
|
const INTERFACE_ENTRIES = ["@nebulahome/package-sdk/ui", "@nebulahome/package-sdk/components"];
|
|
12754
12898
|
/**
|
|
12755
|
-
* The code of the interface's entry: the components the config binds,
|
|
12756
|
-
* them.
|
|
12899
|
+
* The code of the interface's entry: the components the config binds, and a loader per
|
|
12900
|
+
* translation catalogue, as the interface reads them.
|
|
12757
12901
|
*
|
|
12758
12902
|
* @param configPath - The config's path.
|
|
12903
|
+
* @param catalogues - The catalogues, by language.
|
|
12759
12904
|
* @returns The module.
|
|
12760
12905
|
*/
|
|
12761
|
-
function uiEntryCode(configPath) {
|
|
12906
|
+
function uiEntryCode(configPath, catalogues) {
|
|
12762
12907
|
return [
|
|
12763
12908
|
`import definition from ${JSON.stringify(configPath.replaceAll("\\", "/"))};`,
|
|
12764
12909
|
"const ui = definition.ui ?? {};",
|
|
@@ -12768,6 +12913,9 @@ function uiEntryCode(configPath) {
|
|
|
12768
12913
|
" page: ui.page?.component,",
|
|
12769
12914
|
" cards: ui.cards ?? {},",
|
|
12770
12915
|
" entityViews: ui.entityViews ?? {},",
|
|
12916
|
+
" locales: {",
|
|
12917
|
+
...catalogues.map((catalogue) => ` ${JSON.stringify(catalogue.language)}: () => import(${JSON.stringify(catalogue.file.replaceAll("\\", "/"))}),`),
|
|
12918
|
+
" },",
|
|
12771
12919
|
"};"
|
|
12772
12920
|
].join("\n");
|
|
12773
12921
|
}
|
|
@@ -12802,7 +12950,9 @@ function nebula(options = {}) {
|
|
|
12802
12950
|
state = {
|
|
12803
12951
|
side: "server",
|
|
12804
12952
|
configPath,
|
|
12805
|
-
packageId
|
|
12953
|
+
packageId,
|
|
12954
|
+
catalogues: [],
|
|
12955
|
+
said: /* @__PURE__ */ new Set()
|
|
12806
12956
|
};
|
|
12807
12957
|
return {
|
|
12808
12958
|
build: {
|
|
@@ -12830,7 +12980,9 @@ function nebula(options = {}) {
|
|
|
12830
12980
|
state = {
|
|
12831
12981
|
side: "ui",
|
|
12832
12982
|
configPath,
|
|
12833
|
-
packageId
|
|
12983
|
+
packageId,
|
|
12984
|
+
catalogues: namedCatalogues(await findCatalogues(path.dirname(configPath))),
|
|
12985
|
+
said: /* @__PURE__ */ new Set()
|
|
12834
12986
|
};
|
|
12835
12987
|
return {
|
|
12836
12988
|
base: "./",
|
|
@@ -12884,23 +13036,42 @@ function nebula(options = {}) {
|
|
|
12884
13036
|
*/
|
|
12885
13037
|
load(id) {
|
|
12886
13038
|
if (state === void 0) return null;
|
|
12887
|
-
if (id === `\0${UI_ENTRY}`) return uiEntryCode(state.configPath);
|
|
13039
|
+
if (id === `\0${UI_ENTRY}`) return uiEntryCode(state.configPath, state.catalogues);
|
|
12888
13040
|
return id.startsWith("\0nebula-shared:") ? sharedModuleCode(id.slice(15)) : null;
|
|
12889
13041
|
},
|
|
12890
13042
|
/**
|
|
12891
13043
|
* Empties the config's loaders in the server: they load components the server never draws.
|
|
13044
|
+
* In the interface, notes the keys the package's own code says, for the catalogues' report.
|
|
12892
13045
|
*
|
|
12893
13046
|
* @param code - The module's code.
|
|
12894
13047
|
* @param id - The module.
|
|
12895
13048
|
* @returns The code without them, or null to leave it.
|
|
12896
13049
|
*/
|
|
12897
13050
|
transform(code, id) {
|
|
13051
|
+
if (state?.side === "ui") {
|
|
13052
|
+
if (OWN_MODULE.test(id)) for (const match of code.matchAll(SAID_KEY)) state.said.add(match[2] ?? "");
|
|
13053
|
+
return null;
|
|
13054
|
+
}
|
|
12898
13055
|
if (state?.side !== "server" || path.resolve(id) !== state.configPath) return null;
|
|
12899
13056
|
return {
|
|
12900
13057
|
code: code.replace(DYNAMIC_IMPORT, "Promise.resolve(undefined)"),
|
|
12901
13058
|
map: null
|
|
12902
13059
|
};
|
|
12903
13060
|
},
|
|
13061
|
+
/**
|
|
13062
|
+
* Says, per catalogue, the strings it lacks and the keys no string uses: English stands in
|
|
13063
|
+
* for the first, and the second are left over from strings that changed.
|
|
13064
|
+
*/
|
|
13065
|
+
async closeBundle() {
|
|
13066
|
+
if (state?.side !== "ui") return;
|
|
13067
|
+
for (const catalogue of state.catalogues) {
|
|
13068
|
+
const { keys } = await readCatalogue(catalogue.file);
|
|
13069
|
+
const { missing, unknown } = catalogueCoverage(state.said, keys);
|
|
13070
|
+
const name = `locales/${catalogue.language}.json`;
|
|
13071
|
+
if (missing.length > 0) this.warn(`${name}: ${String(missing.length)} of ${String(state.said.size)} strings are said in English (${missing.join(", ")})`);
|
|
13072
|
+
if (unknown.length > 0) this.warn(`${name}: no string says ${unknown.join(", ")}`);
|
|
13073
|
+
}
|
|
13074
|
+
},
|
|
12904
13075
|
generateBundle: {
|
|
12905
13076
|
order: "post",
|
|
12906
13077
|
/**
|
|
@@ -12976,8 +13147,8 @@ async function pluginOptions(root, logLevel) {
|
|
|
12976
13147
|
};
|
|
12977
13148
|
}
|
|
12978
13149
|
/**
|
|
12979
|
-
* Checks a package without building it: its declaration against the platform's schemas,
|
|
12980
|
-
* files it names.
|
|
13150
|
+
* Checks a package without building it: its declaration against the platform's schemas, the
|
|
13151
|
+
* files it names, and its translation catalogues.
|
|
12981
13152
|
*
|
|
12982
13153
|
* @param options - Where the package is.
|
|
12983
13154
|
* @returns The package, checked.
|
|
@@ -13003,6 +13174,7 @@ async function checkPackage(options = {}) {
|
|
|
13003
13174
|
if (Buffer.byteLength(text) > COMPOSE_LIMIT) missing.push(`${compose.from}: a compose file is at most 64 KB`);
|
|
13004
13175
|
else missing.push(...composeFileProblems(text, settingKeysOf(generated.setup)).map((problem) => `${compose.from}: ${problem}`));
|
|
13005
13176
|
}
|
|
13177
|
+
missing.push(...await catalogueProblems(await findCatalogues(base), base));
|
|
13006
13178
|
if (missing.length > 0) throw new PackageConfigError(missing);
|
|
13007
13179
|
return {
|
|
13008
13180
|
root,
|
package/dist/chunks/version.js
CHANGED
package/dist/ui.d.ts
CHANGED
|
@@ -128,6 +128,27 @@ type Locale = {
|
|
|
128
128
|
readonly locale: string;
|
|
129
129
|
readonly timeZone: string;
|
|
130
130
|
};
|
|
131
|
+
/** A plural's English: the form for one, and the form for any other number. */
|
|
132
|
+
type PluralEnglish = {
|
|
133
|
+
readonly one: string;
|
|
134
|
+
readonly other: string;
|
|
135
|
+
};
|
|
136
|
+
/** What fills a string's `{{placeholders}}`; `count` also picks a plural's form. */
|
|
137
|
+
type TranslationValues = Readonly<Record<string, string | number>>;
|
|
138
|
+
/**
|
|
139
|
+
* Says one of the package's strings in the viewer's language.
|
|
140
|
+
*
|
|
141
|
+
* The key is looked up in the package's catalogue for that language (`locales/it.json`); when it
|
|
142
|
+
* is not there, the English given here is said instead. A plural takes its English as
|
|
143
|
+
* `{ one, other }` and the number as `count`: each language's catalogue holds the forms it needs.
|
|
144
|
+
*/
|
|
145
|
+
type Translate = (key: string, english: string | PluralEnglish, values?: TranslationValues) => string;
|
|
146
|
+
/** The package's strings, in the viewer's language. */
|
|
147
|
+
type Translation = {
|
|
148
|
+
readonly t: Translate;
|
|
149
|
+
/** The language they are said in, `it`: the viewer's, whether or not the package has it. */
|
|
150
|
+
readonly locale: string;
|
|
151
|
+
};
|
|
131
152
|
/** Which theme the interface draws in. */
|
|
132
153
|
type Theme = {
|
|
133
154
|
readonly scheme: 'light' | 'dark';
|
|
@@ -185,6 +206,8 @@ type UiHost = {
|
|
|
185
206
|
readonly usePackageSettings: <TSettings extends SettingsDefinition | undefined = undefined>(settings?: TSettings) => PackageSettings<TSettings>;
|
|
186
207
|
readonly useViewer: () => Viewer;
|
|
187
208
|
readonly useLocale: () => Locale;
|
|
209
|
+
/** Since 1.1. */
|
|
210
|
+
readonly useTranslation: () => Translation;
|
|
188
211
|
readonly useTheme: () => Theme;
|
|
189
212
|
readonly usePortalContainer: () => HTMLElement | null;
|
|
190
213
|
readonly usePackageRoute: () => PackageRoute;
|
|
@@ -272,6 +295,17 @@ export declare function useViewer(): Viewer;
|
|
|
272
295
|
* @returns Them, for `Intl`.
|
|
273
296
|
*/
|
|
274
297
|
export declare function useLocale(): Locale;
|
|
298
|
+
/**
|
|
299
|
+
* The package's strings in the viewer's language, from the catalogues in its `locales` folder
|
|
300
|
+
* (PACKAGES_DOCS.md 6.5).
|
|
301
|
+
*
|
|
302
|
+
* `t('clients.title', 'Clients')` says the key from the viewer's catalogue, and its English when
|
|
303
|
+
* the catalogue lacks it; outside an interface that lends this - a component's unit test - every
|
|
304
|
+
* string is its English.
|
|
305
|
+
*
|
|
306
|
+
* @returns `t`, and the language it speaks.
|
|
307
|
+
*/
|
|
308
|
+
export declare function useTranslation(): Translation;
|
|
275
309
|
/**
|
|
276
310
|
* The theme the interface draws in, for what draws its own colours: a canvas, a chart.
|
|
277
311
|
*
|
|
@@ -317,4 +351,4 @@ export declare function PackageLink(props: PackageLinkProps): ReactElement;
|
|
|
317
351
|
*/
|
|
318
352
|
export declare function PlatformLink(props: PlatformLinkProps): ReactElement;
|
|
319
353
|
//#endregion
|
|
320
|
-
export type { CardProps, CustomEntityState, EntitiesFilter, EntityView, EntityViewProps, Locale, Mutation, MutationOptions, PackageCall, PackageInfo, PackageLinkProps, PackageNavigate, PackageRoute, PackageSettings, PageProps, PlatformLinkProps, PlatformNavigate, PlatformTarget, RpcOptions, RpcState, SendCommand, SettingsPatch, Theme, UiHost, Viewer, VisibleSettings };
|
|
354
|
+
export type { CardProps, CustomEntityState, EntitiesFilter, EntityView, EntityViewProps, Locale, Mutation, MutationOptions, PackageCall, PackageInfo, PackageLinkProps, PackageNavigate, PackageRoute, PackageSettings, PageProps, PlatformLinkProps, PlatformNavigate, PlatformTarget, PluralEnglish, RpcOptions, RpcState, SendCommand, SettingsPatch, Theme, Translate, Translation, TranslationValues, UiHost, Viewer, VisibleSettings };
|
package/dist/ui.js
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
|
-
import { n as UI_MODULE, r as hostMember } from "./chunks/host.js";
|
|
1
|
+
import { i as lentMember, n as UI_MODULE, r as hostMember } from "./chunks/host.js";
|
|
2
2
|
import { createElement } from "react";
|
|
3
|
+
//#region src/ui/english.ts
|
|
4
|
+
/** A `{{placeholder}}`, as i18next writes it. */
|
|
5
|
+
const PLACEHOLDER = /\{\{\s*(\w+)\s*\}\}/g;
|
|
6
|
+
/**
|
|
7
|
+
* Fills a string's placeholders.
|
|
8
|
+
*
|
|
9
|
+
* @param text - The string.
|
|
10
|
+
* @param values - What fills them; a placeholder with no value is left as written.
|
|
11
|
+
* @returns The string, filled.
|
|
12
|
+
*/
|
|
13
|
+
function interpolate(text, values) {
|
|
14
|
+
return text.replaceAll(PLACEHOLDER, (whole, name) => {
|
|
15
|
+
const value = values?.[name];
|
|
16
|
+
return value === void 0 ? whole : String(value);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Says a string's English, its plural form chosen by `count`.
|
|
21
|
+
*
|
|
22
|
+
* @param english - The English, or a plural's two forms.
|
|
23
|
+
* @param values - What fills it.
|
|
24
|
+
* @returns The English, filled.
|
|
25
|
+
*/
|
|
26
|
+
function englishOf(english, values) {
|
|
27
|
+
return interpolate(typeof english === "string" ? english : values?.["count"] === 1 ? english.one : english.other, values);
|
|
28
|
+
}
|
|
29
|
+
/** The package's strings in English. */
|
|
30
|
+
const ENGLISH = {
|
|
31
|
+
/**
|
|
32
|
+
* Says a string's English.
|
|
33
|
+
*
|
|
34
|
+
* @param key - Its key, unused: there is no catalogue here.
|
|
35
|
+
* @param english - Its English.
|
|
36
|
+
* @param values - What fills it.
|
|
37
|
+
* @returns The English, filled.
|
|
38
|
+
*/
|
|
39
|
+
t: (key, english, values) => englishOf(english, values),
|
|
40
|
+
locale: "en"
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
3
43
|
//#region src/ui/index.ts
|
|
4
44
|
/**
|
|
5
45
|
* `@nebulahome/package-sdk/ui`: what a package's page, cards and entity views are written with
|
|
@@ -119,6 +159,20 @@ function useLocale() {
|
|
|
119
159
|
return ui("useLocale", "1.0")();
|
|
120
160
|
}
|
|
121
161
|
/**
|
|
162
|
+
* The package's strings in the viewer's language, from the catalogues in its `locales` folder
|
|
163
|
+
* (PACKAGES_DOCS.md 6.5).
|
|
164
|
+
*
|
|
165
|
+
* `t('clients.title', 'Clients')` says the key from the viewer's catalogue, and its English when
|
|
166
|
+
* the catalogue lacks it; outside an interface that lends this - a component's unit test - every
|
|
167
|
+
* string is its English.
|
|
168
|
+
*
|
|
169
|
+
* @returns `t`, and the language it speaks.
|
|
170
|
+
*/
|
|
171
|
+
function useTranslation() {
|
|
172
|
+
const lent = lentMember(UI_MODULE, "useTranslation");
|
|
173
|
+
return lent === void 0 ? ENGLISH : lent();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
122
176
|
* The theme the interface draws in, for what draws its own colours: a canvas, a chart.
|
|
123
177
|
*
|
|
124
178
|
* @returns Light or dark.
|
|
@@ -177,4 +231,4 @@ function PlatformLink(props) {
|
|
|
177
231
|
return createElement(ui("PlatformLink", "1.0"), props);
|
|
178
232
|
}
|
|
179
233
|
//#endregion
|
|
180
|
-
export { PackageLink, PlatformLink, useCommand, useCustomEntity, useEntities, useEntity, useLocale, usePackage, usePackageCall, usePackageMutation, usePackageNavigate, usePackageRoute, usePackageRpc, usePackageSettings, usePlatformNavigate, usePortalContainer, useTheme, useViewer };
|
|
234
|
+
export { PackageLink, PlatformLink, useCommand, useCustomEntity, useEntities, useEntity, useLocale, usePackage, usePackageCall, usePackageMutation, usePackageNavigate, usePackageRoute, usePackageRpc, usePackageSettings, usePlatformNavigate, usePortalContainer, useTheme, useTranslation, useViewer };
|
package/dist/vite.d.ts
CHANGED
|
@@ -52,8 +52,8 @@ type CheckedPackage = {
|
|
|
52
52
|
readonly generated: GeneratedPackage;
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
|
-
* Checks a package without building it: its declaration against the platform's schemas,
|
|
56
|
-
* files it names.
|
|
55
|
+
* Checks a package without building it: its declaration against the platform's schemas, the
|
|
56
|
+
* files it names, and its translation catalogues.
|
|
57
57
|
*
|
|
58
58
|
* @param options - Where the package is.
|
|
59
59
|
* @returns The package, checked.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebulahome/package-sdk",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Write a NebulaHome package: declare it, build it, and run it on the platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -96,10 +96,10 @@
|
|
|
96
96
|
"vitest": "^4.1.11",
|
|
97
97
|
"yaml": "^2.9.0",
|
|
98
98
|
"zod": "^4.5.4",
|
|
99
|
-
"@nebulahome/eslint-config": "0.0.0",
|
|
100
|
-
"@nebulahome/tsconfig": "0.0.0",
|
|
101
99
|
"@nebulahome/contracts": "0.0.0",
|
|
102
|
-
"@nebulahome/
|
|
100
|
+
"@nebulahome/tsconfig": "0.0.0",
|
|
101
|
+
"@nebulahome/ui": "0.0.0",
|
|
102
|
+
"@nebulahome/eslint-config": "0.0.0"
|
|
103
103
|
},
|
|
104
104
|
"scripts": {
|
|
105
105
|
"build": "tsdown && node scripts/build-styles.js",
|