ikanban-web 0.2.2 → 0.2.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/README.md +0 -17
- package/bin/cli.js +32 -2
- package/dist/assets/{ghostty-web-DC9h7yp6.js → ghostty-web-jGvmQkEo.js} +1 -1
- package/dist/assets/{home-B2bEEt-Q.js → home-CvPou5Jd.js} +1 -1
- package/dist/assets/{index-CLdyKsqF.css → index-CitLv2ln.css} +1 -1
- package/dist/assets/index-HcsA-1S9.js +2413 -0
- package/dist/assets/session-DXwXMH7X.js +24 -0
- package/dist/index.html +2 -2
- package/package.json +1 -6
- package/src/components/dialog-select-model.tsx +43 -6
- package/src/components/prompt-input.tsx +3 -9
- package/src/context/language.tsx +77 -199
- package/src/i18n/en.ts +1 -0
- package/src/pages/session/use-session-commands.tsx +226 -180
- package/dist/assets/index-BM0fG0_S.js +0 -2443
- package/dist/assets/session-CFBQK6ra.js +0 -24
package/dist/index.html
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
<meta property="og:image" content="/social-share.png" />
|
|
15
15
|
<meta property="twitter:image" content="/social-share.png" />
|
|
16
16
|
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
|
|
17
|
-
<script type="module" crossorigin src="/assets/index-
|
|
18
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
17
|
+
<script type="module" crossorigin src="/assets/index-HcsA-1S9.js"></script>
|
|
18
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CitLv2ln.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
|
21
21
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ikanban-web",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "iKanban web app",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -24,10 +24,6 @@
|
|
|
24
24
|
"test": "bun run test:unit",
|
|
25
25
|
"test:unit": "bun test --preload ./happydom.ts ./src",
|
|
26
26
|
"test:unit:watch": "bun test --watch --preload ./happydom.ts ./src",
|
|
27
|
-
"test:e2e": "playwright test",
|
|
28
|
-
"test:e2e:local": "bun script/e2e-local.ts",
|
|
29
|
-
"test:e2e:ui": "playwright test --ui",
|
|
30
|
-
"test:e2e:report": "playwright show-report e2e/playwright-report",
|
|
31
27
|
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js",
|
|
32
28
|
"build:watch": "vite build --watch"
|
|
33
29
|
},
|
|
@@ -64,7 +60,6 @@
|
|
|
64
60
|
},
|
|
65
61
|
"devDependencies": {
|
|
66
62
|
"@happy-dom/global-registrator": "20.8.3",
|
|
67
|
-
"@playwright/test": "1.58.2",
|
|
68
63
|
"@tailwindcss/vite": "4.2.1",
|
|
69
64
|
"@tsconfig/bun": "1.0.10",
|
|
70
65
|
"@types/bun": "1.3.10",
|
|
@@ -18,6 +18,12 @@ import { useLanguage } from "@/context/language"
|
|
|
18
18
|
const isFree = (provider: string, cost: { input: number } | undefined) =>
|
|
19
19
|
provider === "opencode" && (!cost || cost.input === 0)
|
|
20
20
|
|
|
21
|
+
type ModelItem = ReturnType<ReturnType<typeof useLocal>["model"]["list"]>[number] & {
|
|
22
|
+
_recentKey?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const RECENT_GROUP = "\u0000recent"
|
|
26
|
+
|
|
21
27
|
const ModelList: Component<{
|
|
22
28
|
provider?: string
|
|
23
29
|
class?: string
|
|
@@ -27,31 +33,62 @@ const ModelList: Component<{
|
|
|
27
33
|
const local = useLocal()
|
|
28
34
|
const language = useLanguage()
|
|
29
35
|
|
|
30
|
-
const
|
|
36
|
+
const visibleModels = createMemo(() =>
|
|
31
37
|
local.model
|
|
32
38
|
.list()
|
|
33
39
|
.filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id }))
|
|
34
40
|
.filter((m) => (props.provider ? m.provider.id === props.provider : true)),
|
|
35
41
|
)
|
|
36
42
|
|
|
43
|
+
const recentModels = createMemo(() =>
|
|
44
|
+
local.model
|
|
45
|
+
.recent()
|
|
46
|
+
.filter((m) => m && local.model.visible({ modelID: m.id, providerID: m.provider.id }))
|
|
47
|
+
.filter((m) => m && (props.provider ? m.provider.id === props.provider : true)) as NonNullable<
|
|
48
|
+
ReturnType<ReturnType<typeof useLocal>["model"]["recent"]>[number]
|
|
49
|
+
>[],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const items = (filter: string): ModelItem[] => {
|
|
53
|
+
const visible = visibleModels()
|
|
54
|
+
if (filter.trim()) return visible
|
|
55
|
+
const recent = recentModels()
|
|
56
|
+
if (recent.length === 0) return visible
|
|
57
|
+
const recentKeys = new Set(recent.map((m) => `${m.provider.id}:${m.id}`))
|
|
58
|
+
const nonRecentVisible = visible.filter((m) => !recentKeys.has(`${m.provider.id}:${m.id}`))
|
|
59
|
+
const recentItems: ModelItem[] = recent.map((m) => ({ ...m, _recentKey: `${m.provider.id}:${m.id}` }))
|
|
60
|
+
return [...recentItems, ...nonRecentVisible]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const recentGroupLabel = () => language.t("dialog.model.recent")
|
|
64
|
+
|
|
37
65
|
return (
|
|
38
66
|
<List
|
|
39
67
|
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
|
|
40
68
|
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true, action: props.action }}
|
|
41
69
|
emptyMessage={language.t("dialog.model.empty")}
|
|
42
|
-
key={(x) => `${x.provider.id}:${x.id}`}
|
|
43
|
-
items={
|
|
44
|
-
current={local.model.current()}
|
|
70
|
+
key={(x) => (x._recentKey ? `${RECENT_GROUP}:${x._recentKey}` : `${x.provider.id}:${x.id}`)}
|
|
71
|
+
items={items}
|
|
72
|
+
current={local.model.current() as ModelItem | undefined}
|
|
45
73
|
filterKeys={["provider.name", "name", "id"]}
|
|
46
|
-
sortBy={(a, b) =>
|
|
47
|
-
|
|
74
|
+
sortBy={(a, b) => {
|
|
75
|
+
if (a._recentKey && b._recentKey) return 0
|
|
76
|
+
if (a._recentKey || b._recentKey) return 0
|
|
77
|
+
return a.name.localeCompare(b.name)
|
|
78
|
+
}}
|
|
79
|
+
groupBy={(x) => (x._recentKey ? RECENT_GROUP : x.provider.name)}
|
|
48
80
|
sortGroupsBy={(a, b) => {
|
|
81
|
+
if (a.category === RECENT_GROUP) return -1
|
|
82
|
+
if (b.category === RECENT_GROUP) return 1
|
|
49
83
|
const aProvider = a.items[0].provider.id
|
|
50
84
|
const bProvider = b.items[0].provider.id
|
|
51
85
|
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
|
52
86
|
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
|
53
87
|
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
|
54
88
|
}}
|
|
89
|
+
groupHeader={(group) =>
|
|
90
|
+
group.category === RECENT_GROUP ? recentGroupLabel() : group.category
|
|
91
|
+
}
|
|
55
92
|
itemWrapper={(item, node) => (
|
|
56
93
|
<Tooltip
|
|
57
94
|
class="w-full"
|
|
@@ -400,6 +400,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
400
400
|
|
|
401
401
|
const isFocused = createFocusSignal(() => editorRef)
|
|
402
402
|
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
|
|
403
|
+
const stopKeyLabel = () => (platform.platform === "desktop" && platform.os === "macos" ? "Cmd+C" : "Ctrl+C")
|
|
403
404
|
|
|
404
405
|
const pick = () => fileInputRef?.click()
|
|
405
406
|
|
|
@@ -1037,13 +1038,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
1037
1038
|
return
|
|
1038
1039
|
}
|
|
1039
1040
|
|
|
1040
|
-
if (working()) {
|
|
1041
|
-
abort()
|
|
1042
|
-
event.preventDefault()
|
|
1043
|
-
event.stopPropagation()
|
|
1044
|
-
return
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
1041
|
if (escBlur()) {
|
|
1048
1042
|
editorRef.blur()
|
|
1049
1043
|
event.preventDefault()
|
|
@@ -1097,7 +1091,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
1097
1091
|
}
|
|
1098
1092
|
}
|
|
1099
1093
|
|
|
1100
|
-
if (ctrl && event.code === "KeyG") {
|
|
1094
|
+
if (ctrl && (event.code === "KeyG" || event.code === "KeyC")) {
|
|
1101
1095
|
if (store.popover) {
|
|
1102
1096
|
closePopover()
|
|
1103
1097
|
event.preventDefault()
|
|
@@ -1290,7 +1284,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
1290
1284
|
<Match when={working()}>
|
|
1291
1285
|
<div class="flex items-center gap-2">
|
|
1292
1286
|
<span>{language.t("prompt.action.stop")}</span>
|
|
1293
|
-
<span class="text-icon-base text-12-medium text-[10px]!">{
|
|
1287
|
+
<span class="text-icon-base text-12-medium text-[10px]!">{stopKeyLabel()}</span>
|
|
1294
1288
|
</div>
|
|
1295
1289
|
</Match>
|
|
1296
1290
|
<Match when={true}>
|
package/src/context/language.tsx
CHANGED
|
@@ -1,224 +1,102 @@
|
|
|
1
|
-
import * as i18n from "@solid-primitives/i18n"
|
|
2
|
-
import { createEffect, createMemo } from "solid-js"
|
|
3
|
-
import { createStore } from "solid-js/store"
|
|
4
|
-
import { createSimpleContext } from "ikanban-ui/context"
|
|
5
|
-
import { Persist, persisted } from "@/utils/persist"
|
|
6
|
-
import { dict as en } from "@/i18n/en"
|
|
7
|
-
import { dict as zh } from "@/i18n/zh"
|
|
8
|
-
import { dict as
|
|
9
|
-
import { dict as
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import { dict as pl } from "@/i18n/pl"
|
|
16
|
-
import { dict as ru } from "@/i18n/ru"
|
|
17
|
-
import { dict as ar } from "@/i18n/ar"
|
|
18
|
-
import { dict as no } from "@/i18n/no"
|
|
19
|
-
import { dict as br } from "@/i18n/br"
|
|
20
|
-
import { dict as th } from "@/i18n/th"
|
|
21
|
-
import { dict as bs } from "@/i18n/bs"
|
|
22
|
-
import { dict as tr } from "@/i18n/tr"
|
|
23
|
-
import { dict as uiEn } from "ikanban-ui/i18n/en"
|
|
24
|
-
import { dict as uiZh } from "ikanban-ui/i18n/zh"
|
|
25
|
-
import { dict as uiZht } from "ikanban-ui/i18n/zht"
|
|
26
|
-
import { dict as uiKo } from "ikanban-ui/i18n/ko"
|
|
27
|
-
import { dict as uiDe } from "ikanban-ui/i18n/de"
|
|
28
|
-
import { dict as uiEs } from "ikanban-ui/i18n/es"
|
|
29
|
-
import { dict as uiFr } from "ikanban-ui/i18n/fr"
|
|
30
|
-
import { dict as uiDa } from "ikanban-ui/i18n/da"
|
|
31
|
-
import { dict as uiJa } from "ikanban-ui/i18n/ja"
|
|
32
|
-
import { dict as uiPl } from "ikanban-ui/i18n/pl"
|
|
33
|
-
import { dict as uiRu } from "ikanban-ui/i18n/ru"
|
|
34
|
-
import { dict as uiAr } from "ikanban-ui/i18n/ar"
|
|
35
|
-
import { dict as uiNo } from "ikanban-ui/i18n/no"
|
|
36
|
-
import { dict as uiBr } from "ikanban-ui/i18n/br"
|
|
37
|
-
import { dict as uiTh } from "ikanban-ui/i18n/th"
|
|
38
|
-
import { dict as uiBs } from "ikanban-ui/i18n/bs"
|
|
39
|
-
import { dict as uiTr } from "ikanban-ui/i18n/tr"
|
|
40
|
-
|
|
41
|
-
export type Locale =
|
|
42
|
-
| "en"
|
|
43
|
-
| "zh"
|
|
44
|
-
| "zht"
|
|
45
|
-
| "ko"
|
|
46
|
-
| "de"
|
|
47
|
-
| "es"
|
|
48
|
-
| "fr"
|
|
49
|
-
| "da"
|
|
50
|
-
| "ja"
|
|
51
|
-
| "pl"
|
|
52
|
-
| "ru"
|
|
53
|
-
| "ar"
|
|
54
|
-
| "no"
|
|
55
|
-
| "br"
|
|
56
|
-
| "th"
|
|
57
|
-
| "bs"
|
|
58
|
-
| "tr"
|
|
59
|
-
|
|
60
|
-
type RawDictionary = typeof en & typeof uiEn
|
|
61
|
-
type Dictionary = i18n.Flatten<RawDictionary>
|
|
1
|
+
import * as i18n from "@solid-primitives/i18n";
|
|
2
|
+
import { createEffect, createMemo } from "solid-js";
|
|
3
|
+
import { createStore } from "solid-js/store";
|
|
4
|
+
import { createSimpleContext } from "ikanban-ui/context";
|
|
5
|
+
import { Persist, persisted } from "@/utils/persist";
|
|
6
|
+
import { dict as en } from "@/i18n/en";
|
|
7
|
+
import { dict as zh } from "@/i18n/zh";
|
|
8
|
+
import { dict as uiEn } from "ikanban-ui/i18n/en";
|
|
9
|
+
import { dict as uiZh } from "ikanban-ui/i18n/zh";
|
|
10
|
+
|
|
11
|
+
export type Locale = "en" | "zh";
|
|
12
|
+
|
|
13
|
+
type RawDictionary = typeof en & typeof uiEn;
|
|
14
|
+
type Dictionary = i18n.Flatten<RawDictionary>;
|
|
62
15
|
|
|
63
16
|
function cookie(locale: Locale) {
|
|
64
|
-
return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax
|
|
17
|
+
return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax`;
|
|
65
18
|
}
|
|
66
19
|
|
|
67
|
-
const LOCALES: readonly Locale[] = [
|
|
68
|
-
"en",
|
|
69
|
-
"zh",
|
|
70
|
-
"zht",
|
|
71
|
-
"ko",
|
|
72
|
-
"de",
|
|
73
|
-
"es",
|
|
74
|
-
"fr",
|
|
75
|
-
"da",
|
|
76
|
-
"ja",
|
|
77
|
-
"pl",
|
|
78
|
-
"ru",
|
|
79
|
-
"bs",
|
|
80
|
-
"ar",
|
|
81
|
-
"no",
|
|
82
|
-
"br",
|
|
83
|
-
"th",
|
|
84
|
-
"tr",
|
|
85
|
-
]
|
|
20
|
+
const LOCALES: readonly Locale[] = ["en", "zh"];
|
|
86
21
|
|
|
87
22
|
const LABEL_KEY: Record<Locale, keyof Dictionary> = {
|
|
88
23
|
en: "language.en",
|
|
89
24
|
zh: "language.zh",
|
|
90
|
-
|
|
91
|
-
ko: "language.ko",
|
|
92
|
-
de: "language.de",
|
|
93
|
-
es: "language.es",
|
|
94
|
-
fr: "language.fr",
|
|
95
|
-
da: "language.da",
|
|
96
|
-
ja: "language.ja",
|
|
97
|
-
pl: "language.pl",
|
|
98
|
-
ru: "language.ru",
|
|
99
|
-
ar: "language.ar",
|
|
100
|
-
no: "language.no",
|
|
101
|
-
br: "language.br",
|
|
102
|
-
th: "language.th",
|
|
103
|
-
bs: "language.bs",
|
|
104
|
-
tr: "language.tr",
|
|
105
|
-
}
|
|
25
|
+
};
|
|
106
26
|
|
|
107
|
-
const base = i18n.flatten({ ...en, ...uiEn })
|
|
27
|
+
const base = i18n.flatten({ ...en, ...uiEn });
|
|
108
28
|
const DICT: Record<Locale, Dictionary> = {
|
|
109
29
|
en: base,
|
|
110
30
|
zh: { ...base, ...i18n.flatten({ ...zh, ...uiZh }) },
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
ja: { ...base, ...i18n.flatten({ ...ja, ...uiJa }) },
|
|
118
|
-
pl: { ...base, ...i18n.flatten({ ...pl, ...uiPl }) },
|
|
119
|
-
ru: { ...base, ...i18n.flatten({ ...ru, ...uiRu }) },
|
|
120
|
-
ar: { ...base, ...i18n.flatten({ ...ar, ...uiAr }) },
|
|
121
|
-
no: { ...base, ...i18n.flatten({ ...no, ...uiNo }) },
|
|
122
|
-
br: { ...base, ...i18n.flatten({ ...br, ...uiBr }) },
|
|
123
|
-
th: { ...base, ...i18n.flatten({ ...th, ...uiTh }) },
|
|
124
|
-
bs: { ...base, ...i18n.flatten({ ...bs, ...uiBs }) },
|
|
125
|
-
tr: { ...base, ...i18n.flatten({ ...tr, ...uiTr }) },
|
|
126
|
-
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const localeMatchers: Array<{
|
|
34
|
+
locale: Locale;
|
|
35
|
+
match: (language: string) => boolean;
|
|
36
|
+
}> = [{ locale: "zh", match: (language) => language.startsWith("zh") }];
|
|
127
37
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
{ locale: "de", match: (language) => language.startsWith("de") },
|
|
133
|
-
{ locale: "es", match: (language) => language.startsWith("es") },
|
|
134
|
-
{ locale: "fr", match: (language) => language.startsWith("fr") },
|
|
135
|
-
{ locale: "da", match: (language) => language.startsWith("da") },
|
|
136
|
-
{ locale: "ja", match: (language) => language.startsWith("ja") },
|
|
137
|
-
{ locale: "pl", match: (language) => language.startsWith("pl") },
|
|
138
|
-
{ locale: "ru", match: (language) => language.startsWith("ru") },
|
|
139
|
-
{ locale: "ar", match: (language) => language.startsWith("ar") },
|
|
140
|
-
{
|
|
141
|
-
locale: "no",
|
|
142
|
-
match: (language) => language.startsWith("no") || language.startsWith("nb") || language.startsWith("nn"),
|
|
143
|
-
},
|
|
144
|
-
{ locale: "br", match: (language) => language.startsWith("pt") },
|
|
145
|
-
{ locale: "th", match: (language) => language.startsWith("th") },
|
|
146
|
-
{ locale: "bs", match: (language) => language.startsWith("bs") },
|
|
147
|
-
{ locale: "tr", match: (language) => language.startsWith("tr") },
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
type ParityKey = "command.session.previous.unseen" | "command.session.next.unseen"
|
|
151
|
-
const PARITY_CHECK: Record<Exclude<Locale, "en">, Record<ParityKey, string>> = {
|
|
38
|
+
type ParityKey =
|
|
39
|
+
| "command.session.previous.unseen"
|
|
40
|
+
| "command.session.next.unseen";
|
|
41
|
+
const PARITY_CHECK: Record<"zh", Record<ParityKey, string>> = {
|
|
152
42
|
zh,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
de,
|
|
156
|
-
es,
|
|
157
|
-
fr,
|
|
158
|
-
da,
|
|
159
|
-
ja,
|
|
160
|
-
pl,
|
|
161
|
-
ru,
|
|
162
|
-
ar,
|
|
163
|
-
no,
|
|
164
|
-
br,
|
|
165
|
-
th,
|
|
166
|
-
bs,
|
|
167
|
-
tr,
|
|
168
|
-
}
|
|
169
|
-
void PARITY_CHECK
|
|
43
|
+
};
|
|
44
|
+
void PARITY_CHECK;
|
|
170
45
|
|
|
171
46
|
function detectLocale(): Locale {
|
|
172
|
-
if (typeof navigator !== "object") return "en"
|
|
47
|
+
if (typeof navigator !== "object") return "en";
|
|
173
48
|
|
|
174
|
-
const languages = navigator.languages?.length
|
|
49
|
+
const languages = navigator.languages?.length
|
|
50
|
+
? navigator.languages
|
|
51
|
+
: [navigator.language];
|
|
175
52
|
for (const language of languages) {
|
|
176
|
-
if (!language) continue
|
|
177
|
-
const normalized = language.toLowerCase()
|
|
178
|
-
const match = localeMatchers.find((entry) => entry.match(normalized))
|
|
179
|
-
if (match) return match.locale
|
|
53
|
+
if (!language) continue;
|
|
54
|
+
const normalized = language.toLowerCase();
|
|
55
|
+
const match = localeMatchers.find((entry) => entry.match(normalized));
|
|
56
|
+
if (match) return match.locale;
|
|
180
57
|
}
|
|
181
58
|
|
|
182
|
-
return "en"
|
|
59
|
+
return "en";
|
|
183
60
|
}
|
|
184
61
|
|
|
185
62
|
function normalizeLocale(value: string): Locale {
|
|
186
|
-
return LOCALES.includes(value as Locale) ? (value as Locale) : "en"
|
|
63
|
+
return LOCALES.includes(value as Locale) ? (value as Locale) : "en";
|
|
187
64
|
}
|
|
188
65
|
|
|
189
|
-
export const { use: useLanguage, provider: LanguageProvider } =
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
66
|
+
export const { use: useLanguage, provider: LanguageProvider } =
|
|
67
|
+
createSimpleContext({
|
|
68
|
+
name: "Language",
|
|
69
|
+
init: () => {
|
|
70
|
+
const [store, setStore, _, ready] = persisted(
|
|
71
|
+
Persist.global("language", ["language.v1"]),
|
|
72
|
+
createStore({
|
|
73
|
+
locale: detectLocale() as Locale,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const locale = createMemo<Locale>(() => normalizeLocale(store.locale));
|
|
78
|
+
|
|
79
|
+
const dict = createMemo<Dictionary>(() => DICT[locale()]);
|
|
80
|
+
|
|
81
|
+
const t = i18n.translator(dict, i18n.resolveTemplate);
|
|
82
|
+
|
|
83
|
+
const label = (value: Locale) => t(LABEL_KEY[value]);
|
|
84
|
+
|
|
85
|
+
createEffect(() => {
|
|
86
|
+
if (typeof document !== "object") return;
|
|
87
|
+
document.documentElement.lang = locale();
|
|
88
|
+
document.cookie = cookie(locale());
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
ready,
|
|
93
|
+
locale,
|
|
94
|
+
locales: LOCALES,
|
|
95
|
+
label,
|
|
96
|
+
t,
|
|
97
|
+
setLocale(next: Locale) {
|
|
98
|
+
setStore("locale", normalizeLocale(next));
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
});
|
package/src/i18n/en.ts
CHANGED
|
@@ -112,6 +112,7 @@ export const dict = {
|
|
|
112
112
|
"dialog.model.search.placeholder": "Search models",
|
|
113
113
|
"dialog.model.empty": "No model results",
|
|
114
114
|
"dialog.model.manage": "Manage models",
|
|
115
|
+
"dialog.model.recent": "Recent",
|
|
115
116
|
"dialog.model.manage.description": "Customize which models appear in the model selector.",
|
|
116
117
|
"dialog.model.manage.provider.toggle": "Toggle all {{provider}} models",
|
|
117
118
|
|