@akinon/ui-shell-dev 1.5.2 → 1.5.4

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/dist/index.html CHANGED
@@ -24,8 +24,8 @@
24
24
  box-sizing: border-box;
25
25
  }
26
26
  </style>
27
- <script type="module" crossorigin src="/assets/index-DHg_jhdM.js"></script>
28
- <link rel="stylesheet" crossorigin href="/assets/index-CX4XmW02.css">
27
+ <script type="module" crossorigin src="/assets/index-DxAcDLRC.js"></script>
28
+ <link rel="stylesheet" crossorigin href="/assets/index-Ds8Za-ns.css">
29
29
  </head>
30
30
  <body>
31
31
  <div id="root"></div>
@@ -0,0 +1,15 @@
1
+ {
2
+ "dashboard": {
3
+ "title": "Welcome",
4
+ "description": "Development environment for testing and integrating your applications."
5
+ },
6
+ "navbar": {
7
+ "dashboard": "Welcome",
8
+ "orders": "Orders",
9
+ "settings": "Settings",
10
+ "products_and_catalogs": "Products & Catalogs",
11
+ "help_and_support": "Help & Support",
12
+ "development_app": "Development App",
13
+ "plugin_test": "Plugin Test"
14
+ }
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "dashboard": {
3
+ "title": "Hoş Geldiniz",
4
+ "description": "Uygulamalarınızı test etmek ve entegre etmek için geliştirme ortamı."
5
+ },
6
+ "navbar": {
7
+ "dashboard": "Hoş Geldiniz",
8
+ "orders": "Siparişler",
9
+ "settings": "Ayarlar",
10
+ "products_and_catalogs": "Ürünler & Kataloglar",
11
+ "help_and_support": "Yardım & Destek",
12
+ "development_app": "Geliştirme Uygulaması",
13
+ "plugin_test": "Eklenti Testi"
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/ui-shell-dev",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "private": false,
5
5
  "description": "Development shell application for Akinon UI Protocol plugins",
6
6
  "type": "module",
@@ -21,11 +21,14 @@
21
21
  "react-dom": "*",
22
22
  "react-router-dom": "^6.28.0",
23
23
  "vite": "*",
24
- "@akinon/app-shell": "1.4.1",
25
- "@akinon/fonts-jost-variable": "2.1.0",
26
- "@akinon/icons": "1.1.0",
27
- "@akinon/ui-react": "1.3.1",
28
- "@akinon/ui-utils": "1.1.0"
24
+ "@akinon/app-shell": "1.4.2",
25
+ "@akinon/fonts-jost-variable": "2.1.1",
26
+ "@akinon/icons": "1.1.1",
27
+ "@akinon/ui-react": "1.3.3",
28
+ "@akinon/ui-utils": "1.1.2",
29
+ "@akinon/ui-hooks": "1.1.1",
30
+ "@akinon/akidate": "1.1.2",
31
+ "@akinon/akilocale": "1.2.1"
29
32
  },
30
33
  "devDependencies": {
31
34
  "@types/cors": "^2.8.17",
@@ -35,7 +38,7 @@
35
38
  "@vitejs/plugin-react": "^5.0.0",
36
39
  "@vitejs/plugin-react-swc": "^3.7.2",
37
40
  "typescript": "*",
38
- "@akinon/typescript-config": "1.1.0"
41
+ "@akinon/typescript-config": "1.1.1"
39
42
  },
40
43
  "engines": {
41
44
  "node": ">=18"
@@ -0,0 +1,15 @@
1
+ {
2
+ "dashboard": {
3
+ "title": "Welcome",
4
+ "description": "Development environment for testing and integrating your applications."
5
+ },
6
+ "navbar": {
7
+ "dashboard": "Welcome",
8
+ "orders": "Orders",
9
+ "settings": "Settings",
10
+ "products_and_catalogs": "Products & Catalogs",
11
+ "help_and_support": "Help & Support",
12
+ "development_app": "Development App",
13
+ "plugin_test": "Plugin Test"
14
+ }
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "dashboard": {
3
+ "title": "Hoş Geldiniz",
4
+ "description": "Uygulamalarınızı test etmek ve entegre etmek için geliştirme ortamı."
5
+ },
6
+ "navbar": {
7
+ "dashboard": "Hoş Geldiniz",
8
+ "orders": "Siparişler",
9
+ "settings": "Ayarlar",
10
+ "products_and_catalogs": "Ürünler & Kataloglar",
11
+ "help_and_support": "Yardım & Destek",
12
+ "development_app": "Geliştirme Uygulaması",
13
+ "plugin_test": "Eklenti Testi"
14
+ }
15
+ }
@@ -1,5 +1,14 @@
1
+ import { akidate } from '@akinon/akidate';
2
+ import { AKILOCALE_STORAGE_KEY } from '@akinon/akilocale';
3
+ import { useTranslation } from '@akinon/akilocale/react';
4
+ import { useAppShell } from '@akinon/app-shell';
5
+ import { useLocalStorage } from '@akinon/ui-hooks';
1
6
  import { Button } from '@akinon/ui-react';
2
- import React from 'react';
7
+ import type { FC } from 'react';
8
+ import React, { useEffect } from 'react';
9
+
10
+ import { ApplicationLocales } from '../constants/locales';
11
+ import { LocaleChanger } from './LocaleChanger';
3
12
 
4
13
  // Get version from package.json via Vite environment variable
5
14
  const getVersion = (): string => {
@@ -11,10 +20,29 @@ interface HeaderProps {
11
20
  onToggleRightSider: () => void;
12
21
  }
13
22
 
14
- export const Header: React.FC<HeaderProps> = ({
23
+ export const Header: FC<HeaderProps> = ({
15
24
  onToggleLeftSider,
16
25
  onToggleRightSider
17
26
  }) => {
27
+ const { i18n } = useTranslation();
28
+ const { updateLocale } = useAppShell();
29
+ const [, setLocale] = useLocalStorage<string | null>(
30
+ AKILOCALE_STORAGE_KEY,
31
+ null,
32
+ { raw: true }
33
+ );
34
+
35
+ const handleLocaleChange = (locale: string) => {
36
+ i18n.changeLanguage(locale);
37
+ akidate.setLocale(locale);
38
+ setLocale(locale);
39
+ updateLocale(locale);
40
+ };
41
+
42
+ useEffect(() => {
43
+ updateLocale(i18n.language);
44
+ }, [updateLocale, i18n.language]);
45
+
18
46
  return (
19
47
  <>
20
48
  <div className="flex items-center">
@@ -50,6 +78,11 @@ export const Header: React.FC<HeaderProps> = ({
50
78
  </div>
51
79
 
52
80
  <div className="flex items-center">
81
+ <LocaleChanger
82
+ currentLocale={i18n.language}
83
+ locales={ApplicationLocales}
84
+ onLocaleChange={handleLocaleChange}
85
+ />
53
86
  <Button
54
87
  type="text"
55
88
  icon="siralama"
@@ -0,0 +1,42 @@
1
+ import { Button, Dropdown } from '@akinon/ui-react';
2
+ import type { FC } from 'react';
3
+ import React, { useMemo } from 'react';
4
+
5
+ import type { LocaleChangerProps } from '../types';
6
+
7
+ export const LocaleChanger: FC<LocaleChangerProps> = ({
8
+ currentLocale,
9
+ locales,
10
+ onLocaleChange
11
+ }) => {
12
+ const handleLocaleChange = (locale: string) => () => {
13
+ onLocaleChange(locale);
14
+ };
15
+
16
+ const items = useMemo(
17
+ () =>
18
+ Object.keys(locales).map((key: string) => ({
19
+ key: `locale-${key}`,
20
+ label: locales[key].name,
21
+ onClick: handleLocaleChange(key)
22
+ })),
23
+ [locales]
24
+ );
25
+
26
+ return (
27
+ <div className="header-locale">
28
+ <Dropdown
29
+ menu={{
30
+ items
31
+ }}
32
+ trigger={['click']}
33
+ placement="bottomRight"
34
+ arrow
35
+ >
36
+ <Button type="text" className="flex items-center">
37
+ {locales[currentLocale]?.name ?? currentLocale}
38
+ </Button>
39
+ </Dropdown>
40
+ </div>
41
+ );
42
+ };
@@ -1,3 +1,4 @@
1
+ import { useTranslation } from '@akinon/akilocale/react';
1
2
  import { useAppShell } from '@akinon/app-shell';
2
3
  import { IMenuProps, Menu, TMenuItemType } from '@akinon/ui-react';
3
4
  import { useEffect, useMemo, useState } from 'react';
@@ -13,17 +14,18 @@ import {
13
14
 
14
15
  export const Navigation: React.FC = () => {
15
16
  const { apps, configs } = useAppShell();
17
+ const { t } = useTranslation();
16
18
  const location = useLocation();
17
19
  const navigate = useNavigate();
18
20
  const [selectedKey, setSelectedKey] = useState('/');
19
21
  const [openKeys, setOpenKeys] = useState<string[]>([]);
20
- const items = getStaticNavigationItems();
21
- const itemsBottom = getStaticNavigationItemsBottom();
22
+ const items = getStaticNavigationItems(t);
23
+ const itemsBottom = getStaticNavigationItemsBottom(t);
22
24
  const fullpageItems = getFullpageNavigationItems(
23
25
  apps,
24
26
  configs as Map<number, unknown>
25
27
  );
26
- const pluginItems = getPluginNavigationItems(apps);
28
+ const pluginItems = getPluginNavigationItems(apps, t);
27
29
  const navigationItems = useMemo(
28
30
  () => [...items, ...fullpageItems, ...pluginItems, ...itemsBottom],
29
31
  [items, fullpageItems, pluginItems, itemsBottom]
@@ -0,0 +1,12 @@
1
+ export const ApplicationLocales = {
2
+ en: {
3
+ locale: 'en',
4
+ isoLocale: 'en-us',
5
+ name: 'English'
6
+ },
7
+ tr: {
8
+ locale: 'tr',
9
+ isoLocale: 'tr-tr',
10
+ name: 'Türkçe'
11
+ }
12
+ };
package/src/main.tsx CHANGED
@@ -2,12 +2,20 @@ import '@akinon/fonts-jost-variable';
2
2
  import '@akinon/ui-utils';
3
3
  import './global.scss';
4
4
 
5
+ import { akidate } from '@akinon/akidate';
6
+ import { AKILOCALE_STORAGE_KEY } from '@akinon/akilocale';
7
+ import { i18n } from '@akinon/akilocale/react';
5
8
  import React from 'react';
6
9
  import ReactDOM from 'react-dom/client';
7
10
 
8
11
  import { ShellApp } from './shell-app';
9
12
  import type { ShellConfig } from './types';
10
13
 
14
+ const initialLocale = localStorage.getItem(AKILOCALE_STORAGE_KEY) || 'en';
15
+
16
+ i18n.changeLanguage(initialLocale);
17
+ akidate.setLocale(initialLocale);
18
+
11
19
  // Get configuration injected by the server
12
20
  const config: ShellConfig = (
13
21
  window as unknown as { __SHELL_CONFIG__?: ShellConfig }
@@ -1,5 +1,6 @@
1
1
  import './pages.scss';
2
2
 
3
+ import { useTranslation } from '@akinon/akilocale/react';
3
4
  import React from 'react';
4
5
 
5
6
  import { PageContent } from '../components/PageContent';
@@ -7,12 +8,14 @@ import { PageHeading } from '../components/PageHeading';
7
8
  import { WelcomePage } from '../components/WelcomePage';
8
9
 
9
10
  export const PageDashboard: React.FC = () => {
11
+ const { t } = useTranslation();
12
+
10
13
  return (
11
14
  <div className="page-dashboard">
12
15
  <div className="page-heading-container">
13
16
  <PageHeading
14
- title="Welcome"
15
- description="Development environment for testing and integrating your applications"
17
+ title={t('dashboard.title')}
18
+ description={t('dashboard.description')}
16
19
  />
17
20
  </div>
18
21
  <div className="page-content-container">
@@ -11,7 +11,11 @@ export const PageExternal: React.FC<{ id: number; path?: string }> = ({
11
11
  const location = useLocation();
12
12
  // Compute splat (unmatched) path after the configured base `path`.
13
13
  // Example: base "/keywords" and current pathname "/external/app/keywords/5/edit" -> splat "/5/edit".
14
- const basePath = path ?? '';
14
+ const basePathRaw = path ?? '';
15
+ const basePath =
16
+ basePathRaw && !basePathRaw.startsWith('/')
17
+ ? `/${basePathRaw}`
18
+ : basePathRaw;
15
19
  const pathname = location.pathname;
16
20
  const indexOfBase = pathname.indexOf(basePath);
17
21
  const splat =
package/src/providers.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import { useTranslation } from '@akinon/akilocale/react';
1
2
  import type { ApplicationActions, RegisteredApp } from '@akinon/app-shell';
2
3
  import { AppShellProvider, type ShellNavigation } from '@akinon/app-shell';
3
4
  import { notification } from '@akinon/ui-react';
@@ -15,6 +16,7 @@ export const Providers: React.FC<ProvidersProps> = ({ config, children }) => {
15
16
  const [api, contextHolder] = notification.useNotification();
16
17
  const [searchParams] = useSearchParams();
17
18
  const navigate = useNavigate();
19
+ const { i18n } = useTranslation();
18
20
 
19
21
  // Convert shell config to apps format for AppShellProvider
20
22
  const apps: RegisteredApp[] = config.apps.map((app, index) => ({
@@ -37,7 +39,7 @@ export const Providers: React.FC<ProvidersProps> = ({ config, children }) => {
37
39
  };
38
40
 
39
41
  const data = {
40
- lng: 'en',
42
+ lng: i18n.language,
41
43
  username: 'Developer',
42
44
  searchParams: Object.fromEntries([...searchParams]),
43
45
  ...(config.data || {})
@@ -86,7 +86,9 @@ export class ShellDevServer {
86
86
  configFile: path.resolve(shellRoot, 'vite.config.ts'),
87
87
  server: {
88
88
  middlewareMode: true,
89
- cors: true
89
+ cors: true,
90
+ port: this.config.shell.port,
91
+ strictPort: false
90
92
  },
91
93
  appType: 'spa'
92
94
  });
package/src/types.ts CHANGED
@@ -44,3 +44,15 @@ export interface ThemeConfig {
44
44
  }
45
45
 
46
46
  export type ViewType = 'welcome' | 'app' | 'plugin-test';
47
+
48
+ export interface LocaleOption {
49
+ locale: string;
50
+ isoLocale: string;
51
+ name: string;
52
+ }
53
+
54
+ export interface LocaleChangerProps {
55
+ currentLocale: string;
56
+ locales: Record<string, LocaleOption>;
57
+ onLocaleChange: (locale: string) => void;
58
+ }
@@ -1,55 +1,76 @@
1
1
  import type { RegisteredApp } from '@akinon/app-shell';
2
2
  import type { TMenuItemType } from '@akinon/ui-react';
3
3
 
4
- const STATIC_NAVIGATION_ITEMS: TMenuItemType[] = [
5
- {
6
- key: '/',
7
- label: 'Welcome',
8
- icon: 'dashboard'
9
- },
10
- {
11
- key: '/products-and-catalogs',
12
- label: 'Products & Catalogs',
13
- icon: 'fiyatlistesi',
14
- disabled: true
15
- },
16
- {
17
- key: '/orders',
18
- label: 'Orders',
19
- icon: 'siparis',
20
- disabled: true
21
- }
22
- ];
4
+ type TranslateFunction = (key: string) => string;
23
5
 
24
- const STATIC_NAVIGATION_ITEMS_BOTTOM: TMenuItemType[] = [
25
- {
26
- key: '/placeholder-2',
27
- label: 'Help & Support',
28
- icon: 'bilgilendirme_servis',
29
- disabled: true
30
- },
31
- {
32
- key: '/placeholder-1',
33
- label: 'Settings',
34
- icon: 'ayarlar',
35
- disabled: true
36
- }
37
- ];
6
+ interface LocaleLabelMap {
7
+ [locale: string]: string;
8
+ }
38
9
 
39
- const PLUGIN_NAVIGATION_ITEMS: TMenuItemType[] = [
40
- {
41
- key: '/plugin-test',
42
- label: 'Plugin Test',
43
- icon: 'box'
10
+ function resolveLabel(label: unknown, locale: string): string {
11
+ if (typeof label === 'string') return label;
12
+ if (
13
+ label &&
14
+ typeof label === 'object' &&
15
+ !Array.isArray(label) &&
16
+ typeof (label as LocaleLabelMap)[locale] === 'string'
17
+ )
18
+ return (label as LocaleLabelMap)[locale];
19
+ // Fallbacks: try common keys, then stringify-safe default
20
+ const map = label as LocaleLabelMap | undefined;
21
+ if (map) {
22
+ if (typeof map.en === 'string') return map.en;
23
+ if (typeof map.tr === 'string') return map.tr;
44
24
  }
45
- ];
25
+ return '';
26
+ }
27
+
28
+ function ensureLeadingSlash(path: string): string {
29
+ if (!path) return '/';
30
+ return path.startsWith('/') ? path : `/${path}`;
31
+ }
46
32
 
47
- export const getStaticNavigationItems = () => {
48
- return STATIC_NAVIGATION_ITEMS;
33
+ export const getStaticNavigationItems = (
34
+ t: TranslateFunction
35
+ ): TMenuItemType[] => {
36
+ return [
37
+ {
38
+ key: '/',
39
+ label: t('navbar.dashboard'),
40
+ icon: 'dashboard'
41
+ },
42
+ {
43
+ key: '/products-and-catalogs',
44
+ label: t('navbar.products_and_catalogs'),
45
+ icon: 'fiyatlistesi',
46
+ disabled: true
47
+ },
48
+ {
49
+ key: '/orders',
50
+ label: t('navbar.orders'),
51
+ icon: 'siparis',
52
+ disabled: true
53
+ }
54
+ ];
49
55
  };
50
56
 
51
- export const getStaticNavigationItemsBottom = () => {
52
- return STATIC_NAVIGATION_ITEMS_BOTTOM;
57
+ export const getStaticNavigationItemsBottom = (
58
+ t: TranslateFunction
59
+ ): TMenuItemType[] => {
60
+ return [
61
+ {
62
+ key: '/placeholder-2',
63
+ label: t('navbar.help_and_support'),
64
+ icon: 'bilgilendirme_servis',
65
+ disabled: true
66
+ },
67
+ {
68
+ key: '/placeholder-1',
69
+ label: t('navbar.settings'),
70
+ icon: 'ayarlar',
71
+ disabled: true
72
+ }
73
+ ];
53
74
  };
54
75
 
55
76
  export const getFullpageNavigationItems = (
@@ -59,36 +80,48 @@ export const getFullpageNavigationItems = (
59
80
  return apps
60
81
  .filter(app => app.type === 'full_page')
61
82
  .map((app): TMenuItemType => {
83
+ const appConfig = configs.get(app.id) as { menu?: unknown[] } | undefined;
84
+
85
+ const children = appConfig?.menu
86
+ ?.map((sub: unknown): TMenuItemType | false => {
87
+ const { child, path, label } = (sub || {}) as {
88
+ child?: unknown;
89
+ path?: string;
90
+ label?: unknown;
91
+ };
92
+ if (child || !path) return false;
93
+ const normalizedPath = ensureLeadingSlash(path);
94
+
95
+ return {
96
+ key: `/external/${app.slug}${normalizedPath}`,
97
+ label: resolveLabel(label, 'en') || undefined,
98
+ icon: 'eksi'
99
+ } as TMenuItemType;
100
+ })
101
+ .filter(Boolean) as TMenuItemType[] | undefined;
102
+
62
103
  return {
63
104
  key: `/external/${app.slug}`,
64
- label: app.name,
105
+ label: resolveLabel(app.name as unknown, 'en') || app.name,
65
106
  icon: 'stoklistesi',
66
- children: (
67
- configs.get(app.id) as {
68
- menu: TMenuItemType[];
69
- }
70
- )?.menu
71
- ?.map((sub: unknown): TMenuItemType | false => {
72
- const { child, path, label } = sub as {
73
- child: unknown;
74
- path: string;
75
- label: string;
76
- };
77
- if (child) return false;
78
-
79
- return {
80
- key: `/external/${app.slug}${path}`,
81
- label: label,
82
- icon: 'eksi'
83
- };
84
- })
85
- .filter(Boolean) as TMenuItemType[]
86
- };
107
+ children
108
+ } as TMenuItemType;
87
109
  });
88
110
  };
89
111
 
90
- export const getPluginNavigationItems = (apps: RegisteredApp[]) => {
112
+ export const getPluginNavigationItems = (
113
+ apps: RegisteredApp[],
114
+ t: TranslateFunction
115
+ ): TMenuItemType[] => {
91
116
  // Only show plugin test page if there are plugin type apps
92
117
  const hasPluginApps = apps.some(app => app.type === 'plugin');
93
- return hasPluginApps ? PLUGIN_NAVIGATION_ITEMS : [];
118
+ return hasPluginApps
119
+ ? [
120
+ {
121
+ key: '/plugin-test',
122
+ label: t('navbar.plugin_test'),
123
+ icon: 'box'
124
+ }
125
+ ]
126
+ : [];
94
127
  };