Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ When extending `SettingsObject`, you must update **four** sync points (search th

And the `SettingsObject` type in [src/common/types/messaging.ts](src/common/types/messaging.ts).

### Languages (multilingual round 1)

The learner's languages come from the server's `language` module, built to the API contract in the docs repo (`subturtle-doc/docs/tech/multilingual-api.md`; work list rows E-01 to E-14).

- [src/common/language/api.ts](src/common/language/api.ts) is the only place that calls `get-language-state`, `get-language-capabilities`, `set-active-language` and `keep-phrase-pending`. The readers never throw: a missing function answers `null`.
- [src/stores/language.ts](src/stores/language.ts) holds the state. It is its own store on purpose: the profile store imports the sibling dashboard repo's types, and the translate service and the save window must not pull that chain in (Vitest in CI would read the dashboard's Nuxt tsconfig and fail). The profile bootstrap loads it, `captureSession` / `adoptSession` share it across bundles, and `LanguageStateChangedMessage` updates open tabs after the popup switches language.
- [src/common/language/state.ts](src/common/language/state.ts) has the pure rules (dark-launch gate, own-language resolution, save-row states, plan limit, dashboard paths). Test new rules there.
- **Dark launch (E-12):** with no state, or no active language, everything new is hidden and every request is the same as before (`language` and `translationLanguage` are sent only with a state). Keep it that way until the round ships.
- **One RTL list:** `RTL_LANGUAGES` in [catalog.ts](src/common/language/catalog.ts). Right-to-left text uses the bundled Vazirmatn (`static/assets/fonts`, added by [rtl-font.ts](src/common/helper/rtl-font.ts)).
- **Data without `language` is English.** Query English with `languageFilter("en")` (`{ $in: ["en", null] }`).

### Marker store (subtitle surfaces only)

[src/stores/marker.ts](src/stores/marker.ts) — central authority for word marking, hover, anchors, auto-clear timers. Used by `<Word>` / `<WordSelectionRectangle>` / `<SelectionAnchor>` / `<TranslatedPhrase>` in [src/subtitle/components/specific/](src/subtitle/components/specific/). **Nibble does not use the marker store** — it reads `text` + `context` from `window.getSelection()` and forwards them through the ConsoleCrane bridge.
Expand Down Expand Up @@ -435,6 +446,13 @@ tests/
translate-card.test.ts # popup translate input flow
login-password.test.ts # popup password form (ENABLE_PASSWORD_AUTH=true)
login-password-disabled.test.ts # popup password form hidden when flag is unset
language-state.test.ts # round-1 rules: dark gate, own language, save row, plan limit, deep link
language-catalog.test.ts # one RTL list, picker names, bundled Vazirmatn
own-language.test.ts # translate request with / without a server profile; language store
popup-languages.test.ts # popup pill, language list, switch, plan limit, due line
save-language.test.ts # save window language row, Keep / Save to, Undo, dark phase
language-lookups.test.ts # saved-phrase lookup, bundle suggestion, last-used bundles per language
language-analytics.test.ts # learning_language and target_language super properties
e2e/
extension-fixture.ts # chromium.launchPersistentContext + extension load
server.mjs # static fixtures HTTP server
Expand All @@ -450,7 +468,7 @@ tests/

### Test totals

138 unit / component tests across 19 files; 16 E2E specs across 6 files. Full suite runs in ~20s once Playwright's Chromium is warm.
348 unit / component tests across 38 files; 16 E2E specs across 6 files. Full suite runs in ~20s once Playwright's Chromium is warm.

## Cloud agent workflow (Claude Code on the Web)

Expand Down
21 changes: 18 additions & 3 deletions src/console-crane/modules/settings/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,15 @@
<!-- Language Settings -->
<Fieldset class="mb-6 dark:bg-gray-900" legend="Language">
<div class="flex flex-col space-y-4">
<div class="flex items-center justify-between">
<span class="text-lg dark:text-gray-100">Interface Language</span>
<!-- The own language (translations are written in it). With
language profiles it lives on the server (E-01) and is changed
on the dashboard; before that, the local select as before. -->
<div v-if="languages.isMultilingual" class="flex items-center justify-between gap-4" data-testid="settings-own-language">
<span class="text-lg dark:text-gray-100">Your own language</span>
<span class="text-sm text-gray-600 dark:text-gray-300" dir="auto">{{ ownLanguage }}</span>
</div>
<div v-else class="flex items-center justify-between">
<span class="text-lg dark:text-gray-100">Your own language</span>
<SelectTarget class="w-48" />
</div>
</div>
Expand All @@ -38,11 +45,19 @@
</template>

<script setup lang="ts">
import { inject, onMounted } from "vue";
import { computed, inject, onMounted } from "vue";
import Fieldset from "../../../common/components/Fieldset.vue";
import SelectTarget from "../../../common/components/inputs/SelectTarget.vue";
import ThemeSwitcher from "../../../common/components/inputs/ThemeSwitcher.vue";
import { analytic } from "../../../plugins/mixpanel";
import { useLanguageStore } from "../../../stores/language";
import { pickerLabel } from "../../../common/language/catalog";

// The own language from the server profile (E-01).
const languages = useLanguageStore();
const ownLanguage = computed(() =>
pickerLabel(languages.state?.translationLanguage, languages.caps)
);

// Inject frame size from modal
const frameSize = inject<{ width: number; height: number }>("frameSize");
Expand Down
37 changes: 37 additions & 0 deletions tests/language-analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, it, expect, beforeEach } from "vitest";
import mixpanel from "mixpanel-browser";
import {
registerLanguageAnalytics,
registerSettingsAnalytics,
} from "../src/plugins/mixpanel";

/**
* E-13: the learning language next to target_language, and target_language
* from the server profile (643 of 802 users had none from the local guess).
* Runs first-to-last in one module: the "server named it" flag is module state.
*/
describe("language analytics (E-13)", () => {
const register = mixpanel.register as unknown as ReturnType<typeof import("vitest").vi.fn>;

beforeEach(() => register.mockClear());

it("before the server names the own language, the local value is sent", () => {
registerSettingsAnalytics({ target_language: "fa", theme: "dark" });
expect(register).toHaveBeenCalledWith({ target_language: "fa", theme: "dark" });
});

it("registers the learning language and the server's own language", () => {
registerLanguageAnalytics({ learningLanguage: "de", translationLanguage: "fa" });
expect(register).toHaveBeenCalledWith({ learning_language: "de", target_language: "fa" });
});

it("after that, the local value no longer overwrites target_language", () => {
registerSettingsAnalytics({ target_language: "ar", theme: "light" });
expect(register).toHaveBeenCalledWith({ theme: "light" });
});

it("sends nothing when the state has no languages", () => {
registerLanguageAnalytics({ learningLanguage: null, translationLanguage: null });
expect(register).not.toHaveBeenCalled();
});
});
Loading