Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0a4fd60f2 | ||
|
|
aae877b880 |
No files matched your search
@@ -30,8 +30,7 @@ vi.mock("@one-memory/runtime-control", async (importOriginal) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
function languageState(uiLanguage: string | null, extra = {}) {
|
function languageState(uiLanguage: string | null, extra = {}) {
|
||||||
const sdk =
|
const sdk = uiLanguage === null ? "auto" : uiLanguage;
|
||||||
uiLanguage === null || uiLanguage === "zh-Hans" ? "auto" : uiLanguage;
|
|
||||||
return {
|
return {
|
||||||
ui_language: uiLanguage,
|
ui_language: uiLanguage,
|
||||||
sdk_language: sdk,
|
sdk_language: sdk,
|
||||||
@@ -95,7 +94,7 @@ describe("memhub settings language", () => {
|
|||||||
});
|
});
|
||||||
expect(transport.put).not.toHaveBeenCalled();
|
expect(transport.put).not.toHaveBeenCalled();
|
||||||
const text = (await capturedRun(["settings", "language"])).stdout;
|
const text = (await capturedRun(["settings", "language"])).stdout;
|
||||||
expect(text).toContain("界面语言:跟随系统(未选择)");
|
expect(text).toContain("界面语言:未设置(SDK 自动识别)");
|
||||||
expect(text).toContain("SDK 提示语言:auto");
|
expect(text).toContain("SDK 提示语言:auto");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
* `memhub settings language [<code>|system]`.
|
* `memhub settings language [<code>|system]`.
|
||||||
*
|
*
|
||||||
* The display language the App reports to the Runtime (`ui_language`), and
|
* The display language the App reports to the Runtime (`ui_language`), and
|
||||||
* the SDK prompt language it implies. zh-Hans and "system" (no explicit
|
* the SDK prompt language it implies. Every code, zh-Hans and en included,
|
||||||
* choice) pass no language, so the SDK keeps detecting Chinese or English as
|
* makes extraction and the other SDK prompts use that language; "system"
|
||||||
* before; every other code, en included, makes extraction and the other SDK
|
* (no language set) passes none, so the SDK keeps detecting Chinese or
|
||||||
* prompts use that language. A change applies at once: a write in flight
|
* English as before. The App sends the language it shows, so it is only
|
||||||
|
* unset on an install without the App. A change applies at once: a write in flight
|
||||||
* finishes in the old language, the next ones use the new one. The App sends
|
* finishes in the old language, the next ones use the new one. The App sends
|
||||||
* its own choice again whenever it changes there.
|
* its own choice again whenever it changes there.
|
||||||
*/
|
*/
|
||||||
@@ -162,14 +163,14 @@ export function renderLanguageText(document: LanguageDocument): string {
|
|||||||
const lines = [
|
const lines = [
|
||||||
`界面语言:${
|
`界面语言:${
|
||||||
state.ui_language === null
|
state.ui_language === null
|
||||||
? "跟随系统(未选择)"
|
? "未设置(SDK 自动识别)"
|
||||||
: `${state.ui_language}(${NAMES[state.ui_language]})`
|
: `${state.ui_language}(${NAMES[state.ui_language]})`
|
||||||
}`,
|
}`,
|
||||||
`SDK 提示语言:${sdkText(state.sdk_language)}`,
|
`SDK 提示语言:${sdkText(state.sdk_language)}`,
|
||||||
];
|
];
|
||||||
if (document.state === "ok") {
|
if (document.state === "ok") {
|
||||||
lines.push(
|
lines.push(
|
||||||
`可选:${UI_LANGUAGES.join("、")}、system。简体中文和 system 不指定语言,其余语言(包括 en)会让 SDK 使用该语言。`,
|
`可选:${UI_LANGUAGES.join("、")}、system。任一语言(包括 zh-Hans、en)都会让 SDK 使用该语言;system 不指定语言,SDK 自动识别中文或英文。`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (document.state === "unchanged") lines.push("没有需要改动的设置。");
|
if (document.state === "unchanged") lines.push("没有需要改动的设置。");
|
||||||
|
|||||||
@@ -766,7 +766,7 @@ class PipelineStatus(BaseModel):
|
|||||||
#: default; empty while every feature is at its default.
|
#: default; empty while every feature is at its default.
|
||||||
features: dict[MemoryFeatureName, bool] = Field(default_factory=dict)
|
features: dict[MemoryFeatureName, bool] = Field(default_factory=dict)
|
||||||
#: The display language the App last reported (`ui_language`); null =
|
#: The display language the App last reported (`ui_language`); null =
|
||||||
#: never chosen or "follow the system".
|
#: none set (the App sends the language it shows, even following the system).
|
||||||
ui_language: UiLanguage | None = None
|
ui_language: UiLanguage | None = None
|
||||||
#: The SDK prompt language: `auto` (the SDK detects zh/en) or a code.
|
#: The SDK prompt language: `auto` (the SDK detects zh/en) or a code.
|
||||||
sdk_language: str = SDK_LANGUAGE_AUTO
|
sdk_language: str = SDK_LANGUAGE_AUTO
|
||||||
@@ -777,9 +777,9 @@ class UiLanguageState(BaseModel):
|
|||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
#: What the App reported; null = never chosen or "follow the system".
|
#: What the App reported; null = none set (CLI/API-only, or `system`).
|
||||||
ui_language: UiLanguage | None
|
ui_language: UiLanguage | None
|
||||||
#: What SDK clients are built with: `auto` (zh-Hans or no choice: the SDK
|
#: What SDK clients are built with: `auto` (no language set: the SDK
|
||||||
#: detects zh/en, as before) or the code passed as `MemoryConfig.language`.
|
#: detects zh/en, as before) or the code passed as `MemoryConfig.language`.
|
||||||
sdk_language: str
|
sdk_language: str
|
||||||
languages: tuple[UiLanguage, ...] = UI_LANGUAGES
|
languages: tuple[UiLanguage, ...] = UI_LANGUAGES
|
||||||
@@ -4616,9 +4616,9 @@ def create_app(
|
|||||||
async def put_ui_language(request: UiLanguageUpdate) -> UiLanguageChange:
|
async def put_ui_language(request: UiLanguageUpdate) -> UiLanguageChange:
|
||||||
"""Record the App's display language; one global setting, not per user.
|
"""Record the App's display language; one global setting, not per user.
|
||||||
|
|
||||||
zh-Hans and null pass no language to the SDK, which keeps detecting
|
Every code (zh-Hans and en included) is passed as
|
||||||
zh/en as before; every other code (en included) is passed as
|
`MemoryConfig.language`; only null passes no language, so the SDK
|
||||||
`MemoryConfig.language`. A change of that SDK language rebuilds the
|
keeps detecting zh/en as before. A change of that SDK language rebuilds the
|
||||||
SDK clients like a feature change: a write in flight finishes in the
|
SDK clients like a feature change: a write in flight finishes in the
|
||||||
old language. Nothing stored is migrated and no collection is renamed.
|
old language. Nothing stored is migrated and no collection is renamed.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -367,8 +367,8 @@ def language_kwargs(language: str | None) -> LanguageKwargs:
|
|||||||
def apply_sdk_language(config: MemoryConfig, language: str | None) -> MemoryConfig:
|
def apply_sdk_language(config: MemoryConfig, language: str | None) -> MemoryConfig:
|
||||||
"""Set the SDK prompt language (`MemoryConfig.language`), when there is one.
|
"""Set the SDK prompt language (`MemoryConfig.language`), when there is one.
|
||||||
|
|
||||||
None (zh-Hans, or no choice) sets nothing: the SDK detects zh/en as it
|
None (no language set) sets nothing: the SDK detects zh/en as it always
|
||||||
always did. Like a feature, the language never touches `vector_store`, so
|
did. Every display language, zh-Hans included, is set explicitly. Like a feature, the language never touches `vector_store`, so
|
||||||
the collection identity never depends on it.
|
the collection identity never depends on it.
|
||||||
"""
|
"""
|
||||||
if language is not None:
|
if language is not None:
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ class ConfigDocument(BaseModel):
|
|||||||
#: SDK retrieval and extraction features a person set; an unset one keeps
|
#: SDK retrieval and extraction features a person set; an unset one keeps
|
||||||
#: the SDK default. See `one_memory.memory_features`.
|
#: the SDK default. See `one_memory.memory_features`.
|
||||||
memory_features: MemoryFeatures = Field(default_factory=MemoryFeatures)
|
memory_features: MemoryFeatures = Field(default_factory=MemoryFeatures)
|
||||||
#: The display language a person chose in the App; None = never chosen or
|
#: The display language the App shows (its choice, or the system language
|
||||||
#: "follow the system". See `one_memory.ui_language` for what it does to
|
#: it follows); None = none set. See `one_memory.ui_language` for what it
|
||||||
#: the SDK.
|
#: does to the SDK.
|
||||||
ui_language: UiLanguage | None = None
|
ui_language: UiLanguage | None = None
|
||||||
|
|
||||||
@field_validator("sources")
|
@field_validator("sources")
|
||||||
|
|||||||
@@ -3,14 +3,19 @@
|
|||||||
The App owns the choice (its language selector) and tells Core whenever it
|
The App owns the choice (its language selector) and tells Core whenever it
|
||||||
changes, so Core can build every SDK client with it. It is one global value in
|
changes, so Core can build every SDK client with it. It is one global value in
|
||||||
`config.json` (`ui_language`), beside `memory_mode` and `memory_features`;
|
`config.json` (`ui_language`), beside `memory_mode` and `memory_features`;
|
||||||
absent (never chosen, or "follow the system") means no explicit choice.
|
absent means no language was set (a CLI/API-only install, or
|
||||||
|
`settings language system`). An App that follows the system sends the
|
||||||
|
language it resolved and shows, not null.
|
||||||
|
|
||||||
The owner's rule for the SDK:
|
The owner's rule for the SDK:
|
||||||
|
|
||||||
- zh-Hans, or no explicit choice: pass no language. The SDK keeps detecting
|
- Any supported code, zh-Hans and en included: pass it as
|
||||||
Chinese or English from the text, exactly as before this setting existed.
|
`MemoryConfig.language`, so extraction and the other prompts use that
|
||||||
- Any other code, including en: pass it as `MemoryConfig.language`, so
|
language. Every display language is passed explicitly (#243).
|
||||||
extraction and the other prompts use that language.
|
- No language at all (never set, as on a CLI/API-only install): pass no
|
||||||
|
language. The SDK keeps detecting Chinese or English from the text, exactly
|
||||||
|
as before this setting existed. The App does not leave it unset: it sends
|
||||||
|
the language it shows, including when it follows the system.
|
||||||
|
|
||||||
The language never names a collection; a change rebuilds the SDK handles the
|
The language never names a collection; a change rebuilds the SDK handles the
|
||||||
way a mode or feature change does (#218 use count).
|
way a mode or feature change does (#218 use count).
|
||||||
@@ -51,8 +56,6 @@ UI_LANGUAGES: tuple[UiLanguage, ...] = (
|
|||||||
)
|
)
|
||||||
#: Codes earlier App builds stored, and the code each one is now.
|
#: Codes earlier App builds stored, and the code each one is now.
|
||||||
LEGACY_UI_LANGUAGES: dict[str, UiLanguage] = {"zh-CN": "zh-Hans"}
|
LEGACY_UI_LANGUAGES: dict[str, UiLanguage] = {"zh-CN": "zh-Hans"}
|
||||||
#: Display languages that leave the SDK's own zh/en detection in charge.
|
|
||||||
AUTO_DETECTED: frozenset[str] = frozenset({"zh-Hans"})
|
|
||||||
#: What status shows when no language is passed to the SDK.
|
#: What status shows when no language is passed to the SDK.
|
||||||
SDK_LANGUAGE_AUTO = "auto"
|
SDK_LANGUAGE_AUTO = "auto"
|
||||||
|
|
||||||
@@ -66,8 +69,9 @@ def normalize_ui_language(value: object) -> UiLanguage | None:
|
|||||||
|
|
||||||
|
|
||||||
def sdk_language(ui_language: str | None) -> str | None:
|
def sdk_language(ui_language: str | None) -> str | None:
|
||||||
"""The `MemoryConfig.language` for a display language; None = SDK auto-detect."""
|
"""The `MemoryConfig.language` for a display language; None = SDK auto-detect.
|
||||||
language = normalize_ui_language(ui_language)
|
|
||||||
if language is None or language in AUTO_DETECTED:
|
Every supported code is passed as is; only an unset (or unknown) value
|
||||||
return None
|
leaves the SDK to detect zh/en.
|
||||||
return language
|
"""
|
||||||
|
return normalize_ui_language(ui_language)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"""The App's display language, and the SDK prompt language it implies.
|
"""The App's display language, and the SDK prompt language it implies.
|
||||||
|
|
||||||
The owner's rule: zh-Hans, or no explicit choice, passes no language, so the
|
The owner's rule (#243): every display language, zh-Hans and en included, is
|
||||||
SDK keeps detecting zh/en exactly as before; every other code (en included)
|
passed as `MemoryConfig.language` (hy-memory 2.0.11); only an unset language
|
||||||
is passed as `MemoryConfig.language` (hy-memory 2.0.11). A change rebuilds the
|
passes none, so the SDK keeps detecting zh/en exactly as before. A change rebuilds the
|
||||||
handles the way a mode or feature change does, and the language never touches
|
handles the way a mode or feature change does, and the language never touches
|
||||||
the vector collection identity.
|
the vector collection identity.
|
||||||
"""
|
"""
|
||||||
@@ -28,8 +28,8 @@ from one_memory.secrets import InMemorySecretStore
|
|||||||
from one_memory.ui_language import UI_LANGUAGES, normalize_ui_language, sdk_language
|
from one_memory.ui_language import UI_LANGUAGES, normalize_ui_language, sdk_language
|
||||||
|
|
||||||
TOKEN = "ui-language-test"
|
TOKEN = "ui-language-test"
|
||||||
#: Every code except zh-Hans is passed to the SDK.
|
#: Every code is passed to the SDK, zh-Hans included.
|
||||||
PASSED = ("en", "zh-Hant", "ja", "es", "de", "pt", "fr", "ko", "ru", "id")
|
PASSED = ("en", "zh-Hans", "zh-Hant", "ja", "es", "de", "pt", "fr", "ko", "ru", "id")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@@ -51,7 +51,7 @@ def _config(tmp_path: Path, language: str | None):
|
|||||||
|
|
||||||
|
|
||||||
def test_eleven_codes_and_the_legacy_chinese_code() -> None:
|
def test_eleven_codes_and_the_legacy_chinese_code() -> None:
|
||||||
assert ("en", "zh-Hans", *PASSED[1:]) == UI_LANGUAGES
|
assert PASSED == UI_LANGUAGES
|
||||||
assert len(UI_LANGUAGES) == 11
|
assert len(UI_LANGUAGES) == 11
|
||||||
# Earlier App builds stored zh-CN; it is zh-Hans now.
|
# Earlier App builds stored zh-CN; it is zh-Hans now.
|
||||||
assert normalize_ui_language("zh-CN") == "zh-Hans"
|
assert normalize_ui_language("zh-CN") == "zh-Hans"
|
||||||
@@ -60,19 +60,20 @@ def test_eleven_codes_and_the_legacy_chinese_code() -> None:
|
|||||||
assert normalize_ui_language(None) is None
|
assert normalize_ui_language(None) is None
|
||||||
|
|
||||||
|
|
||||||
def test_only_zh_hans_and_no_choice_leave_the_sdk_to_detect() -> None:
|
def test_only_an_unset_language_leaves_the_sdk_to_detect() -> None:
|
||||||
assert sdk_language(None) is None
|
assert sdk_language(None) is None
|
||||||
assert sdk_language("zh-Hans") is None
|
|
||||||
assert sdk_language("zh-CN") is None
|
|
||||||
assert sdk_language("system") is None
|
assert sdk_language("system") is None
|
||||||
|
assert sdk_language("xx") is None
|
||||||
|
# zh-Hans is passed like every other code; the legacy zh-CN maps to it.
|
||||||
|
assert sdk_language("zh-Hans") == "zh-Hans"
|
||||||
|
assert sdk_language("zh-CN") == "zh-Hans"
|
||||||
for code in PASSED:
|
for code in PASSED:
|
||||||
assert sdk_language(code) == code
|
assert sdk_language(code) == code
|
||||||
|
|
||||||
|
|
||||||
def test_zh_hans_and_no_choice_build_the_config_of_before(tmp_path) -> None:
|
def test_no_language_builds_the_config_of_before(tmp_path) -> None:
|
||||||
before = repr(dataclasses.asdict(_config(tmp_path, None)))
|
|
||||||
assert _config(tmp_path, None).language is None
|
assert _config(tmp_path, None).language is None
|
||||||
assert repr(dataclasses.asdict(_config(tmp_path, sdk_language("zh-Hans")))) == before
|
assert _config(tmp_path, sdk_language("zh-Hans")).language == "zh-Hans"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("code", PASSED)
|
@pytest.mark.parametrize("code", PASSED)
|
||||||
@@ -282,17 +283,31 @@ def test_ui_language_api_get_put_and_status(tmp_path, monkeypatch) -> None:
|
|||||||
assert current["languages"] == list(UI_LANGUAGES)
|
assert current["languages"] == list(UI_LANGUAGES)
|
||||||
assert reads.current_language() is None and writes.sdk_language() is None
|
assert reads.current_language() is None and writes.sdk_language() is None
|
||||||
|
|
||||||
# zh-Hans: recorded, but the SDK still detects zh/en; nothing rebuilt.
|
# zh-Hans is passed explicitly: Chinese prompts, handles rebuilt (#243).
|
||||||
body = (
|
body = (
|
||||||
await client.put("/api/v1/settings/ui-language", json={"ui_language": "zh-Hans"})
|
await client.put("/api/v1/settings/ui-language", json={"ui_language": "zh-Hans"})
|
||||||
).json()
|
).json()
|
||||||
assert body == {**current, "ui_language": "zh-Hans", "rebuilt": False}
|
assert body == {
|
||||||
assert retired == [] and reads.current_language() is None
|
**current,
|
||||||
|
"ui_language": "zh-Hans",
|
||||||
|
"sdk_language": "zh-Hans",
|
||||||
|
"rebuilt": True,
|
||||||
|
}
|
||||||
|
assert reads.current_language() == "zh-Hans" and writes.sdk_language() == "zh-Hans"
|
||||||
|
assert retired == ["zh-Hans"]
|
||||||
|
processing = (await client.get("/api/v1/memory-processing")).json()
|
||||||
|
assert processing["pipeline"]["sdk_language"] == "zh-Hans"
|
||||||
|
|
||||||
|
# The same value again changes nothing.
|
||||||
|
body = (
|
||||||
|
await client.put("/api/v1/settings/ui-language", json={"ui_language": "zh-Hans"})
|
||||||
|
).json()
|
||||||
|
assert body["rebuilt"] is False and retired == ["zh-Hans"]
|
||||||
|
|
||||||
body = (await client.put("/api/v1/settings/ui-language", json={"ui_language": "ja"})).json()
|
body = (await client.put("/api/v1/settings/ui-language", json={"ui_language": "ja"})).json()
|
||||||
assert body["sdk_language"] == "ja" and body["rebuilt"] is True
|
assert body["sdk_language"] == "ja" and body["rebuilt"] is True
|
||||||
assert reads.current_language() == "ja" and writes.sdk_language() == "ja"
|
assert reads.current_language() == "ja" and writes.sdk_language() == "ja"
|
||||||
assert retired == ["ja"]
|
assert retired == ["zh-Hans", "ja"]
|
||||||
|
|
||||||
# An explicit English choice forces English prompts.
|
# An explicit English choice forces English prompts.
|
||||||
body = (await client.put("/api/v1/settings/ui-language", json={"ui_language": "en"})).json()
|
body = (await client.put("/api/v1/settings/ui-language", json={"ui_language": "en"})).json()
|
||||||
@@ -302,7 +317,7 @@ def test_ui_language_api_get_put_and_status(tmp_path, monkeypatch) -> None:
|
|||||||
assert processing["pipeline"]["ui_language"] == "en"
|
assert processing["pipeline"]["ui_language"] == "en"
|
||||||
assert processing["pipeline"]["sdk_language"] == "en"
|
assert processing["pipeline"]["sdk_language"] == "en"
|
||||||
|
|
||||||
# Back to "follow the system": auto again.
|
# Unset (CLI `settings language system`): auto again.
|
||||||
body = (await client.put("/api/v1/settings/ui-language", json={"ui_language": None})).json()
|
body = (await client.put("/api/v1/settings/ui-language", json={"ui_language": None})).json()
|
||||||
assert body["sdk_language"] == "auto" and body["rebuilt"] is True
|
assert body["sdk_language"] == "auto" and body["rebuilt"] is True
|
||||||
assert reads.current_language() is None
|
assert reads.current_language() is None
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ it("bakes fallback release identity when the packaging pipeline omits it", async
|
|||||||
expect(config).toMatch(
|
expect(config).toMatch(
|
||||||
/__ONE_MEMORY_BUILD_NUMBER__: JSON\.stringify\(\s*String\(releaseBuildNumber\)/,
|
/__ONE_MEMORY_BUILD_NUMBER__: JSON\.stringify\(\s*String\(releaseBuildNumber\)/,
|
||||||
);
|
);
|
||||||
expect(config).toMatch(
|
expect(config).toContain(
|
||||||
/process\.env\.ONE_MEMORY_COMMIT\?\.trim\(\) \|\| readSourceCommit\(\)/,
|
"const releaseCommit = resolveReleaseCommit(process.env.ONE_MEMORY_COMMIT)",
|
||||||
);
|
);
|
||||||
|
expect(config).toMatch(/if \(!trimmed\) return readSourceCommit\(\);/);
|
||||||
|
// A supplied commit must be the full 40-character form the runtime accepts.
|
||||||
|
expect(config).toContain("/^[0-9a-f]{40}$/i.test(trimmed)");
|
||||||
|
expect(config).toContain("return trimmed.toLowerCase();");
|
||||||
expect(config).toMatch(
|
expect(config).toMatch(
|
||||||
/execFileSync\("git", \["rev-parse", "HEAD"\],[\s\S]*?encoding: "utf8"/,
|
/execFileSync\("git", \["rev-parse", "HEAD"\],[\s\S]*?encoding: "utf8"/,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { LanguageSnapshot } from "@one-memory/i18n";
|
import type { GuiLocale, LanguageSnapshot } from "@one-memory/i18n";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { coreUiLanguage, installCoreLanguageSync } from "./core-language-sync";
|
import { coreUiLanguage, installCoreLanguageSync } from "./core-language-sync";
|
||||||
|
|
||||||
@@ -21,11 +21,16 @@ function source(initial: LanguageSnapshot) {
|
|||||||
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
describe("Core language sync", () => {
|
describe("Core language sync", () => {
|
||||||
it("tells Core null for system and the code for an explicit choice", () => {
|
it("tells Core the language the GUI shows, system included", () => {
|
||||||
expect(coreUiLanguage("system")).toBeNull();
|
const shown = (
|
||||||
expect(coreUiLanguage("zh-Hans")).toBe("zh-Hans");
|
preference: LanguageSnapshot["preference"],
|
||||||
expect(coreUiLanguage("en")).toBe("en");
|
locale: GuiLocale,
|
||||||
expect(coreUiLanguage("ja")).toBe("ja");
|
) => coreUiLanguage({ preference, locale, revision: 0 });
|
||||||
|
expect(shown("system", "zh-Hans")).toBe("zh-Hans");
|
||||||
|
expect(shown("system", "en")).toBe("en");
|
||||||
|
expect(shown("zh-Hans", "zh-Hans")).toBe("zh-Hans");
|
||||||
|
expect(shown("en", "en")).toBe("en");
|
||||||
|
expect(shown("ja", "ja")).toBe("ja");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends the stored choice once at launch, then every change", async () => {
|
it("sends the stored choice once at launch, then every change", async () => {
|
||||||
@@ -38,29 +43,33 @@ describe("Core language sync", () => {
|
|||||||
await settle();
|
await settle();
|
||||||
language.change({ preference: "system", locale: "en", revision: 2 });
|
language.change({ preference: "system", locale: "en", revision: 2 });
|
||||||
await settle();
|
await settle();
|
||||||
expect(send.mock.calls).toEqual([["es"], ["ja"], [null]]);
|
expect(send.mock.calls).toEqual([["es"], ["ja"], ["en"]]);
|
||||||
dispose();
|
dispose();
|
||||||
language.change({ preference: "de", locale: "de", revision: 3 });
|
language.change({ preference: "de", locale: "de", revision: 3 });
|
||||||
await settle();
|
await settle();
|
||||||
expect(send).toHaveBeenCalledTimes(3);
|
expect(send).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("an untouched install sends null, which leaves the SDK on auto", async () => {
|
it("an untouched install sends the system language the GUI shows", async () => {
|
||||||
const language = source({
|
const language = source({
|
||||||
preference: "system",
|
preference: "system",
|
||||||
locale: "ja",
|
locale: "zh-Hans",
|
||||||
revision: 0,
|
revision: 0,
|
||||||
});
|
});
|
||||||
const send = vi.fn(async () => {});
|
const send = vi.fn(async () => {});
|
||||||
installCoreLanguageSync({ language, send });
|
installCoreLanguageSync({ language, send });
|
||||||
await settle();
|
await settle();
|
||||||
expect(send.mock.calls).toEqual([[null]]);
|
expect(send.mock.calls).toEqual([["zh-Hans"]]);
|
||||||
|
// Choosing the language already shown changes nothing Core sees.
|
||||||
|
language.change({ preference: "zh-Hans", locale: "zh-Hans", revision: 1 });
|
||||||
|
await settle();
|
||||||
|
expect(send).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reports a failed send and retries it with the next change", async () => {
|
it("reports a failed send and retries it with the next change", async () => {
|
||||||
const language = source({ preference: "fr", locale: "fr", revision: 0 });
|
const language = source({ preference: "fr", locale: "fr", revision: 0 });
|
||||||
const send = vi
|
const send = vi
|
||||||
.fn<(value: string | null) => Promise<void>>()
|
.fn<(value: string) => Promise<void>>()
|
||||||
.mockRejectedValueOnce(new Error("core_unavailable"))
|
.mockRejectedValueOnce(new Error("core_unavailable"))
|
||||||
.mockResolvedValue(undefined);
|
.mockResolvedValue(undefined);
|
||||||
const reportError = vi.fn();
|
const reportError = vi.fn();
|
||||||
|
|||||||
@@ -1,35 +1,37 @@
|
|||||||
import type { GuiLocale, LanguagePreference } from "@one-memory/i18n";
|
import type { GuiLocale, LanguageSnapshot } from "@one-memory/i18n";
|
||||||
import type { NativeLanguageSource } from "./native-language";
|
import type { NativeLanguageSource } from "./native-language";
|
||||||
|
|
||||||
/** What Core is told: a code, or null for "follow the system". */
|
/**
|
||||||
export function coreUiLanguage(
|
* What Core is told: the language the GUI shows. "Follow the system" sends
|
||||||
preference: LanguagePreference,
|
* the language it resolved to, so the memories match the interface a person
|
||||||
): GuiLocale | null {
|
* sees; the App never leaves the SDK to detect zh/en (#243).
|
||||||
return preference === "system" ? null : preference;
|
*/
|
||||||
|
export function coreUiLanguage(snapshot: LanguageSnapshot): GuiLocale {
|
||||||
|
return snapshot.locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keep Core's `ui_language` setting on the display language a person chose.
|
* Keep Core's `ui_language` setting on the display language the GUI shows.
|
||||||
*
|
*
|
||||||
* Core builds every SDK client with it: zh-Hans and "follow the system" pass
|
* Core builds every SDK client with it and passes it, zh-Hans included, as
|
||||||
* no language (the SDK keeps detecting zh/en), any other choice sets the SDK
|
* the SDK prompt language. Sent once when installed, so a choice made before
|
||||||
* prompt language. Sent once when installed, so a choice made before this
|
* this existed, or the system language followed at this launch, reaches
|
||||||
* existed reaches Core, then on every committed change; a value Core already
|
* Core; then on every committed change. A value Core already has is not sent
|
||||||
* has is not sent again. Best effort: a failed send is reported and retried
|
* again. Best effort: a failed send is reported and retried with the next
|
||||||
* with the next change or launch, and never blocks the GUI.
|
* change or launch, and never blocks the GUI.
|
||||||
*/
|
*/
|
||||||
export function installCoreLanguageSync(options: {
|
export function installCoreLanguageSync(options: {
|
||||||
language: NativeLanguageSource;
|
language: NativeLanguageSource;
|
||||||
send(uiLanguage: GuiLocale | null): Promise<void>;
|
send(uiLanguage: GuiLocale): Promise<void>;
|
||||||
reportError?(error: unknown): void;
|
reportError?(error: unknown): void;
|
||||||
}): () => void {
|
}): () => void {
|
||||||
let closed = false;
|
let closed = false;
|
||||||
let sent: GuiLocale | null | undefined;
|
let sent: GuiLocale | undefined;
|
||||||
let queue: Promise<void> = Promise.resolve();
|
let queue: Promise<void> = Promise.resolve();
|
||||||
const push = () => {
|
const push = () => {
|
||||||
queue = queue.then(async () => {
|
queue = queue.then(async () => {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
const value = coreUiLanguage(options.language.read().preference);
|
const value = coreUiLanguage(options.language.read());
|
||||||
if (value === sent) return;
|
if (value === sent) return;
|
||||||
try {
|
try {
|
||||||
await options.send(value);
|
await options.send(value);
|
||||||
|
|||||||
@@ -526,7 +526,22 @@ describe("hourly isolated desktop launch", () => {
|
|||||||
expect(resolvePackagedBuildNumber("-1")).toBeUndefined();
|
expect(resolvePackagedBuildNumber("-1")).toBeUndefined();
|
||||||
expect(resolvePackagedBuildNumber(undefined, "3907")).toBe(3907);
|
expect(resolvePackagedBuildNumber(undefined, "3907")).toBe(3907);
|
||||||
expect(resolvePackagedBuildNumber("3908", "3907")).toBe(3908);
|
expect(resolvePackagedBuildNumber("3908", "3907")).toBe(3908);
|
||||||
expect(resolvePackagedCommit(undefined, "388fd0e9")).toBe("388fd0e9");
|
});
|
||||||
expect(resolvePackagedCommit("not-a-sha", "388fd0e9")).toBe("388fd0e9");
|
|
||||||
|
it("resolves only a full 40-character commit and lowercases it", () => {
|
||||||
|
// Every downstream check (packaged-runtime install, local install, release
|
||||||
|
// download, release scripts) requires 40 lowercase hex characters.
|
||||||
|
const embedded = "388fd0e9b3c41a2d5e6f7a8b9c0d1e2f3a4b5c6d";
|
||||||
|
expect(resolvePackagedCommit(undefined, embedded)).toBe(embedded);
|
||||||
|
expect(resolvePackagedCommit(embedded.toUpperCase())).toBe(embedded);
|
||||||
|
expect(
|
||||||
|
resolvePackagedCommit(` ${embedded.toUpperCase()} `, undefined),
|
||||||
|
).toBe(embedded);
|
||||||
|
expect(resolvePackagedCommit("388fd0e9", embedded)).toBe(embedded);
|
||||||
|
expect(resolvePackagedCommit("388fd0e9")).toBeUndefined();
|
||||||
|
expect(resolvePackagedCommit("not-a-sha", embedded)).toBe(embedded);
|
||||||
|
expect(resolvePackagedCommit(`${embedded}0`, embedded)).toBe(embedded);
|
||||||
|
expect(resolvePackagedCommit("g".repeat(40), embedded)).toBe(embedded);
|
||||||
|
expect(resolvePackagedCommit(undefined, "unknown")).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -143,6 +143,12 @@ export function resolvePackagedBuildNumber(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The packaged-runtime install, local install, release download and release
|
||||||
|
* scripts all require a full 40-character lowercase commit. Normalise here so
|
||||||
|
* that one format crosses this boundary: an uppercase full SHA is lowercased,
|
||||||
|
* and a short or malformed value is skipped so it cannot shadow the next source.
|
||||||
|
*/
|
||||||
export function resolvePackagedCommit(
|
export function resolvePackagedCommit(
|
||||||
environmentValue: unknown,
|
environmentValue: unknown,
|
||||||
embeddedValue?: unknown,
|
embeddedValue?: unknown,
|
||||||
@@ -150,7 +156,7 @@ export function resolvePackagedCommit(
|
|||||||
for (const value of [environmentValue, embeddedValue]) {
|
for (const value of [environmentValue, embeddedValue]) {
|
||||||
if (typeof value !== "string") continue;
|
if (typeof value !== "string") continue;
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (/^[0-9a-f]{7,40}$/i.test(trimmed)) return trimmed;
|
if (/^[0-9a-f]{40}$/i.test(trimmed)) return trimmed.toLowerCase();
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,20 @@ const { resolveBuildNumber, resolveBuildVersion } =
|
|||||||
};
|
};
|
||||||
const releaseVersion = resolveBuildVersion(process.env);
|
const releaseVersion = resolveBuildVersion(process.env);
|
||||||
const releaseBuildNumber = resolveBuildNumber(process.env);
|
const releaseBuildNumber = resolveBuildNumber(process.env);
|
||||||
const releaseCommit =
|
const releaseCommit = resolveReleaseCommit(process.env.ONE_MEMORY_COMMIT);
|
||||||
process.env.ONE_MEMORY_COMMIT?.trim() || readSourceCommit();
|
|
||||||
|
// The runtime accepts only a full 40-character commit, so an explicitly
|
||||||
|
// supplied malformed value fails the build instead of being baked and skipped.
|
||||||
|
function resolveReleaseCommit(explicit: string | undefined): string {
|
||||||
|
const trimmed = explicit?.trim();
|
||||||
|
if (!trimmed) return readSourceCommit();
|
||||||
|
if (!/^[0-9a-f]{40}$/i.test(trimmed)) {
|
||||||
|
throw new Error(
|
||||||
|
`ONE_MEMORY_COMMIT must be a full 40-character hex commit, got ${JSON.stringify(trimmed)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return trimmed.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
function readSourceCommit(): string {
|
function readSourceCommit(): string {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -189,14 +189,14 @@ node apps/cli/dist/one-memory.cjs settings features reset
|
|||||||
```bash
|
```bash
|
||||||
node apps/cli/dist/one-memory.cjs settings language # 界面语言和 SDK 提示语言
|
node apps/cli/dist/one-memory.cjs settings language # 界面语言和 SDK 提示语言
|
||||||
node apps/cli/dist/one-memory.cjs settings language ja # 设为日语,立即生效
|
node apps/cli/dist/one-memory.cjs settings language ja # 设为日语,立即生效
|
||||||
node apps/cli/dist/one-memory.cjs settings language system # 跟随系统(不指定 SDK 语言)
|
node apps/cli/dist/one-memory.cjs settings language system # 清除设置(不指定 SDK 语言)
|
||||||
```
|
```
|
||||||
|
|
||||||
- 可选 11 种:`en`、`zh-Hans`、`zh-Hant`、`ja`、`es`、`de`、`pt`、`fr`、`ko`、`ru`、`id`,以及 `system`;旧代码 `zh-CN` 按 `zh-Hans` 处理。
|
- 可选 11 种:`en`、`zh-Hans`、`zh-Hant`、`ja`、`es`、`de`、`pt`、`fr`、`ko`、`ru`、`id`,以及 `system`;旧代码 `zh-CN` 按 `zh-Hans` 处理。
|
||||||
- App 切换语言时会自动设置(Desktop 启动时也会同步一次已保存的选择)。全局设置,存于 `config.json` 的 `ui_language`;从未选择时不写入。
|
- App 切换语言时会自动设置;Desktop 每次启动也会同步一次界面实际显示的语言(App 选“跟随系统”时,发送它按系统语言解析出的语言,例如 `zh-Hans`)。全局设置,存于 `config.json` 的 `ui_language`;从未设置时不写入。
|
||||||
- `zh-Hans` 和 `system` 不向 SDK 传语言,SDK 照旧自动识别中文或英文;其余语言(包括 `en`)作为 `MemoryConfig.language` 传给 SDK,提取等提示使用该语言。
|
- 所有语言(包括 `zh-Hans` 和 `en`)都作为 `MemoryConfig.language` 传给 SDK,提取等提示使用该语言;只有未设置(`system`,或只用 CLI/API、从未打开 App 的安装)时不向 SDK 传语言,SDK 照旧自动识别中文或英文。
|
||||||
- 修改会像功能开关一样重建 SDK 客户端;不迁移数据,不改集合名(需要 hy-memory 2.0.11 的 `MemoryConfig.language`)。
|
- 修改会像功能开关一样重建 SDK 客户端;不迁移数据,不改集合名(需要 hy-memory 2.0.11 的 `MemoryConfig.language`)。
|
||||||
- `status --json` 的 `pipeline.ui_language` / `pipeline.sdk_language`(`auto` 或语言代码)。API:`GET`/`PUT /api/v1/settings/ui-language`。
|
- `status --json` 的 `pipeline.ui_language` / `pipeline.sdk_language`(`auto` 或语言代码;`zh-Hans` 用户显示 `zh-Hans`)。API:`GET`/`PUT /api/v1/settings/ui-language`。
|
||||||
|
|
||||||
## 8. 召回(recall)
|
## 8. 召回(recall)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user