akanjs 3.0.0-alpha.81 → 3.0.0-alpha.83
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/client/makePageProto.tsx +5 -3
- package/client/translator.ts +13 -4
- package/common/index.ts +1 -0
- package/common/interpolateTranslation.ts +12 -0
- package/dictionary/dictionaryRegistry.ts +27 -0
- package/dictionary/trans.ts +16 -9
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/akanServer.ts +20 -1
- package/types/client/translator.d.ts +1 -0
- package/types/common/index.d.ts +1 -0
- package/types/common/interpolateTranslation.d.ts +7 -0
- package/types/dictionary/dictionary.d.ts +2 -2
- package/types/dictionary/dictionaryRegistry.d.ts +14 -0
- package/types/dictionary/trans.d.ts +1 -2
- package/ui/System/SelectLanguage.tsx +28 -7
package/client/makePageProto.tsx
CHANGED
|
@@ -170,12 +170,14 @@ export const makePageProto = <
|
|
|
170
170
|
}}
|
|
171
171
|
/>
|
|
172
172
|
) as ReactNode;
|
|
173
|
+
|
|
173
174
|
l.trans = <Returns extends ReactNode>(
|
|
174
175
|
translation: Record<"en" | "ko" | (string & {}), Returns>,
|
|
175
176
|
): Returns extends string ? string : Returns => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
const written = translation[lang as "en" | "ko" | (string & {})];
|
|
178
|
+
if (written !== undefined) return written as Returns extends string ? string : Returns;
|
|
179
|
+
const fallback = translation[parseAkanI18nEnv().defaultLocale] ?? Object.values(translation).at(0);
|
|
180
|
+
return (fallback ?? "") as Returns extends string ? string : Returns;
|
|
179
181
|
};
|
|
180
182
|
return { path, l, lang };
|
|
181
183
|
},
|
package/client/translator.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pathGetLoose } from "akanjs/common";
|
|
1
|
+
import { interpolateTranslation, parseAkanI18nEnv, pathGetLoose } from "akanjs/common";
|
|
2
2
|
|
|
3
3
|
export interface Dictionary {
|
|
4
4
|
[key: string]: {
|
|
@@ -61,10 +61,19 @@ export class Translator {
|
|
|
61
61
|
delete getTranslatorState().activePath;
|
|
62
62
|
}
|
|
63
63
|
static translateByLocale(lang: string, key: string, param?: Record<string, string | number>): string {
|
|
64
|
+
const msg = Translator.#lookup(lang, key) ?? Translator.#lookupDefault(lang, key) ?? key;
|
|
65
|
+
return interpolateTranslation(msg, param);
|
|
66
|
+
}
|
|
67
|
+
static #lookup(lang: string, key: string) {
|
|
64
68
|
const dictionary = getTranslatorState().langDictionaryMap.get(lang);
|
|
65
|
-
if (!dictionary) return
|
|
66
|
-
const
|
|
67
|
-
return
|
|
69
|
+
if (!dictionary) return undefined;
|
|
70
|
+
const node = pathGetLoose(key, dictionary, ".") as { t?: unknown } | null;
|
|
71
|
+
return typeof node?.t === "string" ? node.t : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static #lookupDefault(lang: string, key: string) {
|
|
75
|
+
const { defaultLocale } = parseAkanI18nEnv();
|
|
76
|
+
return defaultLocale === lang ? undefined : Translator.#lookup(defaultLocale, key);
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
static seed(lang: string, dict: Dictionary | undefined) {
|
package/common/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { formatNumber } from "./formatNumber";
|
|
|
18
18
|
export { formatPhone } from "./formatPhone";
|
|
19
19
|
export { getAllPropertyDescriptors } from "./getAllPropertyDescriptors";
|
|
20
20
|
export { type AkanHmrPhase, getAkanHmrPhase, isAkanHmrApplying } from "./hmrPhase";
|
|
21
|
+
export { interpolateTranslation } from "./interpolateTranslation";
|
|
21
22
|
export { isDayjs } from "./isDayjs";
|
|
22
23
|
export { isEmail } from "./isEmail";
|
|
23
24
|
export { isPhoneNumber } from "./isPhoneNumber";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fills `{name}` placeholders in a translated string.
|
|
3
|
+
*
|
|
4
|
+
* A placeholder whose value is absent is left as written: the author's `{name}` reads as a bug in the UI, while
|
|
5
|
+
* the string "undefined" reads as content, and a partially supplied `data` must not corrupt the rest of the line.
|
|
6
|
+
*/
|
|
7
|
+
export const interpolateTranslation = (message: string, data: Record<string, unknown> | undefined) => {
|
|
8
|
+
if (!data) return message;
|
|
9
|
+
return message.replace(/{([^}]+)}/g, (placeholder, name: string) =>
|
|
10
|
+
data[name] === undefined ? placeholder : String(data[name]),
|
|
11
|
+
);
|
|
12
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseAkanI18nEnv } from "akanjs/common";
|
|
2
|
+
|
|
1
3
|
import { ModelDictInfo, ScalarDictInfo, ServiceDictInfo } from "./dictInfo";
|
|
2
4
|
import type { DictModule } from "./locale";
|
|
3
5
|
import type { DictionaryNode, RootDictionary } from "./trans";
|
|
@@ -9,6 +11,12 @@ export interface DictionaryModuleInfo {
|
|
|
9
11
|
languages: string[];
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
export interface LocaleGap {
|
|
15
|
+
locale: string;
|
|
16
|
+
total: number;
|
|
17
|
+
missing: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
/**
|
|
13
21
|
* Collects every dictionary tree built by `makeTrans` so a server process can read the merged result.
|
|
14
22
|
*
|
|
@@ -49,6 +57,25 @@ export class DictionaryRegistry {
|
|
|
49
57
|
return Object.fromEntries([...DictionaryRegistry.#modules.entries()].map(([key, info]) => [key, { ...info }]));
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Which configured locales the registered dictionaries never wrote, per locale.
|
|
62
|
+
*
|
|
63
|
+
* A dictionary declares its own locale tuple — `serviceDictionary(["en", "ko"])` — and a lib ships that tuple to
|
|
64
|
+
* every app that mounts it, so an app that adds a third locale cannot widen it from the outside. Every key those
|
|
65
|
+
* modules own then resolves through the default-locale fallback: the screen stays readable and nothing throws,
|
|
66
|
+
* which is exactly why the gap is invisible without asking.
|
|
67
|
+
*/
|
|
68
|
+
static getLocaleGaps(locales: string[] = parseAkanI18nEnv().locales): LocaleGap[] {
|
|
69
|
+
const modules = [...DictionaryRegistry.#modules.entries()];
|
|
70
|
+
return locales
|
|
71
|
+
.map((locale) => ({
|
|
72
|
+
locale,
|
|
73
|
+
total: modules.length,
|
|
74
|
+
missing: modules.filter(([, info]) => !info.languages.includes(locale)).map(([refName]) => refName),
|
|
75
|
+
}))
|
|
76
|
+
.filter((gap) => gap.missing.length);
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
/** Flattened dotted paths of every leaf translation, e.g. `"user.signal.createUser.arg.data"`. */
|
|
53
80
|
static getKeys(root: RootDictionary = DictionaryRegistry.getRoot()): string[] {
|
|
54
81
|
const keys = new Set<string>();
|
package/dictionary/trans.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GetStateObject, ObjectAssign, Prettify } from "akanjs/base";
|
|
2
|
-
import { pathGetLoose } from "akanjs/common";
|
|
2
|
+
import { interpolateTranslation, parseAkanI18nEnv, pathGetLoose } from "akanjs/common";
|
|
3
3
|
|
|
4
4
|
import { DictionaryRegistry } from "./dictionaryRegistry";
|
|
5
5
|
import type { DictModule } from "./locale";
|
|
@@ -25,9 +25,7 @@ export const makeDictionary = <Dicts extends Record<string, unknown>[]>(
|
|
|
25
25
|
return Object.assign(...(dicts as unknown as [object, object])) as Prettify<ObjectAssign<Dicts>>;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
type Language = (typeof languages)[number];
|
|
28
|
+
type Language = "en" | "ko" | "zhChs" | "zhCht" | "ja" | (string & {});
|
|
31
29
|
export interface TransMessageOption {
|
|
32
30
|
key?: string;
|
|
33
31
|
duration?: number;
|
|
@@ -191,13 +189,22 @@ export const makeTrans = <
|
|
|
191
189
|
}
|
|
192
190
|
};
|
|
193
191
|
}
|
|
192
|
+
const lookup = (lang: string, modelName: string, msgKey: string) => {
|
|
193
|
+
const model = rootDictionary[lang]?.[modelName];
|
|
194
|
+
if (!model) return undefined;
|
|
195
|
+
const node = pathGetLoose(msgKey, model, ".") as { t?: unknown } | null;
|
|
196
|
+
return typeof node?.t === "string" ? node.t : undefined;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const lookupDefault = (lang: string, modelName: string, msgKey: string) => {
|
|
200
|
+
const { defaultLocale } = parseAkanI18nEnv();
|
|
201
|
+
return defaultLocale === lang ? undefined : lookup(defaultLocale, modelName, msgKey);
|
|
202
|
+
};
|
|
194
203
|
const translate = (lang: Language, key: _DictKey, data?: TranslationData) => {
|
|
195
|
-
const [modelName, ...msgKeys] = key.split(".");
|
|
204
|
+
const [modelName = "", ...msgKeys] = key.split(".");
|
|
196
205
|
const msgKey = msgKeys.join(".");
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
const message = pathGetLoose(msgKey as string, model, ".", { t: key }) as { t: string };
|
|
200
|
-
return message.t;
|
|
206
|
+
const message = lookup(lang, modelName, msgKey) ?? lookupDefault(lang, modelName, msgKey) ?? (key as string);
|
|
207
|
+
return interpolateTranslation(message, data);
|
|
201
208
|
};
|
|
202
209
|
const getDictionary = (lang: Language) => {
|
|
203
210
|
return rootDictionary[lang];
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/akanServer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AkanWebConfig, AkanWebOption } from "akanjs";
|
|
2
2
|
import { type BackendEnv, type BaseEnv, getEnv } from "akanjs/base";
|
|
3
3
|
import { Logger, logSeverity, websocketBinaryFrameContract } from "akanjs/common";
|
|
4
|
-
import { DictionaryLookup } from "akanjs/dictionary";
|
|
4
|
+
import { DictionaryLookup, DictionaryRegistry } from "akanjs/dictionary";
|
|
5
5
|
import type {
|
|
6
6
|
Adaptor,
|
|
7
7
|
AdaptorCls,
|
|
@@ -611,6 +611,24 @@ export class AkanServer {
|
|
|
611
611
|
);
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Names, once at boot, every configured locale the dictionaries never wrote.
|
|
616
|
+
*
|
|
617
|
+
* A dictionary declares its own locale tuple and a lib ships that tuple to every app that mounts it, so an app
|
|
618
|
+
* that configures a third locale cannot widen it from the outside — the framework's own `base.*` keys included.
|
|
619
|
+
* Those keys then resolve through the default-locale fallback, which is a readable screen and therefore a
|
|
620
|
+
* silent one: the only other symptom is text that never turned into the new language.
|
|
621
|
+
*/
|
|
622
|
+
#reportLocaleCoverage() {
|
|
623
|
+
for (const { locale, missing, total } of DictionaryRegistry.getLocaleGaps()) {
|
|
624
|
+
const listed = missing.slice(0, 10).join(", ");
|
|
625
|
+
const rest = missing.length > 10 ? `, and ${missing.length - 10} more` : "";
|
|
626
|
+
this.logger.warn(
|
|
627
|
+
`Locale "${locale}" is configured but ${missing.length}/${total} dictionaries do not declare it, so their keys fall back to the default locale: ${listed}${rest}`,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
614
632
|
#createBuiltinRoutes(): HttpRoutes {
|
|
615
633
|
const { appName } = getEnv();
|
|
616
634
|
const openapiRoutes: HttpRoutes = this.openapi
|
|
@@ -658,6 +676,7 @@ export class AkanServer {
|
|
|
658
676
|
: {};
|
|
659
677
|
|
|
660
678
|
mcpRouter?.report();
|
|
679
|
+
this.#reportLocaleCoverage();
|
|
661
680
|
|
|
662
681
|
const devtoolsRoutes = new DevtoolsRouter({
|
|
663
682
|
di: this.#di,
|
|
@@ -7,6 +7,7 @@ export interface AllDictionary {
|
|
|
7
7
|
[key: string]: Dictionary;
|
|
8
8
|
}
|
|
9
9
|
export declare class Translator {
|
|
10
|
+
#private;
|
|
10
11
|
constructor(dictionary: Record<string, Record<string, Record<string, unknown>>>);
|
|
11
12
|
hasDictionary(lang: string): boolean;
|
|
12
13
|
static setActiveLocale(lang: string | undefined): void;
|
package/types/common/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { formatNumber } from "./formatNumber.d.ts";
|
|
|
9
9
|
export { formatPhone } from "./formatPhone.d.ts";
|
|
10
10
|
export { getAllPropertyDescriptors } from "./getAllPropertyDescriptors.d.ts";
|
|
11
11
|
export { type AkanHmrPhase, getAkanHmrPhase, isAkanHmrApplying } from "./hmrPhase.d.ts";
|
|
12
|
+
export { interpolateTranslation } from "./interpolateTranslation.d.ts";
|
|
12
13
|
export { isDayjs } from "./isDayjs.d.ts";
|
|
13
14
|
export { isEmail } from "./isEmail.d.ts";
|
|
14
15
|
export { isPhoneNumber } from "./isPhoneNumber.d.ts";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fills `{name}` placeholders in a translated string.
|
|
3
|
+
*
|
|
4
|
+
* A placeholder whose value is absent is left as written: the author's `{name}` reads as a bug in the UI, while
|
|
5
|
+
* the string "undefined" reads as content, and a partially supplied `data` must not corrupt the rest of the line.
|
|
6
|
+
*/
|
|
7
|
+
export declare const interpolateTranslation: (message: string, data: Record<string, unknown> | undefined) => string;
|
|
@@ -4,10 +4,10 @@ export declare const dictionary: {
|
|
|
4
4
|
agentTurn: import("./locale.d.ts").DictModule<import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc`, never>;
|
|
5
5
|
agent: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">;
|
|
6
6
|
};
|
|
7
|
-
export declare const Err: import("./trans.d.ts").ErrConstructor<"base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse" | "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, data?: import("./trans.d.ts").TranslationData) => string, msg: {
|
|
7
|
+
export declare const Err: import("./trans.d.ts").ErrConstructor<"base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse" | "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | (string & {}) | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, data?: import("./trans.d.ts").TranslationData) => string, msg: {
|
|
8
8
|
info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
9
9
|
success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
10
10
|
error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
11
11
|
warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
12
12
|
loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
|
|
13
|
-
}, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, __Error_Key__: "base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse" | "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed";
|
|
13
|
+
}, getDictionary: (lang: "en" | "ko" | (string & {}) | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "ok" | "agent" | "connecting" | "new" | "success" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "cameraPromptHeader" | "cameraPromptPhoto" | "cameraPromptPicture" | "cameraPromptCancel" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentAttachDuplicate" | "agentAttachTooMany" | "agentAttachTooMuch" | "agentAnswerNeeded" | "agentToolDone" | "agentToolFailed" | "agentToolRunning" | "agentToolResult" | "agentTokens" | "agentSummarizing" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, __Error_Key__: "base.error.gatewayTimeout" | "base.error.serverUnavailable" | "base.error.serverUnreachable" | "base.error.unexpectedResponse" | "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed";
|
|
@@ -5,6 +5,11 @@ export interface DictionaryModuleInfo {
|
|
|
5
5
|
kind: DictionaryModuleKind;
|
|
6
6
|
languages: string[];
|
|
7
7
|
}
|
|
8
|
+
export interface LocaleGap {
|
|
9
|
+
locale: string;
|
|
10
|
+
total: number;
|
|
11
|
+
missing: string[];
|
|
12
|
+
}
|
|
8
13
|
/**
|
|
9
14
|
* Collects every dictionary tree built by `makeTrans` so a server process can read the merged result.
|
|
10
15
|
*
|
|
@@ -19,6 +24,15 @@ export declare class DictionaryRegistry {
|
|
|
19
24
|
static getRoot(): RootDictionary;
|
|
20
25
|
static getLanguages(): string[];
|
|
21
26
|
static getModules(): Record<string, DictionaryModuleInfo>;
|
|
27
|
+
/**
|
|
28
|
+
* Which configured locales the registered dictionaries never wrote, per locale.
|
|
29
|
+
*
|
|
30
|
+
* A dictionary declares its own locale tuple — `serviceDictionary(["en", "ko"])` — and a lib ships that tuple to
|
|
31
|
+
* every app that mounts it, so an app that adds a third locale cannot widen it from the outside. Every key those
|
|
32
|
+
* modules own then resolves through the default-locale fallback: the screen stays readable and nothing throws,
|
|
33
|
+
* which is exactly why the gap is invisible without asking.
|
|
34
|
+
*/
|
|
35
|
+
static getLocaleGaps(locales?: string[]): LocaleGap[];
|
|
22
36
|
/** Flattened dotted paths of every leaf translation, e.g. `"user.signal.createUser.arg.data"`. */
|
|
23
37
|
static getKeys(root?: RootDictionary): string[];
|
|
24
38
|
/** Test-only reset; the registry is module-level global state. */
|
|
@@ -17,8 +17,7 @@ export type TransMessage<Locale extends Record<string, unknown>> = {
|
|
|
17
17
|
[K in keyof Locale]-?: `${K & string}${Locale[K] extends Record<string, unknown> ? `.${keyof Locale[K] extends string ? keyof Locale[K] : never}` : ""}`;
|
|
18
18
|
}[keyof Locale];
|
|
19
19
|
export declare const makeDictionary: <Dicts extends Record<string, unknown>[]>(...dicts: Dicts) => Prettify<ObjectAssign<Dicts>>;
|
|
20
|
-
|
|
21
|
-
type Language = (typeof languages)[number];
|
|
20
|
+
type Language = "en" | "ko" | "zhChs" | "zhCht" | "ja" | (string & {});
|
|
22
21
|
export interface TransMessageOption {
|
|
23
22
|
key?: string;
|
|
24
23
|
duration?: number;
|
|
@@ -12,15 +12,36 @@ const languageNames = {
|
|
|
12
12
|
ko: "한국어",
|
|
13
13
|
zhChs: "简体中文",
|
|
14
14
|
zhCht: "繁體中文",
|
|
15
|
+
ja: "日本語",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
const nameOf = (locale: string) => languageNames[locale as keyof typeof languageNames] ?? locale;
|
|
19
|
+
|
|
20
|
+
const warned = new Set<string>();
|
|
21
|
+
|
|
22
|
+
const warnDropped = (dropped: string[], locales: string[]) => {
|
|
23
|
+
if (process.env.AKAN_PUBLIC_ENV !== "local") return;
|
|
24
|
+
const unseen = dropped.filter((locale) => !warned.has(locale));
|
|
25
|
+
if (!unseen.length) return;
|
|
26
|
+
for (const locale of unseen) warned.add(locale);
|
|
27
|
+
console.warn(
|
|
28
|
+
`<SelectLanguage> dropped ${unseen.join(", ")}: not in AKAN_PUBLIC_LOCALES (${locales.join(",")}). Add the locale to the app i18n config, or drop it from the \`languages\` prop.`,
|
|
29
|
+
);
|
|
15
30
|
};
|
|
16
31
|
|
|
17
32
|
export interface SelectLanguageProps {
|
|
18
33
|
className?: string;
|
|
19
34
|
languages?: string[];
|
|
20
35
|
}
|
|
21
|
-
export const SelectLanguage = ({ className, languages
|
|
36
|
+
export const SelectLanguage = ({ className, languages }: SelectLanguageProps) => {
|
|
22
37
|
const { lang } = usePage();
|
|
23
|
-
const
|
|
38
|
+
const { locales } = parseAkanI18nEnv();
|
|
39
|
+
const requested = languages ?? locales;
|
|
40
|
+
const offered = requested.filter((locale) => locales.includes(locale));
|
|
41
|
+
warnDropped(
|
|
42
|
+
requested.filter((locale) => !locales.includes(locale)),
|
|
43
|
+
locales,
|
|
44
|
+
);
|
|
24
45
|
const setLanguage = st
|
|
25
46
|
.tool("setLanguage")
|
|
26
47
|
.desc("Show the site in another language.")
|
|
@@ -32,18 +53,18 @@ export const SelectLanguage = ({ className, languages = parseAkanI18nEnv().local
|
|
|
32
53
|
<Dropdown
|
|
33
54
|
className={className}
|
|
34
55
|
buttonClassName="mx-2 my-auto h-8 min-h-0 border-none px-3 font-medium text-xs md:mx-4"
|
|
35
|
-
value={
|
|
36
|
-
content={offered.map((
|
|
37
|
-
<li key={
|
|
56
|
+
value={nameOf(lang)}
|
|
57
|
+
content={offered.map((locale) => (
|
|
58
|
+
<li key={locale}>
|
|
38
59
|
<button
|
|
39
60
|
type="button"
|
|
40
61
|
className={buttonRecipe({ variant: "ghost", size: "sm" }, "w-full justify-start")}
|
|
41
62
|
onClick={() => {
|
|
42
|
-
void setLanguage(
|
|
63
|
+
void setLanguage(locale);
|
|
43
64
|
}}
|
|
44
65
|
{...agentAttrs(setLanguage)}
|
|
45
66
|
>
|
|
46
|
-
{
|
|
67
|
+
{nameOf(locale)}
|
|
47
68
|
</button>
|
|
48
69
|
</li>
|
|
49
70
|
))}
|