feat(hooks): add AI chat and project presence hooks

This commit is contained in:
ZhenYi 2026-05-17 16:38:18 +08:00
parent 30cc9f885e
commit 88a51f45cb
26 changed files with 5269 additions and 608 deletions

View File

@ -127,12 +127,13 @@ export function useChatStreamRunner(setIsStreaming?: (value: boolean) => void) {
const queryClient = useQueryClient();
const streamingStore = useStreamingStore();
return async (conversationId: string, messageId: string) => {
return async (conversationId: string, messageId: string, signal?: AbortSignal) => {
streamingStore.clear(conversationId);
setIsStreaming?.(true);
let clearOnFinish = false;
try {
for await (const chunk of streamChat(conversationId, messageId)) {
if (signal?.aborted) break;
if (chunk.type === "token") {
streamingStore.append(conversationId, "token", String(chunk.data || ""), messageId);
} else if (chunk.type === "thinking") {
@ -143,14 +144,22 @@ export function useChatStreamRunner(setIsStreaming?: (value: boolean) => void) {
content: chunk.data?.display || chunk.data?.tool || "",
toolName: chunk.data?.tool || "unknown",
toolArgs: chunk.data?.args || {},
children_id: chunk.children_id,
}, messageId);
} else if (chunk.type === "tool_result") {
const isSubAgent = chunk.data?.tool === "call_sub_agent";
const toolName = chunk.data?.tool || "unknown";
// For call_sub_agent, use the display field from metadata
const content = isSubAgent
? (chunk.data?.display || "")
: (chunk.data?.result || chunk.data?.display || "");
streamingStore.addToolPart(conversationId, {
type: "tool_result",
content: String(chunk.data?.result || chunk.data?.display || ""),
toolName: chunk.data?.tool || "unknown",
toolArgs: {},
content,
toolName,
toolStatus: chunk.data?.status || "ok",
children_id: chunk.children_id,
...(isSubAgent && { subAgentOutput: chunk.data?.output, toolArgs: { role: chunk.data?.role, task: chunk.data?.task } }),
}, messageId);
} else if (chunk.type === "title") {
queryClient.invalidateQueries({ queryKey: [CONVERSATIONS_KEY, conversationId] });

View File

@ -1,6 +1,7 @@
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
import { apiAuthMe, apiAuthLogin, apiAuthLogout } from "@/client/api";
import type { ContextMe, LoginParams } from "@/client/model";
import { applyLocalePreference } from "@/i18n/T";
const QUERY_KEY = "auth";
@ -9,7 +10,11 @@ export function useCurrentUserQuery() {
queryKey: [QUERY_KEY, "me"],
queryFn: async (): Promise<ContextMe | null> => {
const res = await apiAuthMe();
return (res.data?.data as ContextMe) ?? null;
const user = (res.data?.data as ContextMe) ?? null;
if (user?.language) {
applyLocalePreference(user.language);
}
return user;
},
staleTime: 5 * 60 * 1000,
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -28,7 +28,7 @@ export function useIssueDetailQuery({
queryKey: [ISSUE_DETAIL_QUERY_KEY, projectName, issueNumber],
queryFn: async (): Promise<IssueResponse> => {
const res = await issueGet(projectName, issueNumber);
return res.data.data!;
return res.data.data ?? null;
},
enabled: !!projectName && !!issueNumber,
});
@ -59,7 +59,7 @@ export function useCreateCommentMutation() {
}: IssueDetailParams & { body: string }) => {
const req: IssueCommentCreateRequest = { body };
const res = await issueCommentCreate(projectName, issueNumber, req);
return res.data.data!;
return res.data.data ?? null;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
@ -81,7 +81,7 @@ export function useUpdateCommentMutation() {
}: IssueDetailParams & { commentId: number; body: string }) => {
const req: IssueCommentUpdateRequest = { body };
const res = await issueCommentUpdate(projectName, issueNumber, commentId, req);
return res.data.data!;
return res.data.data ?? null;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({

View File

@ -1,3 +1,4 @@
import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { projectPresence } from "@/client/api";
import type { PresenceChanged, PresenceStatus } from "@/client/model";
@ -35,13 +36,15 @@ export function useProjectPresenceQuery(projectId: string | undefined) {
*/
export function useProjectPresenceMap(projectId: string | undefined) {
const query = useProjectPresenceQuery(projectId);
const presenceMap = new Map<string, PresenceStatus>();
if (query.data) {
for (const p of query.data) {
presenceMap.set(p.user_id, p.effectiveStatus);
const presenceMap = useMemo(() => {
const map = new Map<string, PresenceStatus>();
if (query.data) {
for (const p of query.data) {
map.set(p.user_id, p.effectiveStatus);
}
}
}
return map;
}, [query.data]);
return { presenceMap, isLoading: query.isLoading, error: query.error };
}

View File

@ -33,7 +33,9 @@ export function useRepoDetailQuery({ namespace, repo }: RepoParams) {
return useQuery({
queryKey: [REPO_DETAIL_QUERY_KEY, namespace, repo],
queryFn: async () => {
return { namespace, repo };
const res = await projectRepos(namespace);
const items = res.data.data?.items ?? [];
return items.find((item) => item.repo_name === repo) ?? null;
},
enabled: !!namespace && !!repo,
});

View File

@ -1,8 +1,8 @@
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
import { roomList, categoryList, roomCreate, categoryCreate } from "@/client/api";
import { roomList, categoryList, roomCreate, categoryCreate, categoryDelete, roomUpdate } from "@/client/api";
import type { RoomResponse, RoomCategoryResponse, RoomCreateRequest, RoomCategoryCreateRequest } from "@/client/model";
const QUERY_KEY = "rooms";
export const ROOMS_QUERY_KEY = "rooms";
export interface RoomWithState extends RoomResponse {
isMuted?: boolean;
@ -18,7 +18,7 @@ export interface RoomCategory {
export function useRoomsQuery(projectName: string | undefined) {
return useQuery({
queryKey: [QUERY_KEY, projectName],
queryKey: [ROOMS_QUERY_KEY, projectName],
queryFn: async (): Promise<{ rooms: RoomWithState[]; categories: RoomCategory[] }> => {
if (!projectName) return { rooms: [], categories: [] };
const [roomsRes, categoriesRes] = await Promise.all([
@ -47,7 +47,7 @@ export function useRoomsQuery(projectName: string | undefined) {
export function useInvalidateRooms() {
const queryClient = useQueryClient();
return (projectName: string) => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY, projectName] });
queryClient.invalidateQueries({ queryKey: [ROOMS_QUERY_KEY, projectName] });
};
}
@ -80,3 +80,29 @@ export function useCreateCategoryMutation(projectName: string | undefined) {
},
});
}
export function useMoveRoomMutation(projectName: string | undefined) {
const invalidate = useInvalidateRooms();
return useMutation({
mutationFn: async ({ roomId, category }: { roomId: string; category: string | null }) => {
await roomUpdate(roomId, { category });
},
onSuccess: () => {
if (projectName) invalidate(projectName);
},
});
}
export function useDeleteCategoryMutation(projectName: string | undefined) {
const invalidate = useInvalidateRooms();
return useMutation({
mutationFn: async (categoryId: string) => {
await categoryDelete(categoryId);
},
onSuccess: () => {
if (projectName) invalidate(projectName);
},
});
}

View File

@ -1,6 +1,7 @@
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query";
import { skillList, skillGet, skillCreate, skillUpdate, skillDelete, skillScan } from "@/client/api";
import type { SkillResponse, CreateSkillRequest, UpdateSkillRequest } from "@/client/model";
import { toast } from "sonner";
const QUERY_KEY = "skills";
@ -92,6 +93,10 @@ export function useScanSkillsMutation(projectName: string | undefined) {
},
onSuccess: () => {
if (projectName) invalidate(projectName);
toast.success("Skill scan completed");
},
onError: (error) => {
toast.error(error instanceof Error ? error.message : "Skill scan failed");
},
});
}

View File

@ -103,7 +103,8 @@ any)?.data?.count ?? 0;
});
}
export function useUserActivityQuery(username: string, page = 1, perPage = 20) {
export function useUserActivityQuery(username: string, page = 1, perPage = 20, opts?: { enabled?: boolean }) {
const enabled = opts?.enabled !== undefined ? opts.enabled && !!username : !!username;
return useQuery({
queryKey: [USER_QUERY_KEY, username, "activity", { page, perPage }],
queryFn: async (): Promise<{ items: UserActivityItem[]; total: number }> => {
@ -114,19 +115,20 @@ export function useUserActivityQuery(username: string, page = 1, perPage = 20) {
total: Number(res.data?.data?.total ?? 0),
};
},
enabled: !!username,
enabled,
staleTime: 2 * 60 * 1000,
});
}
export function useUserStarsQuery(username: string) {
export function useUserStarsQuery(username: string, opts?: { enabled?: boolean }) {
const enabled = opts?.enabled !== undefined ? opts.enabled && !!username : !!username;
return useQuery({
queryKey: [USER_QUERY_KEY, username, "stars"],
queryFn: async (): Promise<UserStarsResponse | null> => {
const res = await getUserStars(username);
return res.data?.data ?? null;
},
enabled: !!username,
enabled,
staleTime: 5 * 60 * 1000,
});
}
@ -175,26 +177,28 @@ export function useUserHeatmapQuery(username: string, startDate?: string, endDat
});
}
export function useUserFollowersQuery(username: string) {
export function useUserFollowersQuery(username: string, opts?: { enabled?: boolean }) {
const enabled = opts?.enabled !== undefined ? opts.enabled && !!username : !!username;
return useQuery({
queryKey: [USER_QUERY_KEY, username, "followers"],
queryFn: async (): Promise<SubscriptionInfo[]> => {
const res = await getSubscribers(username);
return res.data?.data ?? [];
},
enabled: !!username,
enabled,
staleTime: 5 * 60 * 1000,
});
}
export function useUserFollowingQuery(username: string) {
export function useUserFollowingQuery(username: string, opts?: { enabled?: boolean }) {
const enabled = opts?.enabled !== undefined ? opts.enabled && !!username : !!username;
return useQuery({
queryKey: [USER_QUERY_KEY, username, "following"],
queryFn: async (): Promise<UserCard[]> => {
const res = await getFollowingList(username);
return res.data?.data ?? [];
},
enabled: !!username,
enabled,
staleTime: 5 * 60 * 1000,
});
}

View File

@ -0,0 +1,203 @@
import {useMemo, useSyncExternalStore} from "react";
import en from "./en.json";
import zh from "./zh.json";
import jp from "./jp.json";
import fr from "./fr.json";
import de from "./de.json";
export type Locale = "en" | "zh" | "jp" | "fr" | "de";
export type TranslationParams = Record<string, string | number | boolean | null | undefined>;
type TranslationDictionary = {
[key: string]: string | TranslationDictionary;
};
const STORAGE_KEY = "locale";
const DEFAULT_LOCALE: Locale = "en";
const translations: Record<Locale, TranslationDictionary> = {
en: en as TranslationDictionary,
zh: zh as TranslationDictionary,
jp: jp as TranslationDictionary,
fr: fr as TranslationDictionary,
de: de as TranslationDictionary,
};
const localeAliases: Record<string, Locale> = {
cn: "zh",
"zh-cn": "zh",
"zh-hans": "zh",
"zh-sg": "zh",
ja: "jp",
"ja-jp": "jp",
jp: "jp",
};
export const locales: { value: Locale; label: string }[] = [
{value: "en", label: "English"},
{value: "zh", label: "\u7b80\u4f53\u4e2d\u6587"},
{value: "jp", label: "\u65e5\u672c\u8a9e"},
{value: "fr", label: "Fran\u00e7ais"},
{value: "de", label: "Deutsch"},
];
const listeners = new Set<() => void>();
function canUseBrowserApis(): boolean {
return typeof window !== "undefined" && typeof document !== "undefined";
}
function getNestedValue(obj: unknown, path: string): string | undefined {
const keys = path.split(".");
let value: unknown = obj;
for (const key of keys) {
if (value && typeof value === "object" && key in value) {
value = (value as Record<string, unknown>)[key];
} else {
return undefined;
}
}
return typeof value === "string" ? value : undefined;
}
export function normalizeLocale(value: unknown): Locale | null {
if (typeof value !== "string") return null;
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
if (!normalized) return null;
if (normalized in translations) return normalized as Locale;
if (normalized in localeAliases) return localeAliases[normalized];
const language = normalized.split("-")[0];
if (language in translations) return language as Locale;
if (language in localeAliases) return localeAliases[language];
return null;
}
function getBrowserLocale(): Locale {
const candidates = navigator.languages?.length
? navigator.languages
: [navigator.language];
for (const candidate of candidates) {
const locale = normalizeLocale(candidate);
if (locale) return locale;
}
return DEFAULT_LOCALE;
}
function getInitialLocale(): Locale {
if (!canUseBrowserApis()) return DEFAULT_LOCALE;
try {
const stored = normalizeLocale(window.localStorage.getItem(STORAGE_KEY));
if (stored) return stored;
} catch {
// Storage unavailable — fall through to browser locale.
}
return getBrowserLocale();
}
let currentLocale: Locale = getInitialLocale();
function emitLocaleChange(): void {
for (const listener of listeners) listener();
}
export function getCurrentLocale(): Locale {
return currentLocale;
}
export function getLocaleFromStorage(): Locale {
if (!canUseBrowserApis()) return currentLocale;
try {
return normalizeLocale(window.localStorage.getItem(STORAGE_KEY)) ?? currentLocale;
} catch {
return currentLocale;
}
}
export function setLocale(locale: Locale | string): Locale {
const nextLocale = normalizeLocale(locale) ?? DEFAULT_LOCALE;
if (canUseBrowserApis()) {
document.documentElement.lang = nextLocale;
try {
window.localStorage.setItem(STORAGE_KEY, nextLocale);
} catch {
// Locale still updates in memory when storage is unavailable.
}
}
if (currentLocale !== nextLocale) {
currentLocale = nextLocale;
emitLocaleChange();
}
return nextLocale;
}
export function applyLocalePreference(locale: unknown): Locale {
const nextLocale = normalizeLocale(locale);
return nextLocale ? setLocale(nextLocale) : currentLocale;
}
export function subscribeLocaleChange(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
// ─── Translation ───────────────────────────────────────────────────────────
// Single regex replaces all ${key} placeholders in one pass,
// avoiding repeated RegExp compilation and multiple string traversals.
const PARAM_PATTERN = /\$\{(\w+)\}/g;
export function createT(locale?: Locale) {
return function translate(key: string, params?: TranslationParams): string {
let text =
getNestedValue(translations[locale ?? currentLocale], key) ??
getNestedValue(translations[DEFAULT_LOCALE], key) ??
key;
if (params) {
text = text.replace(PARAM_PATTERN, (_, k) => String(params[k] ?? ""));
}
return text;
};
}
export const t = createT();
export function useLocale(): Locale {
return useSyncExternalStore(subscribeLocaleChange, getCurrentLocale, () => DEFAULT_LOCALE);
}
export function useT() {
const locale = useLocale();
return useMemo(() => createT(locale), [locale]);
}
export {translations};
if (canUseBrowserApis()) {
document.documentElement.lang = currentLocale;
window.addEventListener("storage", (event) => {
if (event.key !== STORAGE_KEY) return;
const nextLocale = normalizeLocale(event.newValue) ?? getBrowserLocale();
if (currentLocale === nextLocale) return;
currentLocale = nextLocale;
document.documentElement.lang = nextLocale;
emitLocaleChange();
});
}

View File

@ -0,0 +1,831 @@
{
"auth": {
"login": {
"title": "Willkommen zurück!",
"subtitle": "Wir freuen uns, Sie wiederzusehen!",
"account": "Konto",
"account_required": "Bitte Benutzernamen eingeben",
"account_placeholder": "Benutzername oder E-Mail",
"password": "Passwort",
"password_required": "Bitte Passwort eingeben",
"verification": "Verifizierung",
"captcha_placeholder": "Code eingeben",
"captcha_required": "Bitte Code eingeben",
"captcha_title": "Zum Aktualisieren klicken",
"2fa_code": "2FA-Code",
"2fa_placeholder": "6-stelliger Code",
"forgot_password": "Passwort vergessen?",
"submit": "Anmelden",
"submit_loading": "Anmeldung läuft...",
"need_account": "Noch kein Konto?",
"register": "Registrieren",
"error": {
"failed_to_load_captcha": "Captcha konnte nicht geladen werden",
"two_factor_required": "Zwei-Faktor-Authentifizierung erforderlich",
"invalid_credentials": "Ungültiger Benutzername oder Passwort",
"login_failed": "Anmeldung fehlgeschlagen"
}
},
"register": {
"title": "Konto erstellen",
"subtitle": "Registrieren Sie sich noch heute!",
"username": "Benutzername",
"username_placeholder": "Benutzernamen wählen",
"username_required": "Benutzername ist erforderlich",
"username_min_length": "Benutzername muss mindestens 3 Zeichen lang sein",
"username_pattern": "Benutzername darf nur Buchstaben, Zahlen, _ und - enthalten",
"email": "E-Mail",
"email_placeholder": "E-Mail eingeben",
"email_required": "E-Mail ist erforderlich",
"email_invalid": "Ungültige E-Mail-Adresse",
"password_placeholder": "Passwort erstellen",
"confirm_password": "Passwort bestätigen",
"confirm_password_placeholder": "Passwort bestätigen",
"confirm_password_required": "Bitte bestätigen Sie Ihr Passwort",
"password_min_length": "Passwort muss mindestens 8 Zeichen lang sein",
"captcha_placeholder": "Code eingeben",
"submit": "Konto erstellen",
"submit_loading": "Wird erstellt...",
"already_have_account": "Sie haben bereits ein Konto?",
"login": "Anmelden",
"passwords_not_match": "Passwörter stimmen nicht überein",
"user_exists": "Benutzername oder E-Mail existiert bereits",
"registration_failed": "Registrierung fehlgeschlagen"
},
"forgot_password": {
"email_placeholder": "E-Mail eingeben",
"captcha_placeholder": "Code eingeben",
"submit": "Weiter",
"back_to_login": "Zurück zur Anmeldung"
},
"reset_password": {
"new_password_placeholder": "Neues Passwort eingeben",
"confirm_password_placeholder": "Neues Passwort bestätigen",
"captcha_placeholder": "Code eingeben",
"submit": "Passwort zurücksetzen",
"back_to_login": "Zurück zur Anmeldung"
},
"change_password": {
"title": "Passwort ändern",
"subtitle": "Passwort Ihres Kontos aktualisieren",
"current_password_placeholder": "Aktuelles Passwort eingeben",
"new_password_placeholder": "Neues Passwort eingeben",
"confirm_password_placeholder": "Neues Passwort bestätigen",
"captcha_placeholder": "Code eingeben",
"submit": "Passwort aktualisieren",
"back_to_login": "Zurück zur Anmeldung"
},
"two_factor": {
"title": "Zwei-Faktor-Authentifizierung",
"enabled": "2FA ist derzeit aktiviert",
"disabled": "Eine zusätzliche Sicherheitsebene hinzufügen",
"description": "Die Zwei-Faktor-Authentifizierung fügt eine zusätzliche Sicherheitsebene hinzu, indem mehr als nur ein Passwort für die Anmeldung erforderlich ist.",
"enabled_message": "Die Zwei-Faktor-Authentifizierung ist derzeit für Ihr Konto aktiviert.",
"scan_qr_code": "QR-Code scannen",
"or_enter_manually": "Oder diesen Code manuell eingeben",
"verification_code": "Verifizierungscode",
"code_required": "Code ist erforderlich",
"code_placeholder": "6-stelligen Code eingeben",
"password_placeholder": "Passwort eingeben",
"submit": "Verifizieren",
"cancel": "Abbrechen",
"back": "Zurück",
"enable": "2FA aktivieren",
"disable": "2FA deaktivieren",
"disabling": "Deaktivierung...",
"error": {
"load_failed": "Fehler beim Laden des 2FA-Status",
"enable_failed": "2FA konnte nicht aktiviert werden",
"disable_failed": "2FA konnte nicht deaktiviert werden",
"invalid_code": "Ungültiger Verifizierungscode"
}
},
"verify_email": {
"title": "E-Mail verifizieren"
}
},
"common": {
"actions": {
"save": "Speichern",
"save_changes": "Änderungen speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"create": "Erstellen",
"submit": "Absenden",
"close": "Schließen",
"confirm": "Bestätigen",
"back": "Zurück",
"remove": "Entfernen",
"add": "Hinzufügen",
"retry": "Wiederholen",
"generate": "Generieren",
"loading": "Wird geladen..."
},
"states": {
"loading": "Wird geladen...",
"no_results": "Keine Ergebnisse gefunden",
"error_occurred": "Ein Fehler ist aufgetreten"
},
"placeholders": {
"search": "Suchen...",
"message": "Nachricht"
}
},
"navigation": {
"search": "Suchen",
"search_shortcut": "Suchen (Ctrl+K)",
"favorites": "Favorisierte Nachrichten",
"no_channels": "Keine Kanäle"
},
"settings": {
"access_keys": {
"title": "Persönliche Zugriffstokens",
"description": "Tokens, die Sie für den API-Zugriff generiert haben.",
"generate_button": "Neues Token generieren",
"token_name": "Token-Name",
"copy_warning": "Kopieren Sie Ihr persönliches Zugriffstoken jetzt. Sie können es nicht mehr sehen!",
"empty": "Keine persönlichen Zugriffstokens gefunden.",
"created_on": "Erstellt am",
"expires": "Läuft ab",
"generate_token": "Token generieren",
"messages": {
"name_required": "Bitte Token-Namen eingeben",
"created_success": "Token erstellt, bitte jetzt kopieren.",
"delete_success": "Token gelöscht",
"create_failed": "Erstellung fehlgeschlagen, bitte erneut versuchen",
"delete_failed": "Löschen fehlgeschlagen"
}
},
"ssh_keys": {
"title": "SSH-Schlüssel",
"description": "SSH-Schlüssel für Git-Operationen verwalten",
"add_button": "Schlüssel hinzufügen",
"add_title": "Neuen SSH-Schlüssel hinzufügen",
"title_label": "Titel",
"public_key_label": "Öffentlicher Schlüssel",
"name_placeholder": "z.B. Persönlicher MacBook",
"key_placeholder": "ssh-rsa AAAAB3NzaC... user@host",
"empty_title": "Noch keine SSH-Schlüssel hinzugefügt",
"empty_desc": "Fügen Sie SSH-Schlüssel hinzu, um Git-Operationen sicher durchzuführen",
"verified": "Verifiziert",
"messages": {
"title_key_required": "Bitte geben Sie einen Titel und einen öffentlichen Schlüssel ein",
"add_success": "SSH-Schlüssel erfolgreich hinzugefügt",
"add_failed": "Hinzufügen fehlgeschlagen, überprüfen Sie das Schlüsselformat",
"delete_success": "SSH-Schlüssel gelöscht",
"delete_failed": "Löschen fehlgeschlagen",
"title_updated": "Titel aktualisiert",
"update_failed": "Aktualisierung fehlgeschlagen"
}
},
"password": {
"title": "Passwort ändern",
"subtitle": "Schützen Sie Ihr Konto mit einem starken Passwort",
"current_password": "Aktuelles Passwort",
"current_password_placeholder": "Aktuelles Passwort eingeben",
"new_password": "Neues Passwort",
"new_password_placeholder": "Neues Passwort eingeben (mindestens 8 Zeichen)",
"confirm_password": "Neues Passwort bestätigen",
"confirm_password_placeholder": "Neues Passwort erneut eingeben",
"submit": "Passwort ändern",
"mismatch": "Passwörter stimmen nicht überein",
"min_length": "Passwort muss mindestens 8 Zeichen lang sein",
"change_success": "Passwort erfolgreich geändert",
"change_failed": "Passwortänderung fehlgeschlagen, überprüfen Sie Ihr aktuelles Passwort"
},
"account": {
"title": "Mein Konto",
"display_name_placeholder": "Anzeigenamen festlegen",
"website_placeholder": "https://example.com",
"company_placeholder": "Organisation (optional)"
},
"email": {
"title": "E-Mail",
"current_email": "Aktuelle E-Mail",
"new_email_placeholder": "new@example.com",
"current_password_placeholder": "Aktuelles Passwort eingeben"
},
"billing": {
"title": "Abrechnung",
"personal_billing": "Persönliche Abrechnung",
"monthly_quota": "Monatliches Kontingent",
"monthly_usage": "Monatliche Nutzung",
"billing_errors": "Abrechnungsfehler",
"no_history": "Keine Abrechnungshistorie"
},
"appearance": {
"title": "Erscheinungsbild",
"timezone_asia_shanghai": "Asien/Shanghai (UTC+8)",
"timezone_asia_tokyo": "Asien/Tokyo (UTC+9)",
"timezone_america_ny": "Amerika/New_York (UTC-5)",
"timezone_america_la": "Amerika/Los_Angeles (UTC-8)",
"timezone_europe_london": "Europa/London (UTC+0)"
},
"notifications": {
"title": "Benachrichtigungen"
},
"settings_nav": {
"user_settings": "Benutzereinstellungen",
"my_account": "Mein Konto",
"billing": "Abrechnung",
"appearance": "Erscheinungsbild",
"notifications": "Benachrichtigungen",
"push_settings": "Push-Einstellungen",
"security": "Sicherheit",
"change_password": "Passwort ändern",
"email": "E-Mail",
"ssh_keys": "SSH-Schlüssel",
"access_tokens": "Zugriffstokens"
},
"my_account": {
"title": "Mein Konto",
"subtitle": "Verwalten Sie Ihre persönlichen Daten",
"avatar": "Avatar",
"upload_avatar": "Neues Avatar hochladen",
"remove": "Entfernen",
"avatar_hint": "Unterstützt JPG, PNG oder GIF. Max. 2 MB.",
"username": "Benutzername",
"display_name": "Anzeigename",
"display_name_placeholder": "Anzeigenamen festlegen",
"website": "Website",
"website_placeholder": "https://example.com",
"organization": "Organisation",
"org_placeholder": "Organisation (optional)",
"save_changes": "Änderungen speichern",
"load_failed": "Profil konnte nicht geladen werden",
"save_success": "Profil gespeichert",
"save_failed": "Speichern fehlgeschlagen, bitte erneut versuchen",
"avatar_size_error": "Bild muss kleiner als 2 MB sein",
"avatar_upload_success": "Avatar hochgeladen, bitte Änderungen speichern",
"avatar_upload_failed": "Avatar-Upload fehlgeschlagen"
},
"email_page": {
"title": "E-Mail",
"subtitle": "Verwalten Sie Ihre E-Mail-Adresse",
"current_email": "Aktuelle E-Mail",
"no_email_set": "Keine E-Mail gesetzt",
"new_email": "Neue E-Mail",
"current_password": "Aktuelles Passwort (zur Verifizierung)",
"current_password_placeholder": "Aktuelles Passwort eingeben",
"save_button": "E-Mail ändern",
"load_failed": "E-Mail-Informationen konnten nicht geladen werden",
"fill_all_fields": "Bitte füllen Sie alle Felder aus",
"verification_sent": "Bestätigungs-E-Mail gesendet, bitte prüfen Sie Ihr neues Postfach",
"change_failed": "E-Mail-Änderung fehlgeschlagen, bitte Passwort prüfen"
},
"notifications_page": {
"title": "Benachrichtigungen",
"subtitle": "Verwalten Sie Ihre Benachrichtigungseinstellungen",
"channels": "Benachrichtigungskanäle",
"email_notifications": "E-Mail-Benachrichtigungen",
"email_notifications_desc": "Benachrichtigungen per E-Mail erhalten",
"in_app_notifications": "In-App-Benachrichtigungen",
"in_app_notifications_desc": "Erinnerungen in der App erhalten",
"push_notifications": "Push-Benachrichtigungen",
"push_notifications_desc": "Benachrichtigungen über Browser-Push erhalten",
"digest_mode": "Zusammenfassungsmodus",
"instant": "Sofort",
"daily_digest": "Tägliche Zusammenfassung",
"weekly_digest": "Wöchentliche Zusammenfassung",
"off": "Aus",
"notification_types": "Benachrichtigungstypen",
"security_notifications": "Sicherheitsbenachrichtigungen",
"security_notifications_desc": "Kontosicherheitsbezogene Benachrichtigungen",
"product_updates": "Produkt-Updates",
"product_updates_desc": "Neue Funktionen und Produktupdate-Benachrichtigungen",
"marketing_emails": "Marketing-E-Mails",
"marketing_emails_desc": "Werbung und Angebote",
"do_not_disturb": "Nicht stören",
"enable_dnd": "Nicht stören aktivieren",
"enable_dnd_desc": "Keine Benachrichtigungen während eines bestimmten Zeitraums erhalten",
"save_button": "Änderungen speichern",
"load_failed": "Benachrichtigungseinstellungen konnten nicht geladen werden",
"save_success": "Benachrichtigungseinstellungen gespeichert",
"save_failed": "Speichern fehlgeschlagen, bitte erneut versuchen"
},
"appearance_page": {
"title": "Erscheinungsbild",
"subtitle": "Passen Sie das Aussehen und die Sprache der App an",
"theme_scheme": "Themenschema",
"select_theme_scheme": "Themenschema auswählen",
"theme": "Thema",
"language": "Sprache",
"timezone": "Zeitzone",
"dark": "Dunkel",
"light": "Hell",
"system": "System",
"basic_settings": "Grundeinstellungen",
"custom": "Benutzerdefiniert",
"save_button": "Änderungen speichern",
"load_failed": "Einstellungen konnten nicht geladen werden",
"save_success": "Erscheinungseinstellungen gespeichert",
"save_failed": "Speichern fehlgeschlagen, bitte erneut versuchen"
},
"billing": {
"title": "Abrechnung",
"personal_billing": "Persönliche Abrechnung",
"balance": "Guthaben",
"monthly_quota": "Monatliches Kontingent",
"monthly_usage": "Monatliche Nutzung",
"billing_errors": "Abrechnungsfehler",
"history": "Abrechnungshistorie",
"date": "Datum",
"reason": "Grund",
"amount": "Betrag",
"no_history": "Keine Abrechnungshistorie",
"load_failed": "Abrechnung konnte nicht geladen werden",
"insufficient_balance": "Unzureichendes Guthaben"
},
"push": {
"title": "Push-Benachrichtigungen",
"subtitle": "Erhalten Sie Echtzeit-Benachrichtigungen, auch wenn die App geschlossen ist.",
"enable": "Push-Benachrichtigungen aktivieren",
"enable_desc": "Erhalten Sie Benachrichtigungen über Erwähnungen, Issues und Systemmeldungen.",
"filters_title": "Benachrichtigungsfilter",
"mentions": "Erwähnungen und Antworten",
"new_issues": "Neue Issues",
"system_updates": "System-Updates",
"coming_soon": "Detailliertere Steuerung in Kürze verfügbar.",
"not_supported": "Push-Benachrichtigungen werden nicht unterstützt",
"not_supported_desc": "Ihr Browser oder Ihre Umgebung unterstützt kein Web Push. Bitte verwenden Sie einen modernen Desktop-Browser.",
"load_failed": "Fehler beim Laden der Benachrichtigungseinstellungen",
"permission_denied": "Benachrichtigungsberechtigung vom Browser verweigert",
"update_failed": "Fehler beim Aktualisieren der Push-Einstellungen",
"saved": "Einstellungen erfolgreich gespeichert"
}
},
"project": {
"layout": {
"expand_sidebar": "Seitenleiste erweitern",
"join_banner": {
"preview_mode": "Vorschaumodus",
"join_to_participate": "Werden Sie Mitglied dieses Projekts, um teilzunehmen und Tools zu nutzen.",
"read_only": "Dieses öffentliche Projekt ist schreibgeschützt, bis Sie beitreten.",
"join_to_use": "Sie müssen beitreten, bevor Sie Projektaktionen nutzen können.",
"apply_to_join": "Beitritt beantragen"
}
},
"invitation": {
"title": "Projekteinladung",
"no_pending": "Keine ausstehenden Einladungen"
},
"join": {
"title": "${project} beitreten",
"reason_placeholder": "Erklären Sie den Admins, warum Sie beitreten möchten.",
"cancel_request": "Anfrage abbrechen",
"join_without_reason": "Projekt beitreten",
"submit_request": "Beitrittsanfrage senden",
"default_desc": "Senden Sie eine Anfrage, um Projektmitglied zu werden.",
"already_member": "Sie sind bereits Mitglied dieses Projekts.",
"open_project": "Projekt öffnen",
"current_request": "Aktuelle Anfrage",
"submitted_on": "Eingereicht am",
"message": "Nachricht",
"message_desc": "Optional, aber nützlich für private oder genehmigungspflichtige Projekte.",
"submitted": "Beitrittsanfrage gesendet.",
"submit_failed": "Fehler beim Senden der Beitrittsanfrage.",
"cancelled": "Beitrittsanfrage storniert.",
"cancel_failed": "Fehler beim Stornieren der Beitrittsanfrage."
},
"settings": {
"general": {
"title": "Projektidentität",
"project_name": "Projektname",
"project_name_placeholder": "z.B. my-awesome-project",
"display_name": "Anzeigename",
"display_name_placeholder": "z.B. Mein tolles Projekt",
"description": "Beschreibung",
"description_placeholder": "Beschreiben Sie das Projekt...",
"leave": "Dieses Projekt verlassen",
"leave_desc": "Sie verlieren den Zugriff auf alle Projektressourcen.",
"leave_btn": "Projekt verlassen",
"leave_confirm": "Möchten Sie dieses Projekt wirklich verlassen?",
"leave_success": "Sie haben das Projekt verlassen.",
"leave_failed": "Projekt konnte nicht verlassen werden."
},
"billing": {
"title": "Aktuelle Abrechnung",
"monthly_quota": "Monatliches Kontingent",
"monthly_usage": "Monatliche Nutzung",
"billing_errors": "Abrechnungsfehler",
"no_history": "Keine Abrechnungshistorie"
},
"access": {
"invite_member": "Mitglied einladen",
"join_settings": "Beitrittseinstellungen",
"pending_invitations": "Ausstehende Einladungen",
"join_requests": "Beitrittsanfragen",
"email_placeholder": "user@example.com",
"reason_placeholder": "Was möchten Sie fragen?",
"optional_reason": "Optionaler Grund, der in der Anfragehistorie angezeigt wird."
},
"labels": {
"create_label": "Label erstellen",
"label_name_placeholder": "label-name",
"description_placeholder": "Beschreibung",
"no_labels": "Keine Labels"
},
"members": {
"remove_confirm": "${username} aus diesem Projekt entfernen?",
"remove": "Entfernen"
},
"danger_zone": {
"delete_project": "Dieses Projekt löschen",
"delete_button": "Projekt löschen"
}
},
"repos": {
"title": "Repositories",
"subtitle": "Hosten und verwalten Sie den Quellcode Ihres Projekts",
"find_placeholder": "Repository finden...",
"no_repos": "Keine Repositories",
"no_repos_desc": "Erstellen Sie Ihr erstes Repository",
"create_new": "Neues Repository erstellen",
"create_new_desc": "Hosten Sie Ihren Code und arbeiten Sie zusammen",
"loading": "Repositories werden geladen...",
"load_failed": "Fehler beim Laden der Repositories",
"new_repo": "Neues Repo",
"type": "Typ",
"sort": "Sortieren",
"repository": "Repository",
"repositories": "Repositories",
"grid_view": "Rasteransicht",
"list_view": "Listenansicht",
"private": "Privat",
"public": "Öffentlich",
"no_description_default": "Keine Beschreibung angegeben.",
"never": "Nie",
"just_now": "Gerade eben",
"min_ago": "vor ${count} Min",
"hr_ago": "vor ${count} Std",
"day_ago": "vor ${count} Tagen"
},
"repo": {
"settings": {
"title": "Repository-Identität",
"name_placeholder": "Repository-Name",
"description_placeholder": "Beschreiben Sie dieses Repository...",
"default_branch": "Standard-Branch",
"branch_placeholder": "main",
"desc": "Die grundlegenden Informationen des Repository aktualisieren"
},
"code": "Code",
"commits": "Commits",
"pull_requests": "Pull Requests",
"branches": "Branches",
"tags": "Tags",
"settings_tab": "Einstellungen",
"loading": "Repository wird geladen...",
"load_failed": "Repository konnte nicht geladen werden",
"not_found": "Repository nicht gefunden"
},
"branch_protection": {
"title": "Branch-Schutzregeln",
"branch_pattern": "Branch-Muster",
"required_approvals": "Erforderliche Genehmigungen",
"active_rules": "Aktive Regeln",
"no_rules": "Keine Branch-Schutzregeln konfiguriert",
"create_rule_hint": "Erstellen Sie oben eine Regel zum Schutz Ihrer Branches",
"add_rule": "Regel hinzufügen",
"no_fork_sync": "Keine Fork-Synchronisierung"
}
},
"issues": {
"title": "Issues",
"subtitle": "Aufgaben und Bugs des Projekts verfolgen und verwalten",
"new_title": "Neues Issue erstellen",
"new_issue": "Neues Issue",
"search_placeholder": "Alle Issues durchsuchen...",
"sort": "Sortieren",
"label": "Label",
"assignee": "Zugewiesen",
"open": "Offen",
"closed": "Geschlossen",
"no_project": "Kein Projekt ausgewählt",
"select_project": "Wählen Sie ein Projekt aus, um seine Issues anzuzeigen",
"loading": "Issues werden geladen...",
"load_failed": "Fehler beim Laden der Issues",
"no_issues": "Keine ${tab} Issues",
"no_matches": "Keine Übereinstimmungen gefunden",
"all_caught_up": "Alles erledigt! Erstellen Sie ein Issue, um neue Aufgaben zu verfolgen.",
"try_adjusting": "Versuchen Sie, Ihre Suche oder Filter anzupassen.",
"create_first": "Erstellen Sie Ihr erstes Issue",
"brief_description_placeholder": "Beschreiben Sie das Problem kurz...",
"details_placeholder": "Erklären Sie Details, Reproduktionsschritte oder Anforderungen...",
"pro_tip": "Tipp",
"need_help": "Brauchen Sie Hilfe?",
"submit": "Neues Issue absenden",
"issue_title_placeholder": "Issue-Titel"
},
"issue_detail": {
"loading": "Issue wird geladen...",
"no_description": "Keine Beschreibung angegeben.",
"no_comments": "Keine Kommentare. Starten Sie die Diskussion!",
"comment_placeholder": "Kommentar schreiben... (Markdown unterstützt)",
"edit": "Bearbeiten",
"close": "Schließen",
"delete": "Löschen",
"link_pr": "Pull Request verknüpfen",
"link_repo": "Repository verknüpfen",
"pull_requests": "Pull Requests",
"linked_repos": "Verknüpfte Repositories",
"no_assigned": "Niemand zugewiesen"
},
"pulls": {
"title": "Pull Requests",
"project_pulls": "${project} Pull Requests",
"close": "Schließen",
"reopen": "Wieder öffnen",
"confirm_delete": "Bestätigen",
"delete": "Löschen",
"no_project": "Kein Projekt ausgewählt",
"select_project": "Wählen Sie ein Projekt aus, um Pull Requests anzuzeigen",
"loading": "Pull Requests werden geladen...",
"load_failed": "Fehler beim Laden der Pull Requests",
"no_prs": "Keine Pull Requests",
"create_hint": "Erstellen Sie einen Pull Request, um Änderungen vorzuschlagen"
},
"skills": {
"title": "Skills",
"subtitle": "Projektfähigkeiten und KI-Skills",
"no_project": "Kein Projekt ausgewählt",
"select_project": "Wählen Sie ein Projekt aus, um seine Skills anzuzeigen",
"loading": "Skills werden geladen...",
"load_failed": "Fehler beim Laden der Skills",
"no_skills": "Keine Skills",
"no_skills_desc": "Erstellen oder scannen Sie Skills für KI-Unterstützung",
"create": "Erstellen",
"scan": "Scannen",
"scanning": "Scannen...",
"skill_name_placeholder": "my-skill",
"skill_display_name": "Mein Skill",
"skill_description_placeholder": "Was dieser Skill macht",
"skill_content_placeholder": "Skill-Inhalt oder Prompt...",
"delete": "Skill löschen",
"create_title": "Skill erstellen",
"create_desc": "Neuen KI-Skill zum Projekt hinzufügen."
},
"skill_detail": {
"loading": "Skill wird geladen...",
"edit_title": "Skill bearbeiten",
"save_failed": "Speichern fehlgeschlagen"
},
"board": {
"title": "Board",
"no_boards": "Keine Boards",
"no_boards_desc": "Erstellen Sie ein Kanban-Board zur Workflow-Verwaltung.",
"add_column": "Spalte hinzufügen",
"add_column_title": "Spalte hinzufügen",
"delete_board": "Board löschen",
"new_board_title": "Neues Board",
"board_name_placeholder": "z.B. Sprint-Planung",
"board_desc_placeholder": "Optionale Details...",
"column_name": "Spaltenname",
"column_name_placeholder": "z.B. In Bearbeitung",
"card_title_placeholder": "Was muss erledigt werden?",
"card_desc_placeholder": "Optionale Details...",
"card_detail_title": "Kartentitel",
"card_detail_desc_placeholder": "Detaillierte Beschreibung hinzufügen...",
"delete_card": "Karte löschen",
"delete_column": "Spalte löschen?",
"delete_card_confirm": "Diese Karte löschen?",
"empty_board": "Leeres Board",
"empty_board_hint": "Fügen Sie Ihre erste Spalte hinzu (z.B. Zu erledigen, In Bearbeitung, Erledigt), um die Arbeit zu verfolgen.",
"add_first_column": "Erste Spalte hinzufügen",
"add_card": "Karte hinzufügen",
"new_card": "Neue Karte",
"card_title": "Kartentitel",
"id": "ID",
"created": "Erstellt",
"updated": "Aktualisiert"
},
"me": {
"title": "Mein Profil",
"recent_repos": "Letzte Repositories",
"top_projects": "Top-Projekte",
"latest_activity": "Letzte Aktivität",
"no_projects": "Keine Projekte gefunden",
"no_repos": "Keine Repositories gefunden",
"no_activity": "Keine kürzliche Aktivität",
"create_project": {
"title": "Neues Projekt erstellen",
"subtitle": "Starten Sie ein neues kollaboratives Projekt",
"name_placeholder": "z.B. my-awesome-team",
"org_placeholder": "z.B. Acme Corporation",
"desc_placeholder": "Worum geht es in diesem Projekt?",
"slug": "Projekt-Slug",
"slug_required": "Erforderlich",
"slug_hint": "In URLs verwendet. Nur Kleinbuchstaben, Zahlen und Bindestriche.",
"display_name": "Anzeigename",
"display_name_required": "Erforderlich",
"display_name_placeholder": "z.B. Acme Corporation",
"description": "Beschreibung",
"visibility": "Sichtbarkeit",
"public_project": "Öffentliches Projekt",
"public_desc": "Jeder kann das Projekt finden und beitreten",
"private_project": "Privates Projekt",
"private_desc": "Nur eingeladene Mitglieder können zugreifen",
"change": "Ändern",
"create_failed": "Projekt konnte nicht erstellt werden. Der Slug ist möglicherweise bereits vergeben.",
"create": "Projekt erstellen",
"cancel": "Abbrechen"
},
"invitations": {
"pending": "Ausstehende Einladungen",
"no_pending": "Keine ausstehenden Einladungen",
"join_requests": "Beitrittsanfragen",
"no_requests": "Keine Beitrittsanfragen"
},
"notifications": {
"no_notifications": "Keine Benachrichtigungen",
"no_unread": "Keine ungelesenen Benachrichtigungen",
"all_caught_up": "Alles erledigt"
},
"followers": {
"no_followers": "Keine Follower"
},
"following": {
"no_following": "Keine Abonnements"
},
"user_not_found": "Benutzer nicht gefunden",
"please_login": "Bitte stellen Sie sicher, dass Sie angemeldet sind.",
"profile": {
"joined": "Beigetreten",
"projects": "Projekte",
"repos": "Repos",
"stars": "Sterne",
"followers": "Follower",
"follow": "Folgen",
"unfollow": "Entfolgen",
"contributions_in_year": "${count} Beiträge im letzten Jahr",
"contributions_on_date": "${count} Beiträge am ${date}",
"less": "Weniger",
"more": "Mehr"
}
},
"explore": {
"title": "Projekte erkunden",
"subtitle": "Entdecken Sie öffentliche Projekte und Gemeinschaften",
"search_placeholder": "Projekte nach Name oder Beschreibung suchen...",
"no_projects": "Keine Projekte gefunden",
"no_description": "Keine Beschreibung",
"found": "gefunden",
"showing": "Auffindbare Projekte werden angezeigt",
"try_different": "Versuchen Sie einen anderen Suchbegriff",
"view_project": "Projekt ansehen"
},
"chat": {
"conversations": {
"search_placeholder": "Unterhaltungen suchen...",
"no_conversations": "Noch keine Unterhaltungen",
"no_matching": "Keine passenden Unterhaltungen",
"new_chat": "Neuer Chat",
"chat_history": "Chat-Verlauf",
"start_new_chat": "Starten Sie einen neuen Chat, um mit KI zu erkunden.",
"try_different_search": "Versuchen Sie einen anderen Suchbegriff.",
"untitled_chat": "Unbenannter Chat",
"create_failed": "Chat konnte nicht erstellt werden"
},
"model_selector": {
"search_placeholder": "Modelle suchen...",
"loading": "Wird geladen...",
"no_models": "Keine Modelle gefunden",
"please_select_model": "Bitte wählen Sie zuerst ein Modell"
},
"header": {
"new_chat": "Neuer Chat",
"expand_history": "Chat-Verlauf erweitern",
"collapse_history": "Chat-Verlauf einklappen",
"streaming": "Streaming...",
"share": "Gespräch teilen",
"rename": "Umbenennen",
"more": "Mehr"
},
"message_list": {
"welcome_title": "Wie kann ich Ihnen heute helfen?",
"welcome_desc": "Fragen Sie anything - Ich kann bei Code, Schreiben, Analyse und vielem mehr helfen.",
"new_response": "Neue Antwort",
"responding": "antwortet...",
"explain_code": "Code erklären oder Fehler beheben",
"summarize_doc": "Dokument zusammenfassen oder entwerfen",
"review_pr": "Pull Request oder Repo prüfen",
"brainstorm": "Ideen brainstormen oder Plan erstellen",
"jump_to_message": "Zu Ihrer Nachricht ${index} springen"
}
},
"channel": {
"no_selected": "Kein Kanal ausgewählt",
"no_threads": "Noch keine Threads. Verwenden Sie das Nachrichten-Aktionsmenü, um einen zu starten.",
"no_messages": "Noch keine Nachrichten. Starten Sie die Konversation!",
"edit_history": "Bearbeitungshistorie",
"no_edit_history": "Keine Bearbeitungshistorie verfügbar.",
"no_pinned": "Noch keine angehefteten Nachrichten.",
"reply_placeholder": "Im Thread antworten...",
"beginning_of_channel": "Anfang des Kanals",
"scroll_up": "Nach oben scrollen, um ältere Nachrichten zu laden",
"new_message": "${count} neue Nachricht(en)"
},
"search": {
"global_placeholder": "Projekte, Kanäle, Repositories, Boards, Issues suchen...",
"no_results": "Keine Ergebnisse gefunden.",
"no_messages": "Keine passenden Nachrichten.",
"results": "Suchergebnisse",
"jump_to": "Zu Projekten, Kanälen, Repositories, Boards und Issues springen.",
"messages": "Nachrichten",
"quick_switch": "Schnellwechsel",
"projects": "Projekte",
"my_home": "Mein Zuhause",
"my_home_desc": "Persönliche Übersicht öffnen",
"explore_projects": "Projekte erkunden",
"explore_projects_desc": "Öffentliche Projekte finden",
"chat": "Chat",
"chat_desc": "Persönlichen Chat öffnen",
"project_overview": "Projektübersicht",
"repositories": "Repositories",
"issues": "Issues",
"boards": "Boards",
"project_settings": "Projekteinstellungen",
"rooms_in": "Räume in ${project}",
"all_rooms": "Alle Räume in ${project}",
"messages_in": "Nachrichten in ${project}",
"messages_in_room": "Nachrichten in #${room}"
},
"components": {
"message_input": {
"reply_preview": "Antwort/Bearbeitungs-Vorschau-Banner",
"add_emoji": "Emoji hinzufügen",
"placeholder": "Nachricht",
"replying": "Antworte auf Nachricht",
"editing": "Nachricht bearbeiten"
},
"inline_comment": {
"reply_placeholder": "Antwort schreiben...",
"leave_comment": "Kommentar hinterlassen..."
},
"merge_panel": {
"commit_message_placeholder": "Commit-Nachricht eingeben..."
},
"ai_settings": {
"add_ai": "KI hinzufügen",
"no_ai": "Keine aktiven KI-Agenten in diesem Kanal",
"search_placeholder": "Modelle suchen...",
"system_prompt_placeholder": "Sie sind ein hilfreicher Assistent..."
},
"room_settings": {
"name_placeholder": "z.B. general",
"topic_placeholder": "z.B. Ingenieurwesen",
"no_group": "Keine Gruppe",
"create_new_group": "Neue Gruppe erstellen",
"delete_room": "Kanal löschen"
},
"command": {
"placeholder": "Befehl zum Ausführen suchen..."
},
"pr": {
"create_merge_commit": "Merge-Commit erstellen",
"no_changes": "Keine Änderungen in diesem Pull Request.",
"confirm_merge": "Merge bestätigen"
},
"commit": {
"back_to_commits": "Zurück zu Commits",
"no_changes": "Keine Änderungen in diesem Commit."
},
"repo": {
"no_description": "Keine Beschreibung",
"no_description_default": "Keine Beschreibung angegeben."
},
"branches": {
"no_branches": "Keine Branches",
"no_branches_desc": "Erstellen Sie Ihren ersten Branch zum Starten"
},
"tags": {
"no_tags": "Keine Tags",
"no_tags_desc": "Erstellen Sie ein Tag, um einen bestimmten Punkt in der Historie zu markieren"
},
"favorites": {
"no_favorites": "Noch keine favorisierten Nachrichten."
}
},
"project_create": {
"repo_name": "Repository-Name",
"repo_name_placeholder": "z.B. core-api",
"repo_desc_placeholder": "Wofür ist dieses Repository?",
"group_name": "Neuer Gruppenname",
"channel_name": "Kanalname",
"channel_name_placeholder": "z.B. development",
"board_name": "Board-Name",
"board_name_placeholder": "z.B. Sprint-Planung",
"board_desc_placeholder": "Task-Board-Details...",
"skill_name": "Skill-Name",
"skill_name_placeholder": "z.B. Code-Reviewer",
"skill_desc_placeholder": "Was macht dieser KI-Skill?",
"email_placeholder": "user@example.com",
"no_group": "Keine Gruppe",
"create_new_group": "Neue Gruppe erstellen",
"create_repo": "Repository erstellen",
"create_channel": "Kanal erstellen",
"create_board": "Board erstellen",
"create_skill": "Skill erstellen"
}
}

View File

@ -0,0 +1,977 @@
{
"auth": {
"login": {
"title": "Welcome back!",
"subtitle": "We're so excited to see you again!",
"account": "Account",
"account_required": "Username is required",
"account_placeholder": "Username or email",
"password": "Password",
"password_required": "Password is required",
"verification": "Verification",
"captcha_placeholder": "Enter code",
"captcha_required": "Captcha is required",
"captcha_title": "Click to refresh",
"2fa_code": "2FA Code",
"2fa_placeholder": "6-digit code",
"forgot_password": "Forgot your password?",
"submit": "Log In",
"submit_loading": "Logging in...",
"need_account": "Need an account?",
"register": "Register",
"error": {
"failed_to_load_captcha": "Failed to load captcha",
"two_factor_required": "Two-factor authentication required",
"invalid_credentials": "Invalid username or password",
"login_failed": "Login failed"
}
},
"register": {
"title": "Create an account",
"subtitle": "Join us today!",
"username": "Username",
"username_placeholder": "Choose a username",
"username_required": "Username is required",
"username_min_length": "Username must be at least 3 characters",
"username_pattern": "Username can only contain letters, numbers, _ and -",
"email": "Email",
"email_placeholder": "Enter your email",
"email_required": "Email is required",
"email_invalid": "Invalid email address",
"password_placeholder": "Create a password",
"confirm_password": "Confirm Password",
"confirm_password_placeholder": "Confirm your password",
"confirm_password_required": "Please confirm your password",
"password_min_length": "Password must be at least 8 characters",
"captcha_placeholder": "Enter code",
"submit": "Create Account",
"submit_loading": "Creating account...",
"already_have_account": "Already have an account?",
"login": "Login",
"passwords_not_match": "Passwords do not match",
"user_exists": "Username or email already exists",
"registration_failed": "Registration failed"
},
"forgot_password": {
"title": "Reset your password",
"subtitle": "Enter your email and we'll send you a reset link",
"email_placeholder": "Enter your email",
"captcha_placeholder": "Enter code",
"submit": "Send Reset Link",
"sending": "Sending...",
"back_to_login": "Back to login",
"captcha_failed": "Captcha verification failed",
"send_failed": "Failed to send reset email",
"success_message": "If an account exists with that email, you will receive a password reset link shortly."
},
"reset_password": {
"title": "Set new password",
"subtitle": "Choose a strong password for your account",
"new_password_placeholder": "Enter new password",
"confirm_password_placeholder": "Confirm new password",
"captcha_placeholder": "Enter code",
"submit": "Reset Password",
"resetting": "Resetting...",
"back_to_login": "Back to login",
"invalid_token": "Invalid or expired reset token",
"reset_failed": "Failed to reset password"
},
"change_password": {
"title": "Change Password",
"subtitle": "Update your account password",
"current_password_placeholder": "Enter current password",
"new_password_placeholder": "Enter new password",
"confirm_password_placeholder": "Confirm new password",
"captcha_placeholder": "Enter code",
"submit": "Update Password",
"back_to_login": "Back to login"
},
"two_factor": {
"title": "Two-Factor Authentication",
"enabled": "2FA is currently enabled",
"disabled": "Add an extra layer of security",
"description": "Two-factor authentication adds an additional layer of security to your account by requiring more than just a password to log in.",
"enabled_message": "Two-factor authentication is currently enabled on your account.",
"scan_qr_code": "Scan QR Code",
"or_enter_manually": "Or enter this code manually",
"verification_code": "Verification Code",
"code_required": "Code is required",
"code_placeholder": "Enter 6-digit code",
"password_placeholder": "Enter your password",
"submit": "Verify",
"cancel": "Cancel",
"back": "Back",
"enable": "Enable 2FA",
"disable": "Disable 2FA",
"disabling": "Disabling...",
"error": {
"load_failed": "Failed to load 2FA status",
"enable_failed": "Failed to enable 2FA",
"disable_failed": "Failed to disable 2FA",
"invalid_code": "Invalid verification code"
}
},
"verify_email": {
"title": "Verify Email"
},
"logout": "Log Out"
},
"common": {
"actions": {
"save": "Save",
"save_changes": "Save Changes",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"submit": "Submit",
"close": "Close",
"confirm": "Confirm",
"back": "Back",
"remove": "Remove",
"add": "Add",
"retry": "Retry",
"generate": "Generate",
"loading": "Loading...",
"discard": "Discard"
},
"states": {
"loading": "Loading...",
"no_results": "No results found",
"error_occurred": "An error occurred",
"error_title": "Something went wrong",
"error_message": "An unexpected error occurred."
},
"placeholders": {
"search": "Search...",
"message": "Message"
},
"unknown": "Unknown"
},
"navigation": {
"search": "Search",
"search_shortcut": "Search (Ctrl+K)",
"favorites": "Favorite Messages",
"no_channels": "No channels",
"settings": "Settings"
},
"settings": {
"access_keys": {
"title": "Personal Access Tokens",
"description": "Tokens you have generated that can be used to access the API.",
"generate_button": "Generate new token",
"token_name": "Token Name",
"copy_warning": "Make sure to copy your personal access token now. You won't be able to see it again!",
"empty": "No personal access tokens found.",
"created_on": "Created on",
"expires": "Expires",
"generate_token": "Generate token",
"messages": {
"name_required": "Please enter a token name",
"created_success": "Token created, please copy it now.",
"delete_success": "Token deleted",
"create_failed": "Creation failed, please try again",
"delete_failed": "Deletion failed"
}
},
"ssh_keys": {
"title": "SSH Keys",
"description": "Manage SSH keys for Git operations",
"add_button": "Add key",
"add_title": "Add new SSH key",
"title_label": "Title",
"public_key_label": "Public Key",
"name_placeholder": "e.g. Personal MacBook",
"key_placeholder": "ssh-rsa AAAAB3NzaC... user@host",
"empty_title": "No SSH keys added yet",
"empty_desc": "Add SSH keys to securely perform Git operations",
"verified": "Verified",
"messages": {
"title_key_required": "Please enter a title and public key",
"add_success": "SSH key added successfully",
"add_failed": "Failed to add key, please check the key format",
"delete_success": "SSH key deleted",
"delete_failed": "Failed to delete key",
"title_updated": "Title updated",
"update_failed": "Failed to update"
}
},
"password": {
"title": "Change Password",
"subtitle": "Protect your account with a strong password",
"current_password": "Current Password",
"current_password_placeholder": "Enter current password",
"new_password": "New Password",
"new_password_placeholder": "Enter new password (at least 8 characters)",
"confirm_password": "Confirm New Password",
"confirm_password_placeholder": "Re-enter new password",
"submit": "Change Password",
"mismatch": "Passwords do not match",
"min_length": "Password must be at least 8 characters",
"change_success": "Password changed successfully",
"change_failed": "Password change failed, please check your current password"
},
"account": {
"title": "My Account",
"display_name_placeholder": "Set display name",
"website_placeholder": "https://example.com",
"company_placeholder": "Organization (optional)"
},
"email": {
"title": "Email",
"current_email": "Current Email",
"new_email_placeholder": "new@example.com",
"current_password_placeholder": "Enter current password"
},
"billing": {
"title": "Billing",
"personal_billing": "Personal Billing",
"balance": "Balance",
"monthly_quota": "Monthly Quota",
"monthly_usage": "Monthly Usage",
"billing_errors": "Billing Errors",
"history": "Billing History",
"date": "Date",
"reason": "Reason",
"amount": "Amount",
"no_history": "No billing history",
"load_failed": "Failed to load billing"
},
"appearance": {
"title": "Appearance",
"timezone_asia_shanghai": "Asia/Shanghai (UTC+8)",
"timezone_asia_tokyo": "Asia/Tokyo (UTC+9)",
"timezone_america_ny": "America/New_York (UTC-5)",
"timezone_america_la": "America/Los_Angeles (UTC-8)",
"timezone_europe_london": "Europe/London (UTC+0)"
},
"notifications": {
"title": "Notifications"
},
"settings_nav": {
"user_settings": "User Settings",
"my_account": "My Account",
"billing": "Billing",
"appearance": "Appearance",
"notifications": "Notifications",
"push_settings": "Push Settings",
"security": "Security",
"change_password": "Change Password",
"email": "Email",
"ssh_keys": "SSH Keys",
"access_tokens": "Access Tokens"
},
"my_account": {
"title": "My Account",
"subtitle": "Manage your personal information",
"avatar": "Avatar",
"upload_avatar": "Upload new avatar",
"remove": "Remove",
"avatar_hint": "Supports JPG, PNG or GIF. Max 2MB.",
"username": "Username",
"display_name": "Display Name",
"display_name_placeholder": "Set display name",
"website": "Website",
"website_placeholder": "https://example.com",
"organization": "Organization",
"org_placeholder": "Organization (optional)",
"save_changes": "Save Changes",
"load_failed": "Failed to load profile",
"save_success": "Profile saved",
"save_failed": "Failed to save, please try again",
"avatar_size_error": "Image must be less than 2MB",
"avatar_upload_success": "Avatar uploaded, please save changes",
"avatar_upload_failed": "Avatar upload failed"
},
"email_page": {
"title": "Email",
"subtitle": "Manage your email address",
"current_email": "Current Email",
"no_email_set": "No email set",
"new_email": "New Email",
"current_password": "Current Password (for verification)",
"current_password_placeholder": "Enter current password",
"save_button": "Change Email",
"load_failed": "Failed to load email info",
"fill_all_fields": "Please fill in all fields",
"verification_sent": "Verification email sent, please check your new mailbox",
"change_failed": "Failed to change email, please check your password"
},
"notifications_page": {
"title": "Notifications",
"subtitle": "Manage your notification preferences",
"channels": "Notification Channels",
"email_notifications": "Email Notifications",
"email_notifications_desc": "Receive notifications via email",
"in_app_notifications": "In-App Notifications",
"in_app_notifications_desc": "Receive notification reminders in the app",
"push_notifications": "Push Notifications",
"push_notifications_desc": "Receive notifications via browser push",
"digest_mode": "Digest Mode",
"instant": "Instant",
"daily_digest": "Daily Digest",
"weekly_digest": "Weekly Digest",
"off": "Off",
"notification_types": "Notification Types",
"security_notifications": "Security Notifications",
"security_notifications_desc": "Account security related notifications",
"product_updates": "Product Updates",
"product_updates_desc": "New features and product update notifications",
"marketing_emails": "Marketing Emails",
"marketing_emails_desc": "Promotional activities and offers",
"do_not_disturb": "Do Not Disturb",
"enable_dnd": "Enable Do Not Disturb",
"enable_dnd_desc": "Do not receive notifications during specified time period",
"save_button": "Save Changes",
"load_failed": "Failed to load notification preferences",
"save_success": "Notification settings saved",
"save_failed": "Failed to save, please try again"
},
"appearance_page": {
"title": "Appearance",
"subtitle": "Customize the look and language of the app",
"theme_scheme": "Theme Scheme",
"select_theme_scheme": "Select theme scheme",
"theme": "Theme",
"language": "Language",
"timezone": "Timezone",
"dark": "Dark",
"light": "Light",
"system": "Follow System",
"save_button": "Save Changes",
"load_failed": "Failed to load preferences",
"save_success": "Appearance settings saved",
"save_failed": "Failed to save, please try again",
"basic_settings": "Basic Settings",
"custom": "Custom",
"theme_customization": {
"saved": "Saved",
"save_theme": "Save Theme",
"reset_default": "Reset to Default",
"reset_all": "Reset All",
"save_locally": "Changes are saved locally in browser"
}
},
"billing": {
"title": "Billing",
"personal_billing": "Personal Billing",
"balance": "Balance",
"monthly_quota": "Monthly Quota",
"monthly_usage": "Monthly Usage",
"billing_errors": "Billing Errors",
"history": "Billing History",
"date": "Date",
"reason": "Reason",
"amount": "Amount",
"no_history": "No billing history",
"load_failed": "Failed to load billing",
"insufficient_balance": "Insufficient Balance"
},
"push": {
"title": "Push Notifications",
"subtitle": "Receive real-time alerts even when the app is closed.",
"enable": "Enable Push Notifications",
"enable_desc": "Get notified of mentions, issues, and system alerts.",
"filters_title": "Notification Filters",
"mentions": "Mentions & Replies",
"new_issues": "New Issues",
"system_updates": "System Updates",
"coming_soon": "More granular controls coming soon.",
"not_supported": "Push notifications are not supported",
"not_supported_desc": "Your browser or environment doesn't support Web Push. Please use a modern desktop browser.",
"load_failed": "Failed to load notification settings",
"permission_denied": "Notification permission denied by browser",
"update_failed": "Failed to update push settings",
"saved": "Settings saved successfully"
}
},
"project": {
"layout": {
"expand_sidebar": "Expand sidebar",
"join_banner": {
"preview_mode": "Preview mode",
"join_to_participate": "Join this project to participate and use project tools.",
"read_only": "This public project is read-only until you join.",
"join_to_use": "You need to join before using project actions.",
"apply_to_join": "Apply to join"
}
},
"invitation": {
"title": "Project Invitation",
"no_pending": "No pending invitation",
"desc": "Accept or reject your invitation to join this project.",
"no_pending_desc": "This invitation may have already been processed. You can review all invitations from your profile.",
"view_invitations": "View invitations"
},
"join": {
"title": "Join ${project}",
"reason_placeholder": "Tell the admins why you want to join.",
"cancel_request": "Cancel request",
"join_without_reason": "Join project",
"submit_request": "Submit join request",
"default_desc": "Submit a request to become a project member.",
"already_member": "You are already a member of this project.",
"open_project": "Open project",
"current_request": "Current request",
"submitted_on": "Submitted on",
"message": "Message",
"message_desc": "Optional, but useful for private or approval-required projects.",
"submitted": "Join request submitted.",
"submit_failed": "Failed to submit join request.",
"cancelled": "Join request cancelled.",
"cancel_failed": "Failed to cancel join request."
},
"settings": {
"general": {
"title": "Project Identity",
"project_name": "Project Name",
"project_name_placeholder": "e.g. my-awesome-project",
"display_name": "Display Name",
"display_name_placeholder": "e.g. My Awesome Project",
"description": "Description",
"description_placeholder": "Tell people what this project is about...",
"desc": "Update your project's basic information and appearance",
"project_slug": "Project Slug",
"display_name_hint": "Shown in the UI and notifications",
"description": "Description",
"description_hint": "Brief description shown on the project page",
"copy_slug": "Copy slug",
"slug_hint": "Used in URLs — cannot contain spaces",
"avatar": "Project avatar",
"avatar_desc": "Click to upload · PNG, JPG or SVG · max 2 MB",
"updated": "Project settings updated successfully",
"update_failed": "Failed to update project settings",
"visibility": "Visibility",
"visibility_desc": "Control who can discover and view this project",
"visibility_changed": "Project is now ${visibility}",
"visibility_failed": "Failed to update visibility",
"public_desc": "Anyone on the internet can view this project and its code.",
"private_desc": "Only invited members can view this project and its code.",
"make_private": "Make Private",
"make_public": "Make Public",
"stats": "Project Stats",
"members": "Members",
"watchers": "Watchers",
"stars": "Stars",
"created": "Created",
"danger_zone": "Danger Zone",
"archive": "Archive this project",
"archive_desc": "Mark as read-only. Repository can be unarchived later.",
"archive_btn": "Archive",
"delete": "Delete this project",
"delete_desc": "Permanently remove this project and all its data. This cannot be undone.",
"delete_btn": "Delete project",
"leave": "Leave this project",
"leave_desc": "You will lose access to all project resources.",
"leave_btn": "Leave Project",
"leave_confirm": "Are you sure you want to leave this project?",
"leave_success": "You have left the project.",
"leave_failed": "Failed to leave the project."
},
"billing": {
"title": "Current Billing",
"monthly_quota": "Monthly Quota",
"monthly_usage": "Monthly Usage",
"billing_errors": "Billing Errors",
"no_history": "No billing history"
},
"access": {
"invite_member": "Invite Member",
"join_settings": "Join Settings",
"pending_invitations": "Pending Invitations",
"join_requests": "Join Requests",
"email_placeholder": "user@example.com",
"reason_placeholder": "What do you want to ask?",
"optional_reason": "Optional reason shown in request history."
},
"labels": {
"create_label": "Create Label",
"label_name_placeholder": "label-name",
"description_placeholder": "description",
"no_labels": "No labels yet"
},
"members": {
"remove_confirm": "Remove ${username} from this project?",
"remove": "Remove"
},
"danger_zone": {
"delete_project": "Delete this project",
"delete_button": "Delete project"
}
},
"repos": {
"title": "Repositories",
"subtitle": "Host and manage your project source code",
"find_placeholder": "Find a repository...",
"no_repos": "No repositories",
"no_repos_desc": "Get started by creating your first repository",
"create_new": "Create a new repository",
"create_new_desc": "Host your code and collaborate",
"loading": "Loading repositories...",
"load_failed": "Failed to load repositories",
"new_repo": "New repo",
"type": "Type",
"sort": "Sort",
"repository": "repository",
"repositories": "repositories",
"grid_view": "Grid view",
"list_view": "List view",
"private": "Private",
"public": "Public",
"no_description_default": "No description provided.",
"never": "Never",
"just_now": "Just now",
"min_ago": "${count}m ago",
"hr_ago": "${count}h ago",
"day_ago": "${count}d ago"
},
"repo": {
"settings": {
"title": "Repository Identity",
"name_placeholder": "Repository name",
"description_placeholder": "Tell people what this repository is about...",
"default_branch": "Default Branch",
"branch_placeholder": "main",
"desc": "Update your repository's basic information"
},
"code": "Code",
"commits": "Commits",
"pull_requests": "Pull Requests",
"branches": "Branches",
"tags": "Tags",
"settings_tab": "Settings",
"loading": "Loading repository...",
"load_failed": "Failed to load repository",
"not_found": "Repository not found"
},
"branch_protection": {
"title": "Branch Protection Rules",
"branch_pattern": "Branch Pattern",
"required_approvals": "Required Approvals",
"active_rules": "Active Rules",
"no_rules": "No branch protection rules configured",
"create_rule_hint": "Create a rule above to protect your branches",
"add_rule": "Add Rule",
"no_fork_sync": "No fork sync"
},
"issues": {
"title": "Issues",
"subtitle": "Track and manage project tasks and bugs",
"new_title": "Create New Issue",
"new_issue": "New issue",
"search_placeholder": "Search all issues...",
"sort": "Sort",
"label": "Label",
"assignee": "Assignee",
"open": "Open",
"closed": "Closed",
"no_project": "No project selected",
"select_project": "Select a project to view its issues",
"loading": "Loading issues...",
"load_failed": "Failed to load issues",
"no_issues": "No ${tab} issues",
"no_matches": "No matches found",
"all_caught_up": "You're all caught up! Create an issue to track new tasks.",
"try_adjusting": "Try adjusting your search or filters to find what you're looking for.",
"create_first": "Create your first issue",
"brief_description_placeholder": "Briefly describe the issue...",
"details_placeholder": "Explain the details, steps to reproduce, or requirements...",
"pro_tip": "Pro Tip",
"need_help": "Need Help?",
"submit": "Submit New Issue",
"issue_title_placeholder": "Issue Title",
"new_subtitle": "Open a new task or report a bug for this project",
"join_to_create": "Join this project before creating issues.",
"title_required": "Title is required",
"create_failed": "Failed to create issue. Please try again.",
"title_hint": "Keep it concise and descriptive",
"description": "Description (Optional)",
"markdown_supported": "Markdown is supported",
"characters": " characters",
"pro_tip_desc": "Use Markdown to format your code snippets, lists, and checklists to make the issue easier to read.",
"need_help_desc": "Check out our documentation on how to write effective issues and bug reports."
},
"issue_detail": {
"loading": "Loading issue...",
"no_description": "No description provided.",
"no_comments": "No comments yet. Start the discussion!",
"comment_placeholder": "Write a comment... (Markdown supported)",
"edit": "Edit",
"close": "Close",
"delete": "Delete",
"link_pr": "Link a pull request...",
"link_repo": "Link a repo...",
"pull_requests": "Pull Requests",
"linked_repos": "Linked Repos",
"no_assigned": "No one assigned",
"assignees": "Assignees",
"assign_to_me": "Assign to me",
"suggestions": "Suggestions",
"labels": "Labels",
"apply_labels": "Apply labels",
"none_yet": "None yet",
"notifications": "Notifications",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribers_count": " people subscribed",
"development": "Development",
"link_pr_title": "Link Pull Request",
"link_repo_title": "Link Repository",
"load_failed": "Failed to load issue",
"not_found": "Issue not found",
"delete_confirm": "Are you sure you want to delete this issue?",
"delete_comment_confirm": "Are you sure you want to delete this comment?",
"opened_on": "opened this issue on",
"comments": "comments",
"reopen": "Reopen",
"commented": "commented",
"commented_on": "commented on",
"edited": "• edited",
"conversation": "Conversation",
"write_comment": "Write a comment",
"comment_btn": "Comment"
},
"pulls": {
"title": "Pull Requests",
"project_pulls": "${project} pull requests",
"close": "Close",
"reopen": "Reopen",
"confirm_delete": "Confirm",
"delete": "Delete",
"no_project": "No project selected",
"select_project": "Select a project to view its pull requests",
"loading": "Loading pull requests...",
"load_failed": "Failed to load pull requests",
"no_prs": "No pull requests",
"create_hint": "Create a pull request to propose changes"
},
"skills": {
"title": "Skills",
"subtitle": "Project capabilities and AI skills",
"no_project": "No project selected",
"select_project": "Select a project to view its skills",
"loading": "Loading skills...",
"load_failed": "Failed to load skills",
"no_skills": "No skills",
"no_skills_desc": "Create or scan skills to enable AI assistance",
"create": "Create",
"scan": "Scan",
"scanning": "Scanning...",
"skill_name_placeholder": "my-skill",
"skill_display_name": "My Skill",
"skill_description_placeholder": "What this skill does",
"skill_content_placeholder": "Skill content or prompt...",
"delete": "Delete skill",
"create_title": "Create skill",
"create_desc": "Add a new AI skill to the project."
},
"skill_detail": {
"loading": "Loading skill...",
"edit_title": "Edit skill",
"save_failed": "Save failed"
},
"board": {
"title": "Board",
"no_boards": "No boards yet",
"no_boards_desc": "Create a Kanban board to start managing your workflow.",
"add_column": "Add Column",
"add_column_title": "Add Column",
"delete_board": "Delete Board",
"new_board_title": "New Board",
"board_name_placeholder": "e.g. Sprint Planning",
"board_desc_placeholder": "Optional details...",
"column_name": "Column Name",
"column_name_placeholder": "e.g. Doing",
"card_title_placeholder": "What needs to be done?",
"card_desc_placeholder": "Optional details...",
"card_detail_title": "Card Title",
"card_detail_desc_placeholder": "Add a more detailed description...",
"delete_card": "Delete Card",
"delete_column": "Delete column?",
"delete_card_confirm": "Delete this card?",
"empty_board": "Empty Board",
"empty_board_hint": "Add your first column (e.g. To Do, Doing, Done) to start tracking work.",
"add_first_column": "Add First Column",
"add_card": "Add card",
"new_card": "New Card",
"card_title": "Card Title",
"id": "ID",
"created": "Created",
"updated": "Updated"
}
},
"me": {
"title": "My Profile",
"recent_repos": "Recent Repositories",
"top_projects": "Top Projects",
"latest_activity": "Latest Activity",
"no_projects": "No projects found",
"no_repos": "No repositories found",
"no_activity": "No recent activity",
"create_project": {
"title": "Create New Project",
"subtitle": "Start a new collaborative project",
"name_placeholder": "e.g. my-awesome-team",
"org_placeholder": "e.g. Acme Corporation",
"desc_placeholder": "What is this project about?",
"slug": "Project Slug",
"slug_required": "Required",
"slug_hint": "Used in URLs. Only lowercase letters, numbers, and dashes.",
"display_name": "Display Name",
"display_name_required": "Required",
"display_name_placeholder": "e.g. Acme Corporation",
"description": "Description",
"visibility": "Visibility",
"public_project": "Public Project",
"public_desc": "Anyone can discover and join",
"private_project": "Private Project",
"private_desc": "Only invited members can access",
"change": "Change",
"create_failed": "Failed to create project. The slug might already be taken.",
"create": "Create Project",
"cancel": "Cancel"
},
"invitations": {
"title": "Invitations",
"subtitle": "Review project invitations and track your join requests.",
"pending": "Pending Invitations",
"pending_desc": "Invitations sent by project admins.",
"no_pending": "No pending invitations",
"no_pending_desc": "New project invitations will appear here.",
"join_requests": "Join Requests",
"join_requests_desc": "Your project membership applications.",
"no_requests": "No join requests",
"no_requests_desc": "Projects you apply to join will be tracked here.",
"invited_by": "Invited by",
"on": "on",
"accept": "Accept",
"reject": "Reject",
"submitted_on": "Submitted on",
"accepted": "Invitation accepted.",
"rejected": "Invitation rejected.",
"process_failed": "Failed to process invitation.",
"cancelled": "Join request cancelled.",
"cancel_failed": "Failed to cancel join request."
},
"notifications": {
"no_notifications": "No notifications",
"no_unread": "No unread notifications",
"all_caught_up": "You're all caught up"
},
"followers": {
"no_followers": "No followers yet"
},
"following": {
"no_following": "No following"
},
"user_not_found": "User not found",
"please_login": "Please make sure you are logged in.",
"profile": {
"joined": "Joined",
"projects": "Projects",
"repos": "Repos",
"stars": "Stars",
"followers": "Followers",
"follow": "Follow",
"unfollow": "Unfollow",
"contributions_in_year": "${count} contributions in the last year",
"contributions_on_date": "${count} contributions on ${date}",
"less": "Less",
"more": "More"
}
},
"explore": {
"title": "Explore Projects",
"subtitle": "Discover public projects and communities",
"search_placeholder": "Search projects by name or description...",
"no_projects": "No projects found",
"no_description": "No description",
"found": "found",
"showing": "Showing discoverable projects",
"try_different": "Try a different search term",
"view_project": "View project"
},
"chat": {
"conversations": {
"search_placeholder": "Search conversations...",
"no_conversations": "No conversations yet",
"no_matching": "No matching conversations",
"new_chat": "New Chat",
"chat_history": "Chat History",
"start_new_chat": "Start a new chat to begin exploring with AI.",
"try_different_search": "Try a different search term.",
"untitled_chat": "Untitled Chat",
"create_failed": "Failed to create conversation"
},
"model_selector": {
"search_placeholder": "Search models...",
"loading": "Loading...",
"no_models": "No models found",
"please_select_model": "Please select a model first"
},
"header": {
"new_chat": "New Chat",
"expand_history": "Expand Chat History",
"collapse_history": "Collapse Chat History",
"streaming": "Streaming...",
"share": "Share conversation",
"rename": "Rename",
"more": "More"
},
"message_list": {
"welcome_title": "How can I help you today?",
"welcome_desc": "Ask anything - I can help with code, writing, analysis, and much more.",
"new_response": "New response",
"responding": "responding...",
"explain_code": "Explain a piece of code or debug an error",
"summarize_doc": "Summarize or draft a document",
"review_pr": "Review a pull request or repo",
"brainstorm": "Brainstorm ideas or outline a plan",
"jump_to_message": "Jump to your message ${index}"
}
},
"channel": {
"no_selected": "No channel selected",
"no_threads": "No threads yet. Use the message action menu to start one.",
"no_messages": "No messages yet. Start the conversation!",
"edit_history": "Edit History",
"no_edit_history": "No edit history available.",
"no_pinned": "No pinned messages yet.",
"reply_placeholder": "Reply in thread...",
"beginning_of_channel": "Beginning of channel",
"scroll_up": "Scroll up to load older messages",
"new_message": "${count} new message",
"pinned_messages": "Pinned Messages",
"pinned_not_loaded": "Pinned message is not loaded in the current history window.",
"pinned": "Pinned",
"unpin": "Unpin"
},
"search": {
"global_placeholder": "Search projects, rooms, repos, boards, issues...",
"no_results": "No results found.",
"no_messages": "No matching messages.",
"results": "Search Results",
"jump_to": "Jump to projects, rooms, repositories, boards, and issues.",
"messages": "Messages",
"quick_switch": "Quick Switch",
"projects": "Projects",
"my_home": "My Home",
"my_home_desc": "Open personal overview",
"explore_projects": "Explore Projects",
"explore_projects_desc": "Find public projects",
"chat": "Chat",
"chat_desc": "Open personal chat",
"project_overview": "Project Overview",
"repositories": "Repositories",
"issues": "Issues",
"boards": "Boards",
"project_settings": "Project Settings",
"rooms_in": "Rooms in ${project}",
"all_rooms": "All rooms in ${project}",
"messages_in": "Messages in ${project}",
"messages_in_room": "Messages in #${room}"
},
"components": {
"message_input": {
"reply_preview": "Reply/Edit Preview Banner",
"add_emoji": "Add emoji",
"placeholder": "Message",
"replying": "Replying to message",
"editing": "Editing message"
},
"inline_comment": {
"reply_placeholder": "Write a reply...",
"leave_comment": "Leave a comment..."
},
"merge_panel": {
"commit_message_placeholder": "Enter a commit message..."
},
"ai_settings": {
"add_ai": "Add AI",
"no_ai": "No AI agents active in this room",
"search_placeholder": "Search models...",
"system_prompt_placeholder": "You are a helpful assistant..."
},
"room_settings": {
"name_placeholder": "e.g. general",
"topic_placeholder": "e.g. Engineering",
"no_group": "No group",
"create_new_group": "Create new group",
"delete_room": "Delete Room"
},
"command": {
"placeholder": "Search for a command to run..."
},
"pr": {
"create_merge_commit": "Create a merge commit",
"no_changes": "No changes in this pull request.",
"confirm_merge": "Confirm merge"
},
"commit": {
"back_to_commits": "Back to commits",
"no_changes": "No changes in this commit."
},
"repo": {
"no_description": "No description provided",
"no_description_default": "No description provided."
},
"branches": {
"no_branches": "No branches",
"no_branches_desc": "Create your first branch to get started"
},
"tags": {
"no_tags": "No tags",
"no_tags_desc": "Create a tag to mark a specific point in history"
},
"favorites": {
"no_favorites": "No favorite messages yet."
}
},
"project_create": {
"repo_name": "Repository Name",
"repo_name_placeholder": "e.g. core-api",
"repo_desc_placeholder": "What is this repo for?",
"group_name": "New Group Name",
"channel_name": "Channel Name",
"channel_name_placeholder": "e.g. development",
"board_name": "Board Name",
"board_name_placeholder": "e.g. Sprint Planning",
"board_desc_placeholder": "Task board details...",
"skill_name": "Skill Name",
"skill_name_placeholder": "e.g. Code Reviewer",
"skill_desc_placeholder": "What does this AI skill do?",
"email_placeholder": "user@example.com",
"no_group": "No group",
"create_new_group": "Create new group",
"create_repo": "Create Repository",
"create_channel": "Create Channel",
"create_board": "Create Board",
"create_skill": "Create Skill",
"quick_start": "Quick Start",
"repo_tab": "Repo",
"channel_tab": "Channel",
"board_tab": "Board",
"skill_tab": "Skill",
"invite_tab": "Invite",
"description_optional": "Description (Optional)",
"private_desc": "Only members can see",
"public_desc": "Visible to everyone",
"group": "Group",
"group_name_placeholder": "e.g. Engineering",
"enter_group_name": "Enter a group name.",
"public_channel": "Public Channel",
"private_channel": "Private Channel",
"public_channel_desc": "Everyone in project can join",
"private_channel_desc": "Only invited members",
"invite_email": "Invite by Email",
"role": "Role",
"member_role": "Member",
"admin_role": "Admin",
"send_invitation": "Send Invitation",
"create_failed_repo": "Failed to create repository.",
"create_failed_channel": "Failed to create channel.",
"create_failed_board": "Failed to create board.",
"create_failed_skill": "Failed to create skill.",
"create_failed_invite": "Failed to send invitation."
}
}

View File

@ -0,0 +1,831 @@
{
"auth": {
"login": {
"title": "Bon retour !",
"subtitle": "Nous sommes ravis de vous revoir !",
"account": "Compte",
"account_required": "Veuillez saisir le nom d'utilisateur",
"account_placeholder": "Nom d'utilisateur ou e-mail",
"password": "Mot de passe",
"password_required": "Veuillez saisir le mot de passe",
"verification": "Vérification",
"captcha_placeholder": "Entrez le code",
"captcha_required": "Veuillez saisir le code",
"captcha_title": "Cliquez pour actualiser",
"2fa_code": "Code 2FA",
"2fa_placeholder": "code à 6 chiffres",
"forgot_password": "Mot de passe oublié ?",
"submit": "Se connecter",
"submit_loading": "Connexion en cours...",
"need_account": "Pas encore de compte ?",
"register": "S'inscrire",
"error": {
"failed_to_load_captcha": "Échec du chargement du captcha",
"two_factor_required": "Authentification à deux facteurs requise",
"invalid_credentials": "Nom d'utilisateur ou mot de passe invalide",
"login_failed": "Échec de la connexion"
}
},
"register": {
"title": "Créer un compte",
"subtitle": "Rejoignez-nous aujourd'hui !",
"username": "Nom d'utilisateur",
"username_placeholder": "Choisissez un nom d'utilisateur",
"username_required": "Le nom d'utilisateur est requis",
"username_min_length": "Le nom d'utilisateur doit comporter au moins 3 caractères",
"username_pattern": "Le nom d'utilisateur ne peut contenir que des lettres, des chiffres, _ et -",
"email": "E-mail",
"email_placeholder": "Entrez votre e-mail",
"email_required": "L'e-mail est requis",
"email_invalid": "Adresse e-mail invalide",
"password_placeholder": "Créez un mot de passe",
"confirm_password": "Confirmer le mot de passe",
"confirm_password_placeholder": "Confirmez votre mot de passe",
"confirm_password_required": "Veuillez confirmer votre mot de passe",
"password_min_length": "Le mot de passe doit comporter au moins 8 caractères",
"captcha_placeholder": "Entrez le code",
"submit": "Créer un compte",
"submit_loading": "Création en cours...",
"already_have_account": "Vous avez déjà un compte ?",
"login": "Se connecter",
"passwords_not_match": "Les mots de passe ne correspondent pas",
"user_exists": "Le nom d'utilisateur ou l'e-mail existe déjà",
"registration_failed": "Échec de l'inscription"
},
"forgot_password": {
"email_placeholder": "Entrez votre e-mail",
"captcha_placeholder": "Entrez le code",
"submit": "Continuer",
"back_to_login": "Retour à la connexion"
},
"reset_password": {
"new_password_placeholder": "Entrez le nouveau mot de passe",
"confirm_password_placeholder": "Confirmez le nouveau mot de passe",
"captcha_placeholder": "Entrez le code",
"submit": "Réinitialiser le mot de passe",
"back_to_login": "Retour à la connexion"
},
"change_password": {
"title": "Changer le mot de passe",
"subtitle": "Mettez à jour le mot de passe de votre compte",
"current_password_placeholder": "Entrez le mot de passe actuel",
"new_password_placeholder": "Entrez le nouveau mot de passe",
"confirm_password_placeholder": "Confirmez le nouveau mot de passe",
"captcha_placeholder": "Entrez le code",
"submit": "Mettre à jour le mot de passe",
"back_to_login": "Retour à la connexion"
},
"two_factor": {
"title": "Authentification à deux facteurs",
"enabled": "La 2FA est actuellement activée",
"disabled": "Ajoutez une couche de sécurité supplémentaire",
"description": "L'authentification à deux facteurs ajoute une couche de sécurité supplémentaire à votre compte en exigeant plus qu'un simple mot de passe pour vous connecter.",
"enabled_message": "L'authentification à deux facteurs est actuellement activée sur votre compte.",
"scan_qr_code": "Scanner le code QR",
"or_enter_manually": "Ou entrez ce code manuellement",
"verification_code": "Code de vérification",
"code_required": "Le code est requis",
"code_placeholder": "Entrez le code à 6 chiffres",
"password_placeholder": "Entrez votre mot de passe",
"submit": "Vérifier",
"cancel": "Annuler",
"back": "Retour",
"enable": "Activer la 2FA",
"disable": "Désactiver la 2FA",
"disabling": "Désactivation...",
"error": {
"load_failed": "Échec du chargement du statut 2FA",
"enable_failed": "Échec de l'activation de la 2FA",
"disable_failed": "Échec de la désactivation de la 2FA",
"invalid_code": "Code de vérification invalide"
}
},
"verify_email": {
"title": "Vérifier l'e-mail"
}
},
"common": {
"actions": {
"save": "Enregistrer",
"save_changes": "Enregistrer les modifications",
"cancel": "Annuler",
"delete": "Supprimer",
"edit": "Modifier",
"create": "Créer",
"submit": "Soumettre",
"close": "Fermer",
"confirm": "Confirmer",
"back": "Retour",
"remove": "Supprimer",
"add": "Ajouter",
"retry": "Réessayer",
"generate": "Générer",
"loading": "Chargement..."
},
"states": {
"loading": "Chargement...",
"no_results": "Aucun résultat trouvé",
"error_occurred": "Une erreur s'est produite"
},
"placeholders": {
"search": "Rechercher...",
"message": "Message"
}
},
"navigation": {
"search": "Rechercher",
"search_shortcut": "Rechercher (Ctrl+K)",
"favorites": "Messages favoris",
"no_channels": "Aucune chaîne"
},
"settings": {
"access_keys": {
"title": "Jetons d'accès personnels",
"description": "Jetons que vous avez générés pour accéder à l'API.",
"generate_button": "Générer un nouveau jeton",
"token_name": "Nom du jeton",
"copy_warning": "Assurez-vous de copier votre jeton d'accès personnel maintenant. Vous ne pourrez plus le voir !",
"empty": "Aucun jeton d'accès personnel trouvé.",
"created_on": "Créé le",
"expires": "Expire",
"generate_token": "Générer le jeton",
"messages": {
"name_required": "Veuillez saisir le nom du jeton",
"created_success": "Jeton créé, veuillez le copier rapidement.",
"delete_success": "Jeton supprimé",
"create_failed": "Échec de la création, veuillez réessayer",
"delete_failed": "Échec de la suppression"
}
},
"ssh_keys": {
"title": "Clés SSH",
"description": "Gérer les clés SSH pour les opérations Git",
"add_button": "Ajouter une clé",
"add_title": "Ajouter une nouvelle clé SSH",
"title_label": "Titre",
"public_key_label": "Clé publique",
"name_placeholder": "Ex: MacBook personnel",
"key_placeholder": "ssh-rsa AAAAB3NzaC... user@host",
"empty_title": "Aucune clé SSH ajoutée",
"empty_desc": "Ajoutez des clés SSH pour effectuer des opérations Git en toute sécurité",
"verified": "Vérifié",
"messages": {
"title_key_required": "Veuillez saisir un titre et une clé publique",
"add_success": "Clé SSH ajoutée avec succès",
"add_failed": "Échec de l'ajout, vérifiez le format de la clé",
"delete_success": "Clé SSH supprimée",
"delete_failed": "Échec de la suppression",
"title_updated": "Titre mis à jour",
"update_failed": "Échec de la mise à jour"
}
},
"password": {
"title": "Changer le mot de passe",
"subtitle": "Protégez votre compte avec un mot de passe fort",
"current_password": "Mot de passe actuel",
"current_password_placeholder": "Entrez le mot de passe actuel",
"new_password": "Nouveau mot de passe",
"new_password_placeholder": "Entrez le nouveau mot de passe (8 caractères minimum)",
"confirm_password": "Confirmer le nouveau mot de passe",
"confirm_password_placeholder": "Ressaisissez le nouveau mot de passe",
"submit": "Changer le mot de passe",
"mismatch": "Les mots de passe ne correspondent pas",
"min_length": "Le mot de passe doit comporter au moins 8 caractères",
"change_success": "Mot de passe modifié avec succès",
"change_failed": "Échec de la modification, vérifiez votre mot de passe actuel"
},
"account": {
"title": "Mon compte",
"display_name_placeholder": "Définir le nom d'affichage",
"website_placeholder": "https://example.com",
"company_placeholder": "Organisation (facultatif)"
},
"email": {
"title": "E-mail",
"current_email": "E-mail actuel",
"new_email_placeholder": "new@example.com",
"current_password_placeholder": "Entrez le mot de passe actuel"
},
"billing": {
"title": "Facturation",
"personal_billing": "Facturation personnelle",
"monthly_quota": "Quota mensuel",
"monthly_usage": "Utilisation mensuelle",
"billing_errors": "Erreurs de facturation",
"no_history": "Aucun historique de facturation"
},
"appearance": {
"title": "Apparence",
"timezone_asia_shanghai": "Asie/Shanghai (UTC+8)",
"timezone_asia_tokyo": "Asie/Tokyo (UTC+9)",
"timezone_america_ny": "Amérique/New_York (UTC-5)",
"timezone_america_la": "Amérique/Los_Angeles (UTC-8)",
"timezone_europe_london": "Europe/London (UTC+0)"
},
"notifications": {
"title": "Notifications"
},
"settings_nav": {
"user_settings": "Paramètres utilisateur",
"my_account": "Mon compte",
"billing": "Facturation",
"appearance": "Apparence",
"notifications": "Notifications",
"push_settings": "Paramètres push",
"security": "Sécurité",
"change_password": "Modifier le mot de passe",
"email": "E-mail",
"ssh_keys": "Clés SSH",
"access_tokens": "Jetons d'accès"
},
"my_account": {
"title": "Mon compte",
"subtitle": "Gérez vos informations personnelles",
"avatar": "Avatar",
"upload_avatar": "Télécharger un nouvel avatar",
"remove": "Supprimer",
"avatar_hint": "Prend en charge JPG, PNG ou GIF. Max 2 Mo.",
"username": "Nom d'utilisateur",
"display_name": "Nom d'affichage",
"display_name_placeholder": "Définir le nom d'affichage",
"website": "Site web",
"website_placeholder": "https://example.com",
"organization": "Organisation",
"org_placeholder": "Organisation (facultatif)",
"save_changes": "Enregistrer les modifications",
"load_failed": "Échec du chargement du profil",
"save_success": "Profil enregistré",
"save_failed": "Échec de l'enregistrement, veuillez réessayer",
"avatar_size_error": "L'image doit être inférieure à 2 Mo",
"avatar_upload_success": "Avatar téléchargé, veuillez enregistrer les modifications",
"avatar_upload_failed": "Échec du téléchargement de l'avatar"
},
"email_page": {
"title": "E-mail",
"subtitle": "Gérez votre adresse e-mail",
"current_email": "E-mail actuel",
"no_email_set": "Aucun e-mail défini",
"new_email": "Nouvel e-mail",
"current_password": "Mot de passe actuel (pour vérification)",
"current_password_placeholder": "Entrez le mot de passe actuel",
"save_button": "Changer d'e-mail",
"load_failed": "Échec du chargement des informations e-mail",
"fill_all_fields": "Veuillez remplir tous les champs",
"verification_sent": "E-mail de vérification envoyé, veuillez vérifier votre nouvelle boîte",
"change_failed": "Échec du changement d'e-mail, veuillez vérifier votre mot de passe"
},
"notifications_page": {
"title": "Notifications",
"subtitle": "Gérez vos préférences de notification",
"channels": "Canaux de notification",
"email_notifications": "Notifications par e-mail",
"email_notifications_desc": "Recevoir des notifications par e-mail",
"in_app_notifications": "Notifications dans l'application",
"in_app_notifications_desc": "Recevoir des rappels dans l'application",
"push_notifications": "Notifications push",
"push_notifications_desc": "Recevoir des notifications par push du navigateur",
"digest_mode": "Mode résumé",
"instant": "Instantané",
"daily_digest": "Résumé quotidien",
"weekly_digest": "Résumé hebdomadaire",
"off": "Désactivé",
"notification_types": "Types de notification",
"security_notifications": "Notifications de sécurité",
"security_notifications_desc": "Notifications liées à la sécurité du compte",
"product_updates": "Mises à jour produit",
"product_updates_desc": "Nouvelles fonctionnalités et notifications de mise à jour",
"marketing_emails": "E-mails marketing",
"marketing_emails_desc": "Promotions et offres",
"do_not_disturb": "Ne pas déranger",
"enable_dnd": "Activer Ne pas déranger",
"enable_dnd_desc": "Ne pas recevoir de notifications pendant une période donnée",
"save_button": "Enregistrer les modifications",
"load_failed": "Échec du chargement des préférences de notification",
"save_success": "Paramètres de notification enregistrés",
"save_failed": "Échec de l'enregistrement, veuillez réessayer"
},
"appearance_page": {
"title": "Apparence",
"subtitle": "Personnalisez l'apparence et la langue de l'application",
"theme_scheme": "Schéma de thème",
"select_theme_scheme": "Sélectionner le schéma de thème",
"theme": "Thème",
"language": "Langue",
"timezone": "Fuseau horaire",
"dark": "Sombre",
"light": "Clair",
"system": "Système",
"basic_settings": "Paramètres de base",
"custom": "Personnalisé",
"save_button": "Enregistrer les modifications",
"load_failed": "Échec du chargement des préférences",
"save_success": "Paramètres d'apparence enregistrés",
"save_failed": "Échec de l'enregistrement, veuillez réessayer"
},
"billing": {
"title": "Facturation",
"personal_billing": "Facturation personnelle",
"balance": "Solde",
"monthly_quota": "Quota mensuel",
"monthly_usage": "Utilisation mensuelle",
"billing_errors": "Erreurs de facturation",
"history": "Historique de facturation",
"date": "Date",
"reason": "Raison",
"amount": "Montant",
"no_history": "Aucun historique de facturation",
"load_failed": "Échec du chargement de la facturation",
"insufficient_balance": "Solde insuffisant"
},
"push": {
"title": "Notifications push",
"subtitle": "Recevez des alertes en temps réel même lorsque lapplication est fermée.",
"enable": "Activer les notifications push",
"enable_desc": "Soyez averti des mentions, des issues et des alertes système.",
"filters_title": "Filtres de notification",
"mentions": "Mentions et réponses",
"new_issues": "Nouveaux issues",
"system_updates": "Mises à jour système",
"coming_soon": "Contrôles plus granulaires à venir.",
"not_supported": "Les notifications push ne sont pas prises en charge",
"not_supported_desc": "Votre navigateur ou environnement ne prend pas en charge Web Push. Veuillez utiliser un navigateur de bureau moderne.",
"load_failed": "Échec du chargement des paramètres de notification",
"permission_denied": "Permission de notification refusée par le navigateur",
"update_failed": "Échec de la mise à jour des paramètres push",
"saved": "Paramètres enregistrés avec succès"
}
},
"project": {
"layout": {
"expand_sidebar": "Développer la barre latérale",
"join_banner": {
"preview_mode": "Mode aperçu",
"join_to_participate": "Rejoignez ce projet pour participer et utiliser les outils.",
"read_only": "Ce projet public est en lecture seule jusqu'à ce que vous rejoigniez.",
"join_to_use": "Vous devez rejoindre avant d'utiliser les actions du projet.",
"apply_to_join": "Demander à rejoindre"
}
},
"invitation": {
"title": "Invitation au projet",
"no_pending": "Aucune invitation en attente"
},
"join": {
"title": "Rejoindre ${project}",
"reason_placeholder": "Expliquez aux administrateurs pourquoi vous souhaitez rejoindre.",
"cancel_request": "Annuler la demande",
"join_without_reason": "Rejoindre le projet",
"submit_request": "Soumettre la demande d'adhésion",
"default_desc": "Soumettez une demande pour devenir membre du projet.",
"already_member": "Vous êtes déjà membre de ce projet.",
"open_project": "Ouvrir le projet",
"current_request": "Demande actuelle",
"submitted_on": "Soumis le",
"message": "Message",
"message_desc": "Optionnel, mais utile pour les projets privés ou nécessitant une approbation.",
"submitted": "Demande dadhésion soumise.",
"submit_failed": "Échec de la soumission de la demande.",
"cancelled": "Demande dadhésion annulée.",
"cancel_failed": "Échec de lannulation de la demande."
},
"settings": {
"general": {
"title": "Identité du projet",
"project_name": "Nom du projet",
"project_name_placeholder": "Ex: my-awesome-project",
"display_name": "Nom d'affichage",
"display_name_placeholder": "Ex: Mon Super Projet",
"description": "Description",
"description_placeholder": "Parlez aux gens de ce projet...",
"leave": "Quitter ce projet",
"leave_desc": "Vous perdrez accès à toutes les ressources du projet.",
"leave_btn": "Quitter le projet",
"leave_confirm": "Voulez-vous vraiment quitter ce projet ?",
"leave_success": "Vous avez quitté le projet.",
"leave_failed": "Impossible de quitter le projet."
},
"billing": {
"title": "Facturation actuelle",
"monthly_quota": "Quota mensuel",
"monthly_usage": "Utilisation mensuelle",
"billing_errors": "Erreurs de facturation",
"no_history": "Aucun historique de facturation"
},
"access": {
"invite_member": "Inviter un membre",
"join_settings": "Paramètres d'adhésion",
"pending_invitations": "Invitations en attente",
"join_requests": "Demandes d'adhésion",
"email_placeholder": "user@example.com",
"reason_placeholder": "Que voulez-vous demander ?",
"optional_reason": "Raison optionnelle affichée dans l'historique des demandes."
},
"labels": {
"create_label": "Créer une étiquette",
"label_name_placeholder": "label-name",
"description_placeholder": "description",
"no_labels": "Aucune étiquette"
},
"members": {
"remove_confirm": "Supprimer ${username} de ce projet ?",
"remove": "Supprimer"
},
"danger_zone": {
"delete_project": "Supprimer ce projet",
"delete_button": "Supprimer le projet"
}
},
"repos": {
"title": "Dépôts",
"subtitle": "Hébergez et gérez le code source de votre projet",
"find_placeholder": "Trouver un dépôt...",
"no_repos": "Aucun dépôt",
"no_repos_desc": "Créez votre premier dépôt",
"create_new": "Créer un nouveau dépôt",
"create_new_desc": "Hébergez votre code et collaborez",
"loading": "Chargement des dépôts...",
"load_failed": "Échec du chargement des dépôts",
"new_repo": "Nouveau dépôt",
"type": "Type",
"sort": "Trier",
"repository": "dépôt",
"repositories": "dépôts",
"grid_view": "Vue grille",
"list_view": "Vue liste",
"private": "Privé",
"public": "Public",
"no_description_default": "Aucune description fournie.",
"never": "Jamais",
"just_now": "À l'instant",
"min_ago": "il y a ${count} min",
"hr_ago": "il y a ${count} h",
"day_ago": "il y a ${count} j"
},
"repo": {
"settings": {
"title": "Identité du dépôt",
"name_placeholder": "Nom du dépôt",
"description_placeholder": "Parlez aux gens de ce dépôt...",
"default_branch": "Branche par défaut",
"branch_placeholder": "main",
"desc": "Mettre à jour les informations de base du dépôt"
},
"code": "Code",
"commits": "Commits",
"pull_requests": "Pull Requests",
"branches": "Branches",
"tags": "Tags",
"settings_tab": "Paramètres",
"loading": "Chargement du dépôt...",
"load_failed": "Échec du chargement du dépôt",
"not_found": "Dépôt introuvable"
},
"branch_protection": {
"title": "Règles de protection des branches",
"branch_pattern": "Motif de branche",
"required_approvals": "Approbations requises",
"active_rules": "Règles actives",
"no_rules": "Aucune règle de protection configurée",
"create_rule_hint": "Créez une règle ci-dessus pour protéger vos branches",
"add_rule": "Ajouter une règle",
"no_fork_sync": "Pas de synchronisation fork"
}
},
"issues": {
"title": "Issues",
"subtitle": "Suivez et gérez les tâches et les bugs du projet",
"new_title": "Créer un nouvel issue",
"new_issue": "Nouvel issue",
"search_placeholder": "Rechercher tous les issues...",
"sort": "Trier",
"label": "Étiquette",
"assignee": "Assigné",
"open": "Ouvert",
"closed": "Fermé",
"no_project": "Aucun projet sélectionné",
"select_project": "Sélectionnez un projet pour voir ses issues",
"loading": "Chargement des issues...",
"load_failed": "Échec du chargement des issues",
"no_issues": "Aucun issue ${tab}",
"no_matches": "Aucun résultat trouvé",
"all_caught_up": "Tout est à jour ! Créez un issue pour suivre les nouvelles tâches.",
"try_adjusting": "Essayez d'ajuster votre recherche ou vos filtres.",
"create_first": "Créez votre premier issue",
"brief_description_placeholder": "Décrivez brièvement le problème...",
"details_placeholder": "Expliquez les détails, les étapes de reproduction ou les exigences...",
"pro_tip": "Conseil",
"need_help": "Besoin d'aide ?",
"submit": "Soumettre le nouvel issue",
"issue_title_placeholder": "Titre de l'issue"
},
"issue_detail": {
"loading": "Chargement de l'issue...",
"no_description": "Aucune description fournie.",
"no_comments": "Aucun commentaire. Commencez la discussion !",
"comment_placeholder": "Écrire un commentaire... (Markdown pris en charge)",
"edit": "Modifier",
"close": "Fermer",
"delete": "Supprimer",
"link_pr": "Lier une Pull Request",
"link_repo": "Lier un dépôt",
"pull_requests": "Pull Requests",
"linked_repos": "Dépôts liés",
"no_assigned": "Personne assigné"
},
"pulls": {
"title": "Pull Requests",
"project_pulls": "Pull requests ${project}",
"close": "Fermer",
"reopen": "Rouvrir",
"confirm_delete": "Confirmer",
"delete": "Supprimer",
"no_project": "Aucun projet sélectionné",
"select_project": "Sélectionnez un projet pour voir ses pull requests",
"loading": "Chargement des pull requests...",
"load_failed": "Échec du chargement des pull requests",
"no_prs": "Aucune pull request",
"create_hint": "Créez une pull request pour proposer des modifications"
},
"skills": {
"title": "Compétences",
"subtitle": "Capacités du projet et compétences IA",
"no_project": "Aucun projet sélectionné",
"select_project": "Sélectionnez un projet pour voir ses compétences",
"loading": "Chargement des compétences...",
"load_failed": "Échec du chargement des compétences",
"no_skills": "Aucune compétence",
"no_skills_desc": "Créez ou scannez des compétences pour activer l'assistance IA",
"create": "Créer",
"scan": "Scanner",
"scanning": "Scan en cours...",
"skill_name_placeholder": "my-skill",
"skill_display_name": "Ma compétence",
"skill_description_placeholder": "Ce que fait cette compétence",
"skill_content_placeholder": "Contenu ou prompt de la compétence...",
"delete": "Supprimer la compétence",
"create_title": "Créer une compétence",
"create_desc": "Ajoutez une nouvelle compétence IA au projet."
},
"skill_detail": {
"loading": "Chargement de la compétence...",
"edit_title": "Modifier la compétence",
"save_failed": "Échec de l'enregistrement"
},
"board": {
"title": "Tableau",
"no_boards": "Aucun tableau",
"no_boards_desc": "Créez un tableau Kanban pour gérer votre flux de travail.",
"add_column": "Ajouter une colonne",
"add_column_title": "Ajouter une colonne",
"delete_board": "Supprimer le tableau",
"new_board_title": "Nouveau tableau",
"board_name_placeholder": "Ex: Planification Sprint",
"board_desc_placeholder": "Détails (optionnel)...",
"column_name": "Nom de la colonne",
"column_name_placeholder": "Ex: En cours",
"card_title_placeholder": "Que faut-il faire ?",
"card_desc_placeholder": "Détails (optionnel)...",
"card_detail_title": "Titre de la carte",
"card_detail_desc_placeholder": "Ajoutez une description plus détaillée...",
"delete_card": "Supprimer la carte",
"delete_column": "Supprimer cette colonne ?",
"delete_card_confirm": "Supprimer cette carte ?",
"empty_board": "Tableau vide",
"empty_board_hint": "Ajoutez votre première colonne (ex: À faire, En cours, Terminé) pour commencer le suivi.",
"add_first_column": "Ajouter la première colonne",
"add_card": "Ajouter une carte",
"new_card": "Nouvelle carte",
"card_title": "Titre de la carte",
"id": "ID",
"created": "Créé",
"updated": "Mis à jour"
},
"me": {
"title": "Mon profil",
"recent_repos": "Dépôts récents",
"top_projects": "Projets populaires",
"latest_activity": "Activité récente",
"no_projects": "Aucun projet trouvé",
"no_repos": "Aucun dépôt trouvé",
"no_activity": "Aucune activité récente",
"create_project": {
"title": "Créer un nouveau projet",
"subtitle": "Démarrer un nouveau projet collaboratif",
"name_placeholder": "Ex: my-awesome-team",
"org_placeholder": "Ex: Acme Corporation",
"desc_placeholder": "Quel est ce projet ?",
"slug": "Slug du projet",
"slug_required": "Obligatoire",
"slug_hint": "Utilisé dans les URL. Minuscules, chiffres et tirets uniquement.",
"display_name": "Nom d'affichage",
"display_name_required": "Obligatoire",
"display_name_placeholder": "Ex: Acme Corporation",
"description": "Description",
"visibility": "Visibilité",
"public_project": "Projet public",
"public_desc": "Tout le monde peut découvrir et rejoindre",
"private_project": "Projet privé",
"private_desc": "Seuls les membres invités peuvent accéder",
"change": "Modifier",
"create_failed": "Échec de la création du projet. Le slug est peut-être déjà pris.",
"create": "Créer le projet",
"cancel": "Annuler"
},
"invitations": {
"pending": "Invitations en attente",
"no_pending": "Aucune invitation en attente",
"join_requests": "Demandes d'adhésion",
"no_requests": "Aucune demande d'adhésion"
},
"notifications": {
"no_notifications": "Aucune notification",
"no_unread": "Aucune notification non lue",
"all_caught_up": "Tout est à jour"
},
"followers": {
"no_followers": "Aucun follower"
},
"following": {
"no_following": "Aucun abonnement"
},
"user_not_found": "Utilisateur non trouvé",
"please_login": "Assurez-vous dêtre connecté.",
"profile": {
"joined": "Inscrit",
"projects": "Projets",
"repos": "Dépôts",
"stars": "Étoiles",
"followers": "Abonnés",
"follow": "Suivre",
"unfollow": "Ne plus suivre",
"contributions_in_year": "${count} contributions cette année",
"contributions_on_date": "${count} contributions le ${date}",
"less": "Moins",
"more": "Plus"
}
},
"explore": {
"title": "Explorer les projets",
"subtitle": "Découvrez des projets publics et des communautés",
"search_placeholder": "Rechercher des projets par nom ou description...",
"no_projects": "Aucun projet trouvé",
"no_description": "Pas de description",
"found": "trouvé(s)",
"showing": "Affichage des projets découvrables",
"try_different": "Essayez un autre terme de recherche",
"view_project": "Voir le projet"
},
"chat": {
"conversations": {
"search_placeholder": "Rechercher des conversations...",
"no_conversations": "Aucune conversation",
"no_matching": "Aucune conversation correspondante",
"new_chat": "Nouveau chat",
"chat_history": "Historique du chat",
"start_new_chat": "Démarrez une nouvelle conversation pour explorer avec l'IA.",
"try_different_search": "Essayez un autre terme de recherche.",
"untitled_chat": "Chat sans titre",
"create_failed": "Échec de la création de la conversation"
},
"model_selector": {
"search_placeholder": "Rechercher des modèles...",
"loading": "Chargement...",
"no_models": "Aucun modèle trouvé",
"please_select_model": "Veuillez d'abord sélectionner un modèle"
},
"header": {
"new_chat": "Nouveau chat",
"expand_history": "Développer l'historique",
"collapse_history": "Réduire l'historique",
"streaming": "Streaming...",
"share": "Partager la conversation",
"rename": "Renommer",
"more": "Plus"
},
"message_list": {
"welcome_title": "Comment puis-je vous aider aujourd'hui?",
"welcome_desc": "Posez n'importe quelle question - Je peux aider avec du code, de l'écriture, de l'analyse, et bien plus.",
"new_response": "Nouvelle réponse",
"responding": "en train de répondre...",
"explain_code": "Expliquer du code ou déboguer une erreur",
"summarize_doc": "Résumer ou rédiger un document",
"review_pr": "Examiner une pull request ou un dépôt",
"brainstorm": "Brainstormer des idées ou élaborer un plan",
"jump_to_message": "Aller à votre message ${index}"
}
},
"channel": {
"no_selected": "Aucune chaîne sélectionnée",
"no_threads": "Aucun fil encore. Utilisez le menu d'action du message pour en démarrer un.",
"no_messages": "Aucun message. Commencez la conversation !",
"edit_history": "Historique des modifications",
"no_edit_history": "Aucun historique de modification disponible.",
"no_pinned": "Aucun message épinglé.",
"reply_placeholder": "Répondre au fil...",
"beginning_of_channel": "Début de la chaîne",
"scroll_up": "Faites défiler vers le haut pour charger les messages plus anciens",
"new_message": "${count} nouveau(x) message(s)"
},
"search": {
"global_placeholder": "Rechercher projets, chaînes, dépôts, tableaux, issues...",
"no_results": "Aucun résultat.",
"no_messages": "Aucun message correspondant.",
"results": "Résultats de recherche",
"jump_to": "Accédez aux projets, chaînes, dépôts, tableaux et issues.",
"messages": "Messages",
"quick_switch": "Bascule rapide",
"projects": "Projets",
"my_home": "Mon espace",
"my_home_desc": "Ouvrir l'aperçu personnel",
"explore_projects": "Explorer les projets",
"explore_projects_desc": "Trouver des projets publics",
"chat": "Chat",
"chat_desc": "Ouvrir le chat personnel",
"project_overview": "Aperçu du projet",
"repositories": "Dépôts",
"issues": "Issues",
"boards": "Tableaux",
"project_settings": "Paramètres du projet",
"rooms_in": "Salons dans ${project}",
"all_rooms": "Tous les salons dans ${project}",
"messages_in": "Messages dans ${project}",
"messages_in_room": "Messages dans #${room}"
},
"components": {
"message_input": {
"reply_preview": "Bannière d'aperçu réponse/modification",
"add_emoji": "Ajouter un emoji",
"placeholder": "Message",
"replying": "Répondre au message",
"editing": "Modification du message"
},
"inline_comment": {
"reply_placeholder": "Écrire une réponse...",
"leave_comment": "Laisser un commentaire..."
},
"merge_panel": {
"commit_message_placeholder": "Entrez un message de commit..."
},
"ai_settings": {
"add_ai": "Ajouter IA",
"no_ai": "Aucun agent IA actif dans cette chaîne",
"search_placeholder": "Rechercher des modèles...",
"system_prompt_placeholder": "Vous êtes un assistant utile..."
},
"room_settings": {
"name_placeholder": "Ex: général",
"topic_placeholder": "Ex: Ingénierie",
"no_group": "Aucun groupe",
"create_new_group": "Créer un nouveau groupe",
"delete_room": "Supprimer la chaîne"
},
"command": {
"placeholder": "Rechercher une commande à exécuter..."
},
"pr": {
"create_merge_commit": "Créer un commit de fusion",
"no_changes": "Aucune modification dans cette Pull Request.",
"confirm_merge": "Confirmer la fusion"
},
"commit": {
"back_to_commits": "Retour aux commits",
"no_changes": "Aucune modification dans ce commit."
},
"repo": {
"no_description": "Aucune description",
"no_description_default": "Aucune description fournie."
},
"branches": {
"no_branches": "Aucune branche",
"no_branches_desc": "Créez votre première branche pour commencer"
},
"tags": {
"no_tags": "Aucune étiquette",
"no_tags_desc": "Créez une étiquette pour marquer un point spécifique dans l'historique"
},
"favorites": {
"no_favorites": "Aucun message favori pour le moment."
}
},
"project_create": {
"repo_name": "Nom du dépôt",
"repo_name_placeholder": "Ex: core-api",
"repo_desc_placeholder": "À quoi sert ce dépôt ?",
"group_name": "Nom du nouveau groupe",
"channel_name": "Nom de la chaîne",
"channel_name_placeholder": "Ex: development",
"board_name": "Nom du tableau",
"board_name_placeholder": "Ex: Planification Sprint",
"board_desc_placeholder": "Détails du tableau de tâches...",
"skill_name": "Nom de la compétence",
"skill_name_placeholder": "Ex: Réviseur de code",
"skill_desc_placeholder": "Que fait cette compétence IA ?",
"email_placeholder": "user@example.com",
"no_group": "Aucun groupe",
"create_new_group": "Créer un nouveau groupe",
"create_repo": "Créer un dépôt",
"create_channel": "Créer une chaîne",
"create_board": "Créer un tableau",
"create_skill": "Créer une compétence"
}
}

View File

@ -0,0 +1,831 @@
{
"auth": {
"login": {
"title": "おかえりなさい!",
"subtitle": "また会えてとても嬉しいです!",
"account": "アカウント",
"account_required": "ユーザー名を入力してください",
"account_placeholder": "ユーザー名またはメールアドレス",
"password": "パスワード",
"password_required": "パスワードを入力してください",
"verification": "認証",
"captcha_placeholder": "コードを入力",
"captcha_required": "コードを入力してください",
"captcha_title": "クリックで更新",
"2fa_code": "2FAコード",
"2fa_placeholder": "6桁の数字",
"forgot_password": "パスワードをお忘れですか?",
"submit": "ログイン",
"submit_loading": "ログイン中...",
"need_account": "アカウントをお持ちでない方?",
"register": "登録",
"error": {
"failed_to_load_captcha": "キャプチャの読み込みに失敗しました",
"two_factor_required": "二要素認証が必要です",
"invalid_credentials": "ユーザー名またはパスワードが無効です",
"login_failed": "ログインに失敗しました"
}
},
"register": {
"title": "アカウントを作成",
"subtitle": "今すぐ参加!",
"username": "ユーザー名",
"username_placeholder": "ユーザー名を選択",
"username_required": "ユーザー名を入力してください",
"username_min_length": "ユーザー名は3文字以上である必要があります",
"username_pattern": "ユーザー名には文字、数字、アンダースコア、ハイフンのみ使用できます",
"email": "メールアドレス",
"email_placeholder": "メールアドレスを入力",
"email_required": "メールアドレスを入力してください",
"email_invalid": "メールアドレスの形式が無効です",
"password_placeholder": "パスワードを作成",
"confirm_password": "パスワードの確認",
"confirm_password_placeholder": "パスワードを再入力",
"confirm_password_required": "パスワードの確認を入力してください",
"password_min_length": "パスワードは8文字以上である必要があります",
"captcha_placeholder": "コードを入力",
"submit": "アカウントを作成",
"submit_loading": "作成中...",
"already_have_account": "すでにアカウントをお持ちですか?",
"login": "ログイン",
"passwords_not_match": "パスワードが一致しません",
"user_exists": "ユーザー名またはメールアドレスは既に存在します",
"registration_failed": "登録に失敗しました"
},
"forgot_password": {
"email_placeholder": "メールアドレスを入力",
"captcha_placeholder": "コードを入力",
"submit": "続行",
"back_to_login": "ログインに戻る"
},
"reset_password": {
"new_password_placeholder": "新しいパスワードを入力",
"confirm_password_placeholder": "新しいパスワードを確認",
"captcha_placeholder": "コードを入力",
"submit": "パスワードをリセット",
"back_to_login": "ログインに戻る"
},
"change_password": {
"title": "パスワードを変更",
"subtitle": "アカウントのパスワードを更新",
"current_password_placeholder": "現在のパスワードを入力",
"new_password_placeholder": "新しいパスワードを入力",
"confirm_password_placeholder": "新しいパスワードを確認",
"captcha_placeholder": "コードを入力",
"submit": "パスワードを更新",
"back_to_login": "ログインに戻る"
},
"two_factor": {
"title": "二要素認証",
"enabled": "2FAは有効です",
"disabled": "セキュリティの層を追加",
"description": "二要素認証は、パスワードだけではログインできないようにすることで、アカウントにさらなるセキュリティの層を追加します。",
"enabled_message": "アカウントで二要素認証が有効になっています。",
"scan_qr_code": "QRコードをスキャン",
"or_enter_manually": "またはこのコードを手動で入力",
"verification_code": "確認コード",
"code_required": "コードを入力してください",
"code_placeholder": "6桁のコードを入力",
"password_placeholder": "パスワードを入力",
"submit": "確認",
"cancel": "キャンセル",
"back": "戻る",
"enable": "2FAを有効にする",
"disable": "2FAを無効にする",
"disabling": "無効化中...",
"error": {
"load_failed": "2FAステータスの読み込みに失敗しました",
"enable_failed": "2FAを有効にできませんでした",
"disable_failed": "2FAを無効にできませんでした",
"invalid_code": "確認コードが無効です"
}
},
"verify_email": {
"title": "メールアドレスの確認"
}
},
"common": {
"actions": {
"save": "保存",
"save_changes": "変更を保存",
"cancel": "キャンセル",
"delete": "削除",
"edit": "編集",
"create": "作成",
"submit": "送信",
"close": "閉じる",
"confirm": "確認",
"back": "戻る",
"remove": "削除",
"add": "追加",
"retry": "再試行",
"generate": "生成",
"loading": "読み込み中..."
},
"states": {
"loading": "読み込み中...",
"no_results": "結果が見つかりません",
"error_occurred": "エラーが発生しました"
},
"placeholders": {
"search": "検索...",
"message": "メッセージ"
}
},
"navigation": {
"search": "検索",
"search_shortcut": "検索 (Ctrl+K)",
"favorites": "お気に入りのメッセージ",
"no_channels": "チャンネルがありません"
},
"settings": {
"access_keys": {
"title": "パーソナルアクセストークン",
"description": "APIにアクセスするために生成したトークンです。",
"generate_button": "新規トークン生成",
"token_name": "トークン名",
"copy_warning": "今すぐパーソナルアクセストークンをコピーしてください。もう一度表示することはできません!",
"empty": "パーソナルアクセストークンが見つかりません。",
"created_on": "作成日",
"expires": "有効期限",
"generate_token": "トークンを生成",
"messages": {
"name_required": "トークン名を入力してください",
"created_success": "トークンを作成しました。今すぐコピーしてください。",
"delete_success": "トークンを削除しました",
"create_failed": "作成に失敗しました。再試行してください",
"delete_failed": "削除に失敗しました"
}
},
"ssh_keys": {
"title": "SSH キー",
"description": "Git操作用のSSHキーを管理",
"add_button": "キーを追加",
"add_title": "新しいSSHキーを追加",
"title_label": "タイトル",
"public_key_label": "公開鍵",
"name_placeholder": "例個人のMacBook",
"key_placeholder": "ssh-rsa AAAAB3NzaC... user@host",
"empty_title": "SSHキーがまだ追加されていません",
"empty_desc": "SSHキーを追加して安全にGit操作を実行",
"verified": "検証済み",
"messages": {
"title_key_required": "タイトルと公開鍵を入力してください",
"add_success": "SSHキーを追加しました",
"add_failed": "追加に失敗しました。鍵の形式を確認してください",
"delete_success": "SSHキーを削除しました",
"delete_failed": "削除に失敗しました",
"title_updated": "タイトルを更新しました",
"update_failed": "更新に失敗しました"
}
},
"password": {
"title": "パスワードを変更",
"subtitle": "アカウントを強力なパスワードで保護",
"current_password": "現在のパスワード",
"current_password_placeholder": "現在のパスワードを入力",
"new_password": "新しいパスワード",
"new_password_placeholder": "新しいパスワードを入力8文字以上",
"confirm_password": "新しいパスワードを確認",
"confirm_password_placeholder": "新しいパスワードを再入力",
"submit": "パスワードを変更",
"mismatch": "パスワードが一致しません",
"min_length": "パスワードは8文字以上である必要があります",
"change_success": "パスワードを変更しました",
"change_failed": "パスワードの変更に失敗しました。現在のパスワードを確認してください"
},
"account": {
"title": "マイアカウント",
"display_name_placeholder": "表示名を設定",
"website_placeholder": "https://example.com",
"company_placeholder": "所属組織(任意)"
},
"email": {
"title": "メールアドレス",
"current_email": "現在のメールアドレス",
"new_email_placeholder": "new@example.com",
"current_password_placeholder": "現在のパスワードを入力"
},
"billing": {
"title": "請求",
"personal_billing": "個人請求",
"monthly_quota": "月間配额",
"monthly_usage": "今月の使用量",
"billing_errors": "請求エラー",
"no_history": "請求履歴がありません"
},
"appearance": {
"title": "外観",
"timezone_asia_shanghai": "アジア/上海 (UTC+8)",
"timezone_asia_tokyo": "アジア/東京 (UTC+9)",
"timezone_america_ny": "アメリカ/ニューヨーク (UTC-5)",
"timezone_america_la": "アメリカ/ロサンゼルス (UTC-8)",
"timezone_europe_london": "ヨーロッパ/ロンドン (UTC+0)"
},
"notifications": {
"title": "通知"
},
"settings_nav": {
"user_settings": "ユーザー設定",
"my_account": "マイアカウント",
"billing": "請求",
"appearance": "外観",
"notifications": "通知",
"push_settings": "プッシュ設定",
"security": "セキュリティ",
"change_password": "パスワード変更",
"email": "メールアドレス",
"ssh_keys": "SSHキー",
"access_tokens": "アクセストークン"
},
"my_account": {
"title": "マイアカウント",
"subtitle": "個人情報を管理",
"avatar": "アバター",
"upload_avatar": "新しいアバターをアップロード",
"remove": "削除",
"avatar_hint": "JPG、PNG、GIFに対応。最大2MB。",
"username": "ユーザー名",
"display_name": "表示名",
"display_name_placeholder": "表示名を設定",
"website": "ウェブサイト",
"website_placeholder": "https://example.com",
"organization": "組織",
"org_placeholder": "所属組織(任意)",
"save_changes": "変更を保存",
"load_failed": "プロフィールの読み込みに失敗しました",
"save_success": "プロフィールが保存されました",
"save_failed": "保存に失敗しました。もう一度お試しください",
"avatar_size_error": "画像サイズは2MB以下にしてください",
"avatar_upload_success": "アバターがアップロードされました。変更を保存してください",
"avatar_upload_failed": "アバターのアップロードに失敗しました"
},
"email_page": {
"title": "メールアドレス",
"subtitle": "メールアドレスを管理",
"current_email": "現在のメールアドレス",
"no_email_set": "メールアドレス未設定",
"new_email": "新しいメールアドレス",
"current_password": "現在のパスワード(確認用)",
"current_password_placeholder": "現在のパスワードを入力",
"save_button": "メールアドレスを変更",
"load_failed": "メールアドレスの読み込みに失敗しました",
"fill_all_fields": "すべてのフィールドを入力してください",
"verification_sent": "確認メールが送信されました。新しいメールをご確認ください",
"change_failed": "メールアドレスの変更に失敗しました。パスワードを確認してください"
},
"notifications_page": {
"title": "通知",
"subtitle": "通知設定を管理",
"channels": "通知チャンネル",
"email_notifications": "メール通知",
"email_notifications_desc": "メールで通知を受け取る",
"in_app_notifications": "アプリ内通知",
"in_app_notifications_desc": "アプリ内で通知を受け取る",
"push_notifications": "プッシュ通知",
"push_notifications_desc": "ブラウザのプッシュで通知を受け取る",
"digest_mode": "まとめ通知モード",
"instant": "即時",
"daily_digest": "日次まとめ",
"weekly_digest": "週次まとめ",
"off": "オフ",
"notification_types": "通知タイプ",
"security_notifications": "セキュリティ通知",
"security_notifications_desc": "アカウントセキュリティ関連の通知",
"product_updates": "製品アップデート",
"product_updates_desc": "新機能と製品アップデートのお知らせ",
"marketing_emails": "マーケティングメール",
"marketing_emails_desc": "キャンペーンとオファー情報",
"do_not_disturb": "おやすみモード",
"enable_dnd": "おやすみモードを有効にする",
"enable_dnd_desc": "指定した時間帯に通知を受け取らない",
"save_button": "変更を保存",
"load_failed": "通知設定の読み込みに失敗しました",
"save_success": "通知設定が保存されました",
"save_failed": "保存に失敗しました。もう一度お試しください"
},
"appearance_page": {
"title": "外観",
"subtitle": "アプリの外観と言語をカスタマイズ",
"theme_scheme": "テーマスキーム",
"select_theme_scheme": "テーマスキームを選択",
"theme": "テーマ",
"language": "言語",
"timezone": "タイムゾーン",
"dark": "ダーク",
"light": "ライト",
"system": "システムに従う",
"basic_settings": "基本設定",
"custom": "カスタマイズ",
"save_button": "変更を保存",
"load_failed": "設定の読み込みに失敗しました",
"save_success": "外観設定が保存されました",
"save_failed": "保存に失敗しました。もう一度お試しください"
},
"billing": {
"title": "請求",
"personal_billing": "個人請求",
"balance": "残高",
"monthly_quota": "月間配额",
"monthly_usage": "今月の使用量",
"billing_errors": "請求エラー",
"history": "請求履歴",
"date": "日付",
"reason": "理由",
"amount": "金額",
"no_history": "請求履歴がありません",
"load_failed": "請求の読み込みに失敗しました",
"insufficient_balance": "残高不足"
},
"push": {
"title": "プッシュ通知",
"subtitle": "アプリが閉じていてもリアルタイムアラートを受信。",
"enable": "プッシュ通知を有効にする",
"enable_desc": "メンション、イシュー、システムアラートの通知を受け取る。",
"filters_title": "通知フィルター",
"mentions": "メンションと返信",
"new_issues": "新規イシュー",
"system_updates": "システムアップデート",
"coming_soon": "より細かい制御は近日公開予定。",
"not_supported": "プッシュ通知はサポートされていません",
"not_supported_desc": "お使いのブラウザまたは環境はWeb Pushをサポートしていません。最新のデスクトップブラウザをご利用ください。",
"load_failed": "通知設定の読み込みに失敗しました",
"permission_denied": "ブラウザによって通知権限が拒否されました",
"update_failed": "プッシュ設定の更新に失敗しました",
"saved": "設定を保存しました"
}
},
"project": {
"layout": {
"expand_sidebar": "サイドバーを展開",
"join_banner": {
"preview_mode": "プレビューモード",
"join_to_participate": "参加してプロジェクトツールを利用しましょう。",
"read_only": "このパブリックプロジェクトは、参加するまでは読み取り専用です。",
"join_to_use": "プロジェクトアクションを使用するには参加が必要です。",
"apply_to_join": "参加を申請"
}
},
"invitation": {
"title": "プロジェクト招待",
"no_pending": "保留中の招待はありません"
},
"join": {
"title": "${project} に参加",
"reason_placeholder": "管理者への参加理由を告げてください。",
"cancel_request": "リクエストをキャンセル",
"join_without_reason": "プロジェクトに参加",
"submit_request": "参加リクエストを送信",
"default_desc": "リクエストを送信してプロジェクトメンバーになる。",
"already_member": "あなたはすでにこのプロジェクトのメンバーです。",
"open_project": "プロジェクトを開く",
"current_request": "現在のリクエスト",
"submitted_on": "提出日",
"message": "メッセージ",
"message_desc": "任意ですが、プライベートプロジェクトや承認が必要なプロジェクトに役立ちます。",
"submitted": "参加リクエストを送信しました。",
"submit_failed": "参加リクエストの送信に失敗しました。",
"cancelled": "参加リクエストをキャンセルしました。",
"cancel_failed": "参加リクエストのキャンセルに失敗しました。"
},
"settings": {
"general": {
"title": "プロジェクト情報",
"project_name": "プロジェクト名",
"project_name_placeholder": "例my-awesome-project",
"display_name": "表示名",
"display_name_placeholder": "例My Awesome Project",
"description": "説明",
"description_placeholder": "このプロジェクトについて説明してください...",
"leave": "このプロジェクトを退出",
"leave_desc": "すべてのプロジェクトリソースへのアクセスを失います。",
"leave_btn": "プロジェクトを退出",
"leave_confirm": "このプロジェクトを退出してもよろしいですか?",
"leave_success": "プロジェクトを退出しました。",
"leave_failed": "プロジェクトの退出に失敗しました。"
},
"billing": {
"title": "現在の請求",
"monthly_quota": "月間配额",
"monthly_usage": "今月の使用量",
"billing_errors": "請求エラー",
"no_history": "請求履歴がありません"
},
"access": {
"invite_member": "メンバーを招待",
"join_settings": "参加設定",
"pending_invitations": "保留中の招待",
"join_requests": "参加リクエスト",
"email_placeholder": "user@example.com",
"reason_placeholder": "何を聞きたいですか?",
"optional_reason": "リクエスト履歴に表示される任意の説明。"
},
"labels": {
"create_label": "ラベルを作成",
"label_name_placeholder": "label-name",
"description_placeholder": "説明",
"no_labels": "ラベルがありません"
},
"members": {
"remove_confirm": "${username} をプロジェクトから削除しますか?",
"remove": "削除"
},
"danger_zone": {
"delete_project": "このプロジェクトを削除",
"delete_button": "プロジェクトを削除"
}
},
"repos": {
"title": "リポジトリ",
"subtitle": "プロジェクトのソースコードをホスト・管理",
"find_placeholder": "リポジトリを検索...",
"no_repos": "リポジトリがありません",
"no_repos_desc": "最初のリポジトリを作成して始めましょう",
"create_new": "新規リポジトリを作成",
"create_new_desc": "コードをホストしてコラボレーション",
"loading": "リポジトリを読み込み中...",
"load_failed": "リポジトリの読み込みに失敗しました",
"new_repo": "新規リポジトリ",
"type": "タイプ",
"sort": "並び替え",
"repository": "リポジトリ",
"repositories": "リポジトリ",
"grid_view": "グリッド表示",
"list_view": "リスト表示",
"private": "プライベート",
"public": "パブリック",
"no_description_default": "説明は提供されていません。",
"never": "なし",
"just_now": "たった今",
"min_ago": "${count}分前",
"hr_ago": "${count}時間前",
"day_ago": "${count}日前"
},
"repo": {
"settings": {
"title": "リポジトリ情報",
"name_placeholder": "リポジトリ名",
"description_placeholder": "このリポジトリについて説明してください...",
"default_branch": "デフォルトブランチ",
"branch_placeholder": "main",
"desc": "リポジトリの基本情報を更新"
},
"code": "コード",
"commits": "コミット",
"pull_requests": "Pull Requests",
"branches": "ブランチ",
"tags": "タグ",
"settings_tab": "設定",
"loading": "リポジトリを読み込み中...",
"load_failed": "リポジトリの読み込みに失敗しました",
"not_found": "リポジトリが見つかりません"
},
"branch_protection": {
"title": "ブランチ保護ルール",
"branch_pattern": "ブランチパターン",
"required_approvals": "必要な承認数",
"active_rules": "有効なルール",
"no_rules": "ブランチ保護ルールが設定されていません",
"create_rule_hint": "上記でルールを作成してブランチを保護してください",
"add_rule": "ルールを追加",
"no_fork_sync": "フォーク同期なし"
}
},
"issues": {
"title": "イシュー",
"subtitle": "プロジェクトのタスクとバグを追跡・管理",
"new_title": "新規イシューを作成",
"new_issue": "新規イシュー",
"search_placeholder": "すべてのイシューを検索...",
"sort": "並び替え",
"label": "ラベル",
"assignee": "担当者",
"open": "オープン",
"closed": "クローズ",
"no_project": "プロジェクトが選択されていません",
"select_project": "イシューを表示するプロジェクトを選択してください",
"loading": "イシューを読み込み中...",
"load_failed": "イシューの読み込みに失敗しました",
"no_issues": "${tab} イシューはありません",
"no_matches": "一致するものが見つかりません",
"all_caught_up": "すべて完了しました!イシューを作成して新しいタスクを追跡しましょう。",
"try_adjusting": "検索条件やフィルターを調整してみてください。",
"create_first": "最初のイシューを作成",
"brief_description_placeholder": "問題を簡単に説明...",
"details_placeholder": "詳細、再現手順、要件を説明...",
"pro_tip": "ヒント",
"need_help": "ヘルプが必要ですか?",
"submit": "新規イシューを送信",
"issue_title_placeholder": "イシュータイトル"
},
"issue_detail": {
"loading": "イシューを読み込み中...",
"no_description": "説明がありません。",
"no_comments": "コメントがありません。議論を始めましょう!",
"comment_placeholder": "コメントを書く... (Markdown対応)",
"edit": "編集",
"close": "閉じる",
"delete": "削除",
"link_pr": "Pull Requestをリンク",
"link_repo": "リポジトリをリンク",
"pull_requests": "Pull Requests",
"linked_repos": "リンクされたリポジトリ",
"no_assigned": "未割り当て"
},
"pulls": {
"title": "Pull Requests",
"project_pulls": "${project} のプルリクエスト",
"close": "閉じる",
"reopen": "再開",
"confirm_delete": "確認",
"delete": "削除",
"no_project": "プロジェクトが選択されていません",
"select_project": "プルリクエストを表示するプロジェクトを選択してください",
"loading": "プルリクエストを読み込み中...",
"load_failed": "プルリクエストの読み込みに失敗しました",
"no_prs": "Pull Requestsがありません",
"create_hint": "変更を提案するためにPull Requestを作成"
},
"skills": {
"title": "スキル",
"subtitle": "プロジェクト機能とAIスキル",
"no_project": "プロジェクトが選択されていません",
"select_project": "スキルを表示するプロジェクトを選択してください",
"loading": "スキルを読み込み中...",
"load_failed": "スキルの読み込みに失敗しました",
"no_skills": "スキルがありません",
"no_skills_desc": "AI支援を有効にするためにスキルを作成またはスキャン",
"create": "作成",
"scan": "スキャン",
"scanning": "スキャン中...",
"skill_name_placeholder": "my-skill",
"skill_display_name": "私のスキル",
"skill_description_placeholder": "このスキルの説明",
"skill_content_placeholder": "スキル内容やプロンプト...",
"delete": "スキルを削除",
"create_title": "スキルを作成",
"create_desc": "プロジェクトに新しいAIスキルを追加します。"
},
"skill_detail": {
"loading": "スキルを読み込み中...",
"edit_title": "スキルを編集",
"save_failed": "保存に失敗しました"
},
"board": {
"title": "ボード",
"no_boards": "ボードがありません",
"no_boards_desc": "Kanbanボードを作成してワークフロー管理を始めましょう。",
"add_column": "列を追加",
"add_column_title": "列を追加",
"delete_board": "ボードを削除",
"new_board_title": "新規ボード",
"board_name_placeholder": "例:スプリント計画",
"board_desc_placeholder": "説明(任意)...",
"column_name": "列名",
"column_name_placeholder": "例:進行中",
"card_title_placeholder": "何をする必要がありますか?",
"card_desc_placeholder": "説明(任意)...",
"card_detail_title": "カードタイトル",
"card_detail_desc_placeholder": "詳細な説明を追加...",
"delete_card": "カードを削除",
"delete_column": "この列を削除しますか?",
"delete_card_confirm": "このカードを削除しますか?",
"empty_board": "空のボード",
"empty_board_hint": "最初の列を追加してくださいTo Do、進行中、完了して作業の追跡を開始します。",
"add_first_column": "最初の列を追加",
"add_card": "カードを追加",
"new_card": "新しいカード",
"card_title": "カードタイトル",
"id": "ID",
"created": "作成日時",
"updated": "更新日時"
},
"me": {
"title": "マイページ",
"recent_repos": "最近のリポジトリ",
"top_projects": "トッププロジェクト",
"latest_activity": "最近のアクティビティ",
"no_projects": "プロジェクトが見つかりません",
"no_repos": "リポジトリが見つかりません",
"no_activity": "最近のアクティビティはありません",
"create_project": {
"title": "新規プロジェクトを作成",
"subtitle": "新しいコラボレーションプロジェクトを開始",
"name_placeholder": "例my-awesome-team",
"org_placeholder": "例Acme Corporation",
"desc_placeholder": "このプロジェクトは何ですか?",
"slug": "プロジェクトスラッグ",
"slug_required": "必須",
"slug_hint": "URLに使用。小文字、数字、ハイフンのみ。",
"display_name": "表示名",
"display_name_required": "必須",
"display_name_placeholder": "例Acme Corporation",
"description": "説明",
"visibility": "公開設定",
"public_project": "公開プロジェクト",
"public_desc": "誰でも発見して参加できます",
"private_project": "非公開プロジェクト",
"private_desc": "招待されたメンバーのみアクセス可能",
"change": "変更",
"create_failed": "プロジェクトを作成できませんでした。スラッグが既に取得されている可能性があります。",
"create": "プロジェクトを作成",
"cancel": "キャンセル"
},
"invitations": {
"pending": "保留中の招待",
"no_pending": "保留中の招待はありません",
"join_requests": "参加リクエスト",
"no_requests": "参加リクエストはありません"
},
"notifications": {
"no_notifications": "通知はありません",
"no_unread": "未読の通知はありません",
"all_caught_up": "すべて完了しました"
},
"followers": {
"no_followers": "フォロワーはいません"
},
"following": {
"no_following": "フォロー中はいません"
},
"user_not_found": "ユーザーが見つかりません",
"please_login": "ログインしていることを確認してください。",
"profile": {
"joined": "参加日",
"projects": "プロジェクト",
"repos": "リポジトリ",
"stars": "スター",
"followers": "フォロワー",
"follow": "フォロー",
"unfollow": "フォロー解除",
"contributions_in_year": "過去1年で${count}件のコントリビューション",
"contributions_on_date": "${count}件のコントリビューション(${date}",
"less": "少",
"more": "多"
}
},
"explore": {
"title": "プロジェクトを探索",
"subtitle": "パブリックプロジェクトとコミュニティを発見",
"search_placeholder": "プロジェクトを名前または説明で検索...",
"no_projects": "プロジェクトが見つかりません",
"no_description": "説明なし",
"found": "見つかりました",
"showing": "発見可能なプロジェクトを表示中",
"try_different": "別の検索語を試してください",
"view_project": "プロジェクトを表示"
},
"chat": {
"conversations": {
"search_placeholder": "会話を検索...",
"no_conversations": "会話はまだありません",
"no_matching": "一致する会話はありません",
"new_chat": "新しいチャット",
"chat_history": "チャット履歴",
"start_new_chat": "新しいチャットを開始してAIと探索しましょう。",
"try_different_search": "別の検索語を試してください。",
"untitled_chat": "無題のチャット",
"create_failed": "チャットの作成に失敗しました"
},
"model_selector": {
"search_placeholder": "モデルを検索...",
"loading": "読み込み中...",
"no_models": "モデルが見つかりません",
"please_select_model": "まずモデルを選択してください"
},
"header": {
"new_chat": "新しいチャット",
"expand_history": "チャット履歴を展開",
"collapse_history": "チャット履歴を折りたたむ",
"streaming": "ストリーミング...",
"share": "会話を共有",
"rename": "名前変更",
"more": "もっと"
},
"message_list": {
"welcome_title": "今日はどのようにお手伝いしましょうか?",
"welcome_desc": "どのようなことでも質問してください — コード、執筆、分析など、お手伝いできます。",
"new_response": "新しい返答",
"responding": "返答中...",
"explain_code": "コードを説明するか、エラーをデバッグ",
"summarize_doc": "ドキュメントを要約または作成",
"review_pr": "Pull Request またはリポジトリをレビュー",
"brainstorm": "アイデアをブレインストーミングまたは計画を立案",
"jump_to_message": "あなたのメッセージ ${index} にジャンプ"
}
},
"channel": {
"no_selected": "チャンネルが選択されていません",
"no_threads": "スレッドはまだありません。メッセージアクションメニューから開始してください。",
"no_messages": "メッセージはまだありません。会話を始めましょう!",
"edit_history": "編集履歴",
"no_edit_history": "編集履歴はありません。",
"no_pinned": "ピン留めされたメッセージはまだありません。",
"reply_placeholder": "スレッドに返信...",
"beginning_of_channel": "チャンネルの開始",
"scroll_up": "上にスクロールして古いメッセージを読み込む",
"new_message": "${count} 件の新しいメッセージ"
},
"search": {
"global_placeholder": "プロジェクト、チャンネル、リポジトリ、ボード、イシューを検索...",
"no_results": "結果が見つかりません。",
"no_messages": "一致するメッセージはありません。",
"results": "検索結果",
"jump_to": "プロジェクト、チャンネル、リポジトリ、ボード、イシューにジャンプ。",
"messages": "メッセージ",
"quick_switch": "クイック切替",
"projects": "プロジェクト",
"my_home": "マイホーム",
"my_home_desc": "個人概要を開く",
"explore_projects": "プロジェクトを探索",
"explore_projects_desc": "パブリックプロジェクトを探す",
"chat": "チャット",
"chat_desc": "パーソナルチャットを開く",
"project_overview": "プロジェクト概要",
"repositories": "リポジトリ",
"issues": "イシュー",
"boards": "ボード",
"project_settings": "プロジェクト設定",
"rooms_in": "${project} 内のルーム",
"all_rooms": "${project} 内のすべてのルーム",
"messages_in": "${project} 内のメッセージ",
"messages_in_room": "#${room} 内のメッセージ"
},
"components": {
"message_input": {
"reply_preview": "返信/編集プレビューバナー",
"add_emoji": "絵文字を追加",
"placeholder": "メッセージ",
"replying": "メッセージに返信",
"editing": "メッセージを編集中"
},
"inline_comment": {
"reply_placeholder": "返信を書く...",
"leave_comment": "コメントを残す..."
},
"merge_panel": {
"commit_message_placeholder": "コミットメッセージを入力..."
},
"ai_settings": {
"add_ai": "AIを追加",
"no_ai": "このチャンネルにはアクティブなAIエージェントがありません",
"search_placeholder": "モデルを検索...",
"system_prompt_placeholder": "あなたは有帮助なアシスタントです..."
},
"room_settings": {
"name_placeholder": "例general",
"topic_placeholder": "例:エンジニアリング",
"no_group": "グループなし",
"create_new_group": "新規グループを作成",
"delete_room": "部屋を削除"
},
"command": {
"placeholder": "実行するコマンドを検索..."
},
"pr": {
"create_merge_commit": "マージコミットを作成",
"no_changes": "このPull Requestには変更がありません。",
"confirm_merge": "マージを確認"
},
"commit": {
"back_to_commits": "コミットに戻る",
"no_changes": "このコミットには変更がありません。"
},
"repo": {
"no_description": "説明なし",
"no_description_default": "説明は提供されていません。"
},
"branches": {
"no_branches": "ブランチがありません",
"no_branches_desc": "最初のブランチを作成して始めましょう"
},
"tags": {
"no_tags": "タグがありません",
"no_tags_desc": "履歴の特定のポイントをマークするタグを作成"
},
"favorites": {
"no_favorites": "お気に入りのメッセージはまだありません。"
}
},
"project_create": {
"repo_name": "リポジトリ名",
"repo_name_placeholder": "例core-api",
"repo_desc_placeholder": "このリポジトリは何用ですか?",
"group_name": "新規グループ名",
"channel_name": "チャンネル名",
"channel_name_placeholder": "例development",
"board_name": "ボード名",
"board_name_placeholder": "例:スプリント計画",
"board_desc_placeholder": "タスクボードの詳細...",
"skill_name": "スキル名",
"skill_name_placeholder": "例:コードレビュアー",
"skill_desc_placeholder": "このAIスキルの役割は何ですか",
"email_placeholder": "user@example.com",
"no_group": "グループなし",
"create_new_group": "新規グループを作成",
"create_repo": "リポジトリを作成",
"create_channel": "チャンネルを作成",
"create_board": "ボードを作成",
"create_skill": "スキルを作成"
}
}

View File

@ -0,0 +1,854 @@
{
"auth": {
"login": {
"title": "欢迎回来!",
"subtitle": "很高兴再次见到你!",
"account": "账户",
"account_required": "请输入用户名",
"account_placeholder": "用户名或邮箱",
"password": "密码",
"password_required": "请输入密码",
"verification": "验证码",
"captcha_placeholder": "输入验证码",
"captcha_required": "请输入验证码",
"captcha_title": "点击刷新",
"2fa_code": "两步验证码",
"2fa_placeholder": "6位数字",
"forgot_password": "忘记密码?",
"submit": "登录",
"submit_loading": "登录中...",
"need_account": "还没有账户?",
"register": "注册",
"error": {
"failed_to_load_captcha": "验证码加载失败",
"two_factor_required": "需要两步验证",
"invalid_credentials": "用户名或密码错误",
"login_failed": "登录失败"
}
},
"register": {
"title": "创建账户",
"subtitle": "立即加入!",
"username": "用户名",
"username_placeholder": "选择用户名",
"username_required": "请输入用户名",
"username_min_length": "用户名至少需要3个字符",
"username_pattern": "用户名只能包含字母、数字、下划线和连字符",
"email": "邮箱",
"email_placeholder": "输入邮箱",
"email_required": "请输入邮箱",
"email_invalid": "邮箱格式无效",
"password_placeholder": "创建密码",
"confirm_password": "确认密码",
"confirm_password_placeholder": "确认密码",
"confirm_password_required": "请确认密码",
"password_min_length": "密码至少需要8个字符",
"captcha_placeholder": "输入验证码",
"submit": "创建账户",
"submit_loading": "正在创建...",
"already_have_account": "已有账户?",
"login": "登录",
"passwords_not_match": "两次输入的密码不一致",
"user_exists": "用户名或邮箱已存在",
"registration_failed": "注册失败"
},
"forgot_password": {
"email_placeholder": "输入邮箱",
"captcha_placeholder": "输入验证码",
"submit": "继续",
"back_to_login": "返回登录"
},
"reset_password": {
"new_password_placeholder": "输入新密码",
"confirm_password_placeholder": "确认新密码",
"captcha_placeholder": "输入验证码",
"submit": "重置密码",
"back_to_login": "返回登录"
},
"change_password": {
"title": "修改密码",
"subtitle": "更新您的账户密码",
"current_password_placeholder": "输入当前密码",
"new_password_placeholder": "输入新密码",
"confirm_password_placeholder": "确认新密码",
"captcha_placeholder": "输入验证码",
"submit": "更新密码",
"back_to_login": "返回登录"
},
"two_factor": {
"title": "两步验证",
"enabled": "已启用两步验证",
"disabled": "添加额外的安全层",
"description": "两步验证通过要求除了密码之外的内容来登录,为您的账户增加了额外的安全层。",
"enabled_message": "您的账户已启用两步验证。",
"scan_qr_code": "扫描二维码",
"or_enter_manually": "或手动输入此代码",
"verification_code": "验证码",
"code_required": "请输入验证码",
"code_placeholder": "输入6位数字",
"password_placeholder": "输入密码",
"submit": "验证",
"cancel": "取消",
"back": "返回",
"enable": "启用两步验证",
"disable": "禁用两步验证",
"disabling": "正在禁用...",
"error": {
"load_failed": "加载两步验证状态失败",
"enable_failed": "启用两步验证失败",
"disable_failed": "禁用两步验证失败",
"invalid_code": "验证码无效"
}
},
"verify_email": {
"title": "验证邮箱"
}
},
"common": {
"actions": {
"save": "保存",
"save_changes": "保存更改",
"cancel": "取消",
"delete": "删除",
"edit": "编辑",
"create": "创建",
"submit": "提交",
"close": "关闭",
"confirm": "确认",
"back": "返回",
"remove": "移除",
"add": "添加",
"retry": "重试",
"generate": "生成",
"loading": "加载中..."
},
"states": {
"loading": "加载中...",
"no_results": "未找到结果",
"error_occurred": "发生错误"
},
"placeholders": {
"search": "搜索...",
"message": "消息"
},
"unknown": "未知"
},
"navigation": {
"search": "搜索",
"search_shortcut": "搜索 (Ctrl+K)",
"favorites": "收藏消息",
"no_channels": "暂无频道"
},
"settings": {
"access_keys": {
"title": "个人访问令牌",
"description": "您已生成的用于访问API的令牌。",
"generate_button": "生成新令牌",
"token_name": "令牌名称",
"copy_warning": "请务必现在复制您的个人访问令牌。您将无法再次查看!",
"empty": "未找到个人访问令牌。",
"created_on": "创建于",
"expires": "过期时间",
"generate_token": "生成令牌",
"messages": {
"name_required": "请输入Token名称",
"created_success": "Token创建成功请尽快复制。",
"delete_success": "Token已删除",
"create_failed": "创建失败,请重试",
"delete_failed": "删除失败"
}
},
"ssh_keys": {
"title": "SSH 密钥",
"description": "管理用于 Git 操作的 SSH 密钥",
"add_button": "添加密钥",
"add_title": "添加新的 SSH 密钥",
"title_label": "标题",
"public_key_label": "公钥",
"name_placeholder": "例如:个人 MacBook",
"key_placeholder": "ssh-rsa AAAAB3NzaC... user@host",
"empty_title": "还没有添加 SSH 密钥",
"empty_desc": "添加 SSH 密钥以安全地执行 Git 操作",
"verified": "已验证",
"messages": {
"title_key_required": "请填写标题和公钥",
"add_success": "SSH 密钥添加成功",
"add_failed": "添加失败,请检查公钥格式",
"delete_success": "SSH 密钥已删除",
"delete_failed": "删除失败",
"title_updated": "标题已更新",
"update_failed": "更新失败"
}
},
"password": {
"title": "修改密码",
"subtitle": "为了保护你的账户安全,请设置强密码",
"current_password": "当前密码",
"current_password_placeholder": "输入当前密码",
"new_password": "新密码",
"new_password_placeholder": "输入新密码至少8位",
"confirm_password": "确认新密码",
"confirm_password_placeholder": "再次输入新密码",
"submit": "修改密码",
"mismatch": "两次输入的新密码不一致",
"min_length": "新密码长度至少为8位",
"change_success": "密码修改成功",
"change_failed": "密码修改失败,请检查当前密码是否正确"
},
"account": {
"title": "我的账户",
"display_name_placeholder": "设置显示名称",
"website_placeholder": "https://example.com",
"company_placeholder": "所属组织(可选)"
},
"email": {
"title": "邮箱",
"current_email": "当前邮箱",
"new_email_placeholder": "new@example.com",
"current_password_placeholder": "输入当前密码"
},
"billing": {
"title": "账单",
"personal_billing": "个人账单",
"balance": "余额",
"monthly_quota": "月度配额",
"monthly_usage": "本月使用",
"billing_errors": "账单错误",
"history": "账单历史",
"date": "日期",
"reason": "原因",
"amount": "金额",
"no_history": "暂无账单记录",
"load_failed": "加载账单失败"
},
"appearance": {
"title": "外观",
"timezone_asia_shanghai": "亚洲/上海 (UTC+8)",
"timezone_asia_tokyo": "亚洲/东京 (UTC+9)",
"timezone_america_ny": "美洲/纽约 (UTC-5)",
"timezone_america_la": "美洲/洛杉矶 (UTC-8)",
"timezone_europe_london": "欧洲/伦敦 (UTC+0)"
},
"notifications": {
"title": "通知"
},
"settings_nav": {
"user_settings": "用户设置",
"my_account": "我的账户",
"billing": "账单",
"appearance": "外观",
"notifications": "通知",
"push_settings": "推送设置",
"security": "安全",
"change_password": "修改密码",
"email": "邮箱",
"ssh_keys": "SSH 密钥",
"access_tokens": "访问令牌"
},
"my_account": {
"title": "我的账户",
"subtitle": "管理你的个人信息",
"avatar": "头像",
"upload_avatar": "上传新头像",
"remove": "移除",
"avatar_hint": "支持 JPG, PNG 或 GIF. 最大 2MB.",
"username": "用户名",
"display_name": "显示名称",
"display_name_placeholder": "设置显示名称",
"website": "网站",
"website_placeholder": "https://example.com",
"organization": "组织",
"org_placeholder": "所属组织(可选)",
"save_changes": "保存更改",
"load_failed": "加载个人信息失败",
"save_success": "个人信息已保存",
"save_failed": "保存失败,请重试",
"avatar_size_error": "图片大小不能超过 2MB",
"avatar_upload_success": "头像上传成功,请保存更改",
"avatar_upload_failed": "头像上传失败"
},
"email_page": {
"title": "邮箱",
"subtitle": "管理你的邮箱地址",
"current_email": "当前邮箱",
"no_email_set": "未设置邮箱",
"new_email": "新邮箱",
"current_password": "当前密码(验证身份)",
"current_password_placeholder": "输入当前密码",
"save_button": "更改邮箱",
"load_failed": "加载邮箱信息失败",
"fill_all_fields": "请填写所有字段",
"verification_sent": "验证邮件已发送,请检查新邮箱",
"change_failed": "修改失败,请检查密码是否正确"
},
"notifications_page": {
"title": "通知",
"subtitle": "管理你的通知偏好",
"channels": "通知渠道",
"email_notifications": "邮件通知",
"email_notifications_desc": "通过邮件接收通知",
"in_app_notifications": "应用内通知",
"in_app_notifications_desc": "在应用内接收通知提醒",
"push_notifications": "推送通知",
"push_notifications_desc": "通过浏览器推送接收通知",
"digest_mode": "摘要模式",
"instant": "即时",
"daily_digest": "每日摘要",
"weekly_digest": "每周摘要",
"off": "关闭",
"notification_types": "通知类型",
"security_notifications": "安全通知",
"security_notifications_desc": "账户安全相关的通知",
"product_updates": "产品更新",
"product_updates_desc": "新功能和产品更新通知",
"marketing_emails": "营销邮件",
"marketing_emails_desc": "促销活动和优惠信息",
"do_not_disturb": "免打扰",
"enable_dnd": "启用免打扰",
"enable_dnd_desc": "在指定时间段内不接收通知",
"save_button": "保存更改",
"load_failed": "加载通知偏好失败",
"save_success": "通知设置已保存",
"save_failed": "保存失败,请重试"
},
"appearance_page": {
"title": "外观",
"subtitle": "自定义应用的外观和语言",
"theme_scheme": "主题方案",
"select_theme_scheme": "选择主题方案",
"theme": "主题",
"language": "语言",
"timezone": "时区",
"dark": "深色",
"light": "浅色",
"system": "跟随系统",
"basic_settings": "基础设置",
"custom": "自定义",
"save_button": "保存更改",
"load_failed": "加载偏好设置失败",
"save_success": "外观设置已保存",
"save_failed": "保存失败,请重试"
},
"billing": {
"title": "账单",
"personal_billing": "个人账单",
"balance": "余额",
"monthly_quota": "月度配额",
"monthly_usage": "本月使用",
"billing_errors": "账单错误",
"history": "账单历史",
"date": "日期",
"reason": "原因",
"amount": "金额",
"no_history": "暂无账单记录",
"load_failed": "加载账单失败",
"insufficient_balance": "余额不足"
},
"push": {
"title": "推送通知",
"subtitle": "即使在应用关闭时也能接收实时提醒。",
"enable": "启用推送通知",
"enable_desc": "接收提及、issues 和系统提醒的通知。",
"filters_title": "通知过滤器",
"mentions": "提及和回复",
"new_issues": "新 Issues",
"system_updates": "系统更新",
"coming_soon": "更精细的控制即将推出。",
"not_supported": "不支持推送通知",
"not_supported_desc": "您的浏览器或环境不支持 Web Push。请使用现代桌面浏览器。",
"load_failed": "加载通知设置失败",
"permission_denied": "浏览器拒绝了通知权限",
"update_failed": "更新推送设置失败",
"saved": "设置已保存"
}
},
"project": {
"layout": {
"expand_sidebar": "展开侧边栏",
"join_banner": {
"preview_mode": "预览模式",
"join_to_participate": "加入此项目以参与并使用项目工具。",
"read_only": "此公共项目在加入前为只读状态。",
"join_to_use": "你需要先加入才能使用项目操作。",
"apply_to_join": "申请加入"
}
},
"invitation": {
"title": "项目邀请",
"no_pending": "没有待处理的邀请"
},
"join": {
"title": "加入 ${project}",
"reason_placeholder": "告诉管理员您想加入的原因。",
"cancel_request": "取消请求",
"join_without_reason": "加入项目",
"submit_request": "提交加入请求",
"default_desc": "提交请求以成为项目成员。",
"already_member": "您已经是此项目的成员。",
"open_project": "打开项目",
"current_request": "当前请求",
"submitted_on": "提交于",
"message": "消息",
"message_desc": "可选,但对于私有或需要审批的项目很有用。",
"submitted": "加入请求已提交。",
"submit_failed": "提交加入请求失败。",
"cancelled": "加入请求已取消。",
"cancel_failed": "取消加入请求失败。"
},
"settings": {
"general": {
"title": "项目信息",
"project_name": "项目名称",
"project_name_placeholder": "例如my-awesome-project",
"display_name": "显示名称",
"display_name_placeholder": "例如My Awesome Project",
"description": "描述",
"description_placeholder": "向大家介绍这个项目...",
"leave": "退出此项目",
"leave_desc": "您将失去对所有项目资源的访问权限。",
"leave_btn": "退出项目",
"leave_confirm": "确定要退出此项目吗?",
"leave_success": "您已退出该项目。",
"leave_failed": "退出项目失败。"
},
"billing": {
"title": "当前账单",
"monthly_quota": "月度配额",
"monthly_usage": "本月使用",
"billing_errors": "账单错误",
"no_history": "暂无账单记录"
},
"access": {
"invite_member": "邀请成员",
"join_settings": "加入设置",
"pending_invitations": "待处理邀请",
"join_requests": "加入请求",
"email_placeholder": "user@example.com",
"reason_placeholder": "您想咨询什么?",
"optional_reason": "可选原因,将显示在请求历史中。"
},
"labels": {
"create_label": "创建标签",
"label_name_placeholder": "label-name",
"description_placeholder": "描述",
"no_labels": "暂无标签"
},
"members": {
"remove_confirm": "确定要从项目中移除 ${username} 吗?",
"remove": "移除"
},
"danger_zone": {
"delete_project": "删除此项目",
"delete_button": "删除项目"
}
},
"repos": {
"title": "仓库",
"subtitle": "托管和管理项目源代码",
"find_placeholder": "查找仓库...",
"no_repos": "暂无仓库",
"no_repos_desc": "创建您的第一个仓库",
"create_new": "创建新仓库",
"create_new_desc": "托管代码并协作",
"loading": "正在加载仓库...",
"load_failed": "加载仓库失败",
"new_repo": "新建仓库",
"type": "类型",
"sort": "排序",
"repository": "仓库",
"repositories": "仓库",
"grid_view": "网格视图",
"list_view": "列表视图",
"private": "私有",
"public": "公开",
"no_description_default": "未提供描述。",
"never": "从未",
"just_now": "刚刚",
"min_ago": "${count}分钟前",
"hr_ago": "${count}小时前",
"day_ago": "${count}天前"
},
"repo": {
"settings": {
"title": "仓库信息",
"name_placeholder": "仓库名称",
"description_placeholder": "向大家介绍这个仓库...",
"default_branch": "默认分支",
"branch_placeholder": "main",
"desc": "更新仓库的基本信息"
},
"code": "代码",
"commits": "提交",
"pull_requests": "Pull Requests",
"branches": "分支",
"tags": "标签",
"settings_tab": "设置",
"loading": "正在加载仓库...",
"load_failed": "加载仓库失败",
"not_found": "仓库未找到"
},
"branch_protection": {
"title": "分支保护规则",
"branch_pattern": "分支模式",
"required_approvals": "要求审批数",
"active_rules": "生效规则",
"no_rules": "未配置分支保护规则",
"create_rule_hint": "在上方创建规则以保护您的分支",
"add_rule": "添加规则",
"no_fork_sync": "无 fork 同步"
}
},
"issues": {
"title": "Issues",
"subtitle": "跟踪和管理项目任务与缺陷",
"new_title": "创建新 Issue",
"new_issue": "新建 Issue",
"search_placeholder": "搜索所有 issues...",
"sort": "排序",
"label": "标签",
"assignee": "负责人",
"open": "开放",
"closed": "已关闭",
"no_project": "未选择项目",
"select_project": "选择一个项目以查看其 issues",
"loading": "正在加载 issues...",
"load_failed": "加载 issues 失败",
"no_issues": "暂无 ${tab} issues",
"no_matches": "未找到匹配项",
"all_caught_up": "已处理完毕!创建一个 issue 来跟踪新任务。",
"try_adjusting": "尝试调整搜索或筛选条件。",
"create_first": "创建您的第一个 issue",
"brief_description_placeholder": "简要描述问题...",
"details_placeholder": "详细说明、复现步骤或需求...",
"pro_tip": "提示",
"need_help": "需要帮助?",
"submit": "提交新 Issue",
"issue_title_placeholder": "Issue 标题"
},
"issue_detail": {
"loading": "加载 issue 中...",
"no_description": "暂无描述。",
"no_comments": "暂无评论。开始讨论吧!",
"comment_placeholder": "写评论... (支持 Markdown)",
"edit": "编辑",
"close": "关闭",
"delete": "删除",
"link_pr": "关联 Pull Request",
"link_repo": "关联仓库",
"pull_requests": "Pull Requests",
"linked_repos": "关联的仓库",
"no_assigned": "未分配"
},
"pulls": {
"title": "Pull Requests",
"project_pulls": "${project} 的 Pull Requests",
"close": "关闭",
"reopen": "重新打开",
"confirm_delete": "确认",
"delete": "删除",
"no_project": "未选择项目",
"select_project": "选择一个项目以查看其 Pull Requests",
"loading": "正在加载 Pull Requests...",
"load_failed": "加载 Pull Requests 失败",
"no_prs": "暂无 Pull Requests",
"create_hint": "创建 Pull Request 来提议更改"
},
"skills": {
"title": "技能",
"subtitle": "项目能力和 AI 技能",
"no_project": "未选择项目",
"select_project": "选择一个项目以查看其技能",
"loading": "正在加载技能...",
"load_failed": "加载技能失败",
"no_skills": "暂无技能",
"no_skills_desc": "创建或扫描技能以启用 AI 辅助",
"create": "创建",
"scan": "扫描",
"scanning": "扫描中...",
"skill_name_placeholder": "my-skill",
"skill_display_name": "我的技能",
"skill_description_placeholder": "这个技能的作用",
"skill_content_placeholder": "技能内容或提示...",
"delete": "删除技能",
"create_title": "创建技能",
"create_desc": "添加新的 AI 技能到项目中。"
},
"skill_detail": {
"loading": "加载技能中...",
"edit_title": "编辑技能",
"save_failed": "保存失败"
},
"board": {
"title": "看板",
"no_boards": "暂无看板",
"no_boards_desc": "创建看板来开始管理工作流程。",
"add_column": "添加列",
"add_column_title": "添加列",
"delete_board": "删除看板",
"new_board_title": "新建看板",
"board_name_placeholder": "例如:冲刺计划",
"board_desc_placeholder": "可选描述...",
"column_name": "列名称",
"column_name_placeholder": "例如:进行中",
"card_title_placeholder": "需要完成什么?",
"card_desc_placeholder": "可选描述...",
"card_detail_title": "卡片标题",
"card_detail_desc_placeholder": "添加更详细的描述...",
"delete_card": "删除卡片",
"delete_column": "删除此列?",
"delete_card_confirm": "删除此卡片?",
"empty_board": "空白看板",
"empty_board_hint": "添加您的第一列(例如:待办、进行中、已完成)来开始管理工作。",
"add_first_column": "添加首列",
"add_card": "添加卡片",
"new_card": "新卡片",
"card_title": "卡片标题",
"id": "编号",
"created": "创建时间",
"updated": "更新时间"
},
"me": {
"title": "我的主页",
"recent_repos": "最近仓库",
"top_projects": "热门项目",
"latest_activity": "最近活动",
"no_projects": "未找到项目",
"no_repos": "未找到仓库",
"no_activity": "暂无最近活动",
"create_project": {
"title": "创建新项目",
"subtitle": "开始一个新的协作项目",
"name_placeholder": "例如my-awesome-team",
"org_placeholder": "例如Acme Corporation",
"desc_placeholder": "这个项目是关于什么的?",
"slug": "项目 Slug",
"slug_required": "必填",
"slug_hint": "用于 URL只能包含小写字母、数字和连字符。",
"display_name": "显示名称",
"display_name_required": "必填",
"display_name_placeholder": "例如Acme Corporation",
"description": "描述",
"visibility": "可见性",
"public_project": "公开项目",
"public_desc": "任何人都可以发现和加入",
"private_project": "私有项目",
"private_desc": "只有受邀成员可以访问",
"change": "更改",
"create_failed": "创建项目失败。Slug 可能已被使用。",
"create": "创建项目",
"cancel": "取消"
},
"invitations": {
"title": "邀请",
"subtitle": "查看项目邀请并跟踪加入请求。",
"pending": "待处理邀请",
"pending_desc": "由项目管理员发送的邀请。",
"no_pending": "没有待处理的邀请",
"no_pending_desc": "新的项目邀请将显示在这里。",
"join_requests": "加入请求",
"join_requests_desc": "您的项目成员申请。",
"no_requests": "没有加入请求",
"no_requests_desc": "您申请加入的项目将在这里跟踪。",
"invited_by": "邀请人",
"on": "于",
"accept": "接受",
"reject": "拒绝",
"submitted_on": "提交于",
"accepted": "邀请已接受。",
"rejected": "邀请已拒绝。",
"process_failed": "处理邀请失败。",
"cancelled": "加入请求已取消。",
"cancel_failed": "取消加入请求失败。"
},
"notifications": {
"no_notifications": "暂无通知",
"no_unread": "没有未读通知",
"all_caught_up": "已处理完毕"
},
"followers": {
"no_followers": "暂无关注者"
},
"following": {
"no_following": "暂无关注"
},
"user_not_found": "未找到用户",
"please_login": "请确保您已登录。",
"profile": {
"joined": "加入于",
"projects": "项目",
"repos": "仓库",
"stars": "星标",
"followers": "关注者",
"follow": "关注",
"unfollow": "取消关注",
"contributions_in_year": "去年贡献了 ${count} 次",
"contributions_on_date": "${count} 次贡献于 ${date}",
"less": "少",
"more": "多"
}
},
"explore": {
"title": "探索项目",
"subtitle": "发现公共项目和社区",
"search_placeholder": "按名称或描述搜索项目...",
"no_projects": "未找到项目",
"no_description": "暂无描述",
"found": "已找到",
"showing": "显示可发现的项目",
"try_different": "尝试其他搜索词",
"view_project": "查看项目"
},
"chat": {
"conversations": {
"search_placeholder": "搜索对话...",
"no_conversations": "暂无对话",
"no_matching": "没有匹配的对话",
"new_chat": "新对话",
"chat_history": "对话历史",
"start_new_chat": "开始新对话来探索 AI。",
"try_different_search": "尝试其他搜索词。",
"untitled_chat": "无标题对话",
"create_failed": "创建对话失败"
},
"model_selector": {
"search_placeholder": "搜索模型...",
"loading": "加载中...",
"no_models": "未找到模型",
"please_select_model": "请先选择一个模型"
},
"header": {
"new_chat": "新对话",
"expand_history": "展开对话历史",
"collapse_history": "收起对话历史",
"streaming": "生成中...",
"share": "分享对话",
"rename": "重命名",
"more": "更多"
},
"message_list": {
"welcome_title": "有什么我可以帮你的吗?",
"welcome_desc": "你可以问我任何问题——代码、写作、分析等,我都能帮忙。",
"new_response": "新回复",
"responding": "回复中...",
"explain_code": "解释一段代码或调试错误",
"summarize_doc": "总结或起草文档",
"review_pr": "审查 Pull Request 或仓库",
"brainstorm": "头脑风暴或制定计划",
"jump_to_message": "跳转到你的第 ${index} 条消息"
}
},
"channel": {
"no_selected": "未选择频道",
"no_threads": "暂无讨论串。使用消息操作菜单开始一个。",
"no_messages": "暂无消息。开始对话吧!",
"edit_history": "编辑历史",
"no_edit_history": "暂无编辑历史。",
"no_pinned": "暂无置顶消息。",
"reply_placeholder": "在讨论串中回复...",
"beginning_of_channel": "频道开始",
"scroll_up": "向上滚动以加载更早的消息",
"new_message": "${count} 条新消息"
},
"search": {
"global_placeholder": "搜索项目、频道、仓库、看板、issues...",
"no_results": "未找到结果。",
"no_messages": "没有匹配的消息。",
"results": "搜索结果",
"jump_to": "跳转到项目、频道、仓库、看板和 issues。",
"messages": "消息",
"quick_switch": "快速切换",
"projects": "项目",
"my_home": "我的主页",
"my_home_desc": "打开个人概览",
"explore_projects": "探索项目",
"explore_projects_desc": "查找公共项目",
"chat": "聊天",
"chat_desc": "打开个人聊天",
"project_overview": "项目概览",
"repositories": "仓库",
"issues": "Issues",
"boards": "看板",
"project_settings": "项目设置",
"rooms_in": "${project} 中的频道",
"all_rooms": "${project} 中的所有频道",
"messages_in": "${project} 中的消息",
"messages_in_room": "#${room} 中的消息"
},
"components": {
"message_input": {
"reply_preview": "回复/编辑预览横幅",
"add_emoji": "添加表情",
"placeholder": "消息",
"replying": "正在回复消息",
"editing": "正在编辑消息"
},
"inline_comment": {
"reply_placeholder": "写回复...",
"leave_comment": "发表评论..."
},
"merge_panel": {
"commit_message_placeholder": "输入提交信息..."
},
"ai_settings": {
"add_ai": "添加 AI",
"no_ai": "此频道中没有活跃的 AI 代理",
"search_placeholder": "搜索模型...",
"system_prompt_placeholder": "你是一个有用的助手..."
},
"room_settings": {
"name_placeholder": "例如general",
"topic_placeholder": "例如:工程部",
"no_group": "无分组",
"create_new_group": "创建新分组",
"delete_room": "删除频道"
},
"command": {
"placeholder": "搜索要运行的命令..."
},
"pr": {
"create_merge_commit": "创建合并提交",
"no_changes": "此 Pull Request 中没有更改。",
"confirm_merge": "确认合并"
},
"commit": {
"back_to_commits": "返回提交",
"no_changes": "此提交中没有更改。"
},
"repo": {
"no_description": "暂无描述",
"no_description_default": "未提供描述。"
},
"branches": {
"no_branches": "暂无分支",
"no_branches_desc": "创建您的第一个分支以开始使用"
},
"tags": {
"no_tags": "暂无标签",
"no_tags_desc": "创建标签以标记历史中的特定点"
},
"favorites": {
"no_favorites": "暂无收藏消息。"
}
},
"project_create": {
"repo_name": "仓库名称",
"repo_name_placeholder": "例如core-api",
"repo_desc_placeholder": "这个仓库是做什么用的?",
"group_name": "新分组名称",
"channel_name": "频道名称",
"channel_name_placeholder": "例如development",
"board_name": "看板名称",
"board_name_placeholder": "例如:冲刺计划",
"board_desc_placeholder": "任务看板详情...",
"skill_name": "技能名称",
"skill_name_placeholder": "例如:代码审查者",
"skill_desc_placeholder": "这个 AI 技能是做什么的?",
"email_placeholder": "user@example.com",
"no_group": "无分组",
"create_new_group": "创建新分组",
"create_repo": "创建仓库",
"create_channel": "创建频道",
"create_board": "创建看板",
"create_skill": "创建技能"
}
}

View File

@ -34,6 +34,7 @@ interface PatternHit {
function findNextPattern(content: string, fromIndex: number): PatternHit | null {
let earliest: PatternHit | null = null;
let earliestStart = Number.POSITIVE_INFINITY;
// Scan mentions
MENTION_RE.lastIndex = fromIndex;
@ -45,7 +46,10 @@ function findNextPattern(content: string, fromIndex: number): PatternHit | null
end: mentionMatch.index + mentionMatch[0].length,
data: { entityType: mentionMatch[1], entityId: mentionMatch[2], entityLabel: mentionMatch[3] },
};
if (!earliest || candidate.start < earliest.start) earliest = candidate;
if (candidate.start < earliestStart) {
earliest = candidate;
earliestStart = candidate.start;
}
}
// Scan fenced code blocks
@ -58,7 +62,10 @@ function findNextPattern(content: string, fromIndex: number): PatternHit | null
end: codeMatch.index + codeMatch[0].length,
data: { language: codeMatch[1], content: codeMatch[2] },
};
if (!earliest || candidate.start < earliest.start) earliest = candidate;
if (candidate.start < earliestStart) {
earliest = candidate;
earliestStart = candidate.start;
}
}
return earliest;
@ -230,6 +237,44 @@ export function parseContentBlocks(raw: unknown): IrContentBlock[] {
// Legacy format — content is a string, parse into IrNode[]
const content = typeof obj.content === "string" ? obj.content : "";
// Handle tool_call/tool_result roles — parse JSON content directly into IR nodes
if (role === "tool_call") {
try {
const parsed = JSON.parse(content);
const node: IrToolCallNode = {
id: `ir-tool-${++nodeIdCounter}`,
type: "tool_call",
tool: parsed.tool || parsed.display || "unknown",
args: parsed.args || parsed.metadata || {},
children_id: parsed.children_id,
};
blocks.push({ role, nodes: [node] });
} catch {
blocks.push({ role, nodes: extractIrNodes(content) });
}
continue;
}
if (role === "tool_result") {
try {
const parsed = JSON.parse(content);
const node: IrToolResultNode = {
id: `ir-tool-${++nodeIdCounter}`,
type: "tool_result",
tool: parsed.tool || "unknown",
status: parsed.status === "error" ? "error" : parsed.status === "stopped" ? "stopped" : "ok",
content: parsed.output || parsed.result || content,
children_id: parsed.children_id,
role: parsed.role,
task: parsed.task,
};
blocks.push({ role, nodes: [node] });
} catch {
blocks.push({ role, nodes: extractIrNodes(content) });
}
continue;
}
const normalized = role === "thinking" ? normalizeThinking(content) : normalizeAnswer(content);
blocks.push({ role, nodes: extractIrNodes(normalized) });
}
@ -253,7 +298,7 @@ export function extractAnswerText(blocks: IrContentBlock[]): string {
.filter((b) => b.role !== "thinking")
.flatMap((b) => b.nodes.map((n) => {
if (n.type === 'text') return (n as IrTextNode).content;
if (n.type === 'mention') return (n as IrMentionNode).entityLabel;
if (n.type === 'mention') return (n as IrMentionNode).entity_label;
if (n.type === 'code_block') return `\n\`\`\`${(n as IrCodeBlockNode).language}\n${(n as IrCodeBlockNode).content}\n\`\`\`\n`;
if (n.type === 'html_fragment') return `\n\`\`\`html\n${(n as IrHtmlFragmentNode).content}\n\`\`\`\n`;
return '';
@ -266,7 +311,7 @@ export function extractFullText(blocks: IrContentBlock[]): string {
return blocks.map((b) => {
const text = b.nodes.map((n) => {
if (n.type === 'text') return (n as IrTextNode).content;
if (n.type === 'mention') return (n as IrMentionNode).entityLabel;
if (n.type === 'mention') return (n as IrMentionNode).entity_label;
return '';
}).join("");
if (b.role === "thinking") return `[Thinking]\n${text}\n[/Thinking]`;

View File

@ -1,109 +1,108 @@
import { memo } from "react";
import {memo} from "react";
import type {
IrCodeBlockNode,
IrDataCardNode,
IrHtmlFragmentNode,
IrMentionNode,
IrMermaidNode,
IrNode,
IrTextNode,
IrMentionNode,
IrCodeBlockNode,
IrHtmlFragmentNode,
IrMermaidNode,
IrDataCardNode,
} from "@/lib/ir/types";
import { MarkdownRenderer } from "@/components/ui/MarkdownRenderer";
import { MentionChipRenderer } from "@/components/ir/MentionChipRenderer";
import { MermaidRenderer } from "@/components/ir/MermaidRenderer";
import { DataCardRenderer } from "@/components/ir/DataCardRenderer";
import {MarkdownRenderer} from "@/components/ui/MarkdownRenderer";
import {MentionChipRenderer} from "@/components/ir/MentionChipRenderer";
import {MermaidRenderer} from "@/components/ir/MermaidRenderer";
import {DataCardRenderer} from "@/components/ir/DataCardRenderer";
interface IrRendererProps {
nodes: IrNode[];
className?: string;
nodes: IrNode[];
className?: string;
}
/** Dispatch IrNode[] to specialized renderer components.
* Text and code go through MarkdownRenderer so every fenced code language,
* including html, uses the same collapsed/code-panel flow. */
export const IrRenderer = memo(function IrRenderer({ nodes, className }: IrRendererProps) {
if (!nodes.length) return null;
export const IrRenderer = memo(function IrRenderer({nodes, className}: IrRendererProps) {
if (!nodes.length) return null;
const allText = nodes.every((n) => n.type === "text");
if (allText) {
const combined = nodes.map((n) => (n as IrTextNode).content).join("");
return <MarkdownRenderer content={combined} className={className} />;
}
const allText = nodes.every((n) => n.type === "text");
if (allText) {
const combined = nodes.map((n) => (n as IrTextNode).content).join("");
return <MarkdownRenderer content={combined} className={className}/>;
}
return (
<div className={className} style={{ lineHeight: 1.75 }}>
{nodes.map((node) => {
switch (node.type) {
case "text":
return (
<MarkdownRenderer
key={node.id}
content={(node as IrTextNode).content}
/>
);
case "mention":
return (
<MentionChipRenderer
key={node.id}
entityType={(node as IrMentionNode).entity_type}
entityId={(node as IrMentionNode).entity_id}
entityLabel={(node as IrMentionNode).entity_label}
/>
);
case "code_block": {
const cb = node as IrCodeBlockNode;
return (
<MarkdownRenderer
key={node.id}
content={`\`\`\`${cb.language}\n${cb.content}\n\`\`\``}
/>
);
}
case "html_fragment": {
const hf = node as IrHtmlFragmentNode;
return (
<MarkdownRenderer
key={node.id}
content={`\`\`\`html\n${hf.content}\n\`\`\``}
/>
);
}
case "mermaid":
return (
<MermaidRenderer
key={node.id}
source={(node as IrMermaidNode).source}
/>
);
case "data_card": {
const dc = node as IrDataCardNode;
return (
<DataCardRenderer
key={node.id}
cardType={dc.card_type}
data={dc.data}
/>
);
}
case "chart":
case "executable_code":
return (
<div
key={node.id}
className="p-3 my-2 rounded-lg text-xs"
style={{
backgroundColor: "var(--surface-elevated)",
border: "1px solid var(--border-subtle)",
color: "var(--text-muted)",
}}
>
[{node.type} renderer pending]
</div>
);
default:
return null;
}
})}
</div>
);
return (
<div className={className + " pre_nobackground"} style={{lineHeight: 1.75}}>
{nodes.map((node) => {
switch (node.type) {
case "text":
return (
<MarkdownRenderer
key={node.id}
content={(node as IrTextNode).content}
/>
);
case "mention":
return (
<MentionChipRenderer
key={node.id}
entityType={(node as IrMentionNode).entity_type}
entityId={(node as IrMentionNode).entity_id}
entityLabel={(node as IrMentionNode).entity_label}
/>
);
case "code_block": {
const cb = node as IrCodeBlockNode;
return (
<MarkdownRenderer
key={node.id}
content={`\`\`\`${cb.language}\n${cb.content}\n\`\`\``}
/>
);
}
case "html_fragment": {
const hf = node as IrHtmlFragmentNode;
return (
<MarkdownRenderer
key={node.id}
content={`\`\`\`html\n${hf.content}\n\`\`\``}
/>
);
}
case "mermaid":
return (
<MermaidRenderer
key={node.id}
source={(node as IrMermaidNode).source}
/>
);
case "data_card": {
const dc = node as IrDataCardNode;
return (
<DataCardRenderer
key={node.id}
cardType={dc.card_type}
data={dc.data}
/>
);
}
case "chart":
return (
<div
key={node.id}
className="p-3 my-2 rounded-lg text-xs"
style={{
backgroundColor: "var(--surface-elevated)",
border: "1px solid var(--border-subtle)",
color: "var(--text-muted)",
}}
>
[{node.type} renderer pending]
</div>
);
default:
return null;
}
})}
</div>
);
});

View File

@ -74,7 +74,7 @@ export const semanticSchema: Schema = {
function walkHastElements(node: HastNode, callback: (el: HastElement) => void): void {
if (node.type === 'element') callback(node as HastElement);
if (node.children) {
if (node.type === 'element' || node.type === 'root') {
for (const child of node.children) {
walkHastElements(child, callback);
}
@ -97,4 +97,4 @@ export function rehypeFilterClasses() {
});
return tree;
};
}
}

View File

@ -102,13 +102,17 @@ export interface IrToolCallNode extends IrNodeBase {
type: 'tool_call';
tool: string;
args: Record<string, unknown>;
children_id?: string;
}
export interface IrToolResultNode extends IrNodeBase {
type: 'tool_result';
tool: string;
status: 'ok' | 'error';
status: 'ok' | 'stopped' | 'error';
content: string;
children_id?: string;
role?: string;
task?: string;
}
export interface IrMentionNode extends IrNodeBase {
@ -200,4 +204,4 @@ export interface RenderNode {
component?: string; // component name for interactive nodes
data?: Record<string, unknown>;
props?: Record<string, unknown>;
}
}

View File

@ -1,27 +1,44 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import {type ClassValue, clsx} from "clsx"
import {twMerge} from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs))
}
export function truncate(text: string, maxLen: number): string {
if (text.length <= maxLen) return text;
return text.slice(0, maxLen).trimEnd() + "…";
if (text.length <= maxLen) return text;
return text.slice(0, maxLen).trimEnd() + "…";
}
export function stripMarkdown(text: string): string {
return text
.replace(/#{1,6}\s+/g, "")
.replace(/(\*\*|__)(.*?)\1/g, "$2")
.replace(/(\*|_)(.*?)\1/g, "$2")
.replace(/~~(.*?)~~/g, "$1")
.replace(/`{1,3}[^`]*`{1,3}/g, "")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/!\[[^\]]*\]\([^)]+\)/g, "")
.replace(/>\s+/g, "")
.replace(/^[-*+]\s+/gm, "")
.replace(/^\d+\.\s+/gm, "")
.replace(/\n{2,}/g, " ")
.trim();
export function isMacOS(): boolean {
return navigator.platform.toUpperCase().indexOf("MAC") >= 0 ||
navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
}
export function modKey(): string {
return isMacOS() ? "⌘" : "Ctrl";
}
export function altKey(): string {
return isMacOS() ? "⌥" : "Alt";
}
export function shortcutLabel(keys: string[]): string {
return keys.join(isMacOS() ? "" : "+");
}
export const stripMarkdown = (text: string): string => {
return text
.replace(/#{1,6}\s+/g, "")
.replace(/(\*\*|__)(.*?)\1/g, "$2")
.replace(/(\*|_)(.*?)\1/g, "$2")
.replace(/~~(.*?)~~/g, "$1")
.replace(/`{1,3}[^`]*`{1,3}/g, "")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/!\[[^\]]*\]\([^)]+\)/g, "")
.replace(/>\s+/g, "")
.replace(/^[-*+]\s+/gm, "")
.replace(/^\d+\.\s+/gm, "")
.replace(/\n{2,}/g, " ")
.trim();
}

View File

@ -19,7 +19,10 @@ export interface StreamPart {
/** Tool call metadata */
toolName?: string;
toolArgs?: Record<string, unknown>;
toolStatus?: "ok" | "error";
toolStatus?: "ok" | "stopped" | "error";
children_id?: string;
/** Sub-agent delegation output — full text returned by call_sub_agent */
subAgentOutput?: string;
/** Extracted IR nodes for structured rendering.
* Computed from content text segments may contain mentions
* or other patterns that IrRenderer dispatches to dedicated components.

View File

@ -9,138 +9,132 @@
* - Rust raw WS tagged JSON bridge parses `type` field emits Socket.IO event
* - Socket.IO event bridge wraps with `type` field sends raw WS tagged JSON
*/
import { createServer } from 'http';
import { Server as IoServer } from 'socket.io';
import type { WsOutEvent, WsOutEventName } from './types/outbound';
import type { WsInMessage, WsInEventName } from './types/inbound';
import { DEFAULT_WS_PATH } from './constants';
import {createServer} from 'http';
import {Server as IoServer} from 'socket.io';
import type {WsInEventName, WsInMessage, WsOutEvent, WsOutEventName} from '@/ws/types';
import {DEFAULT_WS_PATH} from './constants';
const RUST_BACKEND_URL = process.env.RUST_BACKEND_URL || 'http://localhost:8080';
const BRIDGE_PORT = parseInt(process.env.WS_BRIDGE_PORT || '3001', 10);
// ── HTTP server + Socket.IO ─────────────────────────────
const httpServer = createServer();
const ioServer = new IoServer(httpServer, {
cors: { origin: '*', methods: ['GET', 'POST'] },
transports: ['websocket'],
cors: {origin: '*', methods: ['GET', 'POST']},
transports: ['websocket'],
});
// ── Auth: validate Socket.IO connection via backend WS token ──
ioServer.use(async (socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
// Try to fetch a token from the backend using the user's session cookie
// This requires the frontend to pass the auth token
return next(new Error('no token provided'));
}
// Validate token against Rust backend
try {
const res = await fetch(`${RUST_BACKEND_URL}/api/ws/token`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const body = await res.json();
socket.data.userId = body.data?.user_id || body.user_id;
return next();
const token = socket.handshake.auth.token;
if (!token) {
// Try to fetch a token from the backend using the user's session cookie
// This requires the frontend to pass the auth token
return next(new Error('no token provided'));
}
// Validate token against Rust backend
try {
const res = await fetch(`${RUST_BACKEND_URL}/api/ws/token`, {
method: 'POST',
headers: {Authorization: `Bearer ${token}`},
});
if (res.ok) {
const body = await res.json();
socket.data.userId = body.data?.user_id || body.user_id;
return next();
}
return next(new Error('token validation failed'));
} catch {
return next(new Error('backend unreachable'));
}
return next(new Error('token validation failed'));
} catch {
return next(new Error('backend unreachable'));
}
});
// ── Per-connection raw WS to Rust backend ───────────────
ioServer.on('connection', (ioSocket) => {
const userId = ioSocket.data.userId as string;
console.log(`[bridge] Socket.IO client connected: ${userId}`);
const userId = ioSocket.data.userId as string;
console.log(`[bridge] Socket.IO client connected: ${userId}`);
// Connect to Rust backend via raw WebSocket
const wsUrl = `${RUST_BACKEND_URL.replace('http', 'ws')}${DEFAULT_WS_PATH}?token=${encodeURIComponent(ioSocket.handshake.auth.token)}`;
const rawWs = new WebSocket(wsUrl);
// Connect to Rust backend via raw WebSocket
const wsUrl = `${RUST_BACKEND_URL.replace('http', 'ws')}${DEFAULT_WS_PATH}?token=${encodeURIComponent(ioSocket.handshake.auth.token)}`;
const rawWs = new WebSocket(wsUrl);
// ── Rust → Socket.IO ────────────────────────────────
rawWs.onmessage = (ev) => {
try {
const event = JSON.parse(ev.data as string) as WsOutEvent;
const eventName: WsOutEventName = event.type;
rawWs.onmessage = (ev) => {
try {
const event = JSON.parse(ev.data as string) as WsOutEvent;
const eventName: WsOutEventName = event.type;
// Forward to Socket.IO client
// Remove the `type` field from payload (Socket.IO event name replaces it)
const payload = { ...event };
delete (payload as Record<string, unknown>).type;
ioSocket.emit(eventName, payload);
} catch {
// Raw WS ping/pong — forward as Socket.IO ping event
if ((ev.data as string).trim() === '{"type":"pong"}') {
ioSocket.emit('pong', {});
}
// Forward to Socket.IO client
// Remove the `type` field from payload (Socket.IO event name replaces it)
const payload = {...event};
delete (payload as Record<string, unknown>).type;
ioSocket.emit(eventName, payload);
} catch {
// Raw WS ping/pong — forward as Socket.IO ping event
if ((ev.data as string).trim() === '{"type":"pong"}') {
ioSocket.emit('pong', {});
}
}
};
rawWs.onclose = () => {
console.log(`[bridge] Raw WS closed for user: ${userId}`);
ioSocket.disconnect(true);
};
rawWs.onerror = () => {
console.error(`[bridge] Raw WS error for user: ${userId}`);
};
// Listen for all inbound event names and forward as tagged JSON
const inboundEvents: WsInEventName[] = [
'subscribe', 'unsubscribe',
'typing_start', 'typing_stop', 'read_receipt',
'message_list', 'message_create', 'message_update', 'message_revoke',
'room_get', 'room_create', 'room_update', 'room_delete',
'category_create', 'category_update', 'category_delete',
'access_grant', 'access_revoke',
'state_set_read_seq', 'state_update_dnd',
'reaction_add', 'reaction_remove',
'thread_create', 'thread_resolve', 'thread_archive',
'pin_add', 'pin_remove',
'draft_save', 'draft_clear', 'search',
'notification_mark_read', 'notification_mark_all_read', 'notification_archive',
'presence_update', 'custom_status_update',
'invite_create', 'invite_accept', 'invite_revoke',
'ban_create', 'ban_remove',
'voice_join', 'voice_leave', 'voice_mute', 'voice_deaf', 'screen_share',
'ai_list', 'ai_upsert', 'ai_delete', 'ai_stop',
];
for (const eventName of inboundEvents) {
ioSocket.on(eventName, (data: Record<string, unknown>) => {
const msg: WsInMessage = {type: eventName, ...data} as WsInMessage;
if (rawWs.readyState === WebSocket.OPEN) {
rawWs.send(JSON.stringify(msg));
}
});
}
};
rawWs.onclose = () => {
console.log(`[bridge] Raw WS closed for user: ${userId}`);
ioSocket.disconnect(true);
};
rawWs.onerror = () => {
console.error(`[bridge] Raw WS error for user: ${userId}`);
};
// ── Socket.IO → Rust ────────────────────────────────
// Listen for all inbound event names and forward as tagged JSON
const inboundEvents: WsInEventName[] = [
'subscribe', 'unsubscribe',
'typing_start', 'typing_stop', 'read_receipt',
'message_list', 'message_create', 'message_update', 'message_revoke',
'room_get', 'room_create', 'room_update', 'room_delete',
'category_create', 'category_update', 'category_delete',
'access_grant', 'access_revoke',
'state_set_read_seq', 'state_update_dnd',
'reaction_add', 'reaction_remove',
'thread_create', 'thread_resolve', 'thread_archive',
'pin_add', 'pin_remove',
'draft_save', 'draft_clear', 'search',
'notification_mark_read', 'notification_mark_all_read', 'notification_archive',
'presence_update', 'custom_status_update',
'invite_create', 'invite_accept', 'invite_revoke',
'ban_create', 'ban_remove',
'voice_join', 'voice_leave', 'voice_mute', 'voice_deaf', 'screen_share',
'ai_list', 'ai_upsert', 'ai_delete', 'ai_stop',
];
for (const eventName of inboundEvents) {
ioSocket.on(eventName, (data: Record<string, unknown>) => {
const msg: WsInMessage = { type: eventName, ...data } as WsInMessage;
if (rawWs.readyState === WebSocket.OPEN) {
rawWs.send(JSON.stringify(msg));
}
// Special: Socket.IO ping → Rust WS ping
ioSocket.on('ping', () => {
if (rawWs.readyState === WebSocket.OPEN) {
rawWs.send(JSON.stringify({type: 'ping'}));
}
});
}
// Special: Socket.IO ping → Rust WS ping
ioSocket.on('ping', () => {
if (rawWs.readyState === WebSocket.OPEN) {
rawWs.send(JSON.stringify({ type: 'ping' }));
}
});
// ── Cleanup ──────────────────────────────────────────
ioSocket.on('disconnect', () => {
console.log(`[bridge] Socket.IO client disconnected: ${userId}`);
if (rawWs.readyState === WebSocket.OPEN) {
rawWs.close();
}
});
ioSocket.on('disconnect', () => {
console.log(`[bridge] Socket.IO client disconnected: ${userId}`);
if (rawWs.readyState === WebSocket.OPEN) {
rawWs.close();
}
});
});
// ── Start ───────────────────────────────────────────────
httpServer.listen(BRIDGE_PORT, () => {
console.log(`[bridge] Socket.IO bridge server running on port ${BRIDGE_PORT}`);
console.log(`[bridge] Connecting to Rust backend at ${RUST_BACKEND_URL}`);
console.log(`[bridge] Socket.IO bridge server running on port ${BRIDGE_PORT}`);
console.log(`[bridge] Connecting to Rust backend at ${RUST_BACKEND_URL}`);
});

View File

@ -19,15 +19,20 @@ import {ReconnectManager} from './reconnect';
import {DedupManager} from './dedup';
import {fetchWsToken, invalidateWsToken} from './auth';
import {
CLIENT_PING_INTERVAL_MS,
DEFAULT_WS_PATH,
HEARTBEAT_TIMEOUT_MS,
MAX_TEXT_MESSAGE_LEN,
WS_PROTOCOL_VERSION,
CLIENT_PING_INTERVAL_MS,
DEFAULT_WS_PATH,
HEARTBEAT_TIMEOUT_MS,
MAX_TEXT_MESSAGE_LEN,
WS_PROTOCOL_VERSION,
} from './constants';
import type {WsOutEvent, WsOutEventName, WsMessageStreamChunkEvent} from './types/outbound';
import type {WsInEventName, WsInMessage} from './types/inbound';
import type {RoomId} from './types/core';
import type {
RoomId,
WsInEventName,
WsInMessage,
WsMessageStreamChunkEvent,
WsOutEvent,
WsOutEventName
} from '@/ws/types';
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
@ -73,7 +78,6 @@ export class WsClient {
/** Max offline queue size — prevents unbounded memory growth. */
private readonly OFFLINE_QUEUE_MAX = 100;
// ── Request-response ────────────────────────────────────
private pendingAcks = new Map<string, {
resolve: (data: unknown) => void;
reject: (err: Error) => void;
@ -82,7 +86,6 @@ export class WsClient {
private readonly ACK_TIMEOUT_MS = 15_000;
private protocolChecked = false;
// ── Token management ────────────────────────────────────
constructor(config: WsClientConfig = {}) {
this.wsUrl = config.url ?? '';
@ -97,7 +100,6 @@ export class WsClient {
this.reconnect.setCallback(() => this.doConnect());
// ── Visibility & network recovery ───────────────
this.handleVisibilityChange = () => this.onVisibilityChange();
this.handleOnline = () => this.onNetworkOnline();
document.addEventListener('visibilitychange', this.handleVisibilityChange);
@ -108,7 +110,6 @@ export class WsClient {
this.wsToken = token;
}
// ── URL helpers ─────────────────────────────────────────
getToken(): string | null {
return this.wsToken;
@ -118,7 +119,6 @@ export class WsClient {
return this.backendUrl;
}
// ── Connection lifecycle ────────────────────────────────
async connect(): Promise<void> {
if (this.status === 'connected' || this.status === 'connecting') return;
@ -218,7 +218,6 @@ export class WsClient {
return Promise.reject(new Error('WebSocket not connected'));
}
// ── Ping ───────────────────────────────────────────────
onStatusChange(cb: (status: ConnectionStatus) => void): () => void {
this.statusListeners.add(cb);
@ -229,7 +228,6 @@ export class WsClient {
return this.status;
}
// ── Protocol version ───────────────────────────────────
on<N extends WsOutEventName>(name: N, cb: (data: Extract<WsOutEvent, { type: N }>) => void): this {
this.emitter.on(name, cb);
@ -241,7 +239,6 @@ export class WsClient {
return this;
}
// ── Sending ────────────────────────────────────────────
off<N extends WsOutEventName>(name: N, cb: (data: Extract<WsOutEvent, { type: N }>) => void): this {
this.emitter.off(name, cb);
@ -262,8 +259,6 @@ export class WsClient {
this.subscriptions.subscribe(roomId);
}
// ── Receiving ─────────────────────────────────────────
leaveRoom(roomId: RoomId): void {
this.subscriptions.unsubscribe(roomId);
}
@ -272,7 +267,6 @@ export class WsClient {
return this.subscriptions.getSubscribedRooms().includes(roomId);
}
// ── Status ──────────────────────────────────────────────
getSubscribedRooms(): RoomId[] {
return this.subscriptions.getSubscribedRooms();
@ -286,7 +280,6 @@ export class WsClient {
this.emit('typing_stop', {room: roomId});
}
// ── Public typed API ─────────────────────────────────────
sendReadReceipt(roomId: RoomId, lastReadSeq: number) {
this.emit('read_receipt', {room: roomId, last_read_seq: lastReadSeq});
@ -314,7 +307,6 @@ export class WsClient {
this.emit('message_revoke', {message: messageId});
}
// ── Room subscriptions ──────────────────────────────────
addReaction(roomId: RoomId, messageId: string, emoji: string) {
this.emit('reaction_add', {room: roomId, message: messageId, emoji});
@ -336,7 +328,6 @@ export class WsClient {
this.emit('notification_archive', {id});
}
// ── Room operations ─────────────────────────────────────
saveDraft(roomId: RoomId, content: string) {
this.emit('draft_save', {room: roomId, content});
@ -346,7 +337,6 @@ export class WsClient {
this.emit('draft_clear', {room: roomId});
}
// ── Room Management ─────────────────────────────────────
createRoom(project: string, name: string, isPublic: boolean, category?: string) {
this.emit('room_create', {project, room_name: name, public: isPublic, category: category ?? null});
@ -360,7 +350,6 @@ export class WsClient {
this.emit('room_delete', {room: roomId});
}
// ── Category Management ─────────────────────────────────
createCategory(project: string, name: string, position?: number) {
this.emit('category_create', {project, name, position: position ?? null});
@ -374,7 +363,6 @@ export class WsClient {
this.emit('category_delete', {id});
}
// ── Thread operations ───────────────────────────────────
resolveThread(threadId: string) {
this.emit('thread_resolve', {thread_id: threadId});
@ -384,7 +372,6 @@ export class WsClient {
this.emit('thread_archive', {thread_id: threadId});
}
// ── AI operations ───────────────────────────────────────
getAiList(roomId: RoomId) {
return this.emitWithAck('ai_list', {room: roomId});
@ -410,11 +397,10 @@ export class WsClient {
}
getUserSummary(username: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any
httpFallback?: () => Promise<any>) {
return this.emitWithAck('user_summary', { username }, httpFallback);
httpFallback?: () => Promise<any>) {
return this.emitWithAck('user_summary', {username}, httpFallback);
}
// ── Search ──────────────────────────────────────────────
search(query: string, opts?: {
room?: RoomId;
@ -428,26 +414,75 @@ httpFallback?: () => Promise<any>) {
return this.emitWithAck('search', {q: query, ...opts});
}
// ── Additional Coverage ─────────────────────────────────
getRoom(roomId: string) { return this.emitWithAck('room_get', { room: roomId }); }
setStateReadSeq(roomId: string, lastReadSeq: number) { this.emit('state_set_read_seq', { room: roomId, last_read_seq: lastReadSeq }); }
grantAccess(roomId: string, targetUserId: string, role: string) { this.emit('access_grant', { room: roomId, user_id: targetUserId, role }); }
revokeAccess(roomId: string, targetUserId: string) { this.emit('access_revoke', { room: roomId, user_id: targetUserId }); }
updatePresence(status: string) { this.emit('presence_update', { status }); }
updateCustomStatus(emoji: string, text: string, expiresAt?: string) { this.emit('custom_status_update', { emoji, text, expires_at: expiresAt ?? null }); }
getRoom(roomId: string) {
return this.emitWithAck('room_get', {room: roomId});
}
setStateReadSeq(roomId: string, lastReadSeq: number) {
this.emit('state_set_read_seq', {room: roomId, last_read_seq: lastReadSeq});
}
grantAccess(roomId: string, targetUserId: string, role: string) {
this.emit('access_grant', {room: roomId, user_id: targetUserId, role});
}
revokeAccess(roomId: string, targetUserId: string) {
this.emit('access_revoke', {room: roomId, user_id: targetUserId});
}
updatePresence(status: string) {
this.emit('presence_update', {status});
}
updateCustomStatus(emoji: string, text: string, expiresAt?: string) {
this.emit('custom_status_update', {emoji, text, expires_at: expiresAt ?? null});
}
createInvite(roomId: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any
opts?: any) { return this.emitWithAck('invite_create', { room: roomId, ...opts }); }
acceptInvite(inviteId: string) { return this.emitWithAck('invite_accept', { invite_id: inviteId }); }
revokeInvite(inviteId: string) { this.emit('invite_revoke', { invite_id: inviteId }); }
banUser(roomId: string, userId: string, reason?: string) { this.emit('ban_create', { room: roomId, user_id: userId, reason: reason ?? null }); }
unbanUser(roomId: string, userId: string) { this.emit('ban_remove', { room: roomId, user_id: userId }); }
createThread(roomId: string, parentSeq: number) { return this.emitWithAck('thread_create', { room: roomId, parent_seq: parentSeq }); }
joinVoice(roomId: string) { this.emit('voice_join', { room: roomId }); }
leaveVoice(roomId: string) { this.emit('voice_leave', { room: roomId }); }
muteVoice(roomId: string, muted: boolean) { this.emit('voice_mute', { room: roomId, muted }); }
deafVoice(roomId: string, deafened: boolean) { this.emit('voice_deaf', { room: roomId, deafened }); }
screenShare(roomId: string, start: boolean) { this.emit('screen_share', { room: roomId, start }); }
opts?: any) {
return this.emitWithAck('invite_create', {room: roomId, ...opts});
}
acceptInvite(inviteId: string) {
return this.emitWithAck('invite_accept', {invite_id: inviteId});
}
revokeInvite(inviteId: string) {
this.emit('invite_revoke', {invite_id: inviteId});
}
banUser(roomId: string, userId: string, reason?: string) {
this.emit('ban_create', {room: roomId, user_id: userId, reason: reason ?? null});
}
unbanUser(roomId: string, userId: string) {
this.emit('ban_remove', {room: roomId, user_id: userId});
}
createThread(roomId: string, parentSeq: number) {
return this.emitWithAck('thread_create', {room: roomId, parent_seq: parentSeq});
}
joinVoice(roomId: string) {
this.emit('voice_join', {room: roomId});
}
leaveVoice(roomId: string) {
this.emit('voice_leave', {room: roomId});
}
muteVoice(roomId: string, muted: boolean) {
this.emit('voice_mute', {room: roomId, muted});
}
deafVoice(roomId: string, deafened: boolean) {
this.emit('voice_deaf', {room: roomId, deafened});
}
screenShare(roomId: string, start: boolean) {
this.emit('screen_share', {room: roomId, start});
}
destroy(): void {
if (this.handleVisibilityChange) {
@ -679,7 +714,7 @@ opts?: any) { return this.emitWithAck('invite_create', { room: roomId, ...opts }
if (innerType) {
const innerEvent = {type: innerType, ...respEvent.data} as WsOutEvent;
this.emitter.emit(innerType as // eslint-disable-next-line @typescript-eslint/no-explicit-any
any, innerEvent);
any, innerEvent);
}
return;
}
@ -734,7 +769,10 @@ any, innerEvent);
// Reset lastHeartbeat to avoid false timeout on next ping check,
// then send an immediate ping to verify the connection is alive.
this.lastHeartbeat = Date.now();
try { this.emit('ping', {}); } catch { /* ws may be dead */ }
try {
this.emit('ping', {});
} catch { /* ws may be dead */
}
return;
}

View File

@ -2,12 +2,11 @@
* React hooks for WebSocket typed event subscription,
* room management, typing indicators, and connection status.
*/
import { useEffect, useRef, useCallback, useState } from 'react';
import { WsClient } from './client';
import type { ConnectionStatus, WsClientConfig } from './client';
import { useWsStore } from './store';
import type { WsOutEventName, WsOutEvent } from './types/outbound';
import type { RoomId } from './types/core';
import {useCallback, useEffect, useRef, useState} from 'react';
import type {ConnectionStatus, WsClientConfig} from './client';
import {WsClient} from './client';
import {useWsStore} from './store';
import type {RoomId, WsOutEvent, WsOutEventName} from '@/ws/types';
/** Global singleton WsClient instance. */
let globalClient: WsClient | null = null;
@ -17,232 +16,226 @@ let clientId = 0;
/** Get the global WsClient instance, or null if not initialized. */
export function getWsClient(): WsClient | null {
return globalClient;
return globalClient;
}
/** Get the current client identity counter — changes when client is re-initialized. */
export function getWsClientId(): number {
return clientId;
return clientId;
}
/** Initialize the global WsClient with config. */
export function initWsClient(config: WsClientConfig): WsClient {
if (globalClient) globalClient.destroy();
globalClient = new WsClient(config);
clientId++;
if (globalClient) globalClient.destroy();
globalClient = new WsClient(config);
clientId++;
// Bridge status changes to Zustand store
globalClient.onStatusChange((status) => {
useWsStore.getState().setStatus(status);
if (status === 'reconnecting') {
useWsStore.getState().setReconnecting(true);
} else {
useWsStore.getState().setReconnecting(false);
}
if (status === 'connected') {
useWsStore.getState().setLastError(null);
}
});
// Bridge status changes to Zustand store
globalClient.onStatusChange((status) => {
useWsStore.getState().setStatus(status);
if (status === 'reconnecting') {
useWsStore.getState().setReconnecting(true);
} else {
useWsStore.getState().setReconnecting(false);
}
if (status === 'connected') {
useWsStore.getState().setLastError(null);
}
});
// Bridge error events to store
globalClient.on('error', (event) => {
useWsStore.getState().setLastError(event.message);
});
// Bridge error events to store
globalClient.on('error', (event) => {
useWsStore.getState().setLastError(event.message);
});
return globalClient;
return globalClient;
}
// ── Initialization hook ─────────────────────────────────
export interface UseWsInitOptions {
url: string;
backendUrl?: string;
mode?: 'socketio' | 'raw-ws';
autoReconnect?: boolean;
token?: string;
connectOnMount?: boolean;
url: string;
backendUrl?: string;
mode?: 'socketio' | 'raw-ws';
autoReconnect?: boolean;
token?: string;
connectOnMount?: boolean;
}
/** Initialize and optionally auto-connect the WsClient on mount. */
export function useWsInit(options: UseWsInitOptions): WsClient {
const [client] = useState(() => initWsClient(options));
const [client] = useState(() => initWsClient(options));
useEffect(() => {
if (options.connectOnMount !== false) {
client.connect();
}
return () => {
client.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (options.connectOnMount !== false) {
client.connect();
}
return () => {
client.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return client;
return client;
}
// ── Connection status hook ──────────────────────────────
/** Reactive connection status — updates via Zustand. */
export function useWsStatus(): ConnectionStatus {
return useWsStore((s) => s.status);
return useWsStore((s) => s.status);
}
/** Whether the client is connected. */
export function useWsConnected(): boolean {
return useWsStore((s) => s.status === 'connected');
return useWsStore((s) => s.status === 'connected');
}
/** Last error from the server. */
export function useWsError(): string | null {
return useWsStore((s) => s.lastError);
return useWsStore((s) => s.lastError);
}
// ── Typed event subscription hook ───────────────────────
/** Subscribe to a typed outbound event. Auto-cleanup on unmount. Re-registers on client re-init. */
export function useWsEvent<N extends WsOutEventName>(
eventName: N,
callback: (data: Extract<WsOutEvent, { type: N }>) => void,
eventName: N,
callback: (data: Extract<WsOutEvent, { type: N }>) => void,
): void {
const cbRef = useRef(callback);
useEffect(() => {
cbRef.current = callback;
});
const cbRef = useRef(callback);
useEffect(() => {
cbRef.current = callback;
});
useEffect(() => {
const client = getWsClient();
if (!client) return;
const handler = (data: Extract<WsOutEvent, { type: N }>) => cbRef.current(data);
client.on(eventName, handler);
return () => { client.off(eventName, handler); };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventName, clientId]);
useEffect(() => {
const client = getWsClient();
if (!client) return;
const handler = (data: Extract<WsOutEvent, { type: N }>) => cbRef.current(data);
client.on(eventName, handler);
return () => {
client.off(eventName, handler);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventName, clientId]);
}
// ── Room subscription hook ──────────────────────────────
/** Subscribe to a room on mount, keep subscription alive on unmount/room-change. */
export function useRoomSubscription(roomId: RoomId | null): void {
useEffect(() => {
if (!roomId) return;
const client = getWsClient();
if (!client) return;
client.joinRoom(roomId);
useWsStore.getState().addSubscribedRoom(roomId);
return () => {
// Cleanup: unsubscribe and sync Zustand store on unmount
client.leaveRoom(roomId);
useWsStore.getState().removeSubscribedRoom(roomId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [roomId, clientId]);
useEffect(() => {
if (!roomId) return;
const client = getWsClient();
if (!client) return;
client.joinRoom(roomId);
useWsStore.getState().addSubscribedRoom(roomId);
return () => {
// Cleanup: unsubscribe and sync Zustand store on unmount
client.leaveRoom(roomId);
useWsStore.getState().removeSubscribedRoom(roomId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [roomId, clientId]);
}
/** Get the set of currently subscribed rooms (reactive). */
export function useSubscribedRooms(): Set<RoomId> {
return useWsStore((s) => s.subscribedRooms);
return useWsStore((s) => s.subscribedRooms);
}
// ── Typing indicator hook ───────────────────────────────
export interface TypingUser {
user_id: string;
username: string;
avatar_url: string | null;
user_id: string;
username: string;
avatar_url: string | null;
}
/** Track typing users in a room. Clears on unmount. */
export function useTypingIndicator(roomId: RoomId | null): TypingUser[] {
const [typingUsers, setTypingUsers] = useState<Map<string, TypingUser>>(new Map());
const [typingUsers, setTypingUsers] = useState<Map<string, TypingUser>>(new Map());
useWsEvent('typing_start', (event) => {
if (event.room_id === roomId && roomId) {
const data = event.data;
setTypingUsers((prev) => {
const next = new Map(prev);
next.set(data.user, { user_id: data.user, username: data.username, avatar_url: data.avatar_url ?? null });
return next;
});
}
});
useWsEvent('typing_start', (event) => {
if (event.room_id === roomId && roomId) {
const data = event.data;
setTypingUsers((prev) => {
const next = new Map(prev);
next.set(data.user, {user_id: data.user, username: data.username, avatar_url: data.avatar_url ?? null});
return next;
});
}
});
useWsEvent('typing_stop', (event) => {
if (event.room_id === roomId && roomId) {
setTypingUsers((prev) => {
const next = new Map(prev);
next.delete(event.data.user);
return next;
});
}
});
useWsEvent('typing_stop', (event) => {
if (event.room_id === roomId && roomId) {
setTypingUsers((prev) => {
const next = new Map(prev);
next.delete(event.data.user);
return next;
});
}
});
return [...typingUsers.values()];
return [...typingUsers.values()];
}
// ── Notification count hook ─────────────────────────────
/** Track unread notification count via notify_created events. */
export function useNotificationCount(): number {
const [count, setCount] = useState(0);
const [count, setCount] = useState(0);
useWsEvent('notify_created', () => {
setCount((c) => c + 1);
});
useWsEvent('notify_created', () => {
setCount((c) => c + 1);
});
useWsEvent('notify_read', () => {
setCount((c) => Math.max(0, c - 1));
});
useWsEvent('notify_read', () => {
setCount((c) => Math.max(0, c - 1));
});
return count;
return count;
}
// ── Convenience message sender hook ─────────────────────
/** Returns stable send functions bound to a room. */
export function useRoomSender(roomId: RoomId | null) {
const client = getWsClient();
const client = getWsClient();
const sendMessage = useCallback(
(content: string, opts?: { content_type?: string; thread?: string; in_reply_to?: string }) => {
if (!roomId || !client) return;
client.sendMessage(roomId, content, opts);
},
[roomId, client],
);
const sendMessage = useCallback(
(content: string, opts?: { content_type?: string; thread?: string; in_reply_to?: string }) => {
if (!roomId || !client) return;
client.sendMessage(roomId, content, opts);
},
[roomId, client],
);
const sendTypingStart = useCallback(() => {
if (!roomId || !client) return;
client.sendTypingStart(roomId);
}, [roomId, client]);
const sendTypingStart = useCallback(() => {
if (!roomId || !client) return;
client.sendTypingStart(roomId);
}, [roomId, client]);
const sendTypingStop = useCallback(() => {
if (!roomId || !client) return;
client.sendTypingStop(roomId);
}, [roomId, client]);
const sendTypingStop = useCallback(() => {
if (!roomId || !client) return;
client.sendTypingStop(roomId);
}, [roomId, client]);
const sendReadReceipt = useCallback(
(lastReadSeq: number) => {
if (!roomId || !client) return;
client.sendReadReceipt(roomId, lastReadSeq);
},
[roomId, client],
);
const sendReadReceipt = useCallback(
(lastReadSeq: number) => {
if (!roomId || !client) return;
client.sendReadReceipt(roomId, lastReadSeq);
},
[roomId, client],
);
const addReaction = useCallback(
(messageId: string, emoji: string) => {
if (!roomId || !client) return;
client.addReaction(roomId, messageId, emoji);
},
[roomId, client],
);
const addReaction = useCallback(
(messageId: string, emoji: string) => {
if (!roomId || !client) return;
client.addReaction(roomId, messageId, emoji);
},
[roomId, client],
);
const removeReaction = useCallback(
(messageId: string, emoji: string) => {
if (!roomId || !client) return;
client.removeReaction(roomId, messageId, emoji);
},
[roomId, client],
);
const removeReaction = useCallback(
(messageId: string, emoji: string) => {
if (!roomId || !client) return;
client.removeReaction(roomId, messageId, emoji);
},
[roomId, client],
);
return { sendMessage, sendTypingStart, sendTypingStop, sendReadReceipt, addReaction, removeReaction };
return {sendMessage, sendTypingStart, sendTypingStop, sendReadReceipt, addReaction, removeReaction};
}

View File

@ -10,61 +10,48 @@
* - Bridge: Socket.IO server bridge (Node.js side)
*/
// ── Client ──────────────────────────────────────────────
export { WsClient } from './client';
export type { ConnectionStatus, WsClientConfig } from './client';
// ── Hooks ───────────────────────────────────────────────
export {WsClient} from './client';
export type {ConnectionStatus, WsClientConfig} from './client';
export {
initWsClient,
getWsClient,
useWsInit,
useWsStatus,
useWsConnected,
useWsError,
useWsEvent,
useRoomSubscription,
useSubscribedRooms,
useTypingIndicator,
useNotificationCount,
useRoomSender,
initWsClient,
getWsClient,
useWsInit,
useWsStatus,
useWsConnected,
useWsError,
useWsEvent,
useRoomSubscription,
useSubscribedRooms,
useTypingIndicator,
useNotificationCount,
useRoomSender,
} from './hooks';
export type { UseWsInitOptions, TypingUser } from './hooks';
// ── Store ───────────────────────────────────────────────
export { useWsStore } from './store';
export type { WsState, WsActions } from './store';
// ── Auth ────────────────────────────────────────────────
export { fetchWsToken, invalidateWsToken } from './auth';
export type { WsTokenResponse } from './auth';
// ── Constants ───────────────────────────────────────────
export type {UseWsInitOptions, TypingUser} from './hooks';
export {useWsStore} from './store';
export type {WsState, WsActions} from './store';
export {fetchWsToken, invalidateWsToken} from './auth';
export type {WsTokenResponse} from './auth';
export {
HEARTBEAT_INTERVAL_MS,
HEARTBEAT_TIMEOUT_MS,
MAX_IDLE_TIMEOUT_MS,
CLIENT_PING_INTERVAL_MS,
MAX_MESSAGES_PER_SECOND,
MAX_TEXT_MESSAGE_LEN,
DEDUP_WINDOW_MS,
RECONNECT_BASE_DELAY_MS,
RECONNECT_MAX_DELAY_MS,
RECONNECT_MAX_ATTEMPTS,
DEFAULT_WS_PATH,
WS_TOKEN_ENDPOINT,
WS_TOKEN_TTL_SECONDS,
SSE_AI_STREAM_PATTERN,
HEARTBEAT_INTERVAL_MS,
HEARTBEAT_TIMEOUT_MS,
MAX_IDLE_TIMEOUT_MS,
CLIENT_PING_INTERVAL_MS,
MAX_MESSAGES_PER_SECOND,
MAX_TEXT_MESSAGE_LEN,
DEDUP_WINDOW_MS,
RECONNECT_BASE_DELAY_MS,
RECONNECT_MAX_DELAY_MS,
RECONNECT_MAX_ATTEMPTS,
DEFAULT_WS_PATH,
WS_TOKEN_ENDPOINT,
WS_TOKEN_TTL_SECONDS,
SSE_AI_STREAM_PATTERN,
} from './constants';
// ── Infrastructure ──────────────────────────────────────
export { TypedEventEmitter } from './emitter';
export { RoomSubscriptionManager } from './subscription';
export { ReconnectManager } from './reconnect';
export type { ReconnectState } from './reconnect';
export { DedupManager } from './dedup';
export { WS_PROTOCOL_VERSION } from './constants';
// ── Types ───────────────────────────────────────────────
export {TypedEventEmitter} from './emitter';
export {RoomSubscriptionManager} from './subscription';
export {ReconnectManager} from './reconnect';
export type {ReconnectState} from './reconnect';
export {DedupManager} from './dedup';
export {WS_PROTOCOL_VERSION} from './constants';
export type * from './types';

View File

@ -2,65 +2,65 @@
* Zustand store for WebSocket connection state provides reactive
* connection status and room subscription state to React components.
*/
import { create } from 'zustand';
import type { ConnectionStatus } from './client';
import type { RoomId } from './types/core';
import {create} from 'zustand';
import type {ConnectionStatus} from './client';
import type {RoomId} from '@/ws/types';
export interface WsState {
/** Current connection status. */
status: ConnectionStatus;
/** Set of currently subscribed room IDs. */
subscribedRooms: Set<RoomId>;
/** Last error received from the server. */
lastError: string | null;
/** Whether the client is currently reconnecting. */
isReconnecting: boolean;
/** Reconnection attempt count. */
reconnectAttempt: number;
/** Current connection status. */
status: ConnectionStatus;
/** Set of currently subscribed room IDs. */
subscribedRooms: Set<RoomId>;
/** Last error received from the server. */
lastError: string | null;
/** Whether the client is currently reconnecting. */
isReconnecting: boolean;
/** Reconnection attempt count. */
reconnectAttempt: number;
}
export interface WsActions {
setStatus: (status: ConnectionStatus) => void;
addSubscribedRoom: (roomId: RoomId) => void;
removeSubscribedRoom: (roomId: RoomId) => void;
setLastError: (error: string | null) => void;
setReconnecting: (isReconnecting: boolean, attempt?: number) => void;
reset: () => void;
setStatus: (status: ConnectionStatus) => void;
addSubscribedRoom: (roomId: RoomId) => void;
removeSubscribedRoom: (roomId: RoomId) => void;
setLastError: (error: string | null) => void;
setReconnecting: (isReconnecting: boolean, attempt?: number) => void;
reset: () => void;
}
const initialState: WsState = {
status: 'disconnected',
subscribedRooms: new Set(),
lastError: null,
isReconnecting: false,
reconnectAttempt: 0,
status: 'disconnected',
subscribedRooms: new Set(),
lastError: null,
isReconnecting: false,
reconnectAttempt: 0,
};
export const useWsStore = create<WsState & WsActions>((set) => ({
...initialState,
...initialState,
setStatus: (status) => set({ status }),
setStatus: (status) => set({status}),
addSubscribedRoom: (roomId) =>
set((state) => {
if (state.subscribedRooms.has(roomId)) return state; // no change, skip re-render
const rooms = new Set(state.subscribedRooms);
rooms.add(roomId);
return { subscribedRooms: rooms };
}),
addSubscribedRoom: (roomId) =>
set((state) => {
if (state.subscribedRooms.has(roomId)) return state; // no change, skip re-render
const rooms = new Set(state.subscribedRooms);
rooms.add(roomId);
return {subscribedRooms: rooms};
}),
removeSubscribedRoom: (roomId) =>
set((state) => {
if (!state.subscribedRooms.has(roomId)) return state; // no change, skip re-render
const rooms = new Set(state.subscribedRooms);
rooms.delete(roomId);
return { subscribedRooms: rooms };
}),
removeSubscribedRoom: (roomId) =>
set((state) => {
if (!state.subscribedRooms.has(roomId)) return state; // no change, skip re-render
const rooms = new Set(state.subscribedRooms);
rooms.delete(roomId);
return {subscribedRooms: rooms};
}),
setLastError: (error) => set({ lastError: error }),
setLastError: (error) => set({lastError: error}),
setReconnecting: (isReconnecting, attempt = 0) =>
set({ isReconnecting, reconnectAttempt: attempt }),
setReconnecting: (isReconnecting, attempt = 0) =>
set({isReconnecting, reconnectAttempt: attempt}),
reset: () => set(initialState),
reset: () => set(initialState),
}));

View File

@ -2,67 +2,67 @@
* Room subscription manager tracks active room subscriptions,
* auto-subscribes on reconnect, and unsubscribes on disconnect.
*/
import type { RoomId } from './types/core';
import type {RoomId} from '@/ws/types';
export class RoomSubscriptionManager {
private subscriptions: Set<RoomId> = new Set();
private onSubscribe: ((roomId: RoomId) => void) | null = null;
private onUnsubscribe: ((roomId: RoomId) => void) | null = null;
private subscriptions: Set<RoomId> = new Set();
private onSubscribe: ((roomId: RoomId) => void) | null = null;
private onUnsubscribe: ((roomId: RoomId) => void) | null = null;
/** Register callbacks that fire when a room is subscribed/unsubscribed. */
setCallbacks(
onSubscribe: (roomId: RoomId) => void,
onUnsubscribe: (roomId: RoomId) => void,
): void {
this.onSubscribe = onSubscribe;
this.onUnsubscribe = onUnsubscribe;
}
/** Subscribe to a room — sends WsInMessage.subscribe and tracks locally. */
subscribe(roomId: RoomId): void {
if (this.subscriptions.has(roomId)) return;
this.subscriptions.add(roomId);
this.onSubscribe?.(roomId);
}
/** Unsubscribe from a room — sends WsInMessage.unsubscribe and removes locally. */
unsubscribe(roomId: RoomId): void {
if (!this.subscriptions.has(roomId)) return;
this.subscriptions.delete(roomId);
this.onUnsubscribe?.(roomId);
}
/** Re-subscribe all rooms (used after reconnect). */
resubscribeAll(): void {
for (const roomId of this.subscriptions) {
this.onSubscribe?.(roomId);
/** Get subscription count. */
get count(): number {
return this.subscriptions.size;
}
}
/** Unsubscribe all rooms (used on disconnect). */
unsubscribeAll(): void {
for (const roomId of this.subscriptions) {
this.onUnsubscribe?.(roomId);
/** Register callbacks that fire when a room is subscribed/unsubscribed. */
setCallbacks(
onSubscribe: (roomId: RoomId) => void,
onUnsubscribe: (roomId: RoomId) => void,
): void {
this.onSubscribe = onSubscribe;
this.onUnsubscribe = onUnsubscribe;
}
}
/** Check if currently subscribed to a room. */
isSubscribed(roomId: RoomId): boolean {
return this.subscriptions.has(roomId);
}
/** Subscribe to a room — sends WsInMessage.subscribe and tracks locally. */
subscribe(roomId: RoomId): void {
if (this.subscriptions.has(roomId)) return;
this.subscriptions.add(roomId);
this.onSubscribe?.(roomId);
}
/** Get all currently subscribed room IDs. */
getSubscribedRooms(): RoomId[] {
return [...this.subscriptions];
}
/** Unsubscribe from a room — sends WsInMessage.unsubscribe and removes locally. */
unsubscribe(roomId: RoomId): void {
if (!this.subscriptions.has(roomId)) return;
this.subscriptions.delete(roomId);
this.onUnsubscribe?.(roomId);
}
/** Get subscription count. */
get count(): number {
return this.subscriptions.size;
}
/** Re-subscribe all rooms (used after reconnect). */
resubscribeAll(): void {
for (const roomId of this.subscriptions) {
this.onSubscribe?.(roomId);
}
}
/** Clear all subscriptions (without sending unsubscribe messages). */
clear(): void {
this.subscriptions.clear();
}
/** Unsubscribe all rooms (used on disconnect). */
unsubscribeAll(): void {
for (const roomId of this.subscriptions) {
this.onUnsubscribe?.(roomId);
}
}
/** Check if currently subscribed to a room. */
isSubscribed(roomId: RoomId): boolean {
return this.subscriptions.has(roomId);
}
/** Get all currently subscribed room IDs. */
getSubscribedRooms(): RoomId[] {
return [...this.subscriptions];
}
/** Clear all subscriptions (without sending unsubscribe messages). */
clear(): void {
this.subscriptions.clear();
}
}