Clean up goose2 settings headers (#8951)
Signed-off-by: Clay Delk <clay.delk@gmail.com>
This commit is contained in:
@@ -143,7 +143,6 @@ export function ExtensionsSettings() {
|
|||||||
return (
|
return (
|
||||||
<SettingsPage
|
<SettingsPage
|
||||||
title={t("extensions.title")}
|
title={t("extensions.title")}
|
||||||
description={t("extensions.description")}
|
|
||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,13 +1,97 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SettingsPage } from "@/shared/ui/SettingsPage";
|
import { SettingsPage } from "@/shared/ui/SettingsPage";
|
||||||
|
|
||||||
export function AboutSettings() {
|
interface AboutAppInfo {
|
||||||
const { t } = useTranslation("settings");
|
name: string;
|
||||||
|
version: string;
|
||||||
|
tauriVersion: string;
|
||||||
|
identifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AboutInfoRow({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<SettingsPage
|
<div className="flex items-center justify-between gap-4 py-2">
|
||||||
title={t("about.title")}
|
<span className="text-sm text-muted-foreground">{label}</span>
|
||||||
description={t("about.description")}
|
<span className="min-w-0 truncate text-right text-sm font-medium">
|
||||||
/>
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AboutSettings() {
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
const [appInfo, setAppInfo] = useState<AboutAppInfo | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function loadAppInfo() {
|
||||||
|
if (!window.__TAURI_INTERNALS__) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { getIdentifier, getName, getTauriVersion, getVersion } =
|
||||||
|
await import("@tauri-apps/api/app");
|
||||||
|
const [name, version, tauriVersion, identifier] = await Promise.all([
|
||||||
|
getName(),
|
||||||
|
getVersion(),
|
||||||
|
getTauriVersion(),
|
||||||
|
getIdentifier(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
setAppInfo({ name, version, tauriVersion, identifier });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) {
|
||||||
|
setAppInfo(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadAppInfo();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fallback = t("about.unavailable");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsPage title={t("about.title")}>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="divide-y divide-border">
|
||||||
|
<AboutInfoRow
|
||||||
|
label={t("about.fields.name")}
|
||||||
|
value={appInfo?.name ?? "Goose"}
|
||||||
|
/>
|
||||||
|
<AboutInfoRow
|
||||||
|
label={t("about.fields.version")}
|
||||||
|
value={appInfo?.version ?? fallback}
|
||||||
|
/>
|
||||||
|
<AboutInfoRow
|
||||||
|
label={t("about.fields.buildMode")}
|
||||||
|
value={
|
||||||
|
import.meta.env.DEV
|
||||||
|
? t("about.buildModes.development")
|
||||||
|
: t("about.buildModes.production")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AboutInfoRow
|
||||||
|
label={t("about.fields.tauriVersion")}
|
||||||
|
value={appInfo?.tauriVersion ?? fallback}
|
||||||
|
/>
|
||||||
|
<AboutInfoRow
|
||||||
|
label={t("about.fields.identifier")}
|
||||||
|
value={appInfo?.identifier ?? fallback}
|
||||||
|
/>
|
||||||
|
<AboutInfoRow label={t("about.fields.license")} value="Apache-2.0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function ChatsSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsPage title={t("chats.title")} description={t("chats.description")}>
|
<SettingsPage title={t("chats.title")}>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">{t("chats.sectionTitle")}</h4>
|
<h4 className="text-sm font-semibold">{t("chats.sectionTitle")}</h4>
|
||||||
{!loadingArchivedChats && archivedChats.length === 0 ? (
|
{!loadingArchivedChats && archivedChats.length === 0 ? (
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ export function CompactionSettings() {
|
|||||||
const icon = getProviderIcon("goose", "size-6");
|
const icon = getProviderIcon("goose", "size-6");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsPage
|
<SettingsPage title={t("compaction.title")}>
|
||||||
title={t("compaction.title")}
|
|
||||||
description={t("compaction.description")}
|
|
||||||
>
|
|
||||||
<div className="rounded-lg border bg-background p-4">
|
<div className="rounded-lg border bg-background p-4">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ export function DoctorSettings() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="xxs"
|
size="xxs"
|
||||||
onClick={copyDebugInfo}
|
onClick={copyDebugInfo}
|
||||||
className="text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<Check className="size-3.5" />
|
<Check className="size-3.5" />
|
||||||
@@ -120,7 +119,6 @@ export function DoctorSettings() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="xxs"
|
size="xxs"
|
||||||
onClick={runChecksAndRefresh}
|
onClick={runChecksAndRefresh}
|
||||||
className="text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
>
|
||||||
<RefreshCw className="size-3.5" />
|
<RefreshCw className="size-3.5" />
|
||||||
{t("doctor.rerun")}
|
{t("doctor.rerun")}
|
||||||
|
|||||||
@@ -36,10 +36,7 @@ export function GeneralSettings() {
|
|||||||
const { preference, setLocalePreference, systemLocaleLabel } = useLocale();
|
const { preference, setLocalePreference, systemLocaleLabel } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsPage
|
<SettingsPage title={t("general.title")}>
|
||||||
title={t("general.title")}
|
|
||||||
description={t("general.description")}
|
|
||||||
>
|
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label={t("general.language.label")}
|
label={t("general.language.label")}
|
||||||
description={t("general.language.description")}
|
description={t("general.language.description")}
|
||||||
|
|||||||
@@ -78,30 +78,19 @@ export function GooseAutoCompactSettings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="min-w-0">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="text-sm font-medium">
|
<p className="min-w-0 text-sm font-medium">
|
||||||
{t(`${translationKeyPrefix}.label`)}
|
{t(`${translationKeyPrefix}.label`)}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
<div className="flex items-center gap-1.5 text-xs text-foreground">
|
||||||
{t(`${translationKeyPrefix}.description`)}
|
{isSavingThreshold ? (
|
||||||
</p>
|
<Loader2 className="size-3 animate-spin text-muted-foreground" />
|
||||||
|
) : null}
|
||||||
|
<span className="shrink-0 font-medium">{autoCompactValueLabel}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-2">
|
<div className="w-full space-y-2">
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{t(`${translationKeyPrefix}.current`)}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-foreground">
|
|
||||||
{isSavingThreshold ? (
|
|
||||||
<Loader2 className="size-3 animate-spin text-muted-foreground" />
|
|
||||||
) : null}
|
|
||||||
<span className="shrink-0 font-medium">
|
|
||||||
{autoCompactValueLabel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Slider
|
<Slider
|
||||||
value={[draftThresholdPercent]}
|
value={[draftThresholdPercent]}
|
||||||
min={1}
|
min={1}
|
||||||
|
|||||||
@@ -57,10 +57,7 @@ export function ProjectsSettings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsPage
|
<SettingsPage title={t("projects.title")}>
|
||||||
title={t("projects.title")}
|
|
||||||
description={t("projects.description")}
|
|
||||||
>
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">
|
<h4 className="text-sm font-semibold">
|
||||||
{t("projects.sectionTitle")}
|
{t("projects.sectionTitle")}
|
||||||
|
|||||||
@@ -222,10 +222,7 @@ export function VoiceInputSettings() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<SettingsPage
|
<SettingsPage title={t("general.voiceInput.label")}>
|
||||||
title={t("general.voiceInput.label")}
|
|
||||||
description={t("general.voiceInput.description")}
|
|
||||||
>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("common:labels.loading")}
|
{t("common:labels.loading")}
|
||||||
</p>
|
</p>
|
||||||
@@ -236,7 +233,6 @@ export function VoiceInputSettings() {
|
|||||||
return (
|
return (
|
||||||
<SettingsPage
|
<SettingsPage
|
||||||
title={t("general.voiceInput.label")}
|
title={t("general.voiceInput.label")}
|
||||||
description={t("general.voiceInput.description")}
|
|
||||||
contentClassName="space-y-4"
|
contentClassName="space-y-4"
|
||||||
>
|
>
|
||||||
<div className="space-y-2 rounded-lg border border-border px-3 py-3">
|
<div className="space-y-2 rounded-lg border border-border px-3 py-3">
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe("GooseAutoCompactSettings", () => {
|
|||||||
render(<GooseAutoCompactSettings />);
|
render(<GooseAutoCompactSettings />);
|
||||||
|
|
||||||
const slider = screen.getByRole("slider", {
|
const slider = screen.getByRole("slider", {
|
||||||
name: /auto-compact context/i,
|
name: /auto-compact threshold/i,
|
||||||
});
|
});
|
||||||
slider.focus();
|
slider.focus();
|
||||||
await user.keyboard("{ArrowRight}");
|
await user.keyboard("{ArrowRight}");
|
||||||
@@ -62,7 +62,7 @@ describe("GooseAutoCompactSettings", () => {
|
|||||||
render(<GooseAutoCompactSettings />);
|
render(<GooseAutoCompactSettings />);
|
||||||
|
|
||||||
const slider = screen.getByRole("slider", {
|
const slider = screen.getByRole("slider", {
|
||||||
name: /auto-compact context/i,
|
name: /auto-compact threshold/i,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("Off")).toBeInTheDocument();
|
expect(screen.getByText("Off")).toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"description": "View app version, build info, and licenses.",
|
"buildModes": {
|
||||||
|
"development": "Development",
|
||||||
|
"production": "Production"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"buildMode": "Build mode",
|
||||||
|
"identifier": "App identifier",
|
||||||
|
"license": "License",
|
||||||
|
"name": "App name",
|
||||||
|
"tauriVersion": "Tauri version",
|
||||||
|
"version": "App version"
|
||||||
|
},
|
||||||
|
"unavailable": "Not available",
|
||||||
"title": "About"
|
"title": "About"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
@@ -39,7 +51,6 @@
|
|||||||
"title": "Appearance"
|
"title": "Appearance"
|
||||||
},
|
},
|
||||||
"chats": {
|
"chats": {
|
||||||
"description": "View and restore your archived chats.",
|
|
||||||
"empty": "No archived chats.",
|
"empty": "No archived chats.",
|
||||||
"messageCount_one": "{{displayCount}} message",
|
"messageCount_one": "{{displayCount}} message",
|
||||||
"messageCount_other": "{{displayCount}} messages",
|
"messageCount_other": "{{displayCount}} messages",
|
||||||
@@ -51,19 +62,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compaction": {
|
"compaction": {
|
||||||
"description": "Manage how Goose trims older context in long-running chats.",
|
|
||||||
"goose": {
|
"goose": {
|
||||||
"autoCompact": {
|
"autoCompact": {
|
||||||
"current": "Threshold",
|
|
||||||
"description": "Choose when Goose should compact older context before replying. This applies to all Goose chats.",
|
|
||||||
"helper": "Set to 100% to turn auto-compaction off.",
|
"helper": "Set to 100% to turn auto-compaction off.",
|
||||||
"label": "Auto-compact context",
|
"label": "Auto-compact threshold",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
"saveError": "Failed to save auto-compaction setting."
|
"saveError": "Failed to save auto-compaction setting."
|
||||||
},
|
},
|
||||||
"builtIn": "Built in",
|
"builtIn": "Built in",
|
||||||
"description": "Goose-specific context controls for long-running chats.",
|
"description": "Choose when the Goose agent compacts older context during sessions. This applies to all goose chats.",
|
||||||
"label": "Goose (Agent)"
|
"label": "Goose (Agent)"
|
||||||
},
|
},
|
||||||
"title": "Compaction"
|
"title": "Compaction"
|
||||||
@@ -74,7 +82,6 @@
|
|||||||
},
|
},
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"title": "Extensions",
|
"title": "Extensions",
|
||||||
"description": "Manage MCP extensions that add tools and capabilities to Goose.",
|
|
||||||
"search": "Search extensions...",
|
"search": "Search extensions...",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"enabledCount": "Enabled ({{count}})",
|
"enabledCount": "Enabled ({{count}})",
|
||||||
@@ -82,7 +89,7 @@
|
|||||||
"availableCount": "Available ({{count}})",
|
"availableCount": "Available ({{count}})",
|
||||||
"empty": "No extensions configured.",
|
"empty": "No extensions configured.",
|
||||||
"noResults": "No extensions match your search.",
|
"noResults": "No extensions match your search.",
|
||||||
"addExtension": "Add Extension",
|
"addExtension": "Add extension",
|
||||||
"editExtension": "Edit Extension",
|
"editExtension": "Edit Extension",
|
||||||
"deleteExtension": "Delete Extension",
|
"deleteExtension": "Delete Extension",
|
||||||
"toggle": "Toggle {{name}}",
|
"toggle": "Toggle {{name}}",
|
||||||
@@ -133,7 +140,6 @@
|
|||||||
"tools": "Tools"
|
"tools": "Tools"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"description": "Manage language and other app preferences.",
|
|
||||||
"language": {
|
"language": {
|
||||||
"description": "Choose how Goose formats and displays the interface.",
|
"description": "Choose how Goose formats and displays the interface.",
|
||||||
"english": "English",
|
"english": "English",
|
||||||
@@ -143,8 +149,7 @@
|
|||||||
},
|
},
|
||||||
"title": "General",
|
"title": "General",
|
||||||
"voiceInput": {
|
"voiceInput": {
|
||||||
"label": "Voice Input",
|
"label": "Voice input",
|
||||||
"description": "Configure voice dictation for hands-free input.",
|
|
||||||
"providerLabel": "Transcription Provider",
|
"providerLabel": "Transcription Provider",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"notConfiguredSuffix": "(not configured)",
|
"notConfiguredSuffix": "(not configured)",
|
||||||
@@ -198,7 +203,6 @@
|
|||||||
"voice": "Voice"
|
"voice": "Voice"
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"description": "Manage your projects.",
|
|
||||||
"empty": "No archived projects.",
|
"empty": "No archived projects.",
|
||||||
"sectionTitle": "Archived projects",
|
"sectionTitle": "Archived projects",
|
||||||
"title": "Projects"
|
"title": "Projects"
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
{
|
{
|
||||||
"about": {
|
"about": {
|
||||||
"description": "La información de Goose aparecerá aquí.",
|
"buildModes": {
|
||||||
|
"development": "Desarrollo",
|
||||||
|
"production": "Producción"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"buildMode": "Modo de compilación",
|
||||||
|
"identifier": "Identificador de la app",
|
||||||
|
"license": "Licencia",
|
||||||
|
"name": "Nombre de la app",
|
||||||
|
"tauriVersion": "Versión de Tauri",
|
||||||
|
"version": "Versión de la app"
|
||||||
|
},
|
||||||
|
"unavailable": "No disponible",
|
||||||
"title": "Acerca de"
|
"title": "Acerca de"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
@@ -39,7 +51,6 @@
|
|||||||
"title": "Apariencia"
|
"title": "Apariencia"
|
||||||
},
|
},
|
||||||
"chats": {
|
"chats": {
|
||||||
"description": "Restaura chats archivados.",
|
|
||||||
"empty": "No hay chats archivados.",
|
"empty": "No hay chats archivados.",
|
||||||
"messageCount_one": "{{displayCount}} mensaje",
|
"messageCount_one": "{{displayCount}} mensaje",
|
||||||
"messageCount_other": "{{displayCount}} mensajes",
|
"messageCount_other": "{{displayCount}} mensajes",
|
||||||
@@ -51,19 +62,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compaction": {
|
"compaction": {
|
||||||
"description": "Administra cómo Goose compacta el contexto anterior en chats largos.",
|
|
||||||
"goose": {
|
"goose": {
|
||||||
"autoCompact": {
|
"autoCompact": {
|
||||||
"current": "Umbral",
|
|
||||||
"description": "Elige cuándo Goose debe compactar el contexto anterior antes de responder. Esto se aplica a todos los chats de Goose.",
|
|
||||||
"helper": "Ajústalo al 100% para desactivar la compactación automática.",
|
"helper": "Ajústalo al 100% para desactivar la compactación automática.",
|
||||||
"label": "Compactación automática del contexto",
|
"label": "Umbral de compactación automática",
|
||||||
"loading": "Cargando...",
|
"loading": "Cargando...",
|
||||||
"off": "Desactivado",
|
"off": "Desactivado",
|
||||||
"saveError": "No se pudo guardar la configuración de compactación automática."
|
"saveError": "No se pudo guardar la configuración de compactación automática."
|
||||||
},
|
},
|
||||||
"builtIn": "Integrado",
|
"builtIn": "Integrado",
|
||||||
"description": "Controles de contexto específicos de Goose para chats largos.",
|
"description": "Elige cuándo el agente Goose compacta el contexto anterior durante las sesiones. Esto se aplica a todos los chats de Goose.",
|
||||||
"label": "Goose (Agente)"
|
"label": "Goose (Agente)"
|
||||||
},
|
},
|
||||||
"title": "Compactación"
|
"title": "Compactación"
|
||||||
@@ -74,7 +82,6 @@
|
|||||||
},
|
},
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"title": "Extensiones",
|
"title": "Extensiones",
|
||||||
"description": "Administra las extensiones MCP que agregan herramientas y capacidades a Goose.",
|
|
||||||
"search": "Buscar extensiones...",
|
"search": "Buscar extensiones...",
|
||||||
"enabled": "Habilitadas",
|
"enabled": "Habilitadas",
|
||||||
"enabledCount": "Habilitadas ({{count}})",
|
"enabledCount": "Habilitadas ({{count}})",
|
||||||
@@ -133,7 +140,6 @@
|
|||||||
"tools": "Herramientas"
|
"tools": "Herramientas"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"description": "Administra el idioma y otras preferencias de la app.",
|
|
||||||
"language": {
|
"language": {
|
||||||
"description": "Elige cómo Goose formatea y muestra la interfaz.",
|
"description": "Elige cómo Goose formatea y muestra la interfaz.",
|
||||||
"english": "Inglés",
|
"english": "Inglés",
|
||||||
@@ -144,7 +150,6 @@
|
|||||||
"title": "General",
|
"title": "General",
|
||||||
"voiceInput": {
|
"voiceInput": {
|
||||||
"label": "Entrada de voz",
|
"label": "Entrada de voz",
|
||||||
"description": "Configura el dictado por voz para entrada manos libres.",
|
|
||||||
"providerLabel": "Proveedor de transcripción",
|
"providerLabel": "Proveedor de transcripción",
|
||||||
"disabled": "Desactivado",
|
"disabled": "Desactivado",
|
||||||
"notConfiguredSuffix": "(no configurado)",
|
"notConfiguredSuffix": "(no configurado)",
|
||||||
@@ -198,7 +203,6 @@
|
|||||||
"voice": "Voz"
|
"voice": "Voz"
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"description": "Administra tus proyectos.",
|
|
||||||
"empty": "No hay proyectos archivados.",
|
"empty": "No hay proyectos archivados.",
|
||||||
"sectionTitle": "Proyectos archivados",
|
"sectionTitle": "Proyectos archivados",
|
||||||
"title": "Proyectos"
|
"title": "Proyectos"
|
||||||
|
|||||||
Reference in New Issue
Block a user