@object-ui/app-shell 6.0.4 → 6.2.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +10 -1
  3. package/dist/console/AppContent.js +24 -2
  4. package/dist/console/ai/AiChatPage.d.ts +8 -0
  5. package/dist/console/ai/AiChatPage.js +188 -0
  6. package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
  7. package/dist/console/ai/ConversationsSidebar.js +111 -0
  8. package/dist/console/auth/LoginPage.js +19 -2
  9. package/dist/console/auth/RegisterPage.js +30 -1
  10. package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
  11. package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
  12. package/dist/console/marketplace/MarketplacePackagePage.js +83 -17
  13. package/dist/console/marketplace/MarketplacePage.js +55 -18
  14. package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
  15. package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
  16. package/dist/console/marketplace/usePackageL10n.js +110 -0
  17. package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
  18. package/dist/console/organizations/OrganizationsPage.js +24 -3
  19. package/dist/context/FavoritesProvider.d.ts +40 -2
  20. package/dist/context/FavoritesProvider.js +201 -20
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/hooks/index.js +1 -0
  23. package/dist/hooks/useChatConversation.d.ts +7 -0
  24. package/dist/hooks/useChatConversation.js +37 -5
  25. package/dist/hooks/useConversationList.d.ts +25 -0
  26. package/dist/hooks/useConversationList.js +131 -0
  27. package/dist/hooks/useNavPins.d.ts +11 -4
  28. package/dist/hooks/useNavPins.js +104 -53
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.js +14 -0
  31. package/dist/layout/AppHeader.js +2 -2
  32. package/dist/layout/AppSidebar.js +20 -1
  33. package/dist/layout/UnifiedSidebar.js +1 -1
  34. package/dist/providers/ExpressionProvider.d.ts +11 -1
  35. package/dist/providers/ExpressionProvider.js +11 -6
  36. package/dist/services/builtinComponents.d.ts +1 -0
  37. package/dist/services/builtinComponents.js +166 -0
  38. package/dist/services/componentRegistry.d.ts +63 -0
  39. package/dist/services/componentRegistry.js +36 -0
  40. package/dist/views/ComponentNavView.d.ts +6 -0
  41. package/dist/views/ComponentNavView.js +26 -0
  42. package/dist/views/RecordDetailView.js +72 -0
  43. package/dist/views/RecordFormPage.js +15 -3
  44. package/dist/views/SearchResultsPage.js +4 -0
  45. package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +58 -0
  46. package/dist/views/metadata-admin/DesignerEditorWrapper.js +140 -0
  47. package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
  48. package/dist/views/metadata-admin/DirectoryPage.js +135 -0
  49. package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
  50. package/dist/views/metadata-admin/LayeredDiff.js +26 -0
  51. package/dist/views/metadata-admin/PageShell.d.ts +34 -0
  52. package/dist/views/metadata-admin/PageShell.js +33 -0
  53. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
  54. package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
  55. package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
  56. package/dist/views/metadata-admin/QuickFind.js +152 -0
  57. package/dist/views/metadata-admin/ResourceEditPage.d.ts +7 -0
  58. package/dist/views/metadata-admin/ResourceEditPage.js +256 -0
  59. package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
  60. package/dist/views/metadata-admin/ResourceHistoryPage.js +97 -0
  61. package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
  62. package/dist/views/metadata-admin/ResourceListPage.js +144 -0
  63. package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
  64. package/dist/views/metadata-admin/ResourceRouter.js +47 -0
  65. package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
  66. package/dist/views/metadata-admin/SchemaForm.js +556 -0
  67. package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
  68. package/dist/views/metadata-admin/default-schemas.js +207 -0
  69. package/dist/views/metadata-admin/i18n.d.ts +33 -0
  70. package/dist/views/metadata-admin/i18n.js +303 -0
  71. package/dist/views/metadata-admin/index.d.ts +31 -0
  72. package/dist/views/metadata-admin/index.js +33 -0
  73. package/dist/views/metadata-admin/predicate.d.ts +31 -0
  74. package/dist/views/metadata-admin/predicate.js +150 -0
  75. package/dist/views/metadata-admin/registry.d.ts +125 -0
  76. package/dist/views/metadata-admin/registry.js +48 -0
  77. package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
  78. package/dist/views/metadata-admin/useMetadata.js +96 -0
  79. package/dist/views/metadata-admin/widgets.d.ts +68 -0
  80. package/dist/views/metadata-admin/widgets.js +287 -0
  81. package/package.json +29 -28
@@ -20,12 +20,14 @@ import { useNavigate, useParams } from 'react-router-dom';
20
20
  import { Card, CardContent, CardHeader, CardTitle, Badge, Button, Skeleton, } from '@object-ui/components';
21
21
  import { ArrowLeft, RefreshCcw, Store, Trash2, AlertCircle, ExternalLink } from 'lucide-react';
22
22
  import { useIsWorkspaceAdmin } from '@object-ui/auth';
23
+ import { useObjectTranslation } from '@object-ui/i18n';
23
24
  import { listLocalInstalls, uninstallLocal, } from './marketplaceApi';
24
25
  import { MarketplaceAccessDenied } from './MarketplaceAccessDenied';
25
26
  export function MarketplaceInstalledPage() {
26
27
  const navigate = useNavigate();
27
28
  const { appName } = useParams();
28
29
  const isAdmin = useIsWorkspaceAdmin();
30
+ const { t, language } = useObjectTranslation();
29
31
  const basePath = appName ? `/apps/${appName}` : '';
30
32
  const [items, setItems] = useState([]);
31
33
  const [loading, setLoading] = useState(true);
@@ -43,7 +45,7 @@ export function MarketplaceInstalledPage() {
43
45
  };
44
46
  useEffect(() => { void load(); }, []);
45
47
  const doUninstall = async (entry) => {
46
- if (!confirm(`Uninstall ${entry.manifestId} v${entry.version} from this runtime?\n\nThe cached manifest will be removed. The app will remain loaded in the running kernel until the next restart.`)) {
48
+ if (!confirm(t('marketplace.uninstall.confirm', { manifestId: entry.manifestId, version: entry.version }))) {
47
49
  return;
48
50
  }
49
51
  setWorking(entry.manifestId);
@@ -52,7 +54,7 @@ export function MarketplaceInstalledPage() {
52
54
  await uninstallLocal(entry.manifestId);
53
55
  setResult({
54
56
  ok: true,
55
- message: `Removed ${entry.manifestId}. Restart the runtime to fully unload it from the running kernel.`,
57
+ message: t('marketplace.uninstall.successInList', { manifestId: entry.manifestId }),
56
58
  });
57
59
  await load();
58
60
  }
@@ -65,5 +67,11 @@ export function MarketplaceInstalledPage() {
65
67
  };
66
68
  if (!isAdmin)
67
69
  return _jsx(MarketplaceAccessDenied, {});
68
- return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-5xl", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Store, { className: "h-6 w-6 text-primary", "aria-hidden": "true" }), _jsx("h1", { className: "text-xl sm:text-2xl font-bold tracking-tight", children: "Installed Apps" })] }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: ["Marketplace packages currently installed into this runtime's kernel. Cached manifests live in ", _jsx("code", { className: "font-mono text-xs", children: ".objectstack/installed-packages/" }), " and survive restarts."] })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading, children: [_jsx(RefreshCcw, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Refresh"] })] }), result && (_jsx("div", { className: `rounded-md border p-3 text-sm ${result.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }), _jsx("div", { children: result.message })] }) })), loading && items.length === 0 ? (_jsx("div", { className: "grid gap-3", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-20 w-full" }, i))) })) : items.length === 0 ? (_jsxs("div", { className: "text-center py-12 text-sm text-muted-foreground border rounded-md", children: [_jsx("p", { children: "No marketplace apps installed in this runtime yet." }), _jsx(Button, { variant: "link", className: "mt-2", onClick: () => navigate(`${basePath}/system/marketplace`), children: "Browse the marketplace \u2192" })] })) : (_jsx("div", { className: "grid gap-3", children: items.map((entry) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-3 pb-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs(CardTitle, { className: "text-base truncate flex items-center gap-2", children: [entry.manifestId, _jsxs(Badge, { variant: "outline", children: ["v", entry.version] })] }), _jsxs("div", { className: "text-xs text-muted-foreground mt-1 flex flex-wrap gap-x-3 gap-y-1", children: [_jsxs("span", { children: ["Installed ", new Date(entry.installedAt).toLocaleString()] }), entry.installedBy && _jsxs("span", { children: ["by ", entry.installedBy] }), _jsxs("span", { children: ["package ", _jsx("code", { className: "font-mono", children: entry.packageId })] })] })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace/${entry.packageId}`), children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Details"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void doUninstall(entry), disabled: working === entry.manifestId, children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), working === entry.manifestId ? 'Uninstalling…' : 'Uninstall'] })] })] }), _jsxs(CardContent, { className: "pt-0 text-xs text-muted-foreground", children: ["Cached as ", _jsxs("code", { className: "font-mono", children: [".objectstack/installed-packages/", entry.manifestId.replace(/[^a-zA-Z0-9._-]/g, '_'), ".json"] })] })] }, entry.manifestId))) })), _jsxs("p", { className: "text-xs text-muted-foreground border-t pt-4", children: [_jsx("strong", { children: "Note:" }), " The kernel API is additive only \u2014 uninstall removes the on-disk manifest so the package won't load on next boot, but the running kernel keeps the app registered until you restart the runtime."] })] }));
70
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-5xl", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), t('marketplace.back')] }), _jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Store, { className: "h-6 w-6 text-primary", "aria-hidden": "true" }), _jsx("h1", { className: "text-xl sm:text-2xl font-bold tracking-tight", children: t('marketplace.installedTitle') })] }), _jsx("p", { className: "text-sm text-muted-foreground mt-1",
71
+ // Subtitle contains an inline <code> path; render translated HTML from our own bundle.
72
+ dangerouslySetInnerHTML: { __html: t('marketplace.installedSubtitle') } })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading, children: [_jsx(RefreshCcw, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), t('marketplace.refresh')] })] }), result && (_jsx("div", { className: `rounded-md border p-3 text-sm ${result.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }), _jsx("div", { children: result.message })] }) })), loading && items.length === 0 ? (_jsx("div", { className: "grid gap-3", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-20 w-full" }, i))) })) : items.length === 0 ? (_jsxs("div", { className: "text-center py-12 text-sm text-muted-foreground border rounded-md", children: [_jsx("p", { children: t('marketplace.installedEmpty') }), _jsx(Button, { variant: "link", className: "mt-2", onClick: () => navigate(`${basePath}/system/marketplace`), children: t('marketplace.browseLink') })] })) : (_jsx("div", { className: "grid gap-3", children: items.map((entry) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-3 pb-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs(CardTitle, { className: "text-base truncate flex items-center gap-2", children: [entry.manifestId, _jsx(Badge, { variant: "outline", children: t('marketplace.versionBadge', { version: entry.version }) })] }), _jsxs("div", { className: "text-xs text-muted-foreground mt-1 flex flex-wrap gap-x-3 gap-y-1", children: [_jsx("span", { children: t('marketplace.installedAt', { when: new Date(entry.installedAt).toLocaleString(language || undefined) }) }), entry.installedBy && _jsx("span", { children: t('marketplace.installedBy', { user: entry.installedBy }) }), _jsxs("span", { children: [t('marketplace.installedPackageId'), " ", _jsx("code", { className: "font-mono", children: entry.packageId })] })] })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace/${entry.packageId}`), children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), t('marketplace.action.details')] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void doUninstall(entry), disabled: working === entry.manifestId, children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), working === entry.manifestId ? t('marketplace.action.uninstalling') : t('marketplace.action.uninstall')] })] })] }), _jsx(CardContent, { className: "pt-0 text-xs text-muted-foreground", dangerouslySetInnerHTML: {
73
+ __html: t('marketplace.cachedAs', {
74
+ path: `.objectstack/installed-packages/${entry.manifestId.replace(/[^a-zA-Z0-9._-]/g, '_')}.json`,
75
+ }),
76
+ } })] }, entry.manifestId))) })), _jsx("p", { className: "text-xs text-muted-foreground border-t pt-4", dangerouslySetInnerHTML: { __html: t('marketplace.installedAdditiveNote') } })] }));
69
77
  }
@@ -10,16 +10,21 @@ import { useNavigate, useParams } from 'react-router-dom';
10
10
  import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Skeleton, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Label, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from '@object-ui/components';
11
11
  import { ArrowLeft, ExternalLink, Download, AlertCircle, Package, Trash2, MoreHorizontal, CheckCircle2 } from 'lucide-react';
12
12
  import { useIsWorkspaceAdmin } from '@object-ui/auth';
13
+ import { useObjectTranslation } from '@object-ui/i18n';
13
14
  import { PackageIcon } from './PackageIcon';
14
15
  import { MarkdownText } from './MarkdownText';
15
16
  import { MarketplaceAccessDenied } from './MarketplaceAccessDenied';
17
+ import { localizePackage } from './usePackageL10n';
16
18
  import { getMarketplacePackage, installPackage, installLocal, uninstallLocal, listLocalInstalls, listCloudEnvironments, listInstallableOrgIds, cloudInstallDeepLink, } from './marketplaceApi';
17
19
  import { getRuntimeConfig } from '../../runtime-config';
20
+ import { useMetadata } from '../../providers/MetadataProvider';
18
21
  export function MarketplacePackagePage() {
19
22
  const navigate = useNavigate();
20
23
  const { packageId, appName } = useParams();
21
24
  const isAdmin = useIsWorkspaceAdmin();
25
+ const { t, language } = useObjectTranslation();
22
26
  const basePath = appName ? `/apps/${appName}` : '';
27
+ const { refresh: refreshMetadata } = useMetadata();
23
28
  const [data, setData] = useState(null);
24
29
  const [loading, setLoading] = useState(true);
25
30
  const [error, setError] = useState(null);
@@ -73,6 +78,18 @@ export function MarketplacePackagePage() {
73
78
  setInstallOpen(true);
74
79
  setInstallResult(null);
75
80
  setEnvsError(null);
81
+ // When the runtime tells us which env we're on (tenant subdomain
82
+ // like demo.objectos.app), skip the picker entirely — the operator's
83
+ // domain already identifies the target. Just open the dialog with
84
+ // the env pre-selected so the sample-data checkbox + Install button
85
+ // remain in place.
86
+ const currentEnvId = getRuntimeConfig().defaultEnvironmentId;
87
+ if (currentEnvId) {
88
+ setEnvsLoading(false);
89
+ setSelectedEnv(currentEnvId);
90
+ setEnvs([]);
91
+ return;
92
+ }
76
93
  setEnvsLoading(true);
77
94
  try {
78
95
  const [list, adminOrgIds] = await Promise.all([
@@ -88,8 +105,7 @@ export function MarketplacePackagePage() {
88
105
  });
89
106
  setEnvs(installable);
90
107
  if (list.length > 0 && installable.length === 0) {
91
- setEnvsError('You do not have permission to install apps in any environment. ' +
92
- 'Only organization owners and admins can install — ask your workspace admin.');
108
+ setEnvsError(t('marketplace.install.noPermission'));
93
109
  }
94
110
  else if (installable.length === 1) {
95
111
  setSelectedEnv(installable[0].id);
@@ -98,7 +114,7 @@ export function MarketplacePackagePage() {
98
114
  catch (e) {
99
115
  const status = e?.status;
100
116
  if (status === 401 || status === 403) {
101
- setEnvsError('You need to sign into ObjectStack Cloud first. Click "Open on cloud" below.');
117
+ setEnvsError(t('marketplace.install.signInFirst'));
102
118
  }
103
119
  else {
104
120
  setEnvsError(e?.message ?? String(e));
@@ -115,7 +131,36 @@ export function MarketplacePackagePage() {
115
131
  setInstallResult(null);
116
132
  try {
117
133
  await installPackage({ packageId, environmentId: selectedEnv, seedSampleData });
118
- setInstallResult({ ok: true, message: 'Installed successfully. Open the environment to see the new app.' });
134
+ // Invalidate the metadata cache so the newly-installed app's
135
+ // objects/views/menus are fetched fresh on next access. Without
136
+ // this the user sees the new app in the switcher (the `app` list
137
+ // gets refreshed) but clicking a menu entry fails with "metadata
138
+ // not found" because objects/views/etc. are still cached from
139
+ // before the install. Only useful when installing into the env
140
+ // currently rendered by this SPA — for cross-env installs the
141
+ // refresh is a harmless no-op (re-fetches the current env's
142
+ // metadata, which is unchanged).
143
+ const currentEnvId = getRuntimeConfig().defaultEnvironmentId;
144
+ if (!currentEnvId || currentEnvId === selectedEnv) {
145
+ try {
146
+ // Drop the persisted `app` cache too — refresh() overwrites it
147
+ // when the new app list comes back, but clearing first protects
148
+ // against partial failures leaving stale data on next reload.
149
+ if (typeof sessionStorage !== 'undefined') {
150
+ for (const key of Object.keys(sessionStorage)) {
151
+ if (key.startsWith('objectui:metadata:')) {
152
+ sessionStorage.removeItem(key);
153
+ }
154
+ }
155
+ }
156
+ await refreshMetadata();
157
+ }
158
+ catch {
159
+ // Non-fatal: install succeeded; worst case the user navigates
160
+ // away and back to pick up the new metadata.
161
+ }
162
+ }
163
+ setInstallResult({ ok: true, message: t('marketplace.install.success') });
119
164
  }
120
165
  catch (e) {
121
166
  setInstallResult({ ok: false, message: e?.message ?? String(e) });
@@ -137,22 +182,31 @@ export function MarketplacePackagePage() {
137
182
  setLocalResult(null);
138
183
  try {
139
184
  const result = await installLocal({ packageId });
185
+ try {
186
+ await refreshMetadata('app');
187
+ }
188
+ catch {
189
+ // Non-fatal: the install succeeded; the app list will refresh on next navigation.
190
+ }
140
191
  setLocalResult({
141
192
  ok: true,
142
- message: `Installed v${result.version} to this runtime. Refresh the console to see "${data?.package?.display_name ?? result.manifestId}" in the app switcher.`,
193
+ message: t('marketplace.install.localSuccess', {
194
+ version: result.version,
195
+ name: data?.package ? localizePackage(data.package, language).displayName : result.manifestId,
196
+ }),
143
197
  });
144
198
  }
145
199
  catch (e) {
146
200
  const code = e?.code;
147
201
  let msg = e?.message ?? String(e);
148
202
  if (code === 'manifest_conflict') {
149
- msg = `${msg}\nTip: a local app already owns this manifest_id. Remove it from objectstack.config.ts first.`;
203
+ msg = t('marketplace.install.localManifestConflict', { message: msg });
150
204
  }
151
205
  else if (code === 'unauthorized') {
152
- msg = 'Sign in to this runtime first, then try again.';
206
+ msg = t('marketplace.install.localUnauthorized');
153
207
  }
154
208
  else if (code === 'marketplace_unavailable') {
155
- msg = 'This runtime has no OS_CLOUD_URL configured, so the marketplace catalog is unreachable.';
209
+ msg = t('marketplace.install.localMarketplaceUnavailable');
156
210
  }
157
211
  setLocalResult({ ok: false, message: msg });
158
212
  }
@@ -169,16 +223,22 @@ export function MarketplacePackagePage() {
169
223
  const doUninstallLocal = async () => {
170
224
  if (!localInstall)
171
225
  return;
172
- if (!confirm(`Uninstall ${localInstall.manifestId} v${localInstall.version} from this runtime?\n\nThe cached manifest will be removed. The app will remain loaded in the running kernel until the next restart.`)) {
226
+ if (!confirm(t('marketplace.uninstall.confirm', { manifestId: localInstall.manifestId, version: localInstall.version }))) {
173
227
  return;
174
228
  }
175
229
  setInstallingLocal(true);
176
230
  setLocalResult(null);
177
231
  try {
178
232
  await uninstallLocal(localInstall.manifestId);
233
+ try {
234
+ await refreshMetadata('app');
235
+ }
236
+ catch {
237
+ // Non-fatal.
238
+ }
179
239
  setLocalResult({
180
240
  ok: true,
181
- message: `Removed cached manifest for ${localInstall.manifestId}. Restart the runtime to fully unload the app from the running kernel.`,
241
+ message: t('marketplace.uninstall.successInDetail', { manifestId: localInstall.manifestId }),
182
242
  });
183
243
  }
184
244
  catch (e) {
@@ -192,9 +252,10 @@ export function MarketplacePackagePage() {
192
252
  return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsx(Skeleton, { className: "h-8 w-32" }), _jsx(Skeleton, { className: "h-16 w-full" }), _jsx(Skeleton, { className: "h-64 w-full" })] }));
193
253
  }
194
254
  if (error || !data) {
195
- return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load package" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error ?? 'Not found.' })] })] })] }));
255
+ return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), t('marketplace.back')] }), _jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: t('marketplace.load.packageFailed') }), _jsx("div", { className: "text-muted-foreground mt-1", children: error ?? t('marketplace.load.notFound') })] })] })] }));
196
256
  }
197
257
  const pkg = data.package;
258
+ const loc = localizePackage(pkg, language);
198
259
  const latestVersion = pkg.latest_version?.version ?? data.versions[0]?.version ?? null;
199
260
  const localInstall = localInstalls.find((i) => i.manifestId === pkg.manifest_id) ?? null;
200
261
  const supportsLocal = getRuntimeConfig().features.installLocal;
@@ -202,18 +263,23 @@ export function MarketplacePackagePage() {
202
263
  const primaryAction = supportsLocal
203
264
  ? {
204
265
  label: installingLocal
205
- ? 'Working…'
266
+ ? t('marketplace.action.working')
206
267
  : localInstall
207
- ? 'Reinstall'
208
- : 'Install',
268
+ ? t('marketplace.action.reinstall')
269
+ : t('marketplace.action.install'),
209
270
  onClick: doInstallLocal,
210
271
  }
211
272
  : {
212
- label: installing ? 'Installing…' : 'Install to cloud…',
273
+ label: installing ? t('marketplace.action.installing') : t('marketplace.action.installToCloud'),
213
274
  onClick: openInstall,
214
275
  };
276
+ const categoryLabel = pkg.category
277
+ ? t(`marketplace.category.${pkg.category}`, { defaultValue: pkg.category })
278
+ : null;
215
279
  if (!isAdmin)
216
280
  return _jsx(MarketplaceAccessDenied, {});
217
- return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start -ml-2 text-muted-foreground hover:text-foreground", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-5 flex-wrap sm:flex-nowrap rounded-2xl border bg-gradient-to-br from-primary/5 via-background to-background p-6 sm:p-8", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id, className: "h-20 w-20 rounded-2xl shadow-sm ring-1 ring-border shrink-0", initialClassName: "text-3xl font-bold" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h1", { className: "text-2xl sm:text-3xl font-bold tracking-tight truncate", children: pkg.display_name || pkg.manifest_id }), pkg.homepage_url && (_jsxs("a", { href: pkg.homepage_url, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors", title: "Homepage", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { className: "hidden sm:inline", children: "Homepage" })] }))] }), _jsxs("div", { className: "text-sm text-muted-foreground mt-2 flex flex-wrap items-center gap-1.5", children: [_jsx("code", { className: "font-mono text-xs px-1.5 py-0.5 rounded bg-muted", children: pkg.manifest_id }), latestVersion && _jsxs(Badge, { variant: "outline", children: ["v", latestVersion] }), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', children: pkg.publisher })), pkg.category && _jsx(Badge, { variant: "outline", children: pkg.category }), pkg.license && _jsx(Badge, { variant: "outline", className: "font-normal", children: pkg.license }), localInstall && (_jsxs(Badge, { variant: "default", className: "bg-green-600 hover:bg-green-600 gap-1", children: [_jsx(CheckCircle2, { className: "h-3 w-3", "aria-hidden": "true" }), "Installed \u00B7 v", localInstall.version] }))] }), pkg.description && (_jsx("p", { className: "text-sm sm:text-base text-foreground/80 mt-3 max-w-2xl leading-relaxed", children: pkg.description }))] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0 self-start", children: [_jsxs(Button, { onClick: primaryAction.onClick, disabled: primaryDisabled, size: "lg", className: "min-w-[8rem]", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), primaryAction.label] }), localInstall && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "lg", className: "px-2.5", "aria-label": "More install options", children: _jsx(MoreHorizontal, { className: "h-4 w-4", "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-56", children: _jsxs(DropdownMenuItem, { onSelect: doUninstallLocal, disabled: installingLocal, className: "text-destructive focus:text-destructive", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2", "aria-hidden": "true" }), "Uninstall from this runtime"] }) })] }))] })] }), localResult && (_jsxs("div", { role: "status", className: `flex items-start gap-2 rounded-md border p-3 text-sm whitespace-pre-wrap ${localResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: [localResult.ok ? _jsx(CheckCircle2, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }) : _jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: localResult.message }), _jsx("button", { type: "button", className: "text-xs underline opacity-60 hover:opacity-100", onClick: () => setLocalResult(null), children: "Dismiss" })] })), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsx("div", { className: "lg:col-span-2 space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "About" }) }), _jsx(CardContent, { children: pkg.readme ? (_jsx(MarkdownText, { source: pkg.readme })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No readme provided." })) })] }) }), _jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Versions" }) }), _jsx(CardContent, { children: data.versions.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No approved versions." })) : (_jsx("ul", { className: "space-y-2", children: data.versions.map((v) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Package, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("code", { className: "font-mono", children: ["v", v.version] }), v.is_prerelease && _jsx(Badge, { variant: "outline", className: "text-xs", children: "pre" })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: v.published_at ? new Date(v.published_at).toLocaleDateString() : '—' })] }, v.id))) })) })] }) })] }), _jsx(Dialog, { open: installOpen, onOpenChange: (o) => { setInstallOpen(o); if (!o)
218
- setInstallResult(null); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Install ", pkg.display_name || pkg.manifest_id] }), _jsx(DialogDescription, { children: "Choose an environment to install this app into. You need to be signed into ObjectStack Cloud." })] }), envsLoading ? (_jsx(Skeleton, { className: "h-10 w-full" })) : envsError ? (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm space-y-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-amber-600", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: envsError })] }), _jsx("a", { href: cloudInstallDeepLink(pkg.id), target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Open on cloud"] }) })] })) : envs.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No environments found in your active organization." })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "env-select", children: "Environment" }), _jsxs(Select, { value: selectedEnv, onValueChange: setSelectedEnv, children: [_jsx(SelectTrigger, { id: "env-select", children: _jsx(SelectValue, { placeholder: "Pick an environment" }) }), _jsx(SelectContent, { children: envs.map((e) => (_jsxs(SelectItem, { value: e.id, children: [e.display_name || e.hostname || e.id, e.plan && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", e.plan] })] }, e.id))) })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "seed", checked: seedSampleData, onCheckedChange: (c) => setSeedSampleData(c === true) }), _jsx(Label, { htmlFor: "seed", className: "text-sm font-normal cursor-pointer", children: "Include sample data" })] })] })), installResult && (_jsx("div", { className: `rounded-md border p-3 text-sm ${installResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: installResult.message })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallOpen(false), children: "Close" }), !envsError && (_jsx(Button, { onClick: doInstall, disabled: !selectedEnv || installing || installResult?.ok === true, children: installing ? 'Installing…' : 'Install' }))] })] }) })] }));
281
+ return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start -ml-2 text-muted-foreground hover:text-foreground", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), t('marketplace.back')] }), _jsxs("div", { className: "flex items-start gap-5 flex-wrap sm:flex-nowrap rounded-2xl border bg-gradient-to-br from-primary/5 via-background to-background p-6 sm:p-8", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: loc.displayName, manifestId: pkg.manifest_id, className: "h-20 w-20 rounded-2xl shadow-sm ring-1 ring-border shrink-0", initialClassName: "text-3xl font-bold" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h1", { className: "text-2xl sm:text-3xl font-bold tracking-tight truncate", children: loc.displayName || pkg.manifest_id }), pkg.homepage_url && (_jsxs("a", { href: pkg.homepage_url, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors", title: t('marketplace.detail.homepage'), children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { className: "hidden sm:inline", children: t('marketplace.detail.homepage') })] }))] }), _jsxs("div", { className: "text-sm text-muted-foreground mt-2 flex flex-wrap items-center gap-1.5", children: [_jsx("code", { className: "font-mono text-xs px-1.5 py-0.5 rounded bg-muted", children: pkg.manifest_id }), latestVersion && _jsx(Badge, { variant: "outline", children: t('marketplace.versionBadge', { version: latestVersion }) }), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', children: pkg.publisher })), categoryLabel && _jsx(Badge, { variant: "outline", children: categoryLabel }), pkg.license && _jsx(Badge, { variant: "outline", className: "font-normal", children: pkg.license }), localInstall && (_jsxs(Badge, { variant: "default", className: "bg-green-600 hover:bg-green-600 gap-1", children: [_jsx(CheckCircle2, { className: "h-3 w-3", "aria-hidden": "true" }), t('marketplace.detail.installedV', { version: localInstall.version })] }))] }), loc.description && (_jsx("p", { className: "text-sm sm:text-base text-foreground/80 mt-3 max-w-2xl leading-relaxed", children: loc.description }))] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0 self-start", children: [_jsxs(Button, { onClick: primaryAction.onClick, disabled: primaryDisabled, size: "lg", className: "min-w-[8rem]", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), primaryAction.label] }), localInstall && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "lg", className: "px-2.5", "aria-label": t('marketplace.detail.moreOptions'), children: _jsx(MoreHorizontal, { className: "h-4 w-4", "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-56", children: _jsxs(DropdownMenuItem, { onSelect: doUninstallLocal, disabled: installingLocal, className: "text-destructive focus:text-destructive", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2", "aria-hidden": "true" }), t('marketplace.detail.uninstallFromRuntime')] }) })] }))] })] }), localResult && (_jsxs("div", { role: "status", className: `flex items-start gap-2 rounded-md border p-3 text-sm whitespace-pre-wrap ${localResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: [localResult.ok ? _jsx(CheckCircle2, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }) : _jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: localResult.message }), _jsx("button", { type: "button", className: "text-xs underline opacity-60 hover:opacity-100", onClick: () => setLocalResult(null), children: t('marketplace.action.dismiss') })] })), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsx("div", { className: "lg:col-span-2 space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: t('marketplace.detail.about') }) }), _jsx(CardContent, { children: loc.readme ? (_jsx(MarkdownText, { source: loc.readme })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: t('marketplace.detail.noReadme') })) })] }) }), _jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: t('marketplace.detail.versions') }) }), _jsx(CardContent, { children: data.versions.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('marketplace.detail.noApprovedVersions') })) : (_jsx("ul", { className: "space-y-2", children: data.versions.map((v) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Package, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("code", { className: "font-mono", children: ["v", v.version] }), v.is_prerelease && _jsx(Badge, { variant: "outline", className: "text-xs", children: t('marketplace.detail.prerelease') })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: v.published_at ? new Date(v.published_at).toLocaleDateString() : '—' })] }, v.id))) })) })] }) })] }), _jsx(Dialog, { open: installOpen, onOpenChange: (o) => { setInstallOpen(o); if (!o)
282
+ setInstallResult(null); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('marketplace.install.dialogTitle', { name: loc.displayName || pkg.manifest_id }) }), _jsx(DialogDescription, { children: getRuntimeConfig().defaultEnvironmentId
283
+ ? t('marketplace.install.dialogDescCurrent', { host: typeof window !== 'undefined' ? window.location.host : '' })
284
+ : t('marketplace.install.dialogDescPicker') })] }), envsLoading ? (_jsx(Skeleton, { className: "h-10 w-full" })) : envsError ? (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm space-y-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-amber-600", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: envsError })] }), _jsx("a", { href: cloudInstallDeepLink(pkg.id), target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), t('marketplace.action.openOnCloud')] }) })] })) : getRuntimeConfig().defaultEnvironmentId ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "seed", checked: seedSampleData, onCheckedChange: (c) => setSeedSampleData(c === true) }), _jsx(Label, { htmlFor: "seed", className: "text-sm font-normal cursor-pointer", children: t('marketplace.install.includeSampleData') })] })) : envs.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('marketplace.install.noEnvs') })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "env-select", children: t('marketplace.install.environment') }), _jsxs(Select, { value: selectedEnv, onValueChange: setSelectedEnv, children: [_jsx(SelectTrigger, { id: "env-select", children: _jsx(SelectValue, { placeholder: t('marketplace.install.environmentPlaceholder') }) }), _jsx(SelectContent, { children: envs.map((e) => (_jsxs(SelectItem, { value: e.id, children: [e.display_name || e.hostname || e.id, e.plan && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", e.plan] })] }, e.id))) })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "seed", checked: seedSampleData, onCheckedChange: (c) => setSeedSampleData(c === true) }), _jsx(Label, { htmlFor: "seed", className: "text-sm font-normal cursor-pointer", children: t('marketplace.install.includeSampleData') })] })] })), installResult && (_jsx("div", { className: `rounded-md border p-3 text-sm ${installResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: installResult.message })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallOpen(false), children: t('marketplace.action.close') }), !envsError && (_jsx(Button, { onClick: doInstall, disabled: !selectedEnv || installing || installResult?.ok === true, children: installing ? t('marketplace.action.installing') : t('marketplace.action.install') }))] })] }) })] }));
219
285
  }
@@ -11,28 +11,52 @@ import { useNavigate, useParams } from 'react-router-dom';
11
11
  import { Card, CardContent, CardHeader, CardTitle, CardDescription, Badge, Input, Button, Skeleton, } from '@object-ui/components';
12
12
  import { Package, Search, RefreshCcw, Store, AlertCircle, CheckCircle2, Settings } from 'lucide-react';
13
13
  import { useIsWorkspaceAdmin } from '@object-ui/auth';
14
+ import { useObjectTranslation } from '@object-ui/i18n';
14
15
  import { PackageIcon } from './PackageIcon';
15
16
  import { MarketplaceAccessDenied } from './MarketplaceAccessDenied';
17
+ import { localizePackage } from './usePackageL10n';
16
18
  import { listMarketplacePackages, listLocalInstalls, } from './marketplaceApi';
17
- function formatRelative(iso) {
18
- if (!iso)
19
- return '';
20
- const d = new Date(iso);
21
- if (Number.isNaN(d.getTime()))
22
- return '';
23
- const days = Math.floor((Date.now() - d.getTime()) / 86400000);
24
- if (days < 1)
25
- return 'today';
26
- if (days < 30)
27
- return `${days}d ago`;
28
- if (days < 365)
29
- return `${Math.floor(days / 30)}mo ago`;
30
- return `${Math.floor(days / 365)}y ago`;
19
+ /**
20
+ * Format a published-at timestamp as a localized relative string.
21
+ *
22
+ * Uses `Intl.RelativeTimeFormat` for proper plural/grammatical rules in
23
+ * the active locale (e.g. "il y a 3 jours", "3 天前"). Falls back to
24
+ * translation keys for environments without RTF support.
25
+ */
26
+ function useRelativeFormatter() {
27
+ const { language, t } = useObjectTranslation();
28
+ return (iso) => {
29
+ if (!iso)
30
+ return '';
31
+ const d = new Date(iso);
32
+ if (Number.isNaN(d.getTime()))
33
+ return '';
34
+ const days = Math.floor((Date.now() - d.getTime()) / 86400000);
35
+ if (days < 1)
36
+ return t('marketplace.relativeTime.today');
37
+ try {
38
+ const rtf = new Intl.RelativeTimeFormat(language || 'en', { numeric: 'auto' });
39
+ if (days < 30)
40
+ return rtf.format(-days, 'day');
41
+ if (days < 365)
42
+ return rtf.format(-Math.floor(days / 30), 'month');
43
+ return rtf.format(-Math.floor(days / 365), 'year');
44
+ }
45
+ catch {
46
+ if (days < 30)
47
+ return t('marketplace.relativeTime.daysAgo', { count: days });
48
+ if (days < 365)
49
+ return t('marketplace.relativeTime.monthsAgo', { count: Math.floor(days / 30) });
50
+ return t('marketplace.relativeTime.yearsAgo', { count: Math.floor(days / 365) });
51
+ }
52
+ };
31
53
  }
32
54
  export function MarketplacePage() {
33
55
  const navigate = useNavigate();
34
56
  const { appName } = useParams();
35
57
  const isAdmin = useIsWorkspaceAdmin();
58
+ const { t, language } = useObjectTranslation();
59
+ const formatRelative = useRelativeFormatter();
36
60
  const basePath = appName ? `/apps/${appName}` : '';
37
61
  const [items, setItems] = useState([]);
38
62
  const [loading, setLoading] = useState(true);
@@ -74,6 +98,13 @@ export function MarketplacePage() {
74
98
  }
75
99
  return Array.from(s).sort();
76
100
  }, [items]);
101
+ // Translate a category enum value via the i18n bundle; fall back to the
102
+ // raw value (lowercased) so unknown publisher categories still render.
103
+ const categoryLabel = (cat) => {
104
+ const key = `marketplace.category.${cat}`;
105
+ const translated = t(key, { defaultValue: cat });
106
+ return translated === key ? cat : translated;
107
+ };
77
108
  const filtered = useMemo(() => {
78
109
  const q = query.trim().toLowerCase();
79
110
  return items.filter((it) => {
@@ -81,19 +112,25 @@ export function MarketplacePage() {
81
112
  return false;
82
113
  if (!q)
83
114
  return true;
84
- const hay = `${it.display_name} ${it.manifest_id} ${it.description ?? ''}`.toLowerCase();
115
+ const loc = localizePackage(it, language);
116
+ const hay = `${loc.displayName} ${it.manifest_id} ${loc.description ?? ''}`.toLowerCase();
85
117
  return hay.includes(q);
86
118
  });
87
- }, [items, query, category]);
119
+ }, [items, query, category, language]);
88
120
  if (!isAdmin)
89
121
  return _jsx(MarketplaceAccessDenied, {});
90
- return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Store, { className: "h-6 w-6 text-primary", "aria-hidden": "true" }), _jsx("h1", { className: "text-xl sm:text-2xl font-bold tracking-tight", children: "App Marketplace" })] }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Browse approved apps published to the ObjectStack catalog. Click an app to view details and install it into one of your environments." })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace/installed`), children: [_jsx(Settings, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Installed", installed.length > 0 ? ` (${installed.length})` : ''] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading, children: [_jsx(RefreshCcw, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Refresh"] })] })] }), _jsxs("div", { className: "flex flex-col sm:flex-row gap-3", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps by name or manifest ID\u2026", className: "pl-9", "aria-label": "Search marketplace apps" })] }), categories.length > 0 && (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx(Button, { size: "sm", variant: category === '' ? 'default' : 'outline', onClick: () => setCategory(''), children: "All" }), categories.map((cat) => (_jsx(Button, { size: "sm", variant: category === cat ? 'default' : 'outline', onClick: () => setCategory(cat), children: cat }, cat)))] }))] }), error && (_jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load marketplace" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error }), _jsxs("div", { className: "text-xs text-muted-foreground mt-2", children: ["By default this runtime points at the public ObjectStack cloud. Check the runtime is online, or override ", _jsx("code", { className: "font-mono", children: "OS_CLOUD_URL" }), " to point at a self-hosted control plane."] })] })] })), loading && items.length === 0 ? (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsx(Skeleton, { className: "h-10 w-10 rounded-lg mb-2" }), _jsx(Skeleton, { className: "h-5 w-3/4" }), _jsx(Skeleton, { className: "h-4 w-1/2 mt-1" })] }), _jsx(CardContent, { children: _jsx(Skeleton, { className: "h-12 w-full" }) })] }, i))) })) : filtered.length === 0 ? (_jsx("div", { className: "text-center py-12 text-sm text-muted-foreground", children: items.length === 0 ? 'No apps have been approved for the marketplace yet.' : 'No apps match your filters.' })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((pkg) => {
122
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Store, { className: "h-6 w-6 text-primary", "aria-hidden": "true" }), _jsx("h1", { className: "text-xl sm:text-2xl font-bold tracking-tight", children: t('marketplace.title') })] }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: t('marketplace.subtitle') })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace/installed`), children: [_jsx(Settings, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), installed.length > 0
123
+ ? t('marketplace.installedCount', { count: installed.length })
124
+ : t('marketplace.installed')] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading, children: [_jsx(RefreshCcw, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), t('marketplace.refresh')] })] })] }), _jsxs("div", { className: "flex flex-col sm:flex-row gap-3", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: t('marketplace.searchPlaceholder'), className: "pl-9", "aria-label": t('marketplace.searchAria') })] }), categories.length > 0 && (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx(Button, { size: "sm", variant: category === '' ? 'default' : 'outline', onClick: () => setCategory(''), children: t('marketplace.all') }), categories.map((cat) => (_jsx(Button, { size: "sm", variant: category === cat ? 'default' : 'outline', onClick: () => setCategory(cat), children: categoryLabel(cat) }, cat)))] }))] }), error && (_jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: t('marketplace.load.failed') }), _jsx("div", { className: "text-muted-foreground mt-1", children: error }), _jsx("div", { className: "text-xs text-muted-foreground mt-2",
125
+ // Hint message contains an inline <code> tag — render translated HTML safely from our own bundle.
126
+ dangerouslySetInnerHTML: { __html: t('marketplace.load.failedHint') } })] })] })), loading && items.length === 0 ? (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsx(Skeleton, { className: "h-10 w-10 rounded-lg mb-2" }), _jsx(Skeleton, { className: "h-5 w-3/4" }), _jsx(Skeleton, { className: "h-4 w-1/2 mt-1" })] }), _jsx(CardContent, { children: _jsx(Skeleton, { className: "h-12 w-full" }) })] }, i))) })) : filtered.length === 0 ? (_jsx("div", { className: "text-center py-12 text-sm text-muted-foreground", children: items.length === 0 ? t('marketplace.noApprovedYet') : t('marketplace.noMatchFilters') })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((pkg) => {
91
127
  const localEntry = installedByManifestId.get(pkg.manifest_id);
128
+ const loc = localizePackage(pkg, language);
92
129
  return (_jsxs(Card, { className: "cursor-pointer transition-colors hover:bg-accent/50 flex flex-col", onClick: () => navigate(`${basePath}/system/marketplace/${pkg.id}`), role: "link", tabIndex: 0, onKeyDown: (e) => {
93
130
  if (e.key === 'Enter' || e.key === ' ') {
94
131
  e.preventDefault();
95
132
  navigate(`${basePath}/system/marketplace/${pkg.id}`);
96
133
  }
97
- }, "data-testid": `marketplace-card-${pkg.manifest_id}`, children: [_jsxs(CardHeader, { className: "flex flex-row items-start gap-3 pb-2", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(CardTitle, { className: "text-base truncate", children: pkg.display_name || pkg.manifest_id }), _jsx(CardDescription, { className: "text-xs truncate", children: _jsx("code", { className: "font-mono", children: pkg.manifest_id }) })] }), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', className: "shrink-0", children: pkg.publisher }))] }), _jsxs(CardContent, { className: "flex-1 flex flex-col gap-2", children: [_jsx("p", { className: "text-sm text-muted-foreground line-clamp-2", children: pkg.description || 'No description provided.' }), _jsxs("div", { className: "flex items-center gap-2 mt-auto pt-2 flex-wrap", children: [localEntry && (_jsxs(Badge, { variant: "default", className: "text-xs bg-green-600 hover:bg-green-600", children: [_jsx(CheckCircle2, { className: "h-3 w-3 mr-1", "aria-hidden": "true" }), "Installed v", localEntry.version] })), pkg.latest_version?.version && (_jsxs(Badge, { variant: "outline", className: "text-xs", children: [_jsx(Package, { className: "h-3 w-3 mr-1", "aria-hidden": "true" }), "v", pkg.latest_version.version] })), pkg.category && (_jsx(Badge, { variant: "outline", className: "text-xs", children: pkg.category })), pkg.latest_version?.published_at && (_jsx("span", { className: "text-xs text-muted-foreground ml-auto", children: formatRelative(pkg.latest_version.published_at) }))] })] })] }, pkg.id));
134
+ }, "data-testid": `marketplace-card-${pkg.manifest_id}`, children: [_jsxs(CardHeader, { className: "flex flex-row items-start gap-3 pb-2", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: loc.displayName, manifestId: pkg.manifest_id }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(CardTitle, { className: "text-base truncate", children: loc.displayName }), _jsx(CardDescription, { className: "text-xs truncate", children: _jsx("code", { className: "font-mono", children: pkg.manifest_id }) })] }), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', className: "shrink-0", children: pkg.publisher }))] }), _jsxs(CardContent, { className: "flex-1 flex flex-col gap-2", children: [_jsx("p", { className: "text-sm text-muted-foreground line-clamp-2", children: loc.description || t('marketplace.noDescription') }), _jsxs("div", { className: "flex items-center gap-2 mt-auto pt-2 flex-wrap", children: [localEntry && (_jsxs(Badge, { variant: "default", className: "text-xs bg-green-600 hover:bg-green-600", children: [_jsx(CheckCircle2, { className: "h-3 w-3 mr-1", "aria-hidden": "true" }), t('marketplace.installedBadge', { version: localEntry.version })] })), pkg.latest_version?.version && (_jsxs(Badge, { variant: "outline", className: "text-xs", children: [_jsx(Package, { className: "h-3 w-3 mr-1", "aria-hidden": "true" }), t('marketplace.versionBadge', { version: pkg.latest_version.version })] })), pkg.category && (_jsx(Badge, { variant: "outline", className: "text-xs", children: categoryLabel(pkg.category) })), pkg.latest_version?.published_at && (_jsx("span", { className: "text-xs text-muted-foreground ml-auto", children: formatRelative(pkg.latest_version.published_at) }))] })] })] }, pkg.id));
98
135
  }) }))] }));
99
136
  }
@@ -12,6 +12,20 @@
12
12
  * cloud/packages/service-cloud/src/routes/marketplace.ts
13
13
  * framework/packages/runtime/src/cloud/marketplace-proxy-plugin.ts
14
14
  */
15
+ /**
16
+ * Per-locale overrides for translatable package fields. Mirrors
17
+ * `PackageTranslation` from @objectstack/spec/cloud — duplicated here
18
+ * to avoid pulling the spec package into the app-shell bundle.
19
+ * See framework/packages/spec/src/cloud/package.zod.ts.
20
+ */
21
+ export interface MarketplacePackageTranslation {
22
+ displayName?: string;
23
+ description?: string;
24
+ readme?: string;
25
+ tagline?: string;
26
+ screenshotCaptions?: Record<string, string>;
27
+ }
28
+ export type MarketplacePackageTranslations = Record<string, MarketplacePackageTranslation>;
15
29
  export interface MarketplacePackageSummary {
16
30
  id: string;
17
31
  manifest_id: string;
@@ -26,6 +40,12 @@ export interface MarketplacePackageSummary {
26
40
  is_starter?: boolean;
27
41
  created_at?: string;
28
42
  updated_at?: string;
43
+ /**
44
+ * Locale-keyed overrides for display_name / description / readme.
45
+ * Optional on the wire: older control planes / older runtimes may not
46
+ * project this column yet. UI should fall back to the base field.
47
+ */
48
+ translations?: MarketplacePackageTranslations | null;
29
49
  latest_version: MarketplacePackageVersion | null;
30
50
  }
31
51
  export interface MarketplacePackageDetail extends MarketplacePackageSummary {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * usePackageL10n
3
+ *
4
+ * Resolves a marketplace package's translatable fields against the
5
+ * current i18n locale. Mirrors the server-side resolver
6
+ * `resolvePackageL10n` in @objectstack/spec/cloud but stays inline here
7
+ * so the app-shell doesn't pull the full spec package into its bundle.
8
+ *
9
+ * Resolution chain (first hit wins):
10
+ * 1. translations[<exact requested locale>] (e.g. `zh-CN`)
11
+ * 2. translations[<language-only locale>] (e.g. `zh`)
12
+ * 3. translations[<fallback locale>] (default `en`)
13
+ * 4. base column on the package row (snake_case from REST)
14
+ */
15
+ import type { MarketplacePackageSummary } from './marketplaceApi';
16
+ export interface LocalizedPackage {
17
+ displayName: string;
18
+ description: string | undefined;
19
+ readme: string | undefined;
20
+ tagline: string | undefined;
21
+ }
22
+ /**
23
+ * Resolve `displayName`, `description`, `readme`, and `tagline` for a
24
+ * package against the active locale. `displayName` is guaranteed to be
25
+ * a non-empty string (falls back to `manifest_id` as a last resort) so
26
+ * callers can render it without further guards.
27
+ */
28
+ export declare function usePackageL10n(pkg: Pick<MarketplacePackageSummary, 'manifest_id' | 'display_name' | 'translations'> & {
29
+ description?: string | null;
30
+ readme?: string | null;
31
+ } | null | undefined): LocalizedPackage;
32
+ /**
33
+ * Stateless variant for use in `.map()` over a list of packages where
34
+ * calling `usePackageL10n` per row would violate the rules of hooks.
35
+ * The caller passes the language they obtained once from
36
+ * `useObjectTranslation()` at the top of the component.
37
+ */
38
+ export declare function localizePackage(pkg: MarketplacePackageSummary, language: string): LocalizedPackage;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * usePackageL10n
3
+ *
4
+ * Resolves a marketplace package's translatable fields against the
5
+ * current i18n locale. Mirrors the server-side resolver
6
+ * `resolvePackageL10n` in @objectstack/spec/cloud but stays inline here
7
+ * so the app-shell doesn't pull the full spec package into its bundle.
8
+ *
9
+ * Resolution chain (first hit wins):
10
+ * 1. translations[<exact requested locale>] (e.g. `zh-CN`)
11
+ * 2. translations[<language-only locale>] (e.g. `zh`)
12
+ * 3. translations[<fallback locale>] (default `en`)
13
+ * 4. base column on the package row (snake_case from REST)
14
+ */
15
+ import { useMemo } from 'react';
16
+ import { useObjectTranslation } from '@object-ui/i18n';
17
+ const FALLBACK_LOCALE = 'en';
18
+ function languageOf(locale) {
19
+ const dash = locale.indexOf('-');
20
+ return dash === -1 ? locale : locale.slice(0, dash);
21
+ }
22
+ function uniqueLocales(codes) {
23
+ const seen = new Set();
24
+ const out = [];
25
+ for (const c of codes) {
26
+ if (!c || seen.has(c))
27
+ continue;
28
+ seen.add(c);
29
+ out.push(c);
30
+ }
31
+ return out;
32
+ }
33
+ function pickFromTranslations(translations, locale, field) {
34
+ if (!translations)
35
+ return undefined;
36
+ const lang = languageOf(locale);
37
+ // Build chain: exact → language-only → any regional variant matching the
38
+ // language (e.g. 'zh' → 'zh-CN' / 'zh-Hans') → fallback. The regional
39
+ // expansion covers the common case where the i18n provider returns a
40
+ // bare language code ('zh') but package manifests ship region-tagged
41
+ // translations ('zh-CN').
42
+ const variants = Object.keys(translations).filter((code) => code === lang || code.startsWith(`${lang}-`));
43
+ const chain = uniqueLocales([locale, lang, ...variants, FALLBACK_LOCALE]);
44
+ for (const code of chain) {
45
+ const entry = translations[code];
46
+ if (!entry)
47
+ continue;
48
+ const value = entry[field];
49
+ if (typeof value === 'string' && value.length > 0)
50
+ return value;
51
+ }
52
+ return undefined;
53
+ }
54
+ function baseColumn(pkg, field) {
55
+ // Map camelCase L10n field → REST snake_case column.
56
+ const SNAKE = {
57
+ displayName: 'display_name',
58
+ description: 'description',
59
+ readme: 'readme',
60
+ tagline: 'tagline',
61
+ // Captions live only inside translations on the wire — no base column.
62
+ screenshotCaptions: 'screenshotCaptions',
63
+ };
64
+ const v = pkg[SNAKE[field]] ?? pkg[field];
65
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
66
+ }
67
+ /**
68
+ * Resolve `displayName`, `description`, `readme`, and `tagline` for a
69
+ * package against the active locale. `displayName` is guaranteed to be
70
+ * a non-empty string (falls back to `manifest_id` as a last resort) so
71
+ * callers can render it without further guards.
72
+ */
73
+ export function usePackageL10n(pkg) {
74
+ const { language } = useObjectTranslation();
75
+ return useMemo(() => {
76
+ if (!pkg) {
77
+ return { displayName: '', description: undefined, readme: undefined, tagline: undefined };
78
+ }
79
+ const locale = language || FALLBACK_LOCALE;
80
+ const displayName = pickFromTranslations(pkg.translations ?? undefined, locale, 'displayName')
81
+ ?? baseColumn(pkg, 'displayName')
82
+ ?? pkg.manifest_id;
83
+ const description = pickFromTranslations(pkg.translations ?? undefined, locale, 'description')
84
+ ?? baseColumn(pkg, 'description');
85
+ const readme = pickFromTranslations(pkg.translations ?? undefined, locale, 'readme')
86
+ ?? baseColumn(pkg, 'readme');
87
+ const tagline = pickFromTranslations(pkg.translations ?? undefined, locale, 'tagline')
88
+ ?? baseColumn(pkg, 'tagline');
89
+ return { displayName, description, readme, tagline };
90
+ }, [pkg, language]);
91
+ }
92
+ /**
93
+ * Stateless variant for use in `.map()` over a list of packages where
94
+ * calling `usePackageL10n` per row would violate the rules of hooks.
95
+ * The caller passes the language they obtained once from
96
+ * `useObjectTranslation()` at the top of the component.
97
+ */
98
+ export function localizePackage(pkg, language) {
99
+ const locale = language || FALLBACK_LOCALE;
100
+ const displayName = pickFromTranslations(pkg.translations ?? undefined, locale, 'displayName')
101
+ ?? baseColumn(pkg, 'displayName')
102
+ ?? pkg.manifest_id;
103
+ const description = pickFromTranslations(pkg.translations ?? undefined, locale, 'description')
104
+ ?? baseColumn(pkg, 'description');
105
+ const readme = pickFromTranslations(pkg.translations ?? undefined, locale, 'readme')
106
+ ?? baseColumn(pkg, 'readme');
107
+ const tagline = pickFromTranslations(pkg.translations ?? undefined, locale, 'tagline')
108
+ ?? baseColumn(pkg, 'tagline');
109
+ return { displayName, description, readme, tagline };
110
+ }