@jchat/addon-sdk 1.0.0-alpha.2 → 1.0.0-alpha.20
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 +112 -0
- package/LICENSE +18 -196
- package/README.md +385 -20
- package/dist/cli.d.mts +35 -0
- package/dist/cli.mjs +136 -0
- package/dist/contracts.d.mts +20 -29
- package/dist/contracts.mjs +38 -0
- package/dist/diagnostics.d.mts +55 -9
- package/dist/diagnostics.mjs +66 -1
- package/dist/i18n-validator.d.mts +17 -0
- package/dist/i18n-validator.mjs +90 -0
- package/dist/i18n.mjs +191 -0
- package/dist/icon.d.mts +4 -0
- package/dist/icon.mjs +4 -0
- package/dist/index.d.mts +16 -253
- package/dist/index.mjs +106 -43
- package/dist/index2.d.mts +478 -0
- package/dist/state.d.mts +1 -0
- package/dist/state.mjs +2 -0
- package/dist/types.d.mts +12 -0
- package/dist/ui.d.mts +1 -0
- package/dist/ui.mjs +2 -0
- package/dist/vite.d.mts +64 -10
- package/dist/vite.mjs +222 -32
- package/package.json +119 -17
- package/skills/create-jchat-addon/SKILL.md +140 -0
- package/skills/jchat-addon-development/SKILL.md +260 -0
- package/tsconfig.addon.json +20 -0
- package/src/contracts.d.ts +0 -127
- package/src/contracts.ts +0 -132
- package/src/diagnostics.d.ts +0 -19
- package/src/diagnostics.test.ts +0 -106
- package/src/diagnostics.ts +0 -182
- package/src/index.d.ts +0 -249
- package/src/index.test.ts +0 -35
- package/src/index.ts +0 -299
- package/src/vite.test.ts +0 -62
- package/src/vite.ts +0 -289
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/contracts.ts
|
|
2
|
+
/** Valid locations where an addon can be rendered within the Jchat layout. */
|
|
3
|
+
const MOUNT_POINTS = {
|
|
4
|
+
/** Top of the main chat column */
|
|
5
|
+
CHAT_HEADER: "chat-header",
|
|
6
|
+
/** Below the main chat header, above messages */
|
|
7
|
+
CHAT_TOOLBAR: "chat-toolbar",
|
|
8
|
+
/** The persistent right-side sidebar */
|
|
9
|
+
CHAT_SIDEBAR: "chat-sidebar",
|
|
10
|
+
/** Top of the left sidebar (room list) */
|
|
11
|
+
SIDEBAR_TOP: "sidebar-top",
|
|
12
|
+
/** Bottom of the left sidebar */
|
|
13
|
+
SIDEBAR_BOTTOM: "sidebar-bottom",
|
|
14
|
+
/** Action list shown when clicking a user's avatar */
|
|
15
|
+
USER_POPOVER_ACTIONS: "user-popover-actions",
|
|
16
|
+
/** Menu shown when right-clicking a message */
|
|
17
|
+
MESSAGE_CONTEXT_MENU: "message-context-menu",
|
|
18
|
+
/** Floating overlay rendered on top of the message stream */
|
|
19
|
+
MESSAGE_FEED_OVERLAY: "message-feed-overlay",
|
|
20
|
+
/** Main admin dashboard area */
|
|
21
|
+
ADMIN_PANEL: "admin-panel",
|
|
22
|
+
/** User or site settings dashboard area */
|
|
23
|
+
SETTINGS_PANEL: "settings-panel",
|
|
24
|
+
/** The 'plus' (+) menu in the message composer */
|
|
25
|
+
CHAT_PLUS_MENU: "chat-plus-menu",
|
|
26
|
+
/** Custom tab in the emoji/gif selection panel */
|
|
27
|
+
EMOJI_PICKER_GIF_TAB: "emoji-picker-gif-tab",
|
|
28
|
+
/** Custom tab in the emoji/sticker selection panel */
|
|
29
|
+
EMOJI_PICKER_STICKER_TAB: "emoji-picker-sticker-tab",
|
|
30
|
+
/** Full-screen or modal dialog specifically for TubeScan */
|
|
31
|
+
TUBESCAN_DIALOG: "tubescan-dialog",
|
|
32
|
+
/** Dedicated settings panel rendered in the Owner Dashboard */
|
|
33
|
+
OWNER_SETTINGS: "owner-settings",
|
|
34
|
+
/** Dedicated settings panel rendered in the Site Admin Dashboard */
|
|
35
|
+
ADDON_SETTINGS: "addon-settings"
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { MOUNT_POINTS };
|
package/dist/diagnostics.d.mts
CHANGED
|
@@ -1,22 +1,68 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AddonManifest } from "./contracts.mjs";
|
|
2
2
|
//#region src/diagnostics.d.ts
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Diagnostic result status indicator for addon validation checks. - `"pass"`: The diagnostic check
|
|
5
|
+
* passed successfully. - `"warning"`: Non-critical warning (e.g. source maps present in
|
|
6
|
+
* production). - `"error"`: Critical failure blocking addon installation or distribution.
|
|
7
|
+
*/
|
|
8
|
+
export type AddonDiagnosticStatus = "pass" | "warning" | "error";
|
|
9
|
+
/** Represents an individual diagnostic check result for an addon package. */
|
|
10
|
+
export interface AddonPackageDiagnostic {
|
|
11
|
+
/** Unique identifier for the diagnostic check (e.g. `"jchat"`, `"package-size"`). */
|
|
5
12
|
id: string;
|
|
13
|
+
/** Human-readable title for the diagnostic check. */
|
|
6
14
|
label: string;
|
|
15
|
+
/** Severity status outcome of the check. */
|
|
7
16
|
status: AddonDiagnosticStatus;
|
|
17
|
+
/** Descriptive message or metric value providing check details. */
|
|
8
18
|
message: string;
|
|
9
19
|
}
|
|
10
|
-
|
|
20
|
+
/** Options required to run diagnostic validation on an addon build package. */
|
|
21
|
+
export interface AddonPackageDiagnosticOptions {
|
|
22
|
+
/** Absolute path to the compiled addon build output directory (`build/`). */
|
|
11
23
|
buildDir: string;
|
|
24
|
+
/** Parsed addon manifest object (`manifest.json`). */
|
|
12
25
|
manifest: AddonManifest;
|
|
26
|
+
/** Semantic version of the host JChat platform instance. */
|
|
13
27
|
jchatVersion: string;
|
|
28
|
+
/** Semantic version of the `@jchat/addon-sdk` package. */
|
|
14
29
|
addonSdkVersion: string;
|
|
30
|
+
/** Whether the diagnostic target is a production `.zip` package build. Defaults to `false`. */
|
|
15
31
|
productionPackage?: boolean;
|
|
32
|
+
/** Maximum allowable package size in bytes. Defaults to 25MB (`26,214,400` bytes). */
|
|
16
33
|
maxBytes?: number;
|
|
17
34
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Constructs a semantic version compatibility range covering all minor/patch versions of the
|
|
37
|
+
* current major version.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ;```ts
|
|
41
|
+
* createCompatibleMajorRange("1.4.2") // returns ">=1.4.2 <2.0.0"
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @param version - Valid semver string (e.g. `"1.2.0"`).
|
|
45
|
+
* @returns Semver range string matching the current major series (e.g. `">=1.2.0 <2.0.0"`).
|
|
46
|
+
* @throws {@link Error} If the input is not a valid semantic version string.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createCompatibleMajorRange(version: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Performs comprehensive diagnostic validation checks on an addon package build directory.
|
|
51
|
+
*
|
|
52
|
+
* Validates version compatibility ranges, verifies required build files (frontend/server bundles,
|
|
53
|
+
* migrations, seeds), checks total package size limits, flags production source maps, and verifies
|
|
54
|
+
* publisher digital signatures.
|
|
55
|
+
*
|
|
56
|
+
* @param options - Configuration options specifying build directory, manifest, and environment
|
|
57
|
+
* versions.
|
|
58
|
+
* @returns Array of {@link AddonPackageDiagnostic} results.
|
|
59
|
+
*/
|
|
60
|
+
export declare function diagnoseAddonPackage(options: AddonPackageDiagnosticOptions): AddonPackageDiagnostic[];
|
|
61
|
+
/**
|
|
62
|
+
* Checks whether any diagnostic check in a diagnostic list returned a critical error.
|
|
63
|
+
*
|
|
64
|
+
* @param diagnostics - Array of diagnostic check results to evaluate.
|
|
65
|
+
* @returns `true` if at least one check has `status === "error"`, otherwise `false`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function hasAddonDiagnosticErrors(diagnostics: readonly AddonPackageDiagnostic[]): boolean;
|
|
68
|
+
//#endregion
|
package/dist/diagnostics.mjs
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import * as fs$1 from "node:fs";
|
|
2
2
|
import * as path$1 from "node:path";
|
|
3
3
|
//#region src/diagnostics.ts
|
|
4
|
+
/**
|
|
5
|
+
* Constructs a semantic version compatibility range covering all minor/patch versions of the
|
|
6
|
+
* current major version.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ;```ts
|
|
10
|
+
* createCompatibleMajorRange("1.4.2") // returns ">=1.4.2 <2.0.0"
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* @param version - Valid semver string (e.g. `"1.2.0"`).
|
|
14
|
+
* @returns Semver range string matching the current major series (e.g. `">=1.2.0 <2.0.0"`).
|
|
15
|
+
* @throws {@link Error} If the input is not a valid semantic version string.
|
|
16
|
+
*/
|
|
4
17
|
function createCompatibleMajorRange(version) {
|
|
5
18
|
try {
|
|
6
19
|
Bun.semver.order(version, version);
|
|
@@ -12,6 +25,17 @@ function createCompatibleMajorRange(version) {
|
|
|
12
25
|
if (!Number.isSafeInteger(major)) throw new Error(`Invalid semantic version: ${version}`);
|
|
13
26
|
return `>=${version} <${major + 1}.0.0`;
|
|
14
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Performs comprehensive diagnostic validation checks on an addon package build directory.
|
|
30
|
+
*
|
|
31
|
+
* Validates version compatibility ranges, verifies required build files (frontend/server bundles,
|
|
32
|
+
* migrations, seeds), checks total package size limits, flags production source maps, and verifies
|
|
33
|
+
* publisher digital signatures.
|
|
34
|
+
*
|
|
35
|
+
* @param options - Configuration options specifying build directory, manifest, and environment
|
|
36
|
+
* versions.
|
|
37
|
+
* @returns Array of {@link AddonPackageDiagnostic} results.
|
|
38
|
+
*/
|
|
15
39
|
function diagnoseAddonPackage(options) {
|
|
16
40
|
const { buildDir, manifest } = options;
|
|
17
41
|
const diagnostics = [];
|
|
@@ -52,13 +76,25 @@ function diagnoseAddonPackage(options) {
|
|
|
52
76
|
const productionArchive = path$1.join(buildDir, `${manifest.id}-${manifest.version}.zip`);
|
|
53
77
|
const hasProductionArchive = fs$1.existsSync(productionArchive) && fs$1.statSync(productionArchive).isFile();
|
|
54
78
|
const totalBytes = options.productionPackage && hasProductionArchive ? fs$1.statSync(productionArchive).size : files.filter((file) => file !== productionArchive).reduce((total, file) => total + fs$1.statSync(file).size, 0);
|
|
55
|
-
const maxBytes = options.maxBytes ??
|
|
79
|
+
const maxBytes = options.maxBytes ?? 26214400;
|
|
56
80
|
diagnostics.push({
|
|
57
81
|
id: "package-size",
|
|
58
82
|
label: "Package size",
|
|
59
83
|
status: totalBytes <= maxBytes ? "pass" : "error",
|
|
60
84
|
message: `${totalBytes} of ${maxBytes} bytes`
|
|
61
85
|
});
|
|
86
|
+
const addonPermPrefix = `addon.${manifest.id}.`;
|
|
87
|
+
const invalidPerms = Array.from(/* @__PURE__ */ new Set([
|
|
88
|
+
...manifest.accessPermission ? [manifest.accessPermission] : [],
|
|
89
|
+
...manifest.permissions ?? [],
|
|
90
|
+
...manifest.permissionDefinitions?.map((d) => d.name) ?? []
|
|
91
|
+
])).filter((perm) => !perm.startsWith(addonPermPrefix));
|
|
92
|
+
diagnostics.push({
|
|
93
|
+
id: "permission-namespacing",
|
|
94
|
+
label: "Permission namespacing",
|
|
95
|
+
status: invalidPerms.length === 0 ? "pass" : "error",
|
|
96
|
+
message: invalidPerms.length === 0 ? `All declared permissions start with ${addonPermPrefix}` : `Permission(s) do not follow naming convention ${addonPermPrefix}*: ${invalidPerms.join(", ")}`
|
|
97
|
+
});
|
|
62
98
|
const sourceMaps = files.filter((file) => file.endsWith(".map"));
|
|
63
99
|
diagnostics.push({
|
|
64
100
|
id: "source-maps",
|
|
@@ -81,9 +117,24 @@ function diagnoseAddonPackage(options) {
|
|
|
81
117
|
});
|
|
82
118
|
return diagnostics;
|
|
83
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Checks whether any diagnostic check in a diagnostic list returned a critical error.
|
|
122
|
+
*
|
|
123
|
+
* @param diagnostics - Array of diagnostic check results to evaluate.
|
|
124
|
+
* @returns `true` if at least one check has `status === "error"`, otherwise `false`.
|
|
125
|
+
*/
|
|
84
126
|
function hasAddonDiagnosticErrors(diagnostics) {
|
|
85
127
|
return diagnostics.some(({ status }) => status === "error");
|
|
86
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Evaluates semantic version compatibility between a target version and a semver range string.
|
|
131
|
+
*
|
|
132
|
+
* @param id - Diagnostic check identifier.
|
|
133
|
+
* @param label - Diagnostic check label.
|
|
134
|
+
* @param version - Environment component version string.
|
|
135
|
+
* @param range - Expected semver range string from manifest compatibility declarations.
|
|
136
|
+
* @returns An {@link AddonPackageDiagnostic} result object.
|
|
137
|
+
*/
|
|
87
138
|
function compatibilityDiagnostic(id, label, version, range) {
|
|
88
139
|
const compatible = Boolean(range && Bun.semver.satisfies(version, range));
|
|
89
140
|
return {
|
|
@@ -93,12 +144,26 @@ function compatibilityDiagnostic(id, label, version, range) {
|
|
|
93
144
|
message: range ? `${version} ${compatible ? "satisfies" : "does not satisfy"} ${range}` : "Compatibility range is missing"
|
|
94
145
|
};
|
|
95
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Safely resolves a relative build file reference within a target build directory, guarding against
|
|
149
|
+
* directory traversal attacks.
|
|
150
|
+
*
|
|
151
|
+
* @param buildDir - Absolute path to the build directory.
|
|
152
|
+
* @param reference - Relative file path declared in manifest.
|
|
153
|
+
* @returns Resolved absolute file path if safe and inside `buildDir`, or `null` if unsafe.
|
|
154
|
+
*/
|
|
96
155
|
function resolveBuildFile(buildDir, reference) {
|
|
97
156
|
if (path$1.isAbsolute(reference) || reference.includes("\\") || reference.split("/").includes("..")) return null;
|
|
98
157
|
const target = path$1.resolve(buildDir, ...reference.replace(/^\.\//, "").split("/"));
|
|
99
158
|
const relative = path$1.relative(path$1.resolve(buildDir), target);
|
|
100
159
|
return relative && !relative.startsWith("..") && !path$1.isAbsolute(relative) ? target : null;
|
|
101
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Recursively lists all regular files within a root directory.
|
|
163
|
+
*
|
|
164
|
+
* @param root - Path to search.
|
|
165
|
+
* @returns Array of absolute file paths.
|
|
166
|
+
*/
|
|
102
167
|
function listRegularFiles(root) {
|
|
103
168
|
if (!fs$1.existsSync(root)) return [];
|
|
104
169
|
return fs$1.readdirSync(root, {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/i18n-validator.d.ts
|
|
2
|
+
export interface AddonLocaleValidationResult {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
missingFiles: string[];
|
|
6
|
+
missingKeys: Record<string, string[]>;
|
|
7
|
+
extraKeys: Record<string, string[]>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Validates that an addon's `locales/` directory contains valid translation files for all 20 host
|
|
11
|
+
* languages with 100% key parity against `en.json`.
|
|
12
|
+
*
|
|
13
|
+
* @param dirPath - Path to the addon's locales directory.
|
|
14
|
+
* @param domainName - Optional label for error messages (defaults to "Addon").
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateAddonLocales(dirPath: string, domainName?: string): AddonLocaleValidationResult;
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { t as HOST_LANGUAGES } from "./i18n.mjs";
|
|
2
|
+
import * as fs$1 from "node:fs";
|
|
3
|
+
import * as path$1 from "node:path";
|
|
4
|
+
//#region src/i18n-validator.ts
|
|
5
|
+
function extractNestedKeys(obj, prefix = "") {
|
|
6
|
+
if (!obj || typeof obj !== "object") return [];
|
|
7
|
+
const keys = [];
|
|
8
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
9
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
10
|
+
if (value && typeof value === "object" && !Array.isArray(value)) keys.push(...extractNestedKeys(value, fullKey));
|
|
11
|
+
else keys.push(fullKey);
|
|
12
|
+
}
|
|
13
|
+
return keys;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validates that an addon's `locales/` directory contains valid translation files for all 20 host
|
|
17
|
+
* languages with 100% key parity against `en.json`.
|
|
18
|
+
*
|
|
19
|
+
* @param dirPath - Path to the addon's locales directory.
|
|
20
|
+
* @param domainName - Optional label for error messages (defaults to "Addon").
|
|
21
|
+
*/
|
|
22
|
+
function validateAddonLocales(dirPath, domainName = "Addon") {
|
|
23
|
+
const errors = [];
|
|
24
|
+
const missingFiles = [];
|
|
25
|
+
const missingKeys = {};
|
|
26
|
+
const extraKeys = {};
|
|
27
|
+
const enPath = path$1.join(dirPath, "en.json");
|
|
28
|
+
if (!fs$1.existsSync(enPath)) {
|
|
29
|
+
const errorMsg = `[${domainName}] Missing base locale file: en.json`;
|
|
30
|
+
errors.push(errorMsg);
|
|
31
|
+
missingFiles.push("en.json");
|
|
32
|
+
return {
|
|
33
|
+
valid: false,
|
|
34
|
+
errors,
|
|
35
|
+
missingFiles,
|
|
36
|
+
missingKeys,
|
|
37
|
+
extraKeys
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
let enContent;
|
|
41
|
+
try {
|
|
42
|
+
enContent = JSON.parse(fs$1.readFileSync(enPath, "utf-8"));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const errorMsg = `[${domainName}] Invalid JSON in en.json: ${err instanceof Error ? err.message : String(err)}`;
|
|
45
|
+
errors.push(errorMsg);
|
|
46
|
+
return {
|
|
47
|
+
valid: false,
|
|
48
|
+
errors,
|
|
49
|
+
missingFiles,
|
|
50
|
+
missingKeys,
|
|
51
|
+
extraKeys
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const enKeys = new Set(extractNestedKeys(enContent));
|
|
55
|
+
for (const lang of HOST_LANGUAGES) {
|
|
56
|
+
if (lang === "en") continue;
|
|
57
|
+
const langFileName = `${lang}.json`;
|
|
58
|
+
const langPath = path$1.join(dirPath, langFileName);
|
|
59
|
+
if (!fs$1.existsSync(langPath)) {
|
|
60
|
+
errors.push(`[${domainName}] Missing locale file: ${langFileName}`);
|
|
61
|
+
missingFiles.push(langFileName);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const langContent = JSON.parse(fs$1.readFileSync(langPath, "utf-8"));
|
|
66
|
+
const langKeys = new Set(extractNestedKeys(langContent));
|
|
67
|
+
for (const key of enKeys) if (!langKeys.has(key)) {
|
|
68
|
+
errors.push(`[${domainName}] Missing key in ${langFileName}: "${key}"`);
|
|
69
|
+
if (!missingKeys[lang]) missingKeys[lang] = [];
|
|
70
|
+
missingKeys[lang].push(key);
|
|
71
|
+
}
|
|
72
|
+
for (const key of langKeys) if (!enKeys.has(key)) {
|
|
73
|
+
errors.push(`[${domainName}] Extra key in ${langFileName} (not in en.json): "${key}"`);
|
|
74
|
+
if (!extraKeys[lang]) extraKeys[lang] = [];
|
|
75
|
+
extraKeys[lang].push(key);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
errors.push(`[${domainName}] Invalid JSON in ${langFileName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
valid: errors.length === 0,
|
|
83
|
+
errors,
|
|
84
|
+
missingFiles,
|
|
85
|
+
missingKeys,
|
|
86
|
+
extraKeys
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { validateAddonLocales };
|
package/dist/i18n.mjs
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
//#region src/i18n.ts
|
|
3
|
+
function checkIsRtl(code) {
|
|
4
|
+
if (!code) return false;
|
|
5
|
+
const normalized = code.toLowerCase().split("-")[0];
|
|
6
|
+
return normalized === "ar" || normalized === "he" || normalized === "ur";
|
|
7
|
+
}
|
|
8
|
+
function getLanguageDirection(code) {
|
|
9
|
+
return checkIsRtl(code) ? "rtl" : "ltr";
|
|
10
|
+
}
|
|
11
|
+
function interpolate(text, params) {
|
|
12
|
+
if (!params) return text;
|
|
13
|
+
return text.replace(/\{\{\s*(\w+)\s*\}\}|\{\s*(\w+)\s*\}/g, (match, key1, key2) => {
|
|
14
|
+
const key = key1 || key2;
|
|
15
|
+
return key in params ? String(params[key]) : match;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const HOST_LANGUAGES = [
|
|
19
|
+
"ar",
|
|
20
|
+
"bg",
|
|
21
|
+
"hr",
|
|
22
|
+
"de",
|
|
23
|
+
"el",
|
|
24
|
+
"en",
|
|
25
|
+
"es",
|
|
26
|
+
"fr",
|
|
27
|
+
"he",
|
|
28
|
+
"hi",
|
|
29
|
+
"kn",
|
|
30
|
+
"ml",
|
|
31
|
+
"nl",
|
|
32
|
+
"pt",
|
|
33
|
+
"ro",
|
|
34
|
+
"ru",
|
|
35
|
+
"ta",
|
|
36
|
+
"te",
|
|
37
|
+
"tr",
|
|
38
|
+
"ur"
|
|
39
|
+
];
|
|
40
|
+
const LocaleContext = createContext({
|
|
41
|
+
locale: "en",
|
|
42
|
+
dir: "ltr"
|
|
43
|
+
});
|
|
44
|
+
/** Resolves a nested key in an object dictionary using dot-notation (e.g. "buttons.submit") */
|
|
45
|
+
function resolveNestedKey(obj, keyPath) {
|
|
46
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
47
|
+
const segments = keyPath.split(".");
|
|
48
|
+
let current = obj;
|
|
49
|
+
for (const seg of segments) if (current && typeof current === "object" && seg in current) current = current[seg];
|
|
50
|
+
else return;
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates a standalone translation function for backend server routes (`src/api.ts`) or utilities.
|
|
55
|
+
*
|
|
56
|
+
* @param resources - Dictionary of translation resources (e.g., `{ en: enJson, ta: taJson }`).
|
|
57
|
+
* @param defaultLocale - Default locale to use if not specified per call (defaults to "en").
|
|
58
|
+
* @param fallbackLocale - Fallback locale if a key is missing in the target language (defaults to
|
|
59
|
+
* "en").
|
|
60
|
+
*/
|
|
61
|
+
function createTranslator(resources = {}, defaultLocale = "en", fallbackLocale = "en") {
|
|
62
|
+
return (key, paramsOrLang, maybeLang) => {
|
|
63
|
+
let params;
|
|
64
|
+
let lang = defaultLocale;
|
|
65
|
+
if (typeof paramsOrLang === "string") lang = paramsOrLang;
|
|
66
|
+
else if (paramsOrLang && typeof paramsOrLang === "object") {
|
|
67
|
+
params = paramsOrLang;
|
|
68
|
+
if (typeof maybeLang === "string") lang = maybeLang;
|
|
69
|
+
}
|
|
70
|
+
const normalizedLang = (lang || defaultLocale).toLowerCase().split("-")[0];
|
|
71
|
+
const normalizedFallback = fallbackLocale.toLowerCase().split("-")[0];
|
|
72
|
+
const langDict = resources[normalizedLang];
|
|
73
|
+
let resolved = resolveNestedKey(langDict, key);
|
|
74
|
+
if (resolved === void 0 && normalizedLang !== normalizedFallback) {
|
|
75
|
+
const fallbackDict = resources[normalizedFallback];
|
|
76
|
+
resolved = resolveNestedKey(fallbackDict, key);
|
|
77
|
+
}
|
|
78
|
+
if (typeof resolved === "string") return interpolate(resolved, params);
|
|
79
|
+
return typeof resolved === "number" || typeof resolved === "boolean" ? String(resolved) : interpolate(key, params);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Hook to access the current active host locale and text direction in an addon component.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ;```tsx
|
|
87
|
+
* import { useLocale } from "@jchat/addon-sdk"
|
|
88
|
+
*
|
|
89
|
+
* export default function MyAddon() {
|
|
90
|
+
* const { locale, dir, isRtl } = useLocale()
|
|
91
|
+
* return <div dir={dir}>Current language: {locale}</div>
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
function useLocale() {
|
|
96
|
+
const context = useContext(LocaleContext);
|
|
97
|
+
const locale = context?.locale || "en";
|
|
98
|
+
const dir = context?.dir || getLanguageDirection(locale);
|
|
99
|
+
return {
|
|
100
|
+
locale,
|
|
101
|
+
dir,
|
|
102
|
+
isRtl: dir === "rtl" || checkIsRtl(locale)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const loadedChunkCache = {};
|
|
106
|
+
/**
|
|
107
|
+
* Hook to access translation function and locale metadata inside addon components. Automatically
|
|
108
|
+
* synchronizes with host language switches and falls back to English. Supports static dictionary
|
|
109
|
+
* resources or dynamic async locale loaders.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ;```tsx
|
|
113
|
+
* import { useTranslation } from "@jchat/addon-sdk"
|
|
114
|
+
* import locales from "./locales"
|
|
115
|
+
*
|
|
116
|
+
* export default function MyAddon() {
|
|
117
|
+
* const { t, locale, dir } = useTranslation(locales)
|
|
118
|
+
* return (
|
|
119
|
+
* <div dir={dir}>
|
|
120
|
+
* <h1>{t("welcome", { name: "Guest" })}</h1>
|
|
121
|
+
* </div>
|
|
122
|
+
* )
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @param resourcesOrLoader - Addon-scoped translation dictionaries or async chunk loader.
|
|
127
|
+
* @param fallbackLocale - Fallback language code when a string is missing (default: "en").
|
|
128
|
+
*/
|
|
129
|
+
function useTranslation(resourcesOrLoader = {}, fallbackLocale = "en") {
|
|
130
|
+
const { locale, dir, isRtl } = useLocale();
|
|
131
|
+
const isLoader = typeof resourcesOrLoader === "function";
|
|
132
|
+
const [dynamicResources, setDynamicResources] = useState(() => {
|
|
133
|
+
return isLoader ? loadedChunkCache : resourcesOrLoader;
|
|
134
|
+
});
|
|
135
|
+
const [isReady, setIsReady] = useState(!isLoader);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!isLoader) {
|
|
138
|
+
setDynamicResources(resourcesOrLoader);
|
|
139
|
+
setIsReady(true);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const loader = resourcesOrLoader;
|
|
143
|
+
const normLocale = locale.toLowerCase().split("-")[0];
|
|
144
|
+
if (loadedChunkCache[normLocale]) {
|
|
145
|
+
setDynamicResources((prev) => ({
|
|
146
|
+
...prev,
|
|
147
|
+
[normLocale]: loadedChunkCache[normLocale]
|
|
148
|
+
}));
|
|
149
|
+
setIsReady(true);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let isMounted = true;
|
|
153
|
+
Promise.resolve(loader(normLocale)).then((dict) => {
|
|
154
|
+
if (!isMounted || !dict) return;
|
|
155
|
+
loadedChunkCache[normLocale] = dict;
|
|
156
|
+
setDynamicResources((prev) => ({
|
|
157
|
+
...prev,
|
|
158
|
+
[normLocale]: dict
|
|
159
|
+
}));
|
|
160
|
+
setIsReady(true);
|
|
161
|
+
}).catch((err) => {
|
|
162
|
+
console.warn(`[addon-sdk] Failed to dynamically load locale chunk "${normLocale}":`, err);
|
|
163
|
+
if (isMounted) setIsReady(true);
|
|
164
|
+
});
|
|
165
|
+
return () => {
|
|
166
|
+
isMounted = false;
|
|
167
|
+
};
|
|
168
|
+
}, [
|
|
169
|
+
isLoader,
|
|
170
|
+
resourcesOrLoader,
|
|
171
|
+
locale
|
|
172
|
+
]);
|
|
173
|
+
const translator = useMemo(() => {
|
|
174
|
+
return createTranslator(dynamicResources, locale, fallbackLocale);
|
|
175
|
+
}, [
|
|
176
|
+
dynamicResources,
|
|
177
|
+
locale,
|
|
178
|
+
fallbackLocale
|
|
179
|
+
]);
|
|
180
|
+
return {
|
|
181
|
+
t: useMemo(() => {
|
|
182
|
+
return (key, params) => translator(key, params, locale);
|
|
183
|
+
}, [translator, locale]),
|
|
184
|
+
locale,
|
|
185
|
+
dir,
|
|
186
|
+
isRtl,
|
|
187
|
+
isReady
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
export { useTranslation as a, useLocale as i, LocaleContext as n, createTranslator as r, HOST_LANGUAGES as t };
|
package/dist/icon.d.mts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { i as IconPack, n as IconComponent, r as IconName, t as ICON_NAMES } from "./types.mjs";
|
|
2
|
+
import { a as getIconPack, c as lucidePack, i as fa6Pack, l as phosphorPack, n as Icons, o as heroPack, r as Youtube, s as isIconName, t as IconPackName, u as resolveIcon } from "./index.mjs";
|
|
3
|
+
export * from "lucide-react";
|
|
4
|
+
export { ICON_NAMES, type IconComponent, type IconName, type IconPack, IconPackName, Icons, Icons as default, Youtube, fa6Pack, getIconPack, heroPack, isIconName, lucidePack, phosphorPack, resolveIcon };
|
package/dist/icon.mjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { i as IconPack, n as IconComponent, r as IconName, t as ICON_NAMES } from "./types.d.mts";
|
|
2
|
+
import { a as getIconPack, c as lucidePack, i as fa6Pack, l as phosphorPack, n as Icons, o as heroPack, r as Youtube, s as isIconName, t as IconPackName, u as resolveIcon } from "./index.d.mts";
|
|
3
|
+
export * from "lucide-react";
|
|
4
|
+
export { ICON_NAMES, IconComponent, IconName, IconPack, IconPackName, Icons, Icons as default, Youtube, fa6Pack, getIconPack, heroPack, isIconName, lucidePack, phosphorPack, resolveIcon };
|