feat(hooks): add AI chat and project presence hooks
This commit is contained in:
parent
30cc9f885e
commit
88a51f45cb
@ -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] });
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
203
src/i18n/T.tsx
203
src/i18n/T.tsx
@ -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();
|
||||
});
|
||||
}
|
||||
831
src/i18n/de.json
831
src/i18n/de.json
@ -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"
|
||||
}
|
||||
}
|
||||
977
src/i18n/en.json
977
src/i18n/en.json
@ -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."
|
||||
}
|
||||
}
|
||||
831
src/i18n/fr.json
831
src/i18n/fr.json
@ -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"
|
||||
}
|
||||
}
|
||||
831
src/i18n/jp.json
831
src/i18n/jp.json
@ -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": "スキルを作成"
|
||||
}
|
||||
}
|
||||
854
src/i18n/zh.json
854
src/i18n/zh.json
@ -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": "创建技能"
|
||||
}
|
||||
}
|
||||
@ -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]`;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
204
src/ws/bridge.ts
204
src/ws/bridge.ts
@ -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}`);
|
||||
});
|
||||
138
src/ws/client.ts
138
src/ws/client.ts
@ -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;
|
||||
}
|
||||
|
||||
|
||||
301
src/ws/hooks.ts
301
src/ws/hooks.ts
@ -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};
|
||||
}
|
||||
@ -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';
|
||||
@ -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),
|
||||
}));
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user