From c1bf0dd401c1d016e2018fc225ed37c5d13ccbc9 Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 3 Aug 2026 16:18:54 -0400 Subject: [PATCH 1/6] feat(share): point the app at a self-hosted share/collab server at runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web container could not be pointed at a self-hosted sharing server or collaboration relay. `resolveShareBaseUrl()` and `resolveCollabBaseUrl()` already honored `VITE_GEOLIBRE_SHARE_URL` / `VITE_GEOLIBRE_COLLAB_URL`, but both read `import.meta.env` (build time only) and neither variable reached `Dockerfile` or `docker/entrypoint.sh` — so repointing a published image meant forking it. The entrypoint already writes `geolibre-runtime-config.js` on every boot for the AI proxy and embed origins, so this wires both hosts through that same channel: - `lib/deployment-env.ts` reads one `VITE_*` value, deployment env before build env, matching the precedence `readEmbedOrigins`/`readDeploymentAssistantEnv` already use. - `resolveShareHost()` returns a status (`default` / `configured` / `disabled` / `invalid`) plus the base URL. `GEOLIBRE_SHARE_URL=off` removes Share and the Project Gallery entirely. - `resolveCollabBaseUrl()` reads the deployment env too. - The entrypoint validates both and exits on a malformed value, as the `GEOLIBRE_EMBED_ORIGINS` block already does. A rejected value no longer falls back to `share.geolibre.app`. Previously a self-hosted deployment with a typo'd or plaintext host silently uploaded its users' projects to the public hosted service; `resolveShareBaseUrl()` now returns null, the menu entries disable with a reason, and the gallery reports a new `not-configured` error instead. The hostname in UI copy is also no longer hardcoded: 11 keys across all 16 catalogues take a `{{shareHost}}` interpolation, and the account-settings link in SettingsDialog/ShareProjectDialog derives from the resolved host, so a self-hosted instance no longer names or links to share.geolibre.app. Verified in the built web app against a preview server with the runtime config set four ways: unset (unchanged — Share enabled, copy names share.geolibre.app), `off` (Share and Gallery absent), an invalid host (both present but disabled with the reason), and `https://maps.example.org` (copy and token link both name the self-hosted host). Also exercised the entrypoint's validation across 12 inputs. Desktop note: the Tauri `http:default` capability scope still pins the share host, so self-hosting remains a web/Docker capability for now. Refs #1684, #1665 --- Dockerfile | 10 ++ .../layout/ProjectGalleryDialog.tsx | 20 ++- .../src/components/layout/SettingsDialog.tsx | 17 +- .../components/layout/ShareProjectDialog.tsx | 69 ++++++--- .../src/components/layout/TopToolbar.tsx | 27 +++- .../components/layout/toolbar/ProjectMenu.tsx | 32 +++- .../src/hooks/useProjectFileActions.ts | 24 ++- .../geolibre-desktop/src/i18n/locales/ar.json | 22 +-- .../geolibre-desktop/src/i18n/locales/de.json | 22 +-- .../geolibre-desktop/src/i18n/locales/en.json | 24 +-- .../geolibre-desktop/src/i18n/locales/es.json | 22 +-- .../geolibre-desktop/src/i18n/locales/fr.json | 22 +-- .../geolibre-desktop/src/i18n/locales/hi.json | 22 +-- .../geolibre-desktop/src/i18n/locales/id.json | 22 +-- .../geolibre-desktop/src/i18n/locales/it.json | 22 +-- .../geolibre-desktop/src/i18n/locales/ja.json | 22 +-- .../geolibre-desktop/src/i18n/locales/ka.json | 22 +-- .../geolibre-desktop/src/i18n/locales/ko.json | 22 +-- .../geolibre-desktop/src/i18n/locales/nl.json | 22 +-- .../geolibre-desktop/src/i18n/locales/pt.json | 22 +-- .../geolibre-desktop/src/i18n/locales/ru.json | 22 +-- .../geolibre-desktop/src/i18n/locales/tr.json | 22 +-- .../geolibre-desktop/src/i18n/locales/zh.json | 22 +-- .../geolibre-desktop/src/lib/collab-client.ts | 31 +++- .../src/lib/deployment-env.ts | 46 ++++++ apps/geolibre-desktop/src/lib/share-fetch.ts | 12 +- .../geolibre-desktop/src/lib/share-gallery.ts | 23 ++- .../src/lib/share-geolibre.ts | 146 ++++++++++++++---- docker/entrypoint.sh | 52 +++++++ docs/collaboration.md | 8 + docs/getting-started.md | 36 +++++ tests/collab-protocol.test.ts | 15 ++ tests/deployment-env.test.ts | 61 ++++++++ tests/share-gallery.test.ts | 55 +++++++ tests/share-geolibre.test.ts | 69 ++++++++- 35 files changed, 823 insertions(+), 284 deletions(-) create mode 100644 apps/geolibre-desktop/src/lib/deployment-env.ts create mode 100644 tests/deployment-env.test.ts diff --git a/Dockerfile b/Dockerfile index e25d04f73..a469a1765 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,11 +31,21 @@ ARG VITE_WELCOME_DISABLED= # postMessage API. Usually set at RUN time instead (-e GEOLIBRE_EMBED_ORIGINS=…), # which the entrypoint writes into the runtime config without a rebuild. ARG VITE_GEOLIBRE_EMBED_ORIGINS= +# Self-hosted project sharing server (https://…, or "off" to remove Share and the +# Project Gallery). Unset uses the public hosted service. Like the embed origins, +# normally set at RUN time instead (-e GEOLIBRE_SHARE_URL=…) so a prebuilt image +# can be repointed without a rebuild. +ARG VITE_GEOLIBRE_SHARE_URL= +# Self-hosted collaboration relay (wss://…). Unset leaves collaboration dark. +# Also settable at RUN time (-e GEOLIBRE_COLLAB_URL=…). +ARG VITE_GEOLIBRE_COLLAB_URL= ENV GEOLIBRE_APP_BASE=${GEOLIBRE_APP_BASE} ENV VITE_GEE_OAUTH_CLIENT_ID=${VITE_GEE_OAUTH_CLIENT_ID} ENV VITE_MAPILLARY_ACCESS_TOKEN=${VITE_MAPILLARY_ACCESS_TOKEN} ENV VITE_WELCOME_DISABLED=${VITE_WELCOME_DISABLED} ENV VITE_GEOLIBRE_EMBED_ORIGINS=${VITE_GEOLIBRE_EMBED_ORIGINS} +ENV VITE_GEOLIBRE_SHARE_URL=${VITE_GEOLIBRE_SHARE_URL} +ENV VITE_GEOLIBRE_COLLAB_URL=${VITE_GEOLIBRE_COLLAB_URL} RUN npm run build diff --git a/apps/geolibre-desktop/src/components/layout/ProjectGalleryDialog.tsx b/apps/geolibre-desktop/src/components/layout/ProjectGalleryDialog.tsx index be2cfb648..8508d388e 100644 --- a/apps/geolibre-desktop/src/components/layout/ProjectGalleryDialog.tsx +++ b/apps/geolibre-desktop/src/components/layout/ProjectGalleryDialog.tsx @@ -38,6 +38,7 @@ import { projectOpenToken, type SharedProject, } from "../../lib/share-gallery"; +import { shareHostLabel } from "../../lib/share-geolibre"; import type { TFunction } from "i18next"; type GalleryScope = "featured" | "all" | "mine"; @@ -77,13 +78,15 @@ function galleryErrorMessage(error: unknown, t: TFunction): string { case "timeout": return t("gallery.errorTimeout"); case "network": - return t("gallery.errorNetwork"); + return t("gallery.errorNetwork", { shareHost: shareHostLabel() }); case "invalid-response": return t("gallery.errorInvalidResponse"); case "unauthorized": - return t("gallery.errorUnauthorized"); + return t("gallery.errorUnauthorized", { shareHost: shareHostLabel() }); case "username-required": - return t("gallery.errorUsernameRequired"); + return t("gallery.errorUsernameRequired", { shareHost: shareHostLabel() }); + case "not-configured": + return t("gallery.errorNotConfigured"); case "http": return t("gallery.errorHttp", { status: error.status ?? 0 }); } @@ -92,7 +95,8 @@ function galleryErrorMessage(error: unknown, t: TFunction): string { } /** - * Browse public projects shared on share.geolibre.app and open one in GeoLibre. + * Browse public projects shared on the configured share host and open one in + * GeoLibre. * * The listing endpoint only paginates (no server-side search), so this loads * pages on demand via "Load more" and filters the already-loaded set in the @@ -322,7 +326,9 @@ export function ProjectGalleryDialog({ > {t("gallery.title")} - {t("gallery.description")} + + {t("gallery.description", { shareHost: shareHostLabel() })} +
@@ -360,7 +366,9 @@ export function ProjectGalleryDialog({
{!hasToken ? ( -

{t("gallery.signedOutHint")}

+

+ {t("gallery.signedOutHint", { shareHost: shareHostLabel() })} +

) : null} {openError ? ( diff --git a/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx b/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx index 4d253a154..f73ec503b 100644 --- a/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx +++ b/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx @@ -91,6 +91,7 @@ import type { ThemeMode } from "../../hooks/useThemeMode"; import { isTauri } from "../../lib/is-tauri"; import { THEME_SCHEMES, normalizeHexColor, type ThemeScheme } from "../../lib/theme-schemes"; import { IS_MAS_BUILD } from "../../lib/build-flags"; +import { resolveShareBaseUrl, shareHostLabel } from "../../lib/share-geolibre"; import { IS_STORE_BUILD, type UpdateNotificationLevel } from "../../lib/updates"; import { DATA_SOURCE_CATALOG, @@ -378,6 +379,13 @@ export function SettingsDialog({ onToggleThemeMode, }: SettingsDialogProps) { const { t } = useTranslation(); + // The share host's settings page, where the API token below is created. + // Derived from the resolved host so a self-hosted deployment links to its own + // page; null when the deployment configured no share host, in which case the + // description renders without a link rather than pointing at a stranger's site. + const shareBaseUrl = resolveShareBaseUrl(); + const shareHost = shareHostLabel(); + const shareSettingsUrl = shareBaseUrl ? `${shareBaseUrl}/settings` : null; const { language, options: languageOptions, setLanguage } = useLanguage(); const preferences = useAppStore((s) => s.preferences); const setPreferences = useAppStore((s) => s.setPreferences); @@ -2276,14 +2284,17 @@ export function SettingsDialog({

+ ) : ( + ), }} /> @@ -2298,7 +2309,7 @@ export function SettingsDialog({ onChange={(event) => updateShareToken(event.target.value)} />

- {t("settings.env.tokenStorageNote")} + {t("settings.env.tokenStorageNote", { shareHost })}

diff --git a/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx b/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx index c692a88ed..f28518b33 100644 --- a/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx +++ b/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx @@ -18,6 +18,7 @@ import { isShareableTitle, MAX_PROJECT_TITLE_LENGTH, resolveShareBaseUrl, + shareHostLabel, ShareUploadError, uploadProjectToShare, type ShareUploadErrorCode, @@ -38,9 +39,18 @@ interface ShareProjectDialogProps { getProject: (title: string) => Promise<{ content: string; filename: string }>; } -// The website's account settings page, where the user both creates API tokens -// and sets the username required for sharing. -const ACCOUNT_SETTINGS_URL = `${resolveShareBaseUrl()}/settings`; +/** + * The share host's account settings page, where the user both creates API tokens + * and sets the username required for sharing. + * + * Derived from the resolved host rather than hardcoded, so a self-hosted + * deployment sends its users to its own settings page. Null when no share host is + * configured, in which case the dialog does not render the link. + */ +function accountSettingsUrl(): string | null { + const base = resolveShareBaseUrl(); + return base ? `${base}/settings` : null; +} export function ShareProjectDialog({ open, @@ -49,6 +59,11 @@ export function ShareProjectDialog({ getProject, }: ShareProjectDialogProps) { const { t } = useTranslation(); + // Resolved per render rather than at module load so a deployment env written + // after this module was imported is still honored. + const settingsUrl = accountSettingsUrl(); + // Named in the copy below, so a self-hosted deployment reads its own host. + const shareHost = shareHostLabel(); const shareToken = useDesktopSettingsStore((s) => s.desktopSettings.shareToken); const [title, setTitle] = useState(""); const [visibility, setVisibility] = useState("unlisted"); @@ -166,24 +181,28 @@ export function ShareProjectDialog({ {t("share.title")} - {t("share.description")} + {t("share.description", { shareHost })} {!hasToken ? (
-

{t("share.setupIntro")}

+

{t("share.setupIntro", { shareHost })}

  1. {t("share.step1Title")}

    -

    {t("share.step1Description")}

    - +

    + {t("share.step1Description", { shareHost })} +

    + {settingsUrl && ( + + )}
  2. {t("share.step2Title")}

    @@ -259,16 +278,18 @@ export function ShareProjectDialog({ role="alert" className="space-y-2 rounded-md bg-destructive/10 p-3 text-sm text-destructive" > -

    {t("share.usernameRequired")}

    - +

    {t("share.usernameRequired", { shareHost })}

    + {settingsUrl && ( + + )}
) : error ? (

diff --git a/apps/geolibre-desktop/src/components/layout/TopToolbar.tsx b/apps/geolibre-desktop/src/components/layout/TopToolbar.tsx index 5178c6282..9c1dee613 100644 --- a/apps/geolibre-desktop/src/components/layout/TopToolbar.tsx +++ b/apps/geolibre-desktop/src/components/layout/TopToolbar.tsx @@ -112,6 +112,7 @@ import { NewProjectDialog } from "./NewProjectDialog"; import { ManagePluginsDialog } from "./ManagePluginsDialog"; import { ProjectGalleryDialog } from "./ProjectGalleryDialog"; import { ShareProjectDialog } from "./ShareProjectDialog"; +import { resolveShareHost } from "../../lib/share-geolibre"; import type { CollaborationApi } from "../../hooks/useCollaboration"; import { SettingsDialog } from "./SettingsDialog"; import { SetViewDialog } from "./SetViewDialog"; @@ -854,6 +855,11 @@ export function TopToolbar({ const [managePluginsOpen, setManagePluginsOpen] = useState(false); const [shareDialogOpen, setShareDialogOpen] = useState(false); const [galleryDialogOpen, setGalleryDialogOpen] = useState(false); + // Whether this deployment has a usable share host. Read once per render (the + // deployment env does not change while the app is running) and passed down so + // the menu, the command palette, and the dialogs agree. + const shareHost = resolveShareHost(); + const shareAvailable = shareHost.baseUrl != null; const [aboutOpen, setAboutOpen] = useState(false); const [printLayoutOpen, setPrintLayoutOpen] = useState(false); const [fieldCollectionOpen, setFieldCollectionOpen] = useState(false); @@ -980,13 +986,19 @@ export function TopToolbar({ shortcut: { key: "s", mod: true, shift: true }, run: () => void projectFiles.handleSaveAs(), }, - { - id: "project.share", - title: t("toolbar.command.projectShare"), - group: t("toolbar.commandGroup.project"), - icon: Share2, - run: () => setShareDialogOpen(true), - }, + // Only when the deployment has a usable share host; a command that always + // failed would be worse than an absent one. + ...(shareAvailable + ? [ + { + id: "project.share", + title: t("toolbar.command.projectShare"), + group: t("toolbar.commandGroup.project"), + icon: Share2, + run: () => setShareDialogOpen(true), + }, + ] + : []), // Only surfaced when live collaboration is configured (env flag). ...(collaboration.enabled ? [ @@ -1555,6 +1567,7 @@ export function TopToolbar({ setNewProjectDialogOpen(true)} onOpenFromFile={() => void projectFiles.handleOpenFromFile()} onOpenFromUrl={() => projectFiles.setProjectUrlDialogOpen(true)} diff --git a/apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx b/apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx index bc7ebfadd..7590b523e 100644 --- a/apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx +++ b/apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx @@ -36,11 +36,19 @@ import { import { useTranslation } from "react-i18next"; import { useDesktopSettingsStore } from "../../../hooks/useDesktopSettings"; import { isMenuItemVisible } from "../../../lib/ui-profile"; +import type { ShareHostStatus } from "../../../lib/share-geolibre"; import { formatRecentProjectTime, type ToolbarChrome } from "./constants"; interface ProjectMenuProps { chrome: ToolbarChrome; collaborationEnabled: boolean; + /** + * Availability of the configured share host. `disabled` hides Share and the + * Project Gallery (the deployment turned sharing off); `invalid` leaves them + * visible but disabled with a reason, so a broken configuration is discoverable + * rather than silently missing. + */ + shareHostStatus: ShareHostStatus; onNewProject: () => void; onOpenFromFile: () => void; onOpenFromUrl: () => void; @@ -64,6 +72,7 @@ interface ProjectMenuProps { export function ProjectMenu({ chrome, collaborationEnabled, + shareHostStatus, onNewProject, onOpenFromFile, onOpenFromUrl, @@ -90,6 +99,11 @@ export function ProjectMenu({ const setStorymapPanelOpen = useAppStore((s) => s.setStorymapPanelOpen); const uiProfile = useDesktopSettingsStore((s) => s.desktopSettings.uiProfile); const show = (id: string) => isMenuItemVisible(uiProfile, id); + // A deployment that turned sharing off should not advertise it; one that named + // a host we rejected should say so rather than leave the user wondering. + const shareHidden = shareHostStatus === "disabled"; + const shareBroken = shareHostStatus === "invalid"; + const shareBrokenReason = shareBroken ? t("toolbar.item.shareHostUnavailable") : undefined; // Group-visibility flags so the separators between groups aren't left orphaned // when a whole group is hidden by the active profile. const showSaveGroup = @@ -139,10 +153,16 @@ export function ProjectMenu({ {t("toolbar.item.urlEllipsis")} - - - {t("toolbar.item.galleryEllipsis")} - + {!shareHidden && ( + + + {t("toolbar.item.galleryEllipsis")} + + )} )} @@ -256,8 +276,8 @@ export function ProjectMenu({ {t("toolbar.item.saveAsTemplateEllipsis")} )} - {show("project.share") && ( - + {show("project.share") && !shareHidden && ( + {t("toolbar.item.shareEllipsis")} diff --git a/apps/geolibre-desktop/src/hooks/useProjectFileActions.ts b/apps/geolibre-desktop/src/hooks/useProjectFileActions.ts index db8182222..436d34433 100644 --- a/apps/geolibre-desktop/src/hooks/useProjectFileActions.ts +++ b/apps/geolibre-desktop/src/hooks/useProjectFileActions.ts @@ -493,13 +493,14 @@ export function useProjectFileActions(mapControllerRef: MapControllerRef) { // rethrows on failure so the caller (the gallery dialog) can show the error // inline next to the card it came from. // - // When `authToken` is set (the user has a share.geolibre.app API token), the - // request to the share host carries it as a Bearer token so the owner's - // unlisted and private projects load too. The token is attached only for the - // share host (see shareAuthorizedFetch), never to third-party hosts a project - // might reference. Token-authenticated opens are not remembered as recent - // (path = null), since reopening a private URL on restart would 403 without - // the header. + // When `authToken` is set (the user has a share API token), the request to the + // share host carries it as a Bearer token so the owner's unlisted and private + // projects load too. The token is attached only for the share host (see + // shareAuthorizedFetch), never to third-party hosts a project might reference — + // so when no share host is configured, the plain fetch is used and the token is + // simply not sent anywhere. Token-authenticated opens are not remembered as + // recent (path = null), since reopening a private URL on restart would 403 + // without the header. const [saveTemplateDialogOpen, setSaveTemplateDialogOpen] = useState(false); const openProjectFromShareUrl = async ( @@ -517,14 +518,11 @@ export function useProjectFileActions(mapControllerRef: MapControllerRef) { try { let project: Awaited>; - if (options.authToken) { + const shareBaseUrl = resolveShareBaseUrl(); + if (options.authToken && shareBaseUrl) { const fetched = await fetchProjectFromUrl(normalizedUrl, { signal: controller.signal, - fetchImpl: shareAuthorizedFetch( - options.authToken, - resolveShareBaseUrl(), - getShareFetch(), - ), + fetchImpl: shareAuthorizedFetch(options.authToken, shareBaseUrl, getShareFetch()), }); project = await resolveProjectXyzLayers(fetched, controller.signal); } else { diff --git a/apps/geolibre-desktop/src/i18n/locales/ar.json b/apps/geolibre-desktop/src/i18n/locales/ar.json index d2a58476a..7a7611476 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ar.json +++ b/apps/geolibre-desktop/src/i18n/locales/ar.json @@ -1289,10 +1289,10 @@ }, "share": { "title": "مشاركة المشروع", - "description": "رفع المشروع الحالي إلى share.geolibre.app والحصول على رابط قابل للمشاركة.", - "setupIntro": "اربط حسابك على share.geolibre.app قبل الرفع.", + "description": "رفع المشروع الحالي إلى {{shareHost}} والحصول على رابط قابل للمشاركة.", + "setupIntro": "اربط حسابك على {{shareHost}} قبل الرفع.", "step1Title": "1. الحصول على رمز API", - "step1Description": "سجّل الدخول إلى share.geolibre.app، ثم أنشئ رمزًا من الإعدادات ← رموز API.", + "step1Description": "سجّل الدخول إلى {{shareHost}}، ثم أنشئ رمزًا من الإعدادات ← رموز API.", "getToken": "الحصول على رمز API", "step2Title": "2. إضافة الرمز إلى GeoLibre", "step2Description": "الصق الرمز في الإعدادات ← متغيرات البيئة على هذا الجهاز.", @@ -1311,12 +1311,12 @@ "shareButton": "مشاركة", "sharing": "جارٍ المشاركة…", "errorFallback": "تعذّرت مشاركة المشروع.", - "usernameRequired": "عيّن اسم مستخدم في حسابك على share.geolibre.app قبل المشاركة. افتح إعدادات حسابك لاختيار اسم، ثم حاول مرة أخرى.", + "usernameRequired": "عيّن اسم مستخدم في حسابك على {{shareHost}} قبل المشاركة. افتح إعدادات حسابك لاختيار اسم، ثم حاول مرة أخرى.", "openAccountSettings": "فتح إعدادات الحساب" }, "gallery": { "title": "معرض المشاريع", - "description": "تصفح المشاريع العامة المشتركة على share.geolibre.app وافتح أحدها في GeoLibre.", + "description": "تصفح المشاريع العامة المشتركة على {{shareHost}} وافتح أحدها في GeoLibre.", "searchPlaceholder": "تصفية المشاريع المحمّلة حسب العنوان أو المؤلف أو الوسم", "loading": "جارٍ تحميل المشاريع…", "loadingMore": "جارٍ تحميل المزيد…", @@ -1325,11 +1325,11 @@ "noMatches": "لا توجد مشاريع محمّلة تطابق عامل التصفية.", "errorFallback": "تعذّر تحميل معرض المشاريع.", "errorTimeout": "انتهت مهلة معرض المشاريع. يُرجى المحاولة مرة أخرى.", - "errorNetwork": "تعذّر الوصول إلى share.geolibre.app. تحقق من اتصالك بالإنترنت.", + "errorNetwork": "تعذّر الوصول إلى {{shareHost}}. تحقق من اتصالك بالإنترنت.", "errorHttp": "تعذّر تحميل المعرض (HTTP {{status}}).", "errorInvalidResponse": "تعذّر تحميل المعرض (استجابة الخادم غير صالحة).", - "errorUnauthorized": "رمز API الخاص بك لموقع share.geolibre.app غير صالح أو منتهي الصلاحية. حدّثه في الإعدادات.", - "errorUsernameRequired": "عيّن اسم مستخدم في حسابك على share.geolibre.app قبل تحميل مشاريعك.", + "errorUnauthorized": "رمز API الخاص بك لموقع {{shareHost}} غير صالح أو منتهي الصلاحية. حدّثه في الإعدادات.", + "errorUsernameRequired": "عيّن اسم مستخدم في حسابك على {{shareHost}} قبل تحميل مشاريعك.", "retry": "إعادة المحاولة", "open": "فتح", "openCopy": "فتح نسخة", @@ -1350,7 +1350,7 @@ "scopeFeatured": "المميزة", "scopeAll": "جميع المشاريع", "scopeMine": "مشاريعي", - "signedOutHint": "أضف رمز API لموقع share.geolibre.app في الإعدادات لرؤية مشاريعك غير المدرجة والخاصة.", + "signedOutHint": "أضف رمز API لموقع {{shareHost}} في الإعدادات لرؤية مشاريعك غير المدرجة والخاصة.", "emptyFeatured": "لا توجد مشاريع مميزة بعد.", "emptyMine": "لم تشارك أي مشاريع بعد.", "visibilityUnlisted": "غير مدرج", @@ -1950,8 +1950,8 @@ }, "env": { "tokenTitle": "رمز API لخدمة Share.GeoLibre", - "tokenDescription": "تستخدم قائمة مشروع > مشاركة هذا الرمز لرفع خرائطك إلى share.geolibre.app. افتح share.geolibre.app/settings، وأنشئ رمزًا من الإعدادات > رموز API، ثم الصقه في الحقل أدناه.", - "tokenStorageNote": "يُخزن محليًا على هذا الجهاز ويُرسل فقط إلى share.geolibre.app لمصادقة عمليات الرفع. في نسخة الويب يتشارك مساحة تخزين المتصفح نفسها مع بيانات الموقع الأخرى، لذا ألغِه من share.geolibre.app إذا تعرض جهازك للاختراق.", + "tokenDescription": "تستخدم قائمة مشروع > مشاركة هذا الرمز لرفع خرائطك إلى {{shareHost}}. افتح {{shareHost}}/settings، وأنشئ رمزًا من الإعدادات > رموز API، ثم الصقه في الحقل أدناه.", + "tokenStorageNote": "يُخزن محليًا على هذا الجهاز ويُرسل فقط إلى {{shareHost}} لمصادقة عمليات الرفع. في نسخة الويب يتشارك مساحة تخزين المتصفح نفسها مع بيانات الموقع الأخرى، لذا ألغِه من {{shareHost}} إذا تعرض جهازك للاختراق.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "رمز Cesium Ion", "cesiumTokenDescription": "تستخدم الكرة الأرضية ثلاثية الأبعاد (جزء في العرض المنقسم) صور العالم والتضاريس من Cesium Ion، وهي بيانات تتطلب رمز وصول. أنشئ حسابًا مجانيًا في ion.cesium.com/tokens، وانسخ رمز الوصول الافتراضي الخاص بك، ثم الصقه أدناه. من دون رمز يبقى مفتاح تبديل الكرة الأرضية ثلاثية الأبعاد مخفيًا.", diff --git a/apps/geolibre-desktop/src/i18n/locales/de.json b/apps/geolibre-desktop/src/i18n/locales/de.json index a0e7e6948..77624146e 100644 --- a/apps/geolibre-desktop/src/i18n/locales/de.json +++ b/apps/geolibre-desktop/src/i18n/locales/de.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "Projekt teilen", - "description": "Laden Sie das aktuelle Projekt auf share.geolibre.app hoch und erhalten Sie einen teilbaren Link.", - "setupIntro": "Verbinden Sie Ihr share.geolibre.app-Konto, bevor Sie hochladen.", + "description": "Laden Sie das aktuelle Projekt auf {{shareHost}} hoch und erhalten Sie einen teilbaren Link.", + "setupIntro": "Verbinden Sie Ihr {{shareHost}}-Konto, bevor Sie hochladen.", "step1Title": "1. API-Token erstellen", - "step1Description": "Melden Sie sich bei share.geolibre.app an und erstellen Sie dann ein Token unter Einstellungen → API-Token.", + "step1Description": "Melden Sie sich bei {{shareHost}} an und erstellen Sie dann ein Token unter Einstellungen → API-Token.", "getToken": "API-Token abrufen", "step2Title": "2. Token zu GeoLibre hinzufügen", "step2Description": "Fügen Sie das Token unter Einstellungen → Umgebungsvariablen auf diesem Gerät ein.", @@ -1171,12 +1171,12 @@ "shareButton": "Teilen", "sharing": "Wird geteilt…", "errorFallback": "Das Projekt konnte nicht geteilt werden.", - "usernameRequired": "Legen Sie einen Benutzernamen für Ihr share.geolibre.app-Konto fest, bevor Sie teilen. Öffnen Sie Ihre Kontoeinstellungen, um einen auszuwählen, und versuchen Sie es erneut.", + "usernameRequired": "Legen Sie einen Benutzernamen für Ihr {{shareHost}}-Konto fest, bevor Sie teilen. Öffnen Sie Ihre Kontoeinstellungen, um einen auszuwählen, und versuchen Sie es erneut.", "openAccountSettings": "Kontoeinstellungen öffnen" }, "gallery": { "title": "Projektgalerie", - "description": "Durchsuchen Sie öffentliche Projekte auf share.geolibre.app und öffnen Sie eines in GeoLibre.", + "description": "Durchsuchen Sie öffentliche Projekte auf {{shareHost}} und öffnen Sie eines in GeoLibre.", "searchPlaceholder": "Geladene Projekte nach Titel, Autor oder Tag filtern", "loading": "Projekte werden geladen…", "loadingMore": "Weitere werden geladen…", @@ -1185,11 +1185,11 @@ "noMatches": "Keine geladenen Projekte entsprechen Ihrem Filter.", "errorFallback": "Die Projektgalerie konnte nicht geladen werden.", "errorTimeout": "Zeitüberschreitung beim Laden der Projektgalerie. Bitte versuchen Sie es erneut.", - "errorNetwork": "share.geolibre.app konnte nicht erreicht werden. Überprüfen Sie Ihre Internetverbindung.", + "errorNetwork": "{{shareHost}} konnte nicht erreicht werden. Überprüfen Sie Ihre Internetverbindung.", "errorHttp": "Die Galerie konnte nicht geladen werden (HTTP {{status}}).", "errorInvalidResponse": "Die Galerie konnte nicht geladen werden (ungültige Serverantwort).", - "errorUnauthorized": "Ihr share.geolibre.app-API-Token ist ungültig oder abgelaufen. Aktualisieren Sie es in den Einstellungen.", - "errorUsernameRequired": "Legen Sie einen Benutzernamen für Ihr share.geolibre.app-Konto fest, bevor Sie Ihre Projekte laden.", + "errorUnauthorized": "Ihr {{shareHost}}-API-Token ist ungültig oder abgelaufen. Aktualisieren Sie es in den Einstellungen.", + "errorUsernameRequired": "Legen Sie einen Benutzernamen für Ihr {{shareHost}}-Konto fest, bevor Sie Ihre Projekte laden.", "retry": "Erneut versuchen", "open": "Öffnen", "openCopy": "Kopie öffnen", @@ -1206,7 +1206,7 @@ "scopeFeatured": "Empfohlen", "scopeAll": "Alle Projekte", "scopeMine": "Meine Projekte", - "signedOutHint": "Fügen Sie in den Einstellungen ein share.geolibre.app-API-Token hinzu, um Ihre eigenen nicht gelisteten und privaten Projekte zu sehen.", + "signedOutHint": "Fügen Sie in den Einstellungen ein {{shareHost}}-API-Token hinzu, um Ihre eigenen nicht gelisteten und privaten Projekte zu sehen.", "emptyFeatured": "Noch keine empfohlenen Projekte.", "emptyMine": "Sie haben noch keine Projekte geteilt.", "visibilityUnlisted": "Nicht gelistet", @@ -1791,8 +1791,8 @@ }, "env": { "tokenTitle": "Share.GeoLibre-API-Token", - "tokenDescription": "Projekt > Teilen verwendet dieses Token, um Ihre Karten auf share.geolibre.app hochzuladen. Öffnen Sie share.geolibre.app/settings, erstellen Sie ein Token unter Einstellungen > API-Token und fügen Sie es dann in das Feld unten ein.", - "tokenStorageNote": "Wird lokal auf diesem Gerät gespeichert und nur an share.geolibre.app gesendet, um Uploads zu authentifizieren. Im Web-Build nutzt es denselben Browser-Speicher wie andere Website-Daten. Widerrufen Sie es daher auf share.geolibre.app, falls Ihr Gerät kompromittiert wurde.", + "tokenDescription": "Projekt > Teilen verwendet dieses Token, um Ihre Karten auf {{shareHost}} hochzuladen. Öffnen Sie {{shareHost}}/settings, erstellen Sie ein Token unter Einstellungen > API-Token und fügen Sie es dann in das Feld unten ein.", + "tokenStorageNote": "Wird lokal auf diesem Gerät gespeichert und nur an {{shareHost}} gesendet, um Uploads zu authentifizieren. Im Web-Build nutzt es denselben Browser-Speicher wie andere Website-Daten. Widerrufen Sie es daher auf {{shareHost}}, falls Ihr Gerät kompromittiert wurde.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium-Ion-Token", "cesiumTokenDescription": "Der 3D-Globus (ein geteiltes Ansichtsfenster) verwendet Cesium-Ion-Weltbilder und -Gelände, wofür ein Zugriffstoken erforderlich ist. Erstellen Sie ein kostenloses Konto unter ion.cesium.com/tokens, kopieren Sie Ihr Standard-Zugriffstoken und fügen Sie es unten ein. Ohne Token ist der 3D-Globus-Umschalter ausgeblendet.", diff --git a/apps/geolibre-desktop/src/i18n/locales/en.json b/apps/geolibre-desktop/src/i18n/locales/en.json index 3aadd012d..8f4424b1f 100644 --- a/apps/geolibre-desktop/src/i18n/locales/en.json +++ b/apps/geolibre-desktop/src/i18n/locales/en.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "Share project", - "description": "Upload the current project to share.geolibre.app and get a shareable link.", - "setupIntro": "Connect your share.geolibre.app account before uploading.", + "description": "Upload the current project to {{shareHost}} and get a shareable link.", + "setupIntro": "Connect your {{shareHost}} account before uploading.", "step1Title": "1. Get an API token", - "step1Description": "Sign in to share.geolibre.app, then create a token under Settings → API tokens.", + "step1Description": "Sign in to {{shareHost}}, then create a token under Settings → API tokens.", "getToken": "Get API token", "step2Title": "2. Add the token to GeoLibre", "step2Description": "Paste the token into Settings → Environment Variables on this device.", @@ -1171,12 +1171,12 @@ "shareButton": "Share", "sharing": "Sharing…", "errorFallback": "Could not share the project.", - "usernameRequired": "Set a username on your share.geolibre.app account before sharing. Open your account settings to choose one, then try again.", + "usernameRequired": "Set a username on your {{shareHost}} account before sharing. Open your account settings to choose one, then try again.", "openAccountSettings": "Open account settings" }, "gallery": { "title": "Project gallery", - "description": "Browse public projects shared on share.geolibre.app and open one in GeoLibre.", + "description": "Browse public projects shared on {{shareHost}} and open one in GeoLibre.", "searchPlaceholder": "Filter loaded projects by title, author, or tag", "loading": "Loading projects…", "loadingMore": "Loading more…", @@ -1185,11 +1185,12 @@ "noMatches": "No loaded projects match your filter.", "errorFallback": "Could not load the project gallery.", "errorTimeout": "The project gallery timed out. Please try again.", - "errorNetwork": "Could not reach share.geolibre.app. Check your internet connection.", + "errorNetwork": "Could not reach {{shareHost}}. Check your internet connection.", "errorHttp": "Could not load the gallery (HTTP {{status}}).", "errorInvalidResponse": "Could not load the gallery (invalid server response).", - "errorUnauthorized": "Your share.geolibre.app API token is invalid or expired. Update it in Settings.", - "errorUsernameRequired": "Set a username on your share.geolibre.app account before loading your projects.", + "errorUnauthorized": "Your {{shareHost}} API token is invalid or expired. Update it in Settings.", + "errorUsernameRequired": "Set a username on your {{shareHost}} account before loading your projects.", + "errorNotConfigured": "This deployment has no project sharing server configured.", "retry": "Retry", "open": "Open", "openCopy": "Open a copy", @@ -1206,7 +1207,7 @@ "scopeFeatured": "Featured", "scopeAll": "All projects", "scopeMine": "My projects", - "signedOutHint": "Add a share.geolibre.app API token in Settings to see your own unlisted and private projects.", + "signedOutHint": "Add a {{shareHost}} API token in Settings to see your own unlisted and private projects.", "emptyFeatured": "No featured projects yet.", "emptyMine": "You have not shared any projects yet.", "visibilityUnlisted": "Unlisted", @@ -1791,8 +1792,8 @@ }, "env": { "tokenTitle": "Share.GeoLibre API token", - "tokenDescription": "Project > Share uses this token to upload your maps to share.geolibre.app. Open share.geolibre.app/settings, create a token under Settings > API tokens, then paste it into the field below.", - "tokenStorageNote": "Stored locally on this device and sent only to share.geolibre.app to authenticate uploads. On the web build it shares the same browser storage as other site data, so revoke it on share.geolibre.app if your machine is compromised.", + "tokenDescription": "Project > Share uses this token to upload your maps to {{shareHost}}. Open {{shareHost}}/settings, create a token under Settings > API tokens, then paste it into the field below.", + "tokenStorageNote": "Stored locally on this device and sent only to {{shareHost}} to authenticate uploads. On the web build it shares the same browser storage as other site data, so revoke it on {{shareHost}} if your machine is compromised.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion token", "cesiumTokenDescription": "The 3D globe (a split-view pane) uses Cesium Ion world imagery and terrain, which need an access token. Create a free account at ion.cesium.com/tokens, copy your default access token, and paste it below. Without a token the 3D globe toggle is hidden.", @@ -2339,6 +2340,7 @@ "saveAsEllipsis": "Save As...", "saveAsTemplateEllipsis": "Save as template...", "shareEllipsis": "Share...", + "shareHostUnavailable": "Unavailable: this deployment's sharing server address is not valid.", "exportHtmlEllipsis": "Export as HTML...", "htmlFile": "HTML", "collaborateEllipsis": "Collaborate...", diff --git a/apps/geolibre-desktop/src/i18n/locales/es.json b/apps/geolibre-desktop/src/i18n/locales/es.json index dc77ec0df..5b9e791b0 100644 --- a/apps/geolibre-desktop/src/i18n/locales/es.json +++ b/apps/geolibre-desktop/src/i18n/locales/es.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "Compartir proyecto", - "description": "Suba el proyecto actual a share.geolibre.app y obtenga un enlace para compartir.", - "setupIntro": "Conecte su cuenta de share.geolibre.app antes de subir el proyecto.", + "description": "Suba el proyecto actual a {{shareHost}} y obtenga un enlace para compartir.", + "setupIntro": "Conecte su cuenta de {{shareHost}} antes de subir el proyecto.", "step1Title": "1. Obtener un token de API", - "step1Description": "Inicie sesión en share.geolibre.app y luego cree un token en Configuración → Tokens de API.", + "step1Description": "Inicie sesión en {{shareHost}} y luego cree un token en Configuración → Tokens de API.", "getToken": "Obtener token de API", "step2Title": "2. Añadir el token a GeoLibre", "step2Description": "Pegue el token en Configuración → Variables de entorno en este dispositivo.", @@ -1171,12 +1171,12 @@ "shareButton": "Compartir", "sharing": "Compartiendo…", "errorFallback": "No se pudo compartir el proyecto.", - "usernameRequired": "Configure un nombre de usuario en su cuenta de share.geolibre.app antes de compartir. Abra la configuración de su cuenta para elegir uno y vuelva a intentarlo.", + "usernameRequired": "Configure un nombre de usuario en su cuenta de {{shareHost}} antes de compartir. Abra la configuración de su cuenta para elegir uno y vuelva a intentarlo.", "openAccountSettings": "Abrir configuración de la cuenta" }, "gallery": { "title": "Galería de proyectos", - "description": "Explore los proyectos públicos compartidos en share.geolibre.app y abra uno en GeoLibre.", + "description": "Explore los proyectos públicos compartidos en {{shareHost}} y abra uno en GeoLibre.", "searchPlaceholder": "Filtrar proyectos cargados por título, autor o etiqueta", "loading": "Cargando proyectos…", "loadingMore": "Cargando más…", @@ -1185,11 +1185,11 @@ "noMatches": "Ningún proyecto cargado coincide con su filtro.", "errorFallback": "No se pudo cargar la galería de proyectos.", "errorTimeout": "Se agotó el tiempo de espera de la galería de proyectos. Inténtelo de nuevo.", - "errorNetwork": "No se pudo conectar con share.geolibre.app. Compruebe su conexión a internet.", + "errorNetwork": "No se pudo conectar con {{shareHost}}. Compruebe su conexión a internet.", "errorHttp": "No se pudo cargar la galería (HTTP {{status}}).", "errorInvalidResponse": "No se pudo cargar la galería (respuesta del servidor no válida).", - "errorUnauthorized": "Su token de API de share.geolibre.app no es válido o ha caducado. Actualícelo en Configuración.", - "errorUsernameRequired": "Configure un nombre de usuario en su cuenta de share.geolibre.app antes de cargar sus proyectos.", + "errorUnauthorized": "Su token de API de {{shareHost}} no es válido o ha caducado. Actualícelo en Configuración.", + "errorUsernameRequired": "Configure un nombre de usuario en su cuenta de {{shareHost}} antes de cargar sus proyectos.", "retry": "Reintentar", "open": "Abrir", "openCopy": "Abrir una copia", @@ -1206,7 +1206,7 @@ "scopeFeatured": "Destacados", "scopeAll": "Todos los proyectos", "scopeMine": "Mis proyectos", - "signedOutHint": "Añada un token de API de share.geolibre.app en Configuración para ver sus propios proyectos no listados y privados.", + "signedOutHint": "Añada un token de API de {{shareHost}} en Configuración para ver sus propios proyectos no listados y privados.", "emptyFeatured": "Aún no hay proyectos destacados.", "emptyMine": "Aún no ha compartido ningún proyecto.", "visibilityUnlisted": "No listado", @@ -1791,8 +1791,8 @@ }, "env": { "tokenTitle": "Token de API de Share.GeoLibre", - "tokenDescription": "Proyecto > Compartir usa este token para subir sus mapas a share.geolibre.app. Abra share.geolibre.app/settings, cree un token en Configuración > Tokens de API y luego péguelo en el campo de abajo.", - "tokenStorageNote": "Se almacena localmente en este dispositivo y solo se envía a share.geolibre.app para autenticar las subidas. En la compilación web, comparte el mismo almacenamiento del navegador que otros datos del sitio, así que revóquelo en share.geolibre.app si su equipo se ve comprometido.", + "tokenDescription": "Proyecto > Compartir usa este token para subir sus mapas a {{shareHost}}. Abra {{shareHost}}/settings, cree un token en Configuración > Tokens de API y luego péguelo en el campo de abajo.", + "tokenStorageNote": "Se almacena localmente en este dispositivo y solo se envía a {{shareHost}} para autenticar las subidas. En la compilación web, comparte el mismo almacenamiento del navegador que otros datos del sitio, así que revóquelo en {{shareHost}} si su equipo se ve comprometido.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token de Cesium Ion", "cesiumTokenDescription": "El globo 3D (un panel de vista dividida) usa imágenes y terreno mundiales de Cesium Ion, que necesitan un token de acceso. Cree una cuenta gratuita en ion.cesium.com/tokens, copie su token de acceso predeterminado y péguelo a continuación. Sin un token, el interruptor del globo 3D permanece oculto.", diff --git a/apps/geolibre-desktop/src/i18n/locales/fr.json b/apps/geolibre-desktop/src/i18n/locales/fr.json index 1139ed045..27d463dbf 100644 --- a/apps/geolibre-desktop/src/i18n/locales/fr.json +++ b/apps/geolibre-desktop/src/i18n/locales/fr.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "Partager le projet", - "description": "Téléversez le projet actuel vers share.geolibre.app et obtenez un lien partageable.", - "setupIntro": "Connectez votre compte share.geolibre.app avant de téléverser.", + "description": "Téléversez le projet actuel vers {{shareHost}} et obtenez un lien partageable.", + "setupIntro": "Connectez votre compte {{shareHost}} avant de téléverser.", "step1Title": "1. Obtenir un jeton API", - "step1Description": "Connectez-vous à share.geolibre.app, puis créez un jeton sous Paramètres → Jetons API.", + "step1Description": "Connectez-vous à {{shareHost}}, puis créez un jeton sous Paramètres → Jetons API.", "getToken": "Obtenir un jeton API", "step2Title": "2. Ajouter le jeton à GeoLibre", "step2Description": "Collez le jeton dans Paramètres → Variables d'environnement sur cet appareil.", @@ -1171,12 +1171,12 @@ "shareButton": "Partager", "sharing": "Partage en cours…", "errorFallback": "Impossible de partager le projet.", - "usernameRequired": "Définissez un nom d'utilisateur sur votre compte share.geolibre.app avant de partager. Ouvrez les paramètres de votre compte pour en choisir un, puis réessayez.", + "usernameRequired": "Définissez un nom d'utilisateur sur votre compte {{shareHost}} avant de partager. Ouvrez les paramètres de votre compte pour en choisir un, puis réessayez.", "openAccountSettings": "Ouvrir les paramètres du compte" }, "gallery": { "title": "Galerie de projets", - "description": "Parcourez les projets publics partagés sur share.geolibre.app et ouvrez-en un dans GeoLibre.", + "description": "Parcourez les projets publics partagés sur {{shareHost}} et ouvrez-en un dans GeoLibre.", "searchPlaceholder": "Filtrer les projets chargés par titre, auteur ou étiquette", "loading": "Chargement des projets…", "loadingMore": "Chargement supplémentaire…", @@ -1185,11 +1185,11 @@ "noMatches": "Aucun projet chargé ne correspond à votre filtre.", "errorFallback": "Impossible de charger la galerie de projets.", "errorTimeout": "La galerie de projets a expiré. Veuillez réessayer.", - "errorNetwork": "Impossible de joindre share.geolibre.app. Vérifiez votre connexion internet.", + "errorNetwork": "Impossible de joindre {{shareHost}}. Vérifiez votre connexion internet.", "errorHttp": "Impossible de charger la galerie (HTTP {{status}}).", "errorInvalidResponse": "Impossible de charger la galerie (réponse du serveur invalide).", - "errorUnauthorized": "Votre jeton API share.geolibre.app est invalide ou expiré. Mettez-le à jour dans Paramètres.", - "errorUsernameRequired": "Définissez un nom d'utilisateur sur votre compte share.geolibre.app avant de charger vos projets.", + "errorUnauthorized": "Votre jeton API {{shareHost}} est invalide ou expiré. Mettez-le à jour dans Paramètres.", + "errorUsernameRequired": "Définissez un nom d'utilisateur sur votre compte {{shareHost}} avant de charger vos projets.", "retry": "Réessayer", "open": "Ouvrir", "openCopy": "Ouvrir une copie", @@ -1206,7 +1206,7 @@ "scopeFeatured": "En vedette", "scopeAll": "Tous les projets", "scopeMine": "Mes projets", - "signedOutHint": "Ajoutez un jeton API share.geolibre.app dans Paramètres pour voir vos propres projets non répertoriés et privés.", + "signedOutHint": "Ajoutez un jeton API {{shareHost}} dans Paramètres pour voir vos propres projets non répertoriés et privés.", "emptyFeatured": "Pas encore de projets en vedette.", "emptyMine": "Vous n'avez encore partagé aucun projet.", "visibilityUnlisted": "Non répertorié", @@ -1791,8 +1791,8 @@ }, "env": { "tokenTitle": "Jeton API Share.GeoLibre", - "tokenDescription": "Projet > Partager utilise ce jeton pour téléverser vos cartes vers share.geolibre.app. Ouvrez share.geolibre.app/settings, créez un jeton sous Paramètres > Jetons API, puis collez-le dans le champ ci-dessous.", - "tokenStorageNote": "Stocké localement sur cet appareil et envoyé uniquement à share.geolibre.app pour authentifier les téléversements. Dans la version web, il partage le même stockage de navigateur que les autres données du site, alors révoquez-le sur share.geolibre.app si votre machine est compromise.", + "tokenDescription": "Projet > Partager utilise ce jeton pour téléverser vos cartes vers {{shareHost}}. Ouvrez {{shareHost}}/settings, créez un jeton sous Paramètres > Jetons API, puis collez-le dans le champ ci-dessous.", + "tokenStorageNote": "Stocké localement sur cet appareil et envoyé uniquement à {{shareHost}} pour authentifier les téléversements. Dans la version web, il partage le même stockage de navigateur que les autres données du site, alors révoquez-le sur {{shareHost}} si votre machine est compromise.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Jeton Cesium Ion", "cesiumTokenDescription": "Le globe 3D (un volet en vue partagée) utilise l'imagerie mondiale et le relief de Cesium Ion, qui nécessitent un jeton d'accès. Créez un compte gratuit sur ion.cesium.com/tokens, copiez votre jeton d'accès par défaut, puis collez-le ci-dessous. Sans jeton, le bouton du globe 3D est masqué.", diff --git a/apps/geolibre-desktop/src/i18n/locales/hi.json b/apps/geolibre-desktop/src/i18n/locales/hi.json index 838e1c620..96014fa45 100644 --- a/apps/geolibre-desktop/src/i18n/locales/hi.json +++ b/apps/geolibre-desktop/src/i18n/locales/hi.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "प्रोजेक्ट साझा करें", - "description": "वर्तमान प्रोजेक्ट को share.geolibre.app पर अपलोड करें और एक साझा करने योग्य लिंक प्राप्त करें।", - "setupIntro": "अपलोड करने से पहले अपना share.geolibre.app खाता कनेक्ट करें।", + "description": "वर्तमान प्रोजेक्ट को {{shareHost}} पर अपलोड करें और एक साझा करने योग्य लिंक प्राप्त करें।", + "setupIntro": "अपलोड करने से पहले अपना {{shareHost}} खाता कनेक्ट करें।", "step1Title": "1. एक API टोकन प्राप्त करें", - "step1Description": "share.geolibre.app में साइन इन करें, फिर Settings → API tokens के अंतर्गत एक टोकन बनाएं।", + "step1Description": "{{shareHost}} में साइन इन करें, फिर Settings → API tokens के अंतर्गत एक टोकन बनाएं।", "getToken": "API टोकन प्राप्त करें", "step2Title": "2. टोकन को GeoLibre में जोड़ें", "step2Description": "इस डिवाइस पर Settings → Environment Variables में टोकन पेस्ट करें।", @@ -1171,12 +1171,12 @@ "shareButton": "साझा करें", "sharing": "साझा किया जा रहा है…", "errorFallback": "प्रोजेक्ट साझा नहीं किया जा सका।", - "usernameRequired": "साझा करने से पहले अपने share.geolibre.app खाते पर एक उपयोगकर्ता नाम सेट करें। एक चुनने के लिए अपनी खाता सेटिंग्स खोलें, फिर पुनः प्रयास करें।", + "usernameRequired": "साझा करने से पहले अपने {{shareHost}} खाते पर एक उपयोगकर्ता नाम सेट करें। एक चुनने के लिए अपनी खाता सेटिंग्स खोलें, फिर पुनः प्रयास करें।", "openAccountSettings": "खाता सेटिंग्स खोलें" }, "gallery": { "title": "प्रोजेक्ट गैलरी", - "description": "share.geolibre.app पर साझा किए गए सार्वजनिक प्रोजेक्ट ब्राउज़ करें और उनमें से किसी एक को GeoLibre में खोलें।", + "description": "{{shareHost}} पर साझा किए गए सार्वजनिक प्रोजेक्ट ब्राउज़ करें और उनमें से किसी एक को GeoLibre में खोलें।", "searchPlaceholder": "शीर्षक, लेखक या टैग के आधार पर लोड किए गए प्रोजेक्ट फ़िल्टर करें", "loading": "प्रोजेक्ट लोड हो रहे हैं…", "loadingMore": "अधिक लोड हो रहा है…", @@ -1185,11 +1185,11 @@ "noMatches": "आपके फ़िल्टर से कोई लोड किया गया प्रोजेक्ट मेल नहीं खाता।", "errorFallback": "प्रोजेक्ट गैलरी लोड नहीं हो सकी।", "errorTimeout": "प्रोजेक्ट गैलरी का समय समाप्त हो गया। कृपया पुनः प्रयास करें।", - "errorNetwork": "share.geolibre.app तक नहीं पहुंचा जा सका। अपना इंटरनेट कनेक्शन जांचें।", + "errorNetwork": "{{shareHost}} तक नहीं पहुंचा जा सका। अपना इंटरनेट कनेक्शन जांचें।", "errorHttp": "गैलरी लोड नहीं हो सकी (HTTP {{status}})।", "errorInvalidResponse": "गैलरी लोड नहीं हो सकी (अमान्य सर्वर प्रतिक्रिया)।", - "errorUnauthorized": "आपका share.geolibre.app API टोकन अमान्य है या समाप्त हो गया है। इसे Settings में अपडेट करें।", - "errorUsernameRequired": "अपने प्रोजेक्ट लोड करने से पहले अपने share.geolibre.app खाते पर एक उपयोगकर्ता नाम सेट करें।", + "errorUnauthorized": "आपका {{shareHost}} API टोकन अमान्य है या समाप्त हो गया है। इसे Settings में अपडेट करें।", + "errorUsernameRequired": "अपने प्रोजेक्ट लोड करने से पहले अपने {{shareHost}} खाते पर एक उपयोगकर्ता नाम सेट करें।", "retry": "पुनः प्रयास करें", "open": "खोलें", "openCopy": "एक प्रति खोलें", @@ -1206,7 +1206,7 @@ "scopeFeatured": "विशेष रुप से प्रदर्शित", "scopeAll": "सभी प्रोजेक्ट", "scopeMine": "मेरे प्रोजेक्ट", - "signedOutHint": "अपने असूचीबद्ध और निजी प्रोजेक्ट देखने के लिए Settings में एक share.geolibre.app API टोकन जोड़ें।", + "signedOutHint": "अपने असूचीबद्ध और निजी प्रोजेक्ट देखने के लिए Settings में एक {{shareHost}} API टोकन जोड़ें।", "emptyFeatured": "अभी तक कोई विशेष प्रोजेक्ट नहीं है।", "emptyMine": "आपने अभी तक कोई प्रोजेक्ट साझा नहीं किया है।", "visibilityUnlisted": "असूचीबद्ध", @@ -1791,8 +1791,8 @@ }, "env": { "tokenTitle": "Share.GeoLibre API टोकन", - "tokenDescription": "Project > Share इस टोकन का उपयोग आपके मानचित्रों को share.geolibre.app पर अपलोड करने के लिए करता है। share.geolibre.app/settings खोलें, Settings > API tokens के अंतर्गत एक टोकन बनाएँ, फिर उसे नीचे दिए गए फ़ील्ड में पेस्ट करें।", - "tokenStorageNote": "इस डिवाइस पर स्थानीय रूप से संग्रहीत और अपलोड प्रमाणित करने के लिए केवल share.geolibre.app पर भेजा जाता है। वेब बिल्ड पर यह अन्य साइट डेटा के समान ब्राउज़र स्टोरेज साझा करता है, इसलिए यदि आपकी मशीन से समझौता हो जाए तो इसे share.geolibre.app पर रद्द करें।", + "tokenDescription": "Project > Share इस टोकन का उपयोग आपके मानचित्रों को {{shareHost}} पर अपलोड करने के लिए करता है। {{shareHost}}/settings खोलें, Settings > API tokens के अंतर्गत एक टोकन बनाएँ, फिर उसे नीचे दिए गए फ़ील्ड में पेस्ट करें।", + "tokenStorageNote": "इस डिवाइस पर स्थानीय रूप से संग्रहीत और अपलोड प्रमाणित करने के लिए केवल {{shareHost}} पर भेजा जाता है। वेब बिल्ड पर यह अन्य साइट डेटा के समान ब्राउज़र स्टोरेज साझा करता है, इसलिए यदि आपकी मशीन से समझौता हो जाए तो इसे {{shareHost}} पर रद्द करें।", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion टोकन", "cesiumTokenDescription": "3D ग्लोब (एक स्प्लिट-व्यू पैनल) Cesium Ion वर्ल्ड इमेजरी और टेरेन का उपयोग करता है, जिसके लिए एक एक्सेस टोकन चाहिए। ion.cesium.com/tokens पर एक मुफ़्त खाता बनाएँ, अपना डिफ़ॉल्ट एक्सेस टोकन कॉपी करें, और उसे नीचे पेस्ट करें। टोकन के बिना 3D ग्लोब टॉगल छिपा रहता है।", diff --git a/apps/geolibre-desktop/src/i18n/locales/id.json b/apps/geolibre-desktop/src/i18n/locales/id.json index 9270bfe75..0e0969fba 100644 --- a/apps/geolibre-desktop/src/i18n/locales/id.json +++ b/apps/geolibre-desktop/src/i18n/locales/id.json @@ -1114,10 +1114,10 @@ }, "share": { "title": "Bagikan proyek", - "description": "Unggah proyek saat ini ke share.geolibre.app dan dapatkan tautan yang dapat dibagikan.", - "setupIntro": "Hubungkan akun share.geolibre.app Anda sebelum mengunggah.", + "description": "Unggah proyek saat ini ke {{shareHost}} dan dapatkan tautan yang dapat dibagikan.", + "setupIntro": "Hubungkan akun {{shareHost}} Anda sebelum mengunggah.", "step1Title": "1. Dapatkan token API", - "step1Description": "Masuk ke share.geolibre.app, lalu buat token di Settings → API tokens.", + "step1Description": "Masuk ke {{shareHost}}, lalu buat token di Settings → API tokens.", "getToken": "Dapatkan token API", "step2Title": "2. Tambahkan token ke GeoLibre", "step2Description": "Tempel token ke Settings → Environment Variables pada perangkat ini.", @@ -1136,12 +1136,12 @@ "shareButton": "Bagikan", "sharing": "Membagikan…", "errorFallback": "Tidak dapat membagikan proyek.", - "usernameRequired": "Atur nama pengguna pada akun share.geolibre.app Anda sebelum membagikan. Buka pengaturan akun Anda untuk memilihnya, lalu coba lagi.", + "usernameRequired": "Atur nama pengguna pada akun {{shareHost}} Anda sebelum membagikan. Buka pengaturan akun Anda untuk memilihnya, lalu coba lagi.", "openAccountSettings": "Buka pengaturan akun" }, "gallery": { "title": "Galeri proyek", - "description": "Jelajahi proyek publik yang dibagikan di share.geolibre.app dan buka salah satunya di GeoLibre.", + "description": "Jelajahi proyek publik yang dibagikan di {{shareHost}} dan buka salah satunya di GeoLibre.", "searchPlaceholder": "Filter proyek yang dimuat berdasarkan judul, penulis, atau tag", "loading": "Memuat proyek…", "loadingMore": "Memuat lebih banyak…", @@ -1150,11 +1150,11 @@ "noMatches": "Tidak ada proyek yang dimuat sesuai dengan filter Anda.", "errorFallback": "Tidak dapat memuat galeri proyek.", "errorTimeout": "Galeri proyek habis waktu. Silakan coba lagi.", - "errorNetwork": "Tidak dapat menjangkau share.geolibre.app. Periksa koneksi internet Anda.", + "errorNetwork": "Tidak dapat menjangkau {{shareHost}}. Periksa koneksi internet Anda.", "errorHttp": "Tidak dapat memuat galeri (HTTP {{status}}).", "errorInvalidResponse": "Tidak dapat memuat galeri (respons server tidak valid).", - "errorUnauthorized": "Token API share.geolibre.app Anda tidak valid atau telah kedaluwarsa. Perbarui di Pengaturan.", - "errorUsernameRequired": "Atur nama pengguna pada akun share.geolibre.app Anda sebelum memuat proyek Anda.", + "errorUnauthorized": "Token API {{shareHost}} Anda tidak valid atau telah kedaluwarsa. Perbarui di Pengaturan.", + "errorUsernameRequired": "Atur nama pengguna pada akun {{shareHost}} Anda sebelum memuat proyek Anda.", "retry": "Coba lagi", "open": "Buka", "openCopy": "Buka salinan", @@ -1170,7 +1170,7 @@ "scopeFeatured": "Unggulan", "scopeAll": "Semua proyek", "scopeMine": "Proyek saya", - "signedOutHint": "Tambahkan token API share.geolibre.app di Pengaturan untuk melihat proyek tidak terdaftar dan privat milik Anda.", + "signedOutHint": "Tambahkan token API {{shareHost}} di Pengaturan untuk melihat proyek tidak terdaftar dan privat milik Anda.", "emptyFeatured": "Belum ada proyek unggulan.", "emptyMine": "Anda belum membagikan proyek apa pun.", "visibilityUnlisted": "Tidak terdaftar", @@ -1751,8 +1751,8 @@ }, "env": { "tokenTitle": "Token API Share.GeoLibre", - "tokenDescription": "Project > Share menggunakan token ini untuk mengunggah peta Anda ke share.geolibre.app. Buka share.geolibre.app/settings, buat token di Settings > API tokens, lalu tempelkan ke bidang di bawah.", - "tokenStorageNote": "Disimpan secara lokal di perangkat ini dan hanya dikirim ke share.geolibre.app untuk mengautentikasi unggahan. Pada build web, token ini berbagi penyimpanan browser yang sama dengan data situs lainnya, jadi cabut aksesnya di share.geolibre.app jika perangkat Anda disusupi.", + "tokenDescription": "Project > Share menggunakan token ini untuk mengunggah peta Anda ke {{shareHost}}. Buka {{shareHost}}/settings, buat token di Settings > API tokens, lalu tempelkan ke bidang di bawah.", + "tokenStorageNote": "Disimpan secara lokal di perangkat ini dan hanya dikirim ke {{shareHost}} untuk mengautentikasi unggahan. Pada build web, token ini berbagi penyimpanan browser yang sama dengan data situs lainnya, jadi cabut aksesnya di {{shareHost}} jika perangkat Anda disusupi.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token Cesium Ion", "cesiumTokenDescription": "Bola dunia 3D (panel tampilan terpisah) menggunakan citra dan medan dunia Cesium Ion, yang memerlukan token akses. Buat akun gratis di ion.cesium.com/tokens, salin token akses default Anda, dan tempelkan di bawah. Tanpa token, tombol bola dunia 3D disembunyikan.", diff --git a/apps/geolibre-desktop/src/i18n/locales/it.json b/apps/geolibre-desktop/src/i18n/locales/it.json index 2132252e1..f741e8a1b 100644 --- a/apps/geolibre-desktop/src/i18n/locales/it.json +++ b/apps/geolibre-desktop/src/i18n/locales/it.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "Condividi progetto", - "description": "Carica il progetto corrente su share.geolibre.app e ottieni un link condivisibile.", - "setupIntro": "Collega il tuo account share.geolibre.app prima di caricare.", + "description": "Carica il progetto corrente su {{shareHost}} e ottieni un link condivisibile.", + "setupIntro": "Collega il tuo account {{shareHost}} prima di caricare.", "step1Title": "1. Ottieni un token API", - "step1Description": "Accedi a share.geolibre.app, quindi crea un token in Settings → API tokens.", + "step1Description": "Accedi a {{shareHost}}, quindi crea un token in Settings → API tokens.", "getToken": "Ottieni token API", "step2Title": "2. Aggiungi il token a GeoLibre", "step2Description": "Incolla il token in Settings → Environment Variables su questo dispositivo.", @@ -1171,12 +1171,12 @@ "shareButton": "Condividi", "sharing": "Condivisione in corso…", "errorFallback": "Impossibile condividere il progetto.", - "usernameRequired": "Imposta un nome utente sul tuo account share.geolibre.app prima di condividere. Apri le impostazioni account per sceglierne uno, quindi riprova.", + "usernameRequired": "Imposta un nome utente sul tuo account {{shareHost}} prima di condividere. Apri le impostazioni account per sceglierne uno, quindi riprova.", "openAccountSettings": "Apri impostazioni account" }, "gallery": { "title": "Galleria progetti", - "description": "Sfoglia i progetti pubblici condivisi su share.geolibre.app e aprine uno in GeoLibre.", + "description": "Sfoglia i progetti pubblici condivisi su {{shareHost}} e aprine uno in GeoLibre.", "searchPlaceholder": "Filtra i progetti caricati per titolo, autore o tag", "loading": "Caricamento progetti…", "loadingMore": "Caricamento altri…", @@ -1185,11 +1185,11 @@ "noMatches": "Nessun progetto caricato corrisponde al filtro.", "errorFallback": "Impossibile caricare la galleria progetti.", "errorTimeout": "Timeout nel caricamento della galleria progetti. Riprova.", - "errorNetwork": "Impossibile raggiungere share.geolibre.app. Controlla la connessione a Internet.", + "errorNetwork": "Impossibile raggiungere {{shareHost}}. Controlla la connessione a Internet.", "errorHttp": "Impossibile caricare la galleria (HTTP {{status}}).", "errorInvalidResponse": "Impossibile caricare la galleria (risposta del server non valida).", - "errorUnauthorized": "Il tuo token API di share.geolibre.app non è valido o è scaduto. Aggiornalo nelle Impostazioni.", - "errorUsernameRequired": "Imposta un nome utente sul tuo account share.geolibre.app prima di caricare i tuoi progetti.", + "errorUnauthorized": "Il tuo token API di {{shareHost}} non è valido o è scaduto. Aggiornalo nelle Impostazioni.", + "errorUsernameRequired": "Imposta un nome utente sul tuo account {{shareHost}} prima di caricare i tuoi progetti.", "retry": "Riprova", "open": "Apri", "openCopy": "Apri una copia", @@ -1206,7 +1206,7 @@ "scopeFeatured": "In evidenza", "scopeAll": "Tutti i progetti", "scopeMine": "I miei progetti", - "signedOutHint": "Aggiungi un token API di share.geolibre.app nelle Impostazioni per vedere i tuoi progetti non elencati e privati.", + "signedOutHint": "Aggiungi un token API di {{shareHost}} nelle Impostazioni per vedere i tuoi progetti non elencati e privati.", "emptyFeatured": "Nessun progetto in evidenza al momento.", "emptyMine": "Non hai ancora condiviso alcun progetto.", "visibilityUnlisted": "Non elencato", @@ -1791,8 +1791,8 @@ }, "env": { "tokenTitle": "Token API di Share.GeoLibre", - "tokenDescription": "Progetto > Condividi usa questo token per caricare le tue mappe su share.geolibre.app. Apri share.geolibre.app/settings, crea un token in Settings > API tokens, quindi incollalo nel campo sottostante.", - "tokenStorageNote": "Memorizzato localmente su questo dispositivo e inviato solo a share.geolibre.app per autenticare i caricamenti. Nella build web condivide lo stesso archivio del browser degli altri dati del sito, quindi revocalo su share.geolibre.app se il tuo computer viene compromesso.", + "tokenDescription": "Progetto > Condividi usa questo token per caricare le tue mappe su {{shareHost}}. Apri {{shareHost}}/settings, crea un token in Settings > API tokens, quindi incollalo nel campo sottostante.", + "tokenStorageNote": "Memorizzato localmente su questo dispositivo e inviato solo a {{shareHost}} per autenticare i caricamenti. Nella build web condivide lo stesso archivio del browser degli altri dati del sito, quindi revocalo su {{shareHost}} se il tuo computer viene compromesso.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token Cesium Ion", "cesiumTokenDescription": "Il globo 3D (un riquadro a schermo diviso) usa le immagini satellitari e il terreno di Cesium Ion, che richiedono un token di accesso. Crea un account gratuito su ion.cesium.com/tokens, copia il tuo token di accesso predefinito e incollalo di seguito. Senza un token, l'interruttore del globo 3D resta nascosto.", diff --git a/apps/geolibre-desktop/src/i18n/locales/ja.json b/apps/geolibre-desktop/src/i18n/locales/ja.json index beaa11c93..10305268c 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ja.json +++ b/apps/geolibre-desktop/src/i18n/locales/ja.json @@ -1114,10 +1114,10 @@ }, "share": { "title": "プロジェクトを共有", - "description": "現在のプロジェクトを share.geolibre.app にアップロードし、共有リンクを取得します。", - "setupIntro": "アップロードする前に share.geolibre.app アカウントを接続してください。", + "description": "現在のプロジェクトを {{shareHost}} にアップロードし、共有リンクを取得します。", + "setupIntro": "アップロードする前に {{shareHost}} アカウントを接続してください。", "step1Title": "1. APIトークンを取得", - "step1Description": "share.geolibre.app にサインインし、設定 → APIトークンでトークンを作成してください。", + "step1Description": "{{shareHost}} にサインインし、設定 → APIトークンでトークンを作成してください。", "getToken": "APIトークンを取得", "step2Title": "2. トークンをGeoLibreに追加", "step2Description": "このデバイスの設定 → 環境変数にトークンを貼り付けてください。", @@ -1136,12 +1136,12 @@ "shareButton": "共有", "sharing": "共有中…", "errorFallback": "プロジェクトを共有できませんでした。", - "usernameRequired": "共有する前に share.geolibre.app アカウントでユーザー名を設定してください。アカウント設定を開いて選択し、もう一度お試しください。", + "usernameRequired": "共有する前に {{shareHost}} アカウントでユーザー名を設定してください。アカウント設定を開いて選択し、もう一度お試しください。", "openAccountSettings": "アカウント設定を開く" }, "gallery": { "title": "プロジェクトギャラリー", - "description": "share.geolibre.app で共有されている公開プロジェクトを閲覧し、GeoLibreで開きます。", + "description": "{{shareHost}} で共有されている公開プロジェクトを閲覧し、GeoLibreで開きます。", "searchPlaceholder": "タイトル、作成者、タグで読み込み済みプロジェクトを絞り込む", "loading": "プロジェクトを読み込み中…", "loadingMore": "さらに読み込み中…", @@ -1150,11 +1150,11 @@ "noMatches": "絞り込み条件に一致する読み込み済みプロジェクトがありません。", "errorFallback": "プロジェクトギャラリーを読み込めませんでした。", "errorTimeout": "プロジェクトギャラリーがタイムアウトしました。もう一度お試しください。", - "errorNetwork": "share.geolibre.app に接続できませんでした。インターネット接続を確認してください。", + "errorNetwork": "{{shareHost}} に接続できませんでした。インターネット接続を確認してください。", "errorHttp": "ギャラリーを読み込めませんでした(HTTP {{status}})。", "errorInvalidResponse": "ギャラリーを読み込めませんでした(サーバーの応答が無効です)。", - "errorUnauthorized": "share.geolibre.app のAPIトークンが無効か期限切れです。設定で更新してください。", - "errorUsernameRequired": "自分のプロジェクトを読み込む前に share.geolibre.app アカウントでユーザー名を設定してください。", + "errorUnauthorized": "{{shareHost}} のAPIトークンが無効か期限切れです。設定で更新してください。", + "errorUsernameRequired": "自分のプロジェクトを読み込む前に {{shareHost}} アカウントでユーザー名を設定してください。", "retry": "再試行", "open": "開く", "openCopy": "コピーを開く", @@ -1170,7 +1170,7 @@ "scopeFeatured": "注目", "scopeAll": "すべてのプロジェクト", "scopeMine": "自分のプロジェクト", - "signedOutHint": "自分の限定公開・非公開プロジェクトを表示するには、設定で share.geolibre.app のAPIトークンを追加してください。", + "signedOutHint": "自分の限定公開・非公開プロジェクトを表示するには、設定で {{shareHost}} のAPIトークンを追加してください。", "emptyFeatured": "注目のプロジェクトはまだありません。", "emptyMine": "まだプロジェクトを共有していません。", "visibilityUnlisted": "限定公開", @@ -1751,8 +1751,8 @@ }, "env": { "tokenTitle": "Share.GeoLibre APIトークン", - "tokenDescription": "プロジェクト > 共有では、このトークンを使用して地図をshare.geolibre.appにアップロードします。share.geolibre.app/settings を開き、設定 > APIトークン でトークンを作成して、下のフィールドに貼り付けてください。", - "tokenStorageNote": "このデバイスにローカルで保存され、アップロードの認証のためにshare.geolibre.appにのみ送信されます。Webビルドでは他のサイトデータと同じブラウザストレージを共有するため、端末が侵害された場合はshare.geolibre.appでトークンを失効させてください。", + "tokenDescription": "プロジェクト > 共有では、このトークンを使用して地図を{{shareHost}}にアップロードします。{{shareHost}}/settings を開き、設定 > APIトークン でトークンを作成して、下のフィールドに貼り付けてください。", + "tokenStorageNote": "このデバイスにローカルで保存され、アップロードの認証のために{{shareHost}}にのみ送信されます。Webビルドでは他のサイトデータと同じブラウザストレージを共有するため、端末が侵害された場合は{{shareHost}}でトークンを失効させてください。", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ionトークン", "cesiumTokenDescription": "3Dグローブ(分割表示ペイン)はCesium Ionの世界衛星画像と地形データを使用しており、アクセストークンが必要です。ion.cesium.com/tokens で無料アカウントを作成し、デフォルトのアクセストークンをコピーして下に貼り付けてください。トークンがない場合、3Dグローブの切り替えは表示されません。", diff --git a/apps/geolibre-desktop/src/i18n/locales/ka.json b/apps/geolibre-desktop/src/i18n/locales/ka.json index 19eb7194b..7786e707a 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ka.json +++ b/apps/geolibre-desktop/src/i18n/locales/ka.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "პროექტის გაზიარება", - "description": "ატვირთეთ მიმდინარე პროექტი share.geolibre.app-ზე და მიიღეთ გასაზიარებელი ბმული.", - "setupIntro": "ატვირთვამდე დააკავშირეთ თქვენი share.geolibre.app ანგარიში.", + "description": "ატვირთეთ მიმდინარე პროექტი {{shareHost}}-ზე და მიიღეთ გასაზიარებელი ბმული.", + "setupIntro": "ატვირთვამდე დააკავშირეთ თქვენი {{shareHost}} ანგარიში.", "step1Title": "1. მიიღეთ API token", - "step1Description": "შედით share.geolibre.app-ზე, შემდეგ შექმენით token განყოფილებაში „Settings → API tokens“.", + "step1Description": "შედით {{shareHost}}-ზე, შემდეგ შექმენით token განყოფილებაში „Settings → API tokens“.", "getToken": "API token-ის მიღება", "step2Title": "2. დაამატეთ token GeoLibre-ში", "step2Description": "ჩასვით token ამ მოწყობილობის „პარამეტრები → გარემოს ცვლადებში“.", @@ -1171,12 +1171,12 @@ "shareButton": "გაზიარება", "sharing": "მიმდინარეობს გაზიარება…", "errorFallback": "პროექტის გაზიარება ვერ მოხერხდა.", - "usernameRequired": "გაზიარებამდე დააყენეთ მომხმარებლის სახელი თქვენს share.geolibre.app ანგარიშზე. გახსენით ანგარიშის პარამეტრები ასარჩევად, შემდეგ სცადეთ ხელახლა.", + "usernameRequired": "გაზიარებამდე დააყენეთ მომხმარებლის სახელი თქვენს {{shareHost}} ანგარიშზე. გახსენით ანგარიშის პარამეტრები ასარჩევად, შემდეგ სცადეთ ხელახლა.", "openAccountSettings": "ანგარიშის პარამეტრების გახსნა" }, "gallery": { "title": "პროექტების გალერეა", - "description": "დაათვალიერეთ share.geolibre.app-ზე გაზიარებული საჯარო პროექტები და გახსენით ერთ-ერთი GeoLibre-ში.", + "description": "დაათვალიერეთ {{shareHost}}-ზე გაზიარებული საჯარო პროექტები და გახსენით ერთ-ერთი GeoLibre-ში.", "searchPlaceholder": "გაფილტრეთ ჩატვირთული პროექტები სათაურით, ავტორით ან ტეგით", "loading": "პროექტების ჩატვირთვა…", "loadingMore": "იტვირთება მეტი…", @@ -1185,11 +1185,11 @@ "noMatches": "ჩატვირთული პროექტები თქვენს ფილტრს არ ემთხვევა.", "errorFallback": "პროექტების გალერეის ჩატვირთვა ვერ მოხერხდა.", "errorTimeout": "პროექტების გალერეის მოთხოვნის დრო ამოიწურა. სცადეთ ხელახლა.", - "errorNetwork": "share.geolibre.app-თან დაკავშირება ვერ მოხერხდა. შეამოწმეთ ინტერნეტკავშირი.", + "errorNetwork": "{{shareHost}}-თან დაკავშირება ვერ მოხერხდა. შეამოწმეთ ინტერნეტკავშირი.", "errorHttp": "გალერეის ჩატვირთვა ვერ მოხერხდა (HTTP {{status}}).", "errorInvalidResponse": "გალერეის ჩატვირთვა ვერ მოხერხდა (სერვერის არასწორი პასუხი).", - "errorUnauthorized": "თქვენი share.geolibre.app API-ტოკენი არასწორია ან ვადაგასულია. განაახლეთ პარამეტრებში.", - "errorUsernameRequired": "თქვენი პროექტების ჩატვირთვამდე დააყენეთ მომხმარებლის სახელი share.geolibre.app ანგარიშზე.", + "errorUnauthorized": "თქვენი {{shareHost}} API-ტოკენი არასწორია ან ვადაგასულია. განაახლეთ პარამეტრებში.", + "errorUsernameRequired": "თქვენი პროექტების ჩატვირთვამდე დააყენეთ მომხმარებლის სახელი {{shareHost}} ანგარიშზე.", "retry": "ხელახლა ცდა", "open": "გახსნა", "openCopy": "ასლის გახსნა", @@ -1206,7 +1206,7 @@ "scopeFeatured": "გამორჩეული", "scopeAll": "ყველა პროექტი", "scopeMine": "ჩემი პროექტები", - "signedOutHint": "დაამატეთ share.geolibre.app API-ტოკენი პარამეტრებში, რომ ნახოთ თქვენი ბმულით ხელმისაწვდომი და პირადი პროექტები.", + "signedOutHint": "დაამატეთ {{shareHost}} API-ტოკენი პარამეტრებში, რომ ნახოთ თქვენი ბმულით ხელმისაწვდომი და პირადი პროექტები.", "emptyFeatured": "გამორჩეული პროექტები ჯერ არ არის.", "emptyMine": "ჯერ არცერთი პროექტი გაგიზიარებიათ.", "visibilityUnlisted": "ბმულით ხელმისაწვდომი", @@ -1791,8 +1791,8 @@ }, "env": { "tokenTitle": "Share.GeoLibre-ის API token", - "tokenDescription": "„პროექტი > გაზიარება“ იყენებს ამ token-ს თქვენი რუკების share.geolibre.app-ზე ასატვირთად. გახსენით share.geolibre.app/settings, შექმენით token განყოფილებაში „Settings > API tokens“, შემდეგ ჩასვით ქვემოთ ველში.", - "tokenStorageNote": "ინახება ლოკალურად ამ მოწყობილობაზე და იგზავნება მხოლოდ share.geolibre.app-ზე ატვირთვების ავთენტიფიკაციისთვის. ვებ-ვერსიაში ის იზიარებს იმავე ბრაუზერის საცავს, რასაც საიტის სხვა მონაცემები, ამიტომ გააუქმეთ ის share.geolibre.app-ზე, თუ თქვენი მანქანა კომპრომეტირებულია.", + "tokenDescription": "„პროექტი > გაზიარება“ იყენებს ამ token-ს თქვენი რუკების {{shareHost}}-ზე ასატვირთად. გახსენით {{shareHost}}/settings, შექმენით token განყოფილებაში „Settings > API tokens“, შემდეგ ჩასვით ქვემოთ ველში.", + "tokenStorageNote": "ინახება ლოკალურად ამ მოწყობილობაზე და იგზავნება მხოლოდ {{shareHost}}-ზე ატვირთვების ავთენტიფიკაციისთვის. ვებ-ვერსიაში ის იზიარებს იმავე ბრაუზერის საცავს, რასაც საიტის სხვა მონაცემები, ამიტომ გააუქმეთ ის {{shareHost}}-ზე, თუ თქვენი მანქანა კომპრომეტირებულია.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion-ის token", "cesiumTokenDescription": "3D გლობუსი (გაყოფილი ხედის პანელი) იყენებს Cesium Ion-ის მსოფლიო სურათებსა და რელიეფს, რასაც წვდომის token სჭირდება. შექმენით უფასო ანგარიში მისამართზე ion.cesium.com/tokens, დააკოპირეთ თქვენი ნაგულისხმევი წვდომის token და ჩასვით ქვემოთ. token-ის გარეშე 3D გლობუსის გადამრთველი დამალულია.", diff --git a/apps/geolibre-desktop/src/i18n/locales/ko.json b/apps/geolibre-desktop/src/i18n/locales/ko.json index b25dc1725..74c114af4 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ko.json +++ b/apps/geolibre-desktop/src/i18n/locales/ko.json @@ -1114,10 +1114,10 @@ }, "share": { "title": "프로젝트 공유", - "description": "현재 프로젝트를 share.geolibre.app에 업로드하고 공유 가능한 링크를 받으세요.", - "setupIntro": "업로드하기 전에 share.geolibre.app 계정을 연결하세요.", + "description": "현재 프로젝트를 {{shareHost}}에 업로드하고 공유 가능한 링크를 받으세요.", + "setupIntro": "업로드하기 전에 {{shareHost}} 계정을 연결하세요.", "step1Title": "1. API 토큰 받기", - "step1Description": "share.geolibre.app에 로그인한 다음 설정 → API 토큰에서 토큰을 생성하세요.", + "step1Description": "{{shareHost}}에 로그인한 다음 설정 → API 토큰에서 토큰을 생성하세요.", "getToken": "API 토큰 받기", "step2Title": "2. GeoLibre에 토큰 추가", "step2Description": "이 기기의 설정 → 환경 변수에 토큰을 붙여넣으세요.", @@ -1136,12 +1136,12 @@ "shareButton": "공유", "sharing": "공유 중…", "errorFallback": "프로젝트를 공유할 수 없습니다.", - "usernameRequired": "공유하기 전에 share.geolibre.app 계정에 사용자 이름을 설정하세요. 계정 설정을 열어 사용자 이름을 선택한 다음 다시 시도하세요.", + "usernameRequired": "공유하기 전에 {{shareHost}} 계정에 사용자 이름을 설정하세요. 계정 설정을 열어 사용자 이름을 선택한 다음 다시 시도하세요.", "openAccountSettings": "계정 설정 열기" }, "gallery": { "title": "프로젝트 갤러리", - "description": "share.geolibre.app에 공유된 공개 프로젝트를 둘러보고 GeoLibre에서 여세요.", + "description": "{{shareHost}}에 공유된 공개 프로젝트를 둘러보고 GeoLibre에서 여세요.", "searchPlaceholder": "불러온 프로젝트를 제목, 작성자 또는 태그로 필터링", "loading": "프로젝트 불러오는 중…", "loadingMore": "더 불러오는 중…", @@ -1150,11 +1150,11 @@ "noMatches": "필터와 일치하는 불러온 프로젝트가 없습니다.", "errorFallback": "프로젝트 갤러리를 불러올 수 없습니다.", "errorTimeout": "프로젝트 갤러리 요청 시간이 초과되었습니다. 다시 시도하세요.", - "errorNetwork": "share.geolibre.app에 연결할 수 없습니다. 인터넷 연결을 확인하세요.", + "errorNetwork": "{{shareHost}}에 연결할 수 없습니다. 인터넷 연결을 확인하세요.", "errorHttp": "갤러리를 불러올 수 없습니다(HTTP {{status}}).", "errorInvalidResponse": "갤러리를 불러올 수 없습니다(잘못된 서버 응답).", - "errorUnauthorized": "share.geolibre.app API 토큰이 유효하지 않거나 만료되었습니다. 설정에서 업데이트하세요.", - "errorUsernameRequired": "프로젝트를 불러오기 전에 share.geolibre.app 계정에 사용자 이름을 설정하세요.", + "errorUnauthorized": "{{shareHost}} API 토큰이 유효하지 않거나 만료되었습니다. 설정에서 업데이트하세요.", + "errorUsernameRequired": "프로젝트를 불러오기 전에 {{shareHost}} 계정에 사용자 이름을 설정하세요.", "retry": "다시 시도", "open": "열기", "openCopy": "사본 열기", @@ -1170,7 +1170,7 @@ "scopeFeatured": "추천", "scopeAll": "모든 프로젝트", "scopeMine": "내 프로젝트", - "signedOutHint": "본인의 목록에 없는 프로젝트와 비공개 프로젝트를 보려면 설정에서 share.geolibre.app API 토큰을 추가하세요.", + "signedOutHint": "본인의 목록에 없는 프로젝트와 비공개 프로젝트를 보려면 설정에서 {{shareHost}} API 토큰을 추가하세요.", "emptyFeatured": "아직 추천 프로젝트가 없습니다.", "emptyMine": "아직 공유한 프로젝트가 없습니다.", "visibilityUnlisted": "목록에 없음", @@ -1751,8 +1751,8 @@ }, "env": { "tokenTitle": "Share.GeoLibre API 토큰", - "tokenDescription": "프로젝트 > 공유는 이 토큰을 사용하여 지도를 share.geolibre.app에 업로드합니다. share.geolibre.app/settings를 열고 설정 > API 토큰에서 토큰을 생성한 다음 아래 필드에 붙여넣으세요.", - "tokenStorageNote": "이 기기에 로컬로 저장되며 업로드 인증을 위해 share.geolibre.app에만 전송됩니다. 웹 빌드에서는 다른 사이트 데이터와 동일한 브라우저 저장소를 공유하므로, 기기가 침해된 경우 share.geolibre.app에서 토큰을 폐기하세요.", + "tokenDescription": "프로젝트 > 공유는 이 토큰을 사용하여 지도를 {{shareHost}}에 업로드합니다. {{shareHost}}/settings를 열고 설정 > API 토큰에서 토큰을 생성한 다음 아래 필드에 붙여넣으세요.", + "tokenStorageNote": "이 기기에 로컬로 저장되며 업로드 인증을 위해 {{shareHost}}에만 전송됩니다. 웹 빌드에서는 다른 사이트 데이터와 동일한 브라우저 저장소를 공유하므로, 기기가 침해된 경우 {{shareHost}}에서 토큰을 폐기하세요.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion 토큰", "cesiumTokenDescription": "3D 지구본(분할 화면 패널)은 접근 토큰이 필요한 Cesium Ion 세계 영상과 지형을 사용합니다. ion.cesium.com/tokens에서 무료 계정을 만들고 기본 접근 토큰을 복사하여 아래에 붙여넣으세요. 토큰이 없으면 3D 지구본 전환 버튼이 숨겨집니다.", diff --git a/apps/geolibre-desktop/src/i18n/locales/nl.json b/apps/geolibre-desktop/src/i18n/locales/nl.json index a50e619fc..3aeafa83b 100644 --- a/apps/geolibre-desktop/src/i18n/locales/nl.json +++ b/apps/geolibre-desktop/src/i18n/locales/nl.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "Project delen", - "description": "Upload het huidige project naar share.geolibre.app en ontvang een deelbare link.", - "setupIntro": "Koppel uw share.geolibre.app-account voordat u uploadt.", + "description": "Upload het huidige project naar {{shareHost}} en ontvang een deelbare link.", + "setupIntro": "Koppel uw {{shareHost}}-account voordat u uploadt.", "step1Title": "1. API-token ophalen", - "step1Description": "Meld u aan bij share.geolibre.app en maak vervolgens een token aan onder Instellingen → API-tokens.", + "step1Description": "Meld u aan bij {{shareHost}} en maak vervolgens een token aan onder Instellingen → API-tokens.", "getToken": "API-token ophalen", "step2Title": "2. Token toevoegen aan GeoLibre", "step2Description": "Plak het token in Instellingen → Omgevingsvariabelen op dit apparaat.", @@ -1171,12 +1171,12 @@ "shareButton": "Delen", "sharing": "Bezig met delen…", "errorFallback": "Kan het project niet delen.", - "usernameRequired": "Stel een gebruikersnaam in voor uw share.geolibre.app-account voordat u deelt. Open uw accountinstellingen om er een te kiezen en probeer het opnieuw.", + "usernameRequired": "Stel een gebruikersnaam in voor uw {{shareHost}}-account voordat u deelt. Open uw accountinstellingen om er een te kiezen en probeer het opnieuw.", "openAccountSettings": "Accountinstellingen openen" }, "gallery": { "title": "Projectgalerij", - "description": "Blader door openbare projecten die zijn gedeeld op share.geolibre.app en open er een in GeoLibre.", + "description": "Blader door openbare projecten die zijn gedeeld op {{shareHost}} en open er een in GeoLibre.", "searchPlaceholder": "Filter geladen projecten op titel, auteur of label", "loading": "Projecten laden…", "loadingMore": "Meer laden…", @@ -1185,11 +1185,11 @@ "noMatches": "Geen geladen projecten komen overeen met uw filter.", "errorFallback": "Kan de projectgalerij niet laden.", "errorTimeout": "Time-out bij het laden van de projectgalerij. Probeer het opnieuw.", - "errorNetwork": "Kan share.geolibre.app niet bereiken. Controleer uw internetverbinding.", + "errorNetwork": "Kan {{shareHost}} niet bereiken. Controleer uw internetverbinding.", "errorHttp": "Kan de galerij niet laden (HTTP {{status}}).", "errorInvalidResponse": "Kan de galerij niet laden (ongeldig serverantwoord).", - "errorUnauthorized": "Uw share.geolibre.app API-token is ongeldig of verlopen. Werk het bij in Instellingen.", - "errorUsernameRequired": "Stel een gebruikersnaam in voor uw share.geolibre.app-account voordat u uw projecten laadt.", + "errorUnauthorized": "Uw {{shareHost}} API-token is ongeldig of verlopen. Werk het bij in Instellingen.", + "errorUsernameRequired": "Stel een gebruikersnaam in voor uw {{shareHost}}-account voordat u uw projecten laadt.", "retry": "Opnieuw proberen", "open": "Openen", "openCopy": "Een kopie openen", @@ -1206,7 +1206,7 @@ "scopeFeatured": "Uitgelicht", "scopeAll": "Alle projecten", "scopeMine": "Mijn projecten", - "signedOutHint": "Voeg een share.geolibre.app API-token toe in Instellingen om uw eigen niet-vermelde en privéprojecten te zien.", + "signedOutHint": "Voeg een {{shareHost}} API-token toe in Instellingen om uw eigen niet-vermelde en privéprojecten te zien.", "emptyFeatured": "Nog geen uitgelichte projecten.", "emptyMine": "U heeft nog geen projecten gedeeld.", "visibilityUnlisted": "Niet vermeld", @@ -1791,8 +1791,8 @@ }, "env": { "tokenTitle": "Share.GeoLibre API-token", - "tokenDescription": "Project > Delen gebruikt dit token om uw kaarten te uploaden naar share.geolibre.app. Open share.geolibre.app/settings, maak een token aan onder Instellingen > API-tokens en plak het vervolgens in het onderstaande veld.", - "tokenStorageNote": "Lokaal opgeslagen op dit apparaat en alleen verzonden naar share.geolibre.app om uploads te verifiëren. In de webversie wordt dezelfde browseropslag gebruikt als voor andere sitegegevens; trek het token daarom in op share.geolibre.app als uw machine gecompromitteerd is.", + "tokenDescription": "Project > Delen gebruikt dit token om uw kaarten te uploaden naar {{shareHost}}. Open {{shareHost}}/settings, maak een token aan onder Instellingen > API-tokens en plak het vervolgens in het onderstaande veld.", + "tokenStorageNote": "Lokaal opgeslagen op dit apparaat en alleen verzonden naar {{shareHost}} om uploads te verifiëren. In de webversie wordt dezelfde browseropslag gebruikt als voor andere sitegegevens; trek het token daarom in op {{shareHost}} als uw machine gecompromitteerd is.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion-token", "cesiumTokenDescription": "De 3D-globe (een gesplitst weergavepaneel) gebruikt wereldbeelden en terrein van Cesium Ion, waarvoor een toegangstoken nodig is. Maak een gratis account aan op ion.cesium.com/tokens, kopieer uw standaard toegangstoken en plak het hieronder. Zonder token is de schakelaar voor de 3D-globe verborgen.", diff --git a/apps/geolibre-desktop/src/i18n/locales/pt.json b/apps/geolibre-desktop/src/i18n/locales/pt.json index eca29d25c..3430fc0e2 100644 --- a/apps/geolibre-desktop/src/i18n/locales/pt.json +++ b/apps/geolibre-desktop/src/i18n/locales/pt.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "Compartilhar projeto", - "description": "Envie o projeto atual para share.geolibre.app e obtenha um link compartilhável.", - "setupIntro": "Conecte sua conta share.geolibre.app antes de enviar.", + "description": "Envie o projeto atual para {{shareHost}} e obtenha um link compartilhável.", + "setupIntro": "Conecte sua conta {{shareHost}} antes de enviar.", "step1Title": "1. Obtenha um token de API", - "step1Description": "Faça login em share.geolibre.app e, em seguida, crie um token em Configurações → Tokens de API.", + "step1Description": "Faça login em {{shareHost}} e, em seguida, crie um token em Configurações → Tokens de API.", "getToken": "Obter token de API", "step2Title": "2. Adicione o token ao GeoLibre", "step2Description": "Cole o token em Configurações → Variáveis de ambiente neste dispositivo.", @@ -1171,12 +1171,12 @@ "shareButton": "Compartilhar", "sharing": "Compartilhando…", "errorFallback": "Não foi possível compartilhar o projeto.", - "usernameRequired": "Defina um nome de usuário na sua conta share.geolibre.app antes de compartilhar. Abra as configurações da sua conta para escolher um e tente novamente.", + "usernameRequired": "Defina um nome de usuário na sua conta {{shareHost}} antes de compartilhar. Abra as configurações da sua conta para escolher um e tente novamente.", "openAccountSettings": "Abrir configurações da conta" }, "gallery": { "title": "Galeria de projetos", - "description": "Explore projetos públicos compartilhados em share.geolibre.app e abra um no GeoLibre.", + "description": "Explore projetos públicos compartilhados em {{shareHost}} e abra um no GeoLibre.", "searchPlaceholder": "Filtrar projetos carregados por título, autor ou tag", "loading": "Carregando projetos…", "loadingMore": "Carregando mais…", @@ -1185,11 +1185,11 @@ "noMatches": "Nenhum projeto carregado corresponde ao seu filtro.", "errorFallback": "Não foi possível carregar a galeria de projetos.", "errorTimeout": "A galeria de projetos expirou. Tente novamente.", - "errorNetwork": "Não foi possível acessar share.geolibre.app. Verifique sua conexão com a internet.", + "errorNetwork": "Não foi possível acessar {{shareHost}}. Verifique sua conexão com a internet.", "errorHttp": "Não foi possível carregar a galeria (HTTP {{status}}).", "errorInvalidResponse": "Não foi possível carregar a galeria (resposta inválida do servidor).", - "errorUnauthorized": "Seu token de API do share.geolibre.app é inválido ou expirou. Atualize-o em Configurações.", - "errorUsernameRequired": "Defina um nome de usuário na sua conta share.geolibre.app antes de carregar seus projetos.", + "errorUnauthorized": "Seu token de API do {{shareHost}} é inválido ou expirou. Atualize-o em Configurações.", + "errorUsernameRequired": "Defina um nome de usuário na sua conta {{shareHost}} antes de carregar seus projetos.", "retry": "Tentar novamente", "open": "Abrir", "openCopy": "Abrir uma cópia", @@ -1206,7 +1206,7 @@ "scopeFeatured": "Em destaque", "scopeAll": "Todos os projetos", "scopeMine": "Meus projetos", - "signedOutHint": "Adicione um token de API do share.geolibre.app em Configurações para ver seus próprios projetos não listados e privados.", + "signedOutHint": "Adicione um token de API do {{shareHost}} em Configurações para ver seus próprios projetos não listados e privados.", "emptyFeatured": "Ainda não há projetos em destaque.", "emptyMine": "Você ainda não compartilhou nenhum projeto.", "visibilityUnlisted": "Não listado", @@ -1791,8 +1791,8 @@ }, "env": { "tokenTitle": "Token de API do Share.GeoLibre", - "tokenDescription": "Projeto > Compartilhar usa este token para enviar seus mapas para share.geolibre.app. Abra share.geolibre.app/settings, crie um token em Configurações > Tokens de API e, em seguida, cole-o no campo abaixo.", - "tokenStorageNote": "Armazenado localmente neste dispositivo e enviado apenas para share.geolibre.app para autenticar envios. Na versão web, ele compartilha o mesmo armazenamento do navegador que outros dados do site, então revogue-o em share.geolibre.app se sua máquina for comprometida.", + "tokenDescription": "Projeto > Compartilhar usa este token para enviar seus mapas para {{shareHost}}. Abra {{shareHost}}/settings, crie um token em Configurações > Tokens de API e, em seguida, cole-o no campo abaixo.", + "tokenStorageNote": "Armazenado localmente neste dispositivo e enviado apenas para {{shareHost}} para autenticar envios. Na versão web, ele compartilha o mesmo armazenamento do navegador que outros dados do site, então revogue-o em {{shareHost}} se sua máquina for comprometida.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token do Cesium Ion", "cesiumTokenDescription": "O globo 3D (um painel de visualização dividida) usa imagens e terreno mundiais do Cesium Ion, que precisam de um token de acesso. Crie uma conta gratuita em ion.cesium.com/tokens, copie seu token de acesso padrão e cole-o abaixo. Sem um token, o alternador do globo 3D fica oculto.", diff --git a/apps/geolibre-desktop/src/i18n/locales/ru.json b/apps/geolibre-desktop/src/i18n/locales/ru.json index 915cae89f..f8f650aae 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ru.json +++ b/apps/geolibre-desktop/src/i18n/locales/ru.json @@ -1219,10 +1219,10 @@ }, "share": { "title": "Поделиться проектом", - "description": "Загрузите текущий проект на share.geolibre.app и получите ссылку для общего доступа.", - "setupIntro": "Подключите свою учётную запись share.geolibre.app перед загрузкой.", + "description": "Загрузите текущий проект на {{shareHost}} и получите ссылку для общего доступа.", + "setupIntro": "Подключите свою учётную запись {{shareHost}} перед загрузкой.", "step1Title": "1. Получите API-токен", - "step1Description": "Войдите на share.geolibre.app, затем создайте токен в разделе Настройки → API-токены.", + "step1Description": "Войдите на {{shareHost}}, затем создайте токен в разделе Настройки → API-токены.", "getToken": "Получить API-токен", "step2Title": "2. Добавьте токен в GeoLibre", "step2Description": "Вставьте токен в Настройки → Переменные среды на этом устройстве.", @@ -1241,12 +1241,12 @@ "shareButton": "Поделиться", "sharing": "Публикация…", "errorFallback": "Не удалось поделиться проектом.", - "usernameRequired": "Задайте имя пользователя в учётной записи share.geolibre.app перед публикацией. Откройте настройки учётной записи, чтобы выбрать имя, затем повторите попытку.", + "usernameRequired": "Задайте имя пользователя в учётной записи {{shareHost}} перед публикацией. Откройте настройки учётной записи, чтобы выбрать имя, затем повторите попытку.", "openAccountSettings": "Открыть настройки учётной записи" }, "gallery": { "title": "Галерея проектов", - "description": "Просматривайте публичные проекты, опубликованные на share.geolibre.app, и открывайте их в GeoLibre.", + "description": "Просматривайте публичные проекты, опубликованные на {{shareHost}}, и открывайте их в GeoLibre.", "searchPlaceholder": "Отфильтровать загруженные проекты по названию, автору или тегу", "loading": "Загрузка проектов…", "loadingMore": "Загрузка ещё…", @@ -1255,11 +1255,11 @@ "noMatches": "Загруженные проекты не соответствуют фильтру.", "errorFallback": "Не удалось загрузить галерею проектов.", "errorTimeout": "Истекло время ожидания галереи проектов. Повторите попытку.", - "errorNetwork": "Не удалось подключиться к share.geolibre.app. Проверьте подключение к интернету.", + "errorNetwork": "Не удалось подключиться к {{shareHost}}. Проверьте подключение к интернету.", "errorHttp": "Не удалось загрузить галерею (HTTP {{status}}).", "errorInvalidResponse": "Не удалось загрузить галерею (некорректный ответ сервера).", - "errorUnauthorized": "Ваш API-токен share.geolibre.app недействителен или просрочен. Обновите его в Настройках.", - "errorUsernameRequired": "Задайте имя пользователя в учётной записи share.geolibre.app перед загрузкой ваших проектов.", + "errorUnauthorized": "Ваш API-токен {{shareHost}} недействителен или просрочен. Обновите его в Настройках.", + "errorUsernameRequired": "Задайте имя пользователя в учётной записи {{shareHost}} перед загрузкой ваших проектов.", "retry": "Повторить", "open": "Открыть", "openCopy": "Открыть копию", @@ -1278,7 +1278,7 @@ "scopeFeatured": "Рекомендуемые", "scopeAll": "Все проекты", "scopeMine": "Мои проекты", - "signedOutHint": "Добавьте API-токен share.geolibre.app в Настройках, чтобы увидеть свои непубличные и приватные проекты.", + "signedOutHint": "Добавьте API-токен {{shareHost}} в Настройках, чтобы увидеть свои непубличные и приватные проекты.", "emptyFeatured": "Пока нет рекомендуемых проектов.", "emptyMine": "Вы ещё не опубликовали ни одного проекта.", "visibilityUnlisted": "Не в списке", @@ -1871,8 +1871,8 @@ }, "env": { "tokenTitle": "API-токен Share.GeoLibre", - "tokenDescription": "Раздел Проект > Поделиться использует этот токен для загрузки ваших карт на share.geolibre.app. Откройте share.geolibre.app/settings, создайте токен в разделе Настройки > API-токены, затем вставьте его в поле ниже.", - "tokenStorageNote": "Хранится локально на этом устройстве и передаётся только на share.geolibre.app для аутентификации загрузок. В веб-сборке используется то же хранилище браузера, что и для других данных сайта; отзовите токен на share.geolibre.app, если ваше устройство скомпрометировано.", + "tokenDescription": "Раздел Проект > Поделиться использует этот токен для загрузки ваших карт на {{shareHost}}. Откройте {{shareHost}}/settings, создайте токен в разделе Настройки > API-токены, затем вставьте его в поле ниже.", + "tokenStorageNote": "Хранится локально на этом устройстве и передаётся только на {{shareHost}} для аутентификации загрузок. В веб-сборке используется то же хранилище браузера, что и для других данных сайта; отзовите токен на {{shareHost}}, если ваше устройство скомпрометировано.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Токен Cesium Ion", "cesiumTokenDescription": "3D-глобус (панель раздельного вида) использует мировые снимки и рельеф Cesium Ion, для которых требуется токен доступа. Создайте бесплатную учётную запись на ion.cesium.com/tokens, скопируйте свой токен доступа по умолчанию и вставьте его ниже. Без токена переключатель 3D-глобуса скрыт.", diff --git a/apps/geolibre-desktop/src/i18n/locales/tr.json b/apps/geolibre-desktop/src/i18n/locales/tr.json index d4b63a8ff..9d3c9a461 100644 --- a/apps/geolibre-desktop/src/i18n/locales/tr.json +++ b/apps/geolibre-desktop/src/i18n/locales/tr.json @@ -1149,10 +1149,10 @@ }, "share": { "title": "Projeyi paylaş", - "description": "Mevcut projeyi paylaşmak için share.geolibre.app adresine yükleyin ve paylaşılabilir bir bağlantı alın.", - "setupIntro": "Yüklemeden önce share.geolibre.app hesabınızı bağlayın.", + "description": "Mevcut projeyi paylaşmak için {{shareHost}} adresine yükleyin ve paylaşılabilir bir bağlantı alın.", + "setupIntro": "Yüklemeden önce {{shareHost}} hesabınızı bağlayın.", "step1Title": "1. Bir API belirteci alın", - "step1Description": "share.geolibre.app adresinde oturum açın, ardından Ayarlar → API belirteçleri altında bir belirteç oluşturun.", + "step1Description": "{{shareHost}} adresinde oturum açın, ardından Ayarlar → API belirteçleri altında bir belirteç oluşturun.", "getToken": "API belirteci al", "step2Title": "2. Belirteci GeoLibre'ye ekleyin", "step2Description": "Belirteci bu cihazda Ayarlar → Ortam Değişkenleri kısmına yapıştırın.", @@ -1171,12 +1171,12 @@ "shareButton": "Paylaş", "sharing": "Paylaşılıyor…", "errorFallback": "Proje paylaşılamadı.", - "usernameRequired": "Paylaşmadan önce share.geolibre.app hesabınızda bir kullanıcı adı belirleyin. Bir tane seçmek için hesap ayarlarınızı açın, ardından tekrar deneyin.", + "usernameRequired": "Paylaşmadan önce {{shareHost}} hesabınızda bir kullanıcı adı belirleyin. Bir tane seçmek için hesap ayarlarınızı açın, ardından tekrar deneyin.", "openAccountSettings": "Hesap ayarlarını aç" }, "gallery": { "title": "Proje galerisi", - "description": "share.geolibre.app üzerinde paylaşılan genel projelere göz atın ve birini GeoLibre'de açın.", + "description": "{{shareHost}} üzerinde paylaşılan genel projelere göz atın ve birini GeoLibre'de açın.", "searchPlaceholder": "Yüklenen projeleri başlığa, yazara veya etikete göre filtrele", "loading": "Projeler yükleniyor…", "loadingMore": "Daha fazla yükleniyor…", @@ -1185,11 +1185,11 @@ "noMatches": "Filtrenizle eşleşen yüklenmiş proje yok.", "errorFallback": "Proje galerisi yüklenemedi.", "errorTimeout": "Proje galerisinde zaman aşımı oluştu. Lütfen tekrar deneyin.", - "errorNetwork": "share.geolibre.app adresine ulaşılamadı. İnternet bağlantınızı kontrol edin.", + "errorNetwork": "{{shareHost}} adresine ulaşılamadı. İnternet bağlantınızı kontrol edin.", "errorHttp": "Galeri yüklenemedi (HTTP {{status}}).", "errorInvalidResponse": "Galeri yüklenemedi (geçersiz sunucu yanıtı).", - "errorUnauthorized": "share.geolibre.app API belirteciniz geçersiz veya süresi dolmuş. Ayarlar'da güncelleyin.", - "errorUsernameRequired": "Projelerinizi yüklemeden önce share.geolibre.app hesabınızda bir kullanıcı adı belirleyin.", + "errorUnauthorized": "{{shareHost}} API belirteciniz geçersiz veya süresi dolmuş. Ayarlar'da güncelleyin.", + "errorUsernameRequired": "Projelerinizi yüklemeden önce {{shareHost}} hesabınızda bir kullanıcı adı belirleyin.", "retry": "Yeniden dene", "open": "Aç", "openCopy": "Bir kopyasını aç", @@ -1206,7 +1206,7 @@ "scopeFeatured": "Öne çıkanlar", "scopeAll": "Tüm projeler", "scopeMine": "Projelerim", - "signedOutHint": "Listelenmemiş ve özel projelerinizi görmek için Ayarlar'a bir share.geolibre.app API belirteci ekleyin.", + "signedOutHint": "Listelenmemiş ve özel projelerinizi görmek için Ayarlar'a bir {{shareHost}} API belirteci ekleyin.", "emptyFeatured": "Henüz öne çıkan proje yok.", "emptyMine": "Henüz hiçbir proje paylaşmadınız.", "visibilityUnlisted": "Listelenmemiş", @@ -1791,8 +1791,8 @@ }, "env": { "tokenTitle": "Share.GeoLibre API belirteci", - "tokenDescription": "Proje > Paylaş, haritalarınızı share.geolibre.app adresine yüklemek için bu belirteci kullanır. share.geolibre.app/settings adresini açın, Ayarlar > API belirteçleri altında bir belirteç oluşturun, ardından aşağıdaki alana yapıştırın.", - "tokenStorageNote": "Bu cihazda yerel olarak saklanır ve yalnızca yüklemeleri doğrulamak için share.geolibre.app adresine gönderilir. Web derlemesinde diğer site verileriyle aynı tarayıcı deposunu paylaşır; dolayısıyla makinenizin güvenliği ihlal edilirse share.geolibre.app üzerinden iptal edin.", + "tokenDescription": "Proje > Paylaş, haritalarınızı {{shareHost}} adresine yüklemek için bu belirteci kullanır. {{shareHost}}/settings adresini açın, Ayarlar > API belirteçleri altında bir belirteç oluşturun, ardından aşağıdaki alana yapıştırın.", + "tokenStorageNote": "Bu cihazda yerel olarak saklanır ve yalnızca yüklemeleri doğrulamak için {{shareHost}} adresine gönderilir. Web derlemesinde diğer site verileriyle aynı tarayıcı deposunu paylaşır; dolayısıyla makinenizin güvenliği ihlal edilirse {{shareHost}} üzerinden iptal edin.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion belirteci", "cesiumTokenDescription": "3B küre (bölünmüş görünümlü bir bölme) Cesium Ion dünya görüntüleri ve arazisini kullanır; bunlar bir erişim belirteci gerektirir. ion.cesium.com/tokens adresinde ücretsiz bir hesap oluşturun, varsayılan erişim belirtecinizi kopyalayın ve aşağıya yapıştırın. Belirteç olmadan 3B küre anahtarı gizlenir.", diff --git a/apps/geolibre-desktop/src/i18n/locales/zh.json b/apps/geolibre-desktop/src/i18n/locales/zh.json index 85253329b..638976963 100644 --- a/apps/geolibre-desktop/src/i18n/locales/zh.json +++ b/apps/geolibre-desktop/src/i18n/locales/zh.json @@ -1114,10 +1114,10 @@ }, "share": { "title": "共享项目", - "description": "将当前项目上传到 share.geolibre.app 并获取可分享的链接。", - "setupIntro": "在上传前,请先连接您的 share.geolibre.app 账户。", + "description": "将当前项目上传到 {{shareHost}} 并获取可分享的链接。", + "setupIntro": "在上传前,请先连接您的 {{shareHost}} 账户。", "step1Title": "1. 获取 API 令牌", - "step1Description": "登录 share.geolibre.app,然后在“设置 → API 令牌”下创建一个令牌。", + "step1Description": "登录 {{shareHost}},然后在“设置 → API 令牌”下创建一个令牌。", "getToken": "获取 API 令牌", "step2Title": "2. 将令牌添加到 GeoLibre", "step2Description": "将令牌粘贴到本设备上的“设置 → 环境变量”中。", @@ -1136,12 +1136,12 @@ "shareButton": "共享", "sharing": "正在共享…", "errorFallback": "无法共享该项目。", - "usernameRequired": "共享前请在您的 share.geolibre.app 账户上设置用户名。请打开账户设置进行选择,然后重试。", + "usernameRequired": "共享前请在您的 {{shareHost}} 账户上设置用户名。请打开账户设置进行选择,然后重试。", "openAccountSettings": "打开账户设置" }, "gallery": { "title": "项目图库", - "description": "浏览在 share.geolibre.app 上共享的公开项目,并在 GeoLibre 中打开其中一个。", + "description": "浏览在 {{shareHost}} 上共享的公开项目,并在 GeoLibre 中打开其中一个。", "searchPlaceholder": "按标题、作者或标签筛选已加载的项目", "loading": "正在加载项目…", "loadingMore": "正在加载更多…", @@ -1150,11 +1150,11 @@ "noMatches": "没有已加载的项目匹配您的筛选条件。", "errorFallback": "无法加载项目图库。", "errorTimeout": "项目图库加载超时。请重试。", - "errorNetwork": "无法连接到 share.geolibre.app。请检查您的网络连接。", + "errorNetwork": "无法连接到 {{shareHost}}。请检查您的网络连接。", "errorHttp": "无法加载图库(HTTP {{status}})。", "errorInvalidResponse": "无法加载图库(服务器响应无效)。", - "errorUnauthorized": "您的 share.geolibre.app API 令牌无效或已过期。请在设置中更新它。", - "errorUsernameRequired": "在加载您的项目之前,请在您的 share.geolibre.app 账户上设置用户名。", + "errorUnauthorized": "您的 {{shareHost}} API 令牌无效或已过期。请在设置中更新它。", + "errorUsernameRequired": "在加载您的项目之前,请在您的 {{shareHost}} 账户上设置用户名。", "retry": "重试", "open": "打开", "openCopy": "打开副本", @@ -1170,7 +1170,7 @@ "scopeFeatured": "精选", "scopeAll": "全部项目", "scopeMine": "我的项目", - "signedOutHint": "在设置中添加 share.geolibre.app API 令牌,即可查看您自己的不公开列出及私有项目。", + "signedOutHint": "在设置中添加 {{shareHost}} API 令牌,即可查看您自己的不公开列出及私有项目。", "emptyFeatured": "暂无精选项目。", "emptyMine": "您尚未共享任何项目。", "visibilityUnlisted": "不公开列出", @@ -1751,8 +1751,8 @@ }, "env": { "tokenTitle": "Share.GeoLibre API 令牌", - "tokenDescription": "“项目 > 共享”使用此令牌将您的地图上传到 share.geolibre.app。请打开 share.geolibre.app/settings,在“设置 > API 令牌”下创建一个令牌,然后将其粘贴到下方字段中。", - "tokenStorageNote": "仅存储在本设备上,并且仅发送到 share.geolibre.app 用于验证上传。在 Web 版本中,它与其他站点数据共用同一浏览器存储,因此如果您的设备遭到入侵,请在 share.geolibre.app 上撤销该令牌。", + "tokenDescription": "“项目 > 共享”使用此令牌将您的地图上传到 {{shareHost}}。请打开 {{shareHost}}/settings,在“设置 > API 令牌”下创建一个令牌,然后将其粘贴到下方字段中。", + "tokenStorageNote": "仅存储在本设备上,并且仅发送到 {{shareHost}} 用于验证上传。在 Web 版本中,它与其他站点数据共用同一浏览器存储,因此如果您的设备遭到入侵,请在 {{shareHost}} 上撤销该令牌。", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion 令牌", "cesiumTokenDescription": "3D 地球(分屏视图窗格)使用 Cesium Ion 的世界影像和地形数据,需要访问令牌。请在 ion.cesium.com/tokens 创建免费账户,复制您的默认访问令牌,并粘贴到下方。若无令牌,3D 地球切换开关将被隐藏。", diff --git a/apps/geolibre-desktop/src/lib/collab-client.ts b/apps/geolibre-desktop/src/lib/collab-client.ts index cf46ede0b..4568d5484 100644 --- a/apps/geolibre-desktop/src/lib/collab-client.ts +++ b/apps/geolibre-desktop/src/lib/collab-client.ts @@ -6,6 +6,7 @@ // the hook is an inert no-op. The session-create REST call and the WebSocket URL // are both derived from this one base. +import { readDeploymentEnvValue, type EnvRecord } from "./deployment-env"; import type { ClientMessage, ServerMessage } from "./collab-protocol"; import type { CollaborationMode } from "@geolibre/core"; @@ -21,21 +22,35 @@ const RECONNECT_MIN_MS = 500; const RECONNECT_MAX_MS = 10_000; /** - * Resolve the collaboration relay base from the Vite env, returning `null` when - * unset or invalid so callers can keep the feature dark. + * Deployment variable naming the collaboration relay. Settable at build time or, + * on a prebuilt Docker image, with `-e GEOLIBRE_COLLAB_URL=…`. + */ +export const COLLAB_URL_ENV = "VITE_GEOLIBRE_COLLAB_URL"; + +/** + * Resolve the collaboration relay base, returning `null` when unset or invalid + * so callers can keep the feature dark. + * + * Read from the deployment env (the Docker entrypoint's runtime config) before + * the build-time Vite env, so a prebuilt image can be pointed at a self-hosted + * relay without a rebuild. * - * Only `wss://` (or `ws://` on loopback for local `wrangler dev`) is accepted, - * mirroring `resolveShareBaseUrl`: parse the URL and match the hostname exactly + * Only `wss://` (or `ws://` on loopback for a local relay) is accepted, + * mirroring `resolveShareHost`: parse the URL and match the hostname exactly * so a value like `ws://localhost.evil.com` is rejected. * - * @param configured - The raw env value; defaults to `VITE_GEOLIBRE_COLLAB_URL`. + * @param configured - The raw value; read from the env when omitted. + * @param deploymentEnv - Runtime env override, for tests. * @returns The trimmed base URL without a trailing slash, or `null`. */ export function resolveCollabBaseUrl( - configured: unknown = import.meta.env?.VITE_GEOLIBRE_COLLAB_URL, + configured?: unknown, + deploymentEnv?: EnvRecord, ): string | null { - if (typeof configured !== "string" || !configured.trim()) return null; - const trimmed = configured.trim().replace(/\/+$/, ""); + const value = + configured !== undefined ? configured : readDeploymentEnvValue(COLLAB_URL_ENV, deploymentEnv); + if (typeof value !== "string" || !value.trim()) return null; + const trimmed = value.trim().replace(/\/+$/, ""); try { const url = new URL(trimmed); if ( diff --git a/apps/geolibre-desktop/src/lib/deployment-env.ts b/apps/geolibre-desktop/src/lib/deployment-env.ts new file mode 100644 index 000000000..7728b4f64 --- /dev/null +++ b/apps/geolibre-desktop/src/lib/deployment-env.ts @@ -0,0 +1,46 @@ +// Reads the deployment env the Docker entrypoint injects at container startup. +// +// `docker/entrypoint.sh` rewrites `geolibre-runtime-config.js` on every boot, +// setting `window.__GEOLIBRE_DEPLOYMENT_ENV__` to a JSON object of +// `VITE_*`-keyed values. `index.html` pulls that script in before the bundle, so +// the values are on `window` by the time any module reads them. This is how an +// operator repoints a *prebuilt* image with `-e GEOLIBRE_…=…` instead of +// rebuilding it. +// +// Precedence for anything configurable both ways is deployment env first, then +// the build-time Vite env — the deployment is the more specific statement, and +// the published image is built with the defaults. `readDeploymentAssistantEnv` +// and `readEmbedOrigins` established that order; this module is the shared +// implementation for the settings that are a single URL. + +/** A `VITE_*`-keyed env record, from either the build or the deployment. */ +export type EnvRecord = Record | undefined; + +/** The deployment env on `window`, or undefined outside a browser. */ +export function readDeploymentEnv(): EnvRecord { + if (typeof window === "undefined") return undefined; + return (window as unknown as { __GEOLIBRE_DEPLOYMENT_ENV__?: EnvRecord }) + .__GEOLIBRE_DEPLOYMENT_ENV__; +} + +/** + * Read one `VITE_*` variable, preferring the deployment env over the build env. + * + * @param key - The variable name, e.g. `VITE_GEOLIBRE_SHARE_URL`. + * @param deploymentEnv - Runtime env; defaults to the value on `window`. + * @param buildEnv - Build-time env; defaults to `import.meta.env`. + * @returns The first non-blank value found, or undefined when neither sets it. + */ +export function readDeploymentEnvValue( + key: string, + deploymentEnv: EnvRecord = readDeploymentEnv(), + buildEnv: EnvRecord = import.meta.env as EnvRecord, +): string | undefined { + for (const source of [deploymentEnv, buildEnv]) { + const value = source?.[key]; + // Treat a blank string as unset: the entrypoint omits a key it has no value + // for, but a hand-written config or a `-e VAR=` can still produce "". + if (typeof value === "string" && value.trim()) return value; + } + return undefined; +} diff --git a/apps/geolibre-desktop/src/lib/share-fetch.ts b/apps/geolibre-desktop/src/lib/share-fetch.ts index 59c823fac..b03cfc951 100644 --- a/apps/geolibre-desktop/src/lib/share-fetch.ts +++ b/apps/geolibre-desktop/src/lib/share-fetch.ts @@ -54,15 +54,23 @@ function requestHost(input: RequestInfo | URL): string | null { * * The host is resolved from {@link resolveShareBaseUrl} (the configured or * production share URL) at install time, so a `VITE_GEOLIBRE_SHARE_URL` override - * is honored. + * is honored. When it resolves to null — sharing disabled, or a configured host + * that was rejected — no override is installed and every request keeps the + * browser `fetch`. + * + * Note that a self-hosted host still has to be listed in the Tauri `http:default` + * capability scope to be reachable from the desktop build; the web build (where + * self-hosting is configured) has no such constraint. * * Loaded lazily and only in the desktop build so the web/embedded bundles never * pull in `@tauri-apps/plugin-http`. */ export async function installNativeShareFetch(): Promise { + const baseUrl = resolveShareBaseUrl(); + if (!baseUrl) return; let shareHost: string | null; try { - shareHost = new URL(resolveShareBaseUrl()).host; + shareHost = new URL(baseUrl).host; } catch { shareHost = null; } diff --git a/apps/geolibre-desktop/src/lib/share-gallery.ts b/apps/geolibre-desktop/src/lib/share-gallery.ts index 3af80c881..781c9edae 100644 --- a/apps/geolibre-desktop/src/lib/share-gallery.ts +++ b/apps/geolibre-desktop/src/lib/share-gallery.ts @@ -21,7 +21,9 @@ export type GalleryErrorCode = | "http" | "invalid-response" | "unauthorized" - | "username-required"; + | "username-required" + /** The deployment disabled sharing, or named a share host that was rejected. */ + | "not-configured"; /** Error thrown by the gallery fetchers, carrying a translatable {@link GalleryErrorCode}. */ export class GalleryError extends Error { @@ -40,7 +42,20 @@ export class GalleryError extends Error { } } -/** A public project as returned by share.geolibre.app's listing endpoint. */ +/** + * The share host for a gallery request, with a trailing slash stripped. + * + * @throws {GalleryError} `not-configured` when the deployment disabled sharing or + * named a host that was rejected — the gallery must surface that rather than + * quietly listing the hosted service's projects instead. + */ +function requireShareBase(override?: string): string { + const base = override ?? resolveShareBaseUrl(); + if (!base) throw new GalleryError("not-configured"); + return base.replace(/\/+$/, ""); +} + +/** A project as returned by the share host's listing endpoint. */ export interface SharedProject { id: string; username: string; @@ -182,7 +197,7 @@ function normalizeProject(raw: RawSharedProject, base: string): SharedProject | export async function fetchSharedProjects( options: FetchSharedProjectsOptions = {}, ): Promise { - const base = (options.baseUrl ?? resolveShareBaseUrl()).replace(/\/+$/, ""); + const base = requireShareBase(options.baseUrl); // See share-fetch.ts: on desktop this routes the share host through Tauri's // native HTTP client so the gallery listing isn't blocked by WebView CORS. const fetchImpl = options.fetchImpl ?? getShareFetch(); @@ -310,7 +325,7 @@ export function shareAuthorizedFetch( * caller-initiated abort propagates as `AbortError`. */ export async function fetchMyProjects(options: FetchMyProjectsOptions): Promise { - const base = (options.baseUrl ?? resolveShareBaseUrl()).replace(/\/+$/, ""); + const base = requireShareBase(options.baseUrl); // One auth path for both production and tests: the injected fetch (or the // share fetch, which the desktop build routes natively to bypass CORS — see // share-fetch.ts) flows through the same same-origin token gating. diff --git a/apps/geolibre-desktop/src/lib/share-geolibre.ts b/apps/geolibre-desktop/src/lib/share-geolibre.ts index d0f1b0b9d..e37da81ce 100644 --- a/apps/geolibre-desktop/src/lib/share-geolibre.ts +++ b/apps/geolibre-desktop/src/lib/share-geolibre.ts @@ -1,8 +1,13 @@ -// Uploads a serialized GeoLibre project to share.geolibre.app via its +// Uploads a serialized GeoLibre project to a share server via its // `POST /api/projects` endpoint, authenticated with a personal API token the -// user created on the website. Used by the Project > Share action. +// user created on that server. Used by the Project > Share action. +// +// The host is share.geolibre.app unless the deployment names another one; see +// `resolveShareHost` for the precedence and for why a rejected value disables +// sharing instead of falling back to the hosted service. import { DEFAULT_PROJECT_NAME } from "@geolibre/core"; +import { readDeploymentEnvValue, type EnvRecord } from "./deployment-env"; import { getShareFetch } from "./share-fetch"; export type ShareVisibility = "public" | "unlisted" | "private"; @@ -90,33 +95,110 @@ export function isShareableTitle(title: string): boolean { } /** - * Resolve the share host from the Vite env, falling back to production. The - * `configured` value is read from the env by default but can be passed directly - * in tests. + * Deployment variable naming the share host. Settable at build time or, on a + * prebuilt Docker image, with `-e GEOLIBRE_SHARE_URL=…` (the entrypoint copies it + * into the runtime config under this name). */ -export function resolveShareBaseUrl( - configured: unknown = import.meta.env?.VITE_GEOLIBRE_SHARE_URL, -): string { - if (typeof configured === "string" && configured.trim()) { - const trimmed = configured.trim().replace(/\/+$/, ""); - // Only accept HTTPS (or HTTP on loopback for local dev) so a misconfigured - // env var can't send the Bearer token over a plaintext connection. Parse the - // URL and match the hostname exactly: a prefix check like - // `startsWith("http://localhost")` would also accept hosts such as - // `http://localhost.evil.com`. - try { - const url = new URL(trimmed); - if ( - url.protocol === "https:" || - (url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")) - ) { - return trimmed; - } - } catch { - // Invalid URL; fall through to the production default. +export const SHARE_URL_ENV = "VITE_GEOLIBRE_SHARE_URL"; + +/** Value of {@link SHARE_URL_ENV} that turns project sharing off entirely. */ +export const SHARE_DISABLED_VALUE = "off"; + +/** + * Why the share host is (or is not) what it is. + * + * - `default` — nothing configured, so the hosted service applies. + * - `configured` — a deployment named a host and it was accepted. + * - `disabled` — the deployment set {@link SHARE_DISABLED_VALUE}. + * - `invalid` — a deployment named a host and it was **rejected**. Sharing is + * unavailable; it deliberately does not degrade to the hosted service. + */ +export type ShareHostStatus = "default" | "configured" | "disabled" | "invalid"; + +export interface ShareHost { + status: ShareHostStatus; + /** Host to talk to, or null when sharing is unavailable. */ + baseUrl: string | null; + /** The configured value, kept for the `invalid` message. Null when unset. */ + configured: string | null; +} + +/** + * Whether a URL is safe to send a Bearer token to: HTTPS anywhere, or HTTP on + * loopback for local development. + * + * The hostname is matched exactly rather than by prefix — `startsWith( + * "http://localhost")` would also accept `http://localhost.evil.com`. A + * self-hosted server on a private network therefore needs TLS; see + * `docs/getting-started.md`. + */ +function isSafeShareUrl(url: URL): boolean { + if (url.protocol === "https:") return true; + return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1"); +} + +/** + * Resolve the share host, preferring the deployment env over the build env. + * + * A configured-but-rejected value resolves to `invalid` with a null `baseUrl` + * rather than falling back to {@link DEFAULT_SHARE_BASE_URL}. That fallback used + * to mean a self-hosted deployment with a typo'd or plaintext host silently + * uploaded its users' projects to the public hosted service — the one outcome a + * private deployment must never produce. The hosted default now applies only + * when nothing is configured at all. + * + * @param configured - The raw value; read from the env when omitted. + * @param deploymentEnv - Runtime env override, for tests. + * @returns The resolved host and why. + */ +export function resolveShareHost(configured?: unknown, deploymentEnv?: EnvRecord): ShareHost { + const raw = + configured !== undefined ? configured : readDeploymentEnvValue(SHARE_URL_ENV, deploymentEnv); + if (typeof raw !== "string" || !raw.trim()) { + return { status: "default", baseUrl: DEFAULT_SHARE_BASE_URL, configured: null }; + } + const trimmed = raw.trim().replace(/\/+$/, ""); + if (trimmed.toLowerCase() === SHARE_DISABLED_VALUE) { + return { status: "disabled", baseUrl: null, configured: trimmed }; + } + try { + const url = new URL(trimmed); + if (isSafeShareUrl(url)) { + return { status: "configured", baseUrl: trimmed, configured: trimmed }; } + } catch { + // Unparseable; falls through to `invalid` below. + } + return { status: "invalid", baseUrl: null, configured: trimmed }; +} + +/** + * The share host to talk to, or null when sharing is unavailable. + * + * Callers that need to explain *why* it is unavailable should use + * {@link resolveShareHost} instead. + */ +export function resolveShareBaseUrl(configured?: unknown): string | null { + return resolveShareHost(configured).baseUrl; +} + +/** + * Hostname to name in UI copy — the `{{shareHost}}` interpolation in the message + * catalogues ("Sign in to {{shareHost}}") — so a self-hosted deployment reads its + * own host instead of share.geolibre.app. + * + * Falls back to the hosted default's hostname when no usable host is configured. + * The copy that can still render in that state (the Settings token field) + * describes what the token is *for*, and the hosted service is its documented + * default. + */ +export function shareHostLabel(): string { + const base = resolveShareBaseUrl() ?? DEFAULT_SHARE_BASE_URL; + try { + return new URL(base).host; + } catch { + return new URL(DEFAULT_SHARE_BASE_URL).host; } - return DEFAULT_SHARE_BASE_URL; } interface ShareProjectResponse { @@ -134,10 +216,18 @@ export async function uploadProjectToShare( ): Promise { const token = options.token.trim(); if (!token) { - throw new Error("Add a share.geolibre.app API token in Settings before sharing."); + throw new Error("Add a share API token in Settings before sharing."); } - const base = (options.baseUrl ?? resolveShareBaseUrl()).replace(/\/+$/, ""); + // Null means the deployment disabled sharing or named a host that was + // rejected. The dialog gates on the same state, so reaching here is a bug + // rather than something a user can do — fail loudly instead of falling back to + // the hosted service with the user's project. + const resolved = options.baseUrl ?? resolveShareBaseUrl(); + if (!resolved) { + throw new Error("No share server is configured for this deployment."); + } + const base = resolved.replace(/\/+$/, ""); // Defaults to the share fetch, which the desktop build routes through Tauri's // native HTTP client to bypass WebView CORS (see share-fetch.ts). const fetchImpl = options.fetchImpl ?? getShareFetch(); diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index eab33c831..422e97a81 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -165,12 +165,64 @@ for entry in os.environ.get("GEOLIBRE_EMBED_ORIGINS", "").replace(",", " ").spli if origins: deployment["VITE_GEOLIBRE_EMBED_ORIGINS"] = ",".join(origins) + +def service_url(name, value, schemes, loopback_schemes, loopback_hosts): + """Validate a self-hosted service URL, or exit with an explanation. + + Both of these carry a Bearer token, so a plaintext scheme is only allowed on + loopback (development). The app applies the same rule and *refuses* a value it + rejects rather than falling back to the public hosted service — so a value + that reaches the app unvalidated becomes a silently disabled feature. Failing + the boot instead puts the error where an operator will actually see it. + """ + parsed = urlsplit(value) + if parsed.scheme in loopback_schemes and parsed.hostname in loopback_hosts: + return value + if parsed.scheme not in schemes or not parsed.netloc: + raise SystemExit( + f"ERROR: {name} must be a {schemes[0]}:// URL " + f"(or {loopback_schemes[0]}:// on {'/'.join(loopback_hosts)}), not {value!r}." + ) + if parsed.username or parsed.password: + raise SystemExit(f"ERROR: {name} must not embed credentials.") + return value + + +# Project sharing server. Unset means the public hosted service; "off" removes +# Share and the Project Gallery from the UI entirely. +share_url = os.environ.get("GEOLIBRE_SHARE_URL", "").strip() +if share_url: + if share_url.lower() == "off": + deployment["VITE_GEOLIBRE_SHARE_URL"] = "off" + else: + deployment["VITE_GEOLIBRE_SHARE_URL"] = service_url( + "GEOLIBRE_SHARE_URL", share_url, ("https",), ("http",), ("localhost", "127.0.0.1") + ) + +# Live collaboration relay. Unset leaves collaboration dark. +collab_url = os.environ.get("GEOLIBRE_COLLAB_URL", "").strip() +if collab_url: + deployment["VITE_GEOLIBRE_COLLAB_URL"] = service_url( + "GEOLIBRE_COLLAB_URL", collab_url, ("wss",), ("ws",), ("localhost", "127.0.0.1", "::1") + ) + with open("/usr/share/nginx/html/geolibre-runtime-config.js", "w") as output: output.write("window.__GEOLIBRE_DEPLOYMENT_ENV__ = ") json.dump(deployment, output, separators=(",", ":")) output.write(";\n") ' +if [ -n "${GEOLIBRE_SHARE_URL:-}" ]; then + case "$GEOLIBRE_SHARE_URL" in + off | OFF | Off) echo "Project sharing disabled (GEOLIBRE_SHARE_URL=off)." ;; + *) echo "Project sharing server: $GEOLIBRE_SHARE_URL" ;; + esac +fi + +if [ -n "${GEOLIBRE_COLLAB_URL:-}" ]; then + echo "Collaboration relay: $GEOLIBRE_COLLAB_URL" +fi + if [ -n "${GEOLIBRE_EMBED_ORIGINS:-}" ]; then echo "Embed postMessage API enabled for: $GEOLIBRE_EMBED_ORIGINS" fi diff --git a/docs/collaboration.md b/docs/collaboration.md index cfda966e8..90387d1c1 100644 --- a/docs/collaboration.md +++ b/docs/collaboration.md @@ -202,6 +202,14 @@ unset, the hook is inert and all collaboration UI is hidden, so production build ship the feature dark. The Tauri CSP `connect-src` must list the wss host (the existing `https:` directive does **not** authorize `wss:`). +In the Docker image the same setting is available at **container runtime** as +`-e GEOLIBRE_COLLAB_URL=…`: the entrypoint validates it, writes it into +`geolibre-runtime-config.js`, and `resolveCollabBaseUrl()` prefers that over the +build-time variable — so a prebuilt image can be pointed at a self-hosted relay +without a rebuild. A value that is not `wss://` (or `ws://` on loopback) fails the +container boot rather than silently leaving collaboration dark. See +[Run with Docker](getting-started.md#self-hosted-sharing-and-collaboration-servers). + > **Self-hosting note:** the desktop CSP pins `wss://collab.geolibre.app` (plus > `ws://localhost`/`127.0.0.1` for dev). Pointing the desktop build at a > different relay means updating `connect-src` in diff --git a/docs/getting-started.md b/docs/getting-started.md index 3d4b8c764..fc77f2d89 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -273,6 +273,42 @@ by whoever frames it. See [Talking to the map at runtime](user-guide/embedding.md#talking-to-the-map-at-runtime) for the message reference and a host-page example. +#### Self-hosted sharing and collaboration servers + +Project **Share** and the **Project Gallery** talk to `share.geolibre.app` by +default. Point them at your own server instead, or turn the feature off: + +```bash +docker run --rm -p 8080:80 \ + -e GEOLIBRE_SHARE_URL=https://maps.example.org \ + -e GEOLIBRE_COLLAB_URL=wss://collab.example.org \ + ghcr.io/opengeos/geolibre:latest +``` + +| Variable | Effect | +| --- | --- | +| `GEOLIBRE_SHARE_URL` | Base URL of the project sharing server. Unset uses `share.geolibre.app`. Set it to `off` to remove Share and the Project Gallery from the UI entirely. | +| `GEOLIBRE_COLLAB_URL` | Base URL of the [collaboration](collaboration.md) relay. Unset leaves live collaboration disabled. | + +Both are read at container startup, so a prebuilt image can be repointed by +restarting it with different values — no rebuild. (The equivalent build +arguments, `VITE_GEOLIBRE_SHARE_URL` and `VITE_GEOLIBRE_COLLAB_URL`, exist for +baking a default into your own image.) + +Both must use TLS — `https://` for the share server, `wss://` for the relay — +because the app sends your API token to the share server with every request. +Plaintext is accepted only on `localhost` / `127.0.0.1` for local development, so +put a self-hosted server behind a reverse proxy that terminates TLS. A value that +does not satisfy this **fails the container boot** with an error naming the +variable, rather than starting up and quietly using the public hosted service +with your users' projects. + +> There is no open-source implementation of the sharing server API yet +> ([#1685](https://github.com/opengeos/GeoLibre/issues/1685)), so today +> `GEOLIBRE_SHARE_URL` is for pointing at a compatible or staging deployment you +> already run. `GEOLIBRE_COLLAB_URL` can point at your own deployment of +> `workers/collab`. + ### Run the desktop app ```bash diff --git a/tests/collab-protocol.test.ts b/tests/collab-protocol.test.ts index 13afe4fb7..482bf83c2 100644 --- a/tests/collab-protocol.test.ts +++ b/tests/collab-protocol.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { + COLLAB_URL_ENV, CollabConnection, createSession, httpBaseFromWs, @@ -43,6 +44,20 @@ describe("resolveCollabBaseUrl", () => { assert.equal(resolveCollabBaseUrl(undefined), null); assert.equal(resolveCollabBaseUrl(""), null); }); + + // The Docker entrypoint writes this at container startup, so a prebuilt image + // can point at a self-hosted relay without a rebuild (GeoLibre#1684). + it("prefers the deployment env over the build-time env", () => { + assert.equal( + resolveCollabBaseUrl(undefined, { [COLLAB_URL_ENV]: "wss://collab.example.org" }), + "wss://collab.example.org", + ); + }); + + it("still validates a deployment-provided value", () => { + assert.equal(resolveCollabBaseUrl(undefined, { [COLLAB_URL_ENV]: "ws://relay.corp" }), null); + assert.equal(resolveCollabBaseUrl(undefined, { [COLLAB_URL_ENV]: " " }), null); + }); }); describe("url derivation", () => { diff --git a/tests/deployment-env.test.ts b/tests/deployment-env.test.ts new file mode 100644 index 000000000..c882d6cec --- /dev/null +++ b/tests/deployment-env.test.ts @@ -0,0 +1,61 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + readDeploymentEnv, + readDeploymentEnvValue, +} from "../apps/geolibre-desktop/src/lib/deployment-env"; + +const KEY = "VITE_GEOLIBRE_SHARE_URL"; + +describe("readDeploymentEnvValue", () => { + it("prefers the deployment env over the build env", () => { + assert.equal( + readDeploymentEnvValue( + KEY, + { [KEY]: "https://deploy.example" }, + { [KEY]: "https://build.example" }, + ), + "https://deploy.example", + ); + }); + + it("falls through to the build env when the deployment omits the key", () => { + assert.equal( + readDeploymentEnvValue(KEY, {}, { [KEY]: "https://build.example" }), + "https://build.example", + ); + }); + + // The entrypoint omits a key it has no value for, but a hand-written config or + // a bare `-e VAR=` can still produce an empty string; that must not shadow the + // build-time value. + it("treats a blank deployment value as unset", () => { + assert.equal( + readDeploymentEnvValue(KEY, { [KEY]: " " }, { [KEY]: "https://build.example" }), + "https://build.example", + ); + }); + + it("returns undefined when neither source sets the key", () => { + assert.equal(readDeploymentEnvValue(KEY, {}, {}), undefined); + assert.equal(readDeploymentEnvValue(KEY, undefined, undefined), undefined); + }); +}); + +describe("readDeploymentEnv", () => { + it("returns undefined without a window (node, SSR)", () => { + assert.equal(typeof globalThis.window, "undefined"); + assert.equal(readDeploymentEnv(), undefined); + }); + + it("reads the record the Docker entrypoint writes onto window", () => { + const win = { __GEOLIBRE_DEPLOYMENT_ENV__: { [KEY]: "https://maps.example.org" } }; + (globalThis as { window?: unknown }).window = win; + try { + assert.deepEqual(readDeploymentEnv(), { [KEY]: "https://maps.example.org" }); + assert.equal(readDeploymentEnvValue(KEY, undefined, {}), "https://maps.example.org"); + } finally { + delete (globalThis as { window?: unknown }).window; + } + }); +}); diff --git a/tests/share-gallery.test.ts b/tests/share-gallery.test.ts index 650b923c9..c6c490224 100644 --- a/tests/share-gallery.test.ts +++ b/tests/share-gallery.test.ts @@ -270,6 +270,61 @@ describe("fetchMyProjects", () => { }); }); +// A deployment that disabled sharing (or named a host that was rejected) must +// make the gallery say so rather than silently listing the public hosted +// service's projects instead. See GeoLibre#1684. +describe("gallery with no configured share host", () => { + function withDeploymentEnv(value: string, run: () => Promise): Promise { + (globalThis as { window?: unknown }).window = { + __GEOLIBRE_DEPLOYMENT_ENV__: { VITE_GEOLIBRE_SHARE_URL: value }, + }; + return run().finally(() => { + delete (globalThis as { window?: unknown }).window; + }); + } + + it("throws not-configured from the public listing when sharing is off", async () => { + await withDeploymentEnv("off", async () => { + const error = await fetchSharedProjects({ + fetchImpl: () => assert.fail("must not reach the network"), + }).then( + () => null, + (caught: unknown) => caught, + ); + assert.ok(error instanceof GalleryError); + assert.equal(error.code, "not-configured"); + }); + }); + + it("throws not-configured when the configured host was rejected", async () => { + await withDeploymentEnv("http://internal.corp", async () => { + const error = await fetchMyProjects({ + token: "tok", + fetchImpl: () => assert.fail("must not reach the network"), + }).then( + () => null, + (caught: unknown) => caught, + ); + assert.ok(error instanceof GalleryError); + assert.equal(error.code, "not-configured"); + }); + }); + + it("still honors an explicit baseUrl override", async () => { + await withDeploymentEnv("off", async () => { + const calls: string[] = []; + await fetchSharedProjects({ + baseUrl: BASE, + fetchImpl: (input) => { + calls.push(String(input)); + return Promise.resolve(new Response(JSON.stringify({ projects: [] }))); + }, + }); + assert.equal(calls.length, 1); + }); + }); +}); + describe("shareAuthorizedFetch", () => { it("attaches the token only for the share host, never third parties", async () => { const seen: { url: string; auth: string | null }[] = []; diff --git a/tests/share-geolibre.test.ts b/tests/share-geolibre.test.ts index acc6f4909..67aa78be3 100644 --- a/tests/share-geolibre.test.ts +++ b/tests/share-geolibre.test.ts @@ -6,6 +6,8 @@ import { isShareableTitle, MAX_PROJECT_TITLE_LENGTH, resolveShareBaseUrl, + resolveShareHost, + SHARE_URL_ENV, ShareUploadError, uploadProjectToShare, } from "../apps/geolibre-desktop/src/lib/share-geolibre"; @@ -79,17 +81,70 @@ describe("resolveShareBaseUrl", () => { assert.equal(resolveShareBaseUrl("http://127.0.0.1:8787"), "http://127.0.0.1:8787"); }); - it("rejects plaintext HTTP to non-loopback hosts", () => { - assert.equal(resolveShareBaseUrl("http://internal.corp"), DEFAULT_SHARE_BASE_URL); + // A rejected value must NOT resolve to the public host: a self-hosted + // deployment with a bad share URL would otherwise upload its users' projects + // to share.geolibre.app. See GeoLibre#1684. + it("refuses plaintext HTTP to non-loopback hosts instead of falling back", () => { + assert.equal(resolveShareBaseUrl("http://internal.corp"), null); }); - it("rejects loopback-lookalike hosts that a prefix check would allow", () => { - assert.equal(resolveShareBaseUrl("http://localhost.evil.com"), DEFAULT_SHARE_BASE_URL); - assert.equal(resolveShareBaseUrl("http://127.0.0.1.evil.com"), DEFAULT_SHARE_BASE_URL); + it("refuses loopback-lookalike hosts that a prefix check would allow", () => { + assert.equal(resolveShareBaseUrl("http://localhost.evil.com"), null); + assert.equal(resolveShareBaseUrl("http://127.0.0.1.evil.com"), null); }); - it("falls back to production for an unparseable override", () => { - assert.equal(resolveShareBaseUrl("not a url"), DEFAULT_SHARE_BASE_URL); + it("refuses an unparseable override instead of falling back", () => { + assert.equal(resolveShareBaseUrl("not a url"), null); + }); + + it('treats "off" as sharing disabled', () => { + assert.equal(resolveShareBaseUrl("off"), null); + assert.equal(resolveShareBaseUrl("OFF"), null); + }); +}); + +describe("resolveShareHost", () => { + it("reports why the host is what it is", () => { + assert.deepEqual(resolveShareHost(undefined), { + status: "default", + baseUrl: DEFAULT_SHARE_BASE_URL, + configured: null, + }); + assert.deepEqual(resolveShareHost("https://maps.example.org"), { + status: "configured", + baseUrl: "https://maps.example.org", + configured: "https://maps.example.org", + }); + assert.deepEqual(resolveShareHost("off"), { + status: "disabled", + baseUrl: null, + configured: "off", + }); + assert.deepEqual(resolveShareHost("http://internal.corp"), { + status: "invalid", + baseUrl: null, + configured: "http://internal.corp", + }); + }); + + it("keeps the rejected value so the UI can name it", () => { + assert.equal(resolveShareHost("not a url").configured, "not a url"); + }); + + // The Docker entrypoint writes the deployment env at container startup, so a + // prebuilt image can be repointed without a rebuild. + it("prefers the deployment env over the build-time default", () => { + const resolved = resolveShareHost(undefined, { + [SHARE_URL_ENV]: "https://maps.example.org", + }); + assert.equal(resolved.status, "configured"); + assert.equal(resolved.baseUrl, "https://maps.example.org"); + }); + + it("ignores a blank deployment value", () => { + const resolved = resolveShareHost(undefined, { [SHARE_URL_ENV]: " " }); + assert.equal(resolved.status, "default"); + assert.equal(resolved.baseUrl, DEFAULT_SHARE_BASE_URL); }); }); From 8a879a485bda5344a30d7b57f4021e0a0a9c2f35 Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 3 Aug 2026 20:40:58 -0400 Subject: [PATCH 2/6] Address review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - share-geolibre.ts / collab-client.ts: reject URLs with embedded credentials in `isSafeShareUrl` and `resolveCollabBaseUrl`, so the client matches `service_url()` in the entrypoint. A `https://user:pass@host` base would send Basic Auth alongside the Bearer token (Copilot, Claude). - docker/entrypoint.sh: move the credential check ahead of the loopback early-return, which previously let `http://user:pass@localhost` through despite the docstring — and it is echoed to the boot log a few lines down (Claude, x2). - docker/entrypoint.sh: make the disabled-sharing boot log case-insensitive (`[oO][fF][fF]`) to match the Python validator and `resolveShareHost`, both of which lowercase before comparing (Claude). - share-geolibre.ts: the network-failure and unexpected-response upload errors still named share.geolibre.app; they now name the resolved host via a new `hostOf` helper that `shareHostLabel` also uses (Claude). - share-fetch.ts: scope the CORS-exempt native fetch by `URL.origin` rather than `URL.host`, so a plaintext request to a host configured over HTTPS is not routed through it. `requestOrigin` is exported and unit-tested (CodeRabbit). - useProjectFileActions.ts: derive one `shareAuth` value for both the fetch choice and whether the URL is remembered as recent. A token set on a deployment with no share host previously loaded unauthenticated yet was still not remembered (CodeRabbit). - ProjectMenu.tsx: count Share in `showSaveGroup` only when it is not hidden, so a profile showing only Share cannot leave an orphaned separator (CodeRabbit). - ProjectMenu.tsx: a disabled DropdownMenuItem carries `pointer-events-none`, so the native `title` holding the invalid-host reason could never be hovered. The reason is now a rendered line, referenced by `aria-describedby` with a distinct id per item (CodeRabbit). - SettingsDialog.tsx: hide the share-token description, input, and storage note when no share host is usable, replacing them with `settings.env.tokenUnavailable`. A deployment with `GEOLIBRE_SHARE_URL=off` was still telling users to get a token from the public hosted service (Claude, CodeRabbit). - ShareProjectDialog.tsx: guard the dialog on the same state defensively, so a future caller cannot render host-bearing setup guidance with no host (CodeRabbit). - Locales: add `gallery.errorNotConfigured`, `toolbar.item.shareHostUnavailable`, and the new `settings.env.tokenUnavailable` to all 15 non-English catalogues, translated and inserted in en.json key order (Copilot x2, Claude x2). Verified: the entrypoint validator now rejects credentials on the loopback path too (9 inputs re-checked), the boot log treats any casing of "off" as disabled, and in the built app an invalid host renders the reason as visible text with `aria-describedby` wired up while the Settings token input is replaced by the unavailable message. 5026 frontend tests pass; lint has 0 errors. --- .../src/components/layout/SettingsDialog.tsx | 73 +++++++++++-------- .../components/layout/ShareProjectDialog.tsx | 20 +++++ .../components/layout/toolbar/ProjectMenu.tsx | 52 +++++++++---- .../src/hooks/useProjectFileActions.ts | 13 +++- .../geolibre-desktop/src/i18n/locales/ar.json | 3 + .../geolibre-desktop/src/i18n/locales/de.json | 3 + .../geolibre-desktop/src/i18n/locales/en.json | 1 + .../geolibre-desktop/src/i18n/locales/es.json | 3 + .../geolibre-desktop/src/i18n/locales/fr.json | 3 + .../geolibre-desktop/src/i18n/locales/hi.json | 3 + .../geolibre-desktop/src/i18n/locales/id.json | 3 + .../geolibre-desktop/src/i18n/locales/it.json | 3 + .../geolibre-desktop/src/i18n/locales/ja.json | 3 + .../geolibre-desktop/src/i18n/locales/ka.json | 3 + .../geolibre-desktop/src/i18n/locales/ko.json | 3 + .../geolibre-desktop/src/i18n/locales/nl.json | 3 + .../geolibre-desktop/src/i18n/locales/pt.json | 3 + .../geolibre-desktop/src/i18n/locales/ru.json | 3 + .../geolibre-desktop/src/i18n/locales/tr.json | 3 + .../geolibre-desktop/src/i18n/locales/zh.json | 3 + .../geolibre-desktop/src/lib/collab-client.ts | 5 +- apps/geolibre-desktop/src/lib/share-fetch.ts | 33 +++++---- .../src/lib/share-geolibre.ts | 38 +++++++--- docker/entrypoint.sh | 11 ++- tests/collab-protocol.test.ts | 5 ++ tests/share-fetch.test.ts | 39 ++++++++++ tests/share-geolibre.test.ts | 8 ++ 27 files changed, 266 insertions(+), 77 deletions(-) diff --git a/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx b/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx index f73ec503b..e5170a847 100644 --- a/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx +++ b/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx @@ -386,6 +386,11 @@ export function SettingsDialog({ const shareBaseUrl = resolveShareBaseUrl(); const shareHost = shareHostLabel(); const shareSettingsUrl = shareBaseUrl ? `${shareBaseUrl}/settings` : null; + // No usable host (sharing turned off, or a configured address that was + // rejected) means the token field is dead: it would authenticate against a + // server this deployment never talks to. Say so instead of rendering guidance + // that names the public hosted service — the whole point of the opt-out. + const shareTokenUsable = shareBaseUrl != null; const { language, options: languageOptions, setLanguage } = useLanguage(); const preferences = useAppStore((s) => s.preferences); const setPreferences = useAppStore((s) => s.setPreferences); @@ -2281,36 +2286,44 @@ export function SettingsDialog({

{t("settings.env.tokenTitle")}

-

- - ) : ( - - ), - }} - /> -

- updateShareToken(event.target.value)} - /> -

- {t("settings.env.tokenStorageNote", { shareHost })} -

+ {shareTokenUsable ? ( + <> +

+ + ) : ( + + ), + }} + /> +

+ updateShareToken(event.target.value)} + /> +

+ {t("settings.env.tokenStorageNote", { shareHost })} +

+ + ) : ( +

+ {t("settings.env.tokenUnavailable")} +

+ )}

{t("settings.env.cesiumTokenTitle")}

diff --git a/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx b/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx index f28518b33..7aa058234 100644 --- a/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx +++ b/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx @@ -173,6 +173,26 @@ export function ShareProjectDialog({ }); }; + // Defensive: the Share entry points (menu item and command palette) are gated on + // the same state, so this should be unreachable. Guarding here anyway keeps a + // future caller from rendering setup guidance that names the public hosted + // service on a deployment that configured no share host. + if (!settingsUrl) { + return ( + + + + + + {t("share.title")} + + {t("gallery.errorNotConfigured")} + + + + ); + } + return ( diff --git a/apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx b/apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx index 7590b523e..0a93ad024 100644 --- a/apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx +++ b/apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx @@ -39,6 +39,10 @@ import { isMenuItemVisible } from "../../../lib/ui-profile"; import type { ShareHostStatus } from "../../../lib/share-geolibre"; import { formatRecentProjectTime, type ToolbarChrome } from "./constants"; +// aria-describedby targets for the "sharing server unavailable" explanation. +const SHARE_UNAVAILABLE_ID = "project-menu-share-unavailable"; +const GALLERY_UNAVAILABLE_ID = "project-menu-gallery-unavailable"; + interface ProjectMenuProps { chrome: ToolbarChrome; collaborationEnabled: boolean; @@ -103,7 +107,17 @@ export function ProjectMenu({ // a host we rejected should say so rather than leave the user wondering. const shareHidden = shareHostStatus === "disabled"; const shareBroken = shareHostStatus === "invalid"; - const shareBrokenReason = shareBroken ? t("toolbar.item.shareHostUnavailable") : undefined; + // A disabled DropdownMenuItem gets `pointer-events-none`, so a native `title` + // tooltip can never be hovered. Render the reason as its own line instead, and + // point the item at it with aria-describedby so it is announced too. The id is + // per-item: the Gallery entry (in the Open From submenu) and the Share entry can + // both be mounted at once, and a duplicate id would break the association. + const shareBrokenNote = (id: string) => + shareBroken ? ( + + {t("toolbar.item.shareHostUnavailable")} + + ) : null; // Group-visibility flags so the separators between groups aren't left orphaned // when a whole group is hidden by the active profile. const showSaveGroup = @@ -111,7 +125,7 @@ export function ProjectMenu({ show("project.saveAs") || show("project.duplicate") || show("project.saveAsTemplate") || - show("project.share") || + (!shareHidden && show("project.share")) || show("project.exportHtml") || (collaborationEnabled && show("project.collaborate")); const showPrintGroup = show("project.printLayout") || show("project.offlineRegion"); @@ -154,14 +168,17 @@ export function ProjectMenu({ {t("toolbar.item.urlEllipsis")} {!shareHidden && ( - - - {t("toolbar.item.galleryEllipsis")} - + <> + + + {t("toolbar.item.galleryEllipsis")} + + {shareBrokenNote(GALLERY_UNAVAILABLE_ID)} + )} @@ -277,10 +294,17 @@ export function ProjectMenu({ )} {show("project.share") && !shareHidden && ( - - - {t("toolbar.item.shareEllipsis")} - + <> + + + {t("toolbar.item.shareEllipsis")} + + {shareBrokenNote(SHARE_UNAVAILABLE_ID)} + )} {show("project.exportHtml") && ( diff --git a/apps/geolibre-desktop/src/hooks/useProjectFileActions.ts b/apps/geolibre-desktop/src/hooks/useProjectFileActions.ts index 436d34433..2a5d53902 100644 --- a/apps/geolibre-desktop/src/hooks/useProjectFileActions.ts +++ b/apps/geolibre-desktop/src/hooks/useProjectFileActions.ts @@ -518,11 +518,18 @@ export function useProjectFileActions(mapControllerRef: MapControllerRef) { try { let project: Awaited>; + // One decision drives both the fetch and whether the URL is remembered: a + // token is only actually sent when there is a share host to send it to, and + // an unauthenticated open of a public URL should still be remembered. const shareBaseUrl = resolveShareBaseUrl(); - if (options.authToken && shareBaseUrl) { + const shareAuth = + options.authToken && shareBaseUrl + ? { token: options.authToken, baseUrl: shareBaseUrl } + : null; + if (shareAuth) { const fetched = await fetchProjectFromUrl(normalizedUrl, { signal: controller.signal, - fetchImpl: shareAuthorizedFetch(options.authToken, shareBaseUrl, getShareFetch()), + fetchImpl: shareAuthorizedFetch(shareAuth.token, shareAuth.baseUrl, getShareFetch()), }); project = await resolveProjectXyzLayers(fetched, controller.signal); } else { @@ -537,7 +544,7 @@ export function useProjectFileActions(mapControllerRef: MapControllerRef) { loadProject(detached, null); useAppStore.setState({ isDirty: true }); } else { - loadProject(project, options.authToken ? null : normalizedUrl); + loadProject(project, shareAuth ? null : normalizedUrl); } } finally { if (shareUrlAbortRef.current === controller) { diff --git a/apps/geolibre-desktop/src/i18n/locales/ar.json b/apps/geolibre-desktop/src/i18n/locales/ar.json index 7a7611476..dfdecc78c 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ar.json +++ b/apps/geolibre-desktop/src/i18n/locales/ar.json @@ -1330,6 +1330,7 @@ "errorInvalidResponse": "تعذّر تحميل المعرض (استجابة الخادم غير صالحة).", "errorUnauthorized": "رمز API الخاص بك لموقع {{shareHost}} غير صالح أو منتهي الصلاحية. حدّثه في الإعدادات.", "errorUsernameRequired": "عيّن اسم مستخدم في حسابك على {{shareHost}} قبل تحميل مشاريعك.", + "errorNotConfigured": "لا يوجد خادم لمشاركة المشاريع مُهيّأ في هذا النشر.", "retry": "إعادة المحاولة", "open": "فتح", "openCopy": "فتح نسخة", @@ -1952,6 +1953,7 @@ "tokenTitle": "رمز API لخدمة Share.GeoLibre", "tokenDescription": "تستخدم قائمة مشروع > مشاركة هذا الرمز لرفع خرائطك إلى {{shareHost}}. افتح {{shareHost}}/settings، وأنشئ رمزًا من الإعدادات > رموز API، ثم الصقه في الحقل أدناه.", "tokenStorageNote": "يُخزن محليًا على هذا الجهاز ويُرسل فقط إلى {{shareHost}} لمصادقة عمليات الرفع. في نسخة الويب يتشارك مساحة تخزين المتصفح نفسها مع بيانات الموقع الأخرى، لذا ألغِه من {{shareHost}} إذا تعرض جهازك للاختراق.", + "tokenUnavailable": "لا يوجد خادم لمشاركة المشاريع مُهيّأ في هذا النشر، لذا لا حاجة إلى رمز API.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "رمز Cesium Ion", "cesiumTokenDescription": "تستخدم الكرة الأرضية ثلاثية الأبعاد (جزء في العرض المنقسم) صور العالم والتضاريس من Cesium Ion، وهي بيانات تتطلب رمز وصول. أنشئ حسابًا مجانيًا في ion.cesium.com/tokens، وانسخ رمز الوصول الافتراضي الخاص بك، ثم الصقه أدناه. من دون رمز يبقى مفتاح تبديل الكرة الأرضية ثلاثية الأبعاد مخفيًا.", @@ -2506,6 +2508,7 @@ "saveAsEllipsis": "حفظ باسم...", "saveAsTemplateEllipsis": "الحفظ كقالب...", "shareEllipsis": "مشاركة...", + "shareHostUnavailable": "غير متاح: عنوان خادم المشاركة في هذا النشر غير صالح.", "exportHtmlEllipsis": "تصدير كملف HTML...", "htmlFile": "HTML", "collaborateEllipsis": "تعاون...", diff --git a/apps/geolibre-desktop/src/i18n/locales/de.json b/apps/geolibre-desktop/src/i18n/locales/de.json index 77624146e..e8bf8ef20 100644 --- a/apps/geolibre-desktop/src/i18n/locales/de.json +++ b/apps/geolibre-desktop/src/i18n/locales/de.json @@ -1190,6 +1190,7 @@ "errorInvalidResponse": "Die Galerie konnte nicht geladen werden (ungültige Serverantwort).", "errorUnauthorized": "Ihr {{shareHost}}-API-Token ist ungültig oder abgelaufen. Aktualisieren Sie es in den Einstellungen.", "errorUsernameRequired": "Legen Sie einen Benutzernamen für Ihr {{shareHost}}-Konto fest, bevor Sie Ihre Projekte laden.", + "errorNotConfigured": "In dieser Bereitstellung ist kein Server zum Teilen von Projekten konfiguriert.", "retry": "Erneut versuchen", "open": "Öffnen", "openCopy": "Kopie öffnen", @@ -1793,6 +1794,7 @@ "tokenTitle": "Share.GeoLibre-API-Token", "tokenDescription": "Projekt > Teilen verwendet dieses Token, um Ihre Karten auf {{shareHost}} hochzuladen. Öffnen Sie {{shareHost}}/settings, erstellen Sie ein Token unter Einstellungen > API-Token und fügen Sie es dann in das Feld unten ein.", "tokenStorageNote": "Wird lokal auf diesem Gerät gespeichert und nur an {{shareHost}} gesendet, um Uploads zu authentifizieren. Im Web-Build nutzt es denselben Browser-Speicher wie andere Website-Daten. Widerrufen Sie es daher auf {{shareHost}}, falls Ihr Gerät kompromittiert wurde.", + "tokenUnavailable": "In dieser Bereitstellung ist kein Server zum Teilen von Projekten konfiguriert, daher ist kein API-Token erforderlich.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium-Ion-Token", "cesiumTokenDescription": "Der 3D-Globus (ein geteiltes Ansichtsfenster) verwendet Cesium-Ion-Weltbilder und -Gelände, wofür ein Zugriffstoken erforderlich ist. Erstellen Sie ein kostenloses Konto unter ion.cesium.com/tokens, kopieren Sie Ihr Standard-Zugriffstoken und fügen Sie es unten ein. Ohne Token ist der 3D-Globus-Umschalter ausgeblendet.", @@ -2339,6 +2341,7 @@ "saveAsEllipsis": "Speichern unter...", "saveAsTemplateEllipsis": "Als Vorlage speichern...", "shareEllipsis": "Teilen...", + "shareHostUnavailable": "Nicht verfügbar: Die Adresse des Freigabeservers dieser Bereitstellung ist ungültig.", "exportHtmlEllipsis": "Als HTML exportieren...", "htmlFile": "HTML", "collaborateEllipsis": "Zusammenarbeiten...", diff --git a/apps/geolibre-desktop/src/i18n/locales/en.json b/apps/geolibre-desktop/src/i18n/locales/en.json index da24412f5..4a2c25744 100644 --- a/apps/geolibre-desktop/src/i18n/locales/en.json +++ b/apps/geolibre-desktop/src/i18n/locales/en.json @@ -1794,6 +1794,7 @@ "tokenTitle": "Share.GeoLibre API token", "tokenDescription": "Project > Share uses this token to upload your maps to {{shareHost}}. Open {{shareHost}}/settings, create a token under Settings > API tokens, then paste it into the field below.", "tokenStorageNote": "Stored locally on this device and sent only to {{shareHost}} to authenticate uploads. On the web build it shares the same browser storage as other site data, so revoke it on {{shareHost}} if your machine is compromised.", + "tokenUnavailable": "This deployment has no project sharing server configured, so no API token is needed.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion token", "cesiumTokenDescription": "The 3D globe (a split-view pane) uses Cesium Ion world imagery and terrain, which need an access token. Create a free account at ion.cesium.com/tokens, copy your default access token, and paste it below. Without a token the 3D globe toggle is hidden.", diff --git a/apps/geolibre-desktop/src/i18n/locales/es.json b/apps/geolibre-desktop/src/i18n/locales/es.json index 5b9e791b0..b4b05918b 100644 --- a/apps/geolibre-desktop/src/i18n/locales/es.json +++ b/apps/geolibre-desktop/src/i18n/locales/es.json @@ -1190,6 +1190,7 @@ "errorInvalidResponse": "No se pudo cargar la galería (respuesta del servidor no válida).", "errorUnauthorized": "Su token de API de {{shareHost}} no es válido o ha caducado. Actualícelo en Configuración.", "errorUsernameRequired": "Configure un nombre de usuario en su cuenta de {{shareHost}} antes de cargar sus proyectos.", + "errorNotConfigured": "Esta implementación no tiene configurado ningún servidor para compartir proyectos.", "retry": "Reintentar", "open": "Abrir", "openCopy": "Abrir una copia", @@ -1793,6 +1794,7 @@ "tokenTitle": "Token de API de Share.GeoLibre", "tokenDescription": "Proyecto > Compartir usa este token para subir sus mapas a {{shareHost}}. Abra {{shareHost}}/settings, cree un token en Configuración > Tokens de API y luego péguelo en el campo de abajo.", "tokenStorageNote": "Se almacena localmente en este dispositivo y solo se envía a {{shareHost}} para autenticar las subidas. En la compilación web, comparte el mismo almacenamiento del navegador que otros datos del sitio, así que revóquelo en {{shareHost}} si su equipo se ve comprometido.", + "tokenUnavailable": "Esta implementación no tiene configurado ningún servidor para compartir proyectos, por lo que no se necesita ningún token de API.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token de Cesium Ion", "cesiumTokenDescription": "El globo 3D (un panel de vista dividida) usa imágenes y terreno mundiales de Cesium Ion, que necesitan un token de acceso. Cree una cuenta gratuita en ion.cesium.com/tokens, copie su token de acceso predeterminado y péguelo a continuación. Sin un token, el interruptor del globo 3D permanece oculto.", @@ -2339,6 +2341,7 @@ "saveAsEllipsis": "Guardar como...", "saveAsTemplateEllipsis": "Guardar como plantilla...", "shareEllipsis": "Compartir...", + "shareHostUnavailable": "No disponible: la dirección del servidor para compartir de esta implementación no es válida.", "exportHtmlEllipsis": "Exportar como HTML...", "htmlFile": "HTML", "collaborateEllipsis": "Colaborar...", diff --git a/apps/geolibre-desktop/src/i18n/locales/fr.json b/apps/geolibre-desktop/src/i18n/locales/fr.json index 27d463dbf..266c28e52 100644 --- a/apps/geolibre-desktop/src/i18n/locales/fr.json +++ b/apps/geolibre-desktop/src/i18n/locales/fr.json @@ -1190,6 +1190,7 @@ "errorInvalidResponse": "Impossible de charger la galerie (réponse du serveur invalide).", "errorUnauthorized": "Votre jeton API {{shareHost}} est invalide ou expiré. Mettez-le à jour dans Paramètres.", "errorUsernameRequired": "Définissez un nom d'utilisateur sur votre compte {{shareHost}} avant de charger vos projets.", + "errorNotConfigured": "Aucun serveur de partage de projets n'est configuré pour ce déploiement.", "retry": "Réessayer", "open": "Ouvrir", "openCopy": "Ouvrir une copie", @@ -1793,6 +1794,7 @@ "tokenTitle": "Jeton API Share.GeoLibre", "tokenDescription": "Projet > Partager utilise ce jeton pour téléverser vos cartes vers {{shareHost}}. Ouvrez {{shareHost}}/settings, créez un jeton sous Paramètres > Jetons API, puis collez-le dans le champ ci-dessous.", "tokenStorageNote": "Stocké localement sur cet appareil et envoyé uniquement à {{shareHost}} pour authentifier les téléversements. Dans la version web, il partage le même stockage de navigateur que les autres données du site, alors révoquez-le sur {{shareHost}} si votre machine est compromise.", + "tokenUnavailable": "Aucun serveur de partage de projets n'est configuré pour ce déploiement, aucun jeton API n'est donc nécessaire.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Jeton Cesium Ion", "cesiumTokenDescription": "Le globe 3D (un volet en vue partagée) utilise l'imagerie mondiale et le relief de Cesium Ion, qui nécessitent un jeton d'accès. Créez un compte gratuit sur ion.cesium.com/tokens, copiez votre jeton d'accès par défaut, puis collez-le ci-dessous. Sans jeton, le bouton du globe 3D est masqué.", @@ -2339,6 +2341,7 @@ "saveAsEllipsis": "Enregistrer sous...", "saveAsTemplateEllipsis": "Enregistrer comme modèle...", "shareEllipsis": "Partager...", + "shareHostUnavailable": "Indisponible : l'adresse du serveur de partage de ce déploiement n'est pas valide.", "exportHtmlEllipsis": "Exporter en HTML...", "htmlFile": "HTML", "collaborateEllipsis": "Collaborer...", diff --git a/apps/geolibre-desktop/src/i18n/locales/hi.json b/apps/geolibre-desktop/src/i18n/locales/hi.json index 96014fa45..7ea20eb8a 100644 --- a/apps/geolibre-desktop/src/i18n/locales/hi.json +++ b/apps/geolibre-desktop/src/i18n/locales/hi.json @@ -1190,6 +1190,7 @@ "errorInvalidResponse": "गैलरी लोड नहीं हो सकी (अमान्य सर्वर प्रतिक्रिया)।", "errorUnauthorized": "आपका {{shareHost}} API टोकन अमान्य है या समाप्त हो गया है। इसे Settings में अपडेट करें।", "errorUsernameRequired": "अपने प्रोजेक्ट लोड करने से पहले अपने {{shareHost}} खाते पर एक उपयोगकर्ता नाम सेट करें।", + "errorNotConfigured": "इस परिनियोजन में कोई प्रोजेक्ट साझाकरण सर्वर कॉन्फ़िगर नहीं है।", "retry": "पुनः प्रयास करें", "open": "खोलें", "openCopy": "एक प्रति खोलें", @@ -1793,6 +1794,7 @@ "tokenTitle": "Share.GeoLibre API टोकन", "tokenDescription": "Project > Share इस टोकन का उपयोग आपके मानचित्रों को {{shareHost}} पर अपलोड करने के लिए करता है। {{shareHost}}/settings खोलें, Settings > API tokens के अंतर्गत एक टोकन बनाएँ, फिर उसे नीचे दिए गए फ़ील्ड में पेस्ट करें।", "tokenStorageNote": "इस डिवाइस पर स्थानीय रूप से संग्रहीत और अपलोड प्रमाणित करने के लिए केवल {{shareHost}} पर भेजा जाता है। वेब बिल्ड पर यह अन्य साइट डेटा के समान ब्राउज़र स्टोरेज साझा करता है, इसलिए यदि आपकी मशीन से समझौता हो जाए तो इसे {{shareHost}} पर रद्द करें।", + "tokenUnavailable": "इस परिनियोजन में कोई प्रोजेक्ट साझाकरण सर्वर कॉन्फ़िगर नहीं है, इसलिए किसी API टोकन की आवश्यकता नहीं है।", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion टोकन", "cesiumTokenDescription": "3D ग्लोब (एक स्प्लिट-व्यू पैनल) Cesium Ion वर्ल्ड इमेजरी और टेरेन का उपयोग करता है, जिसके लिए एक एक्सेस टोकन चाहिए। ion.cesium.com/tokens पर एक मुफ़्त खाता बनाएँ, अपना डिफ़ॉल्ट एक्सेस टोकन कॉपी करें, और उसे नीचे पेस्ट करें। टोकन के बिना 3D ग्लोब टॉगल छिपा रहता है।", @@ -2339,6 +2341,7 @@ "saveAsEllipsis": "इस रूप में सहेजें...", "saveAsTemplateEllipsis": "टेम्पलेट के रूप में सहेजें...", "shareEllipsis": "साझा करें...", + "shareHostUnavailable": "अनुपलब्ध: इस परिनियोजन के साझाकरण सर्वर का पता अमान्य है।", "exportHtmlEllipsis": "HTML के रूप में निर्यात करें...", "htmlFile": "HTML", "collaborateEllipsis": "सहयोग करें...", diff --git a/apps/geolibre-desktop/src/i18n/locales/id.json b/apps/geolibre-desktop/src/i18n/locales/id.json index 0e0969fba..be740edd1 100644 --- a/apps/geolibre-desktop/src/i18n/locales/id.json +++ b/apps/geolibre-desktop/src/i18n/locales/id.json @@ -1155,6 +1155,7 @@ "errorInvalidResponse": "Tidak dapat memuat galeri (respons server tidak valid).", "errorUnauthorized": "Token API {{shareHost}} Anda tidak valid atau telah kedaluwarsa. Perbarui di Pengaturan.", "errorUsernameRequired": "Atur nama pengguna pada akun {{shareHost}} Anda sebelum memuat proyek Anda.", + "errorNotConfigured": "Penerapan ini tidak memiliki server pembagian proyek yang dikonfigurasi.", "retry": "Coba lagi", "open": "Buka", "openCopy": "Buka salinan", @@ -1753,6 +1754,7 @@ "tokenTitle": "Token API Share.GeoLibre", "tokenDescription": "Project > Share menggunakan token ini untuk mengunggah peta Anda ke {{shareHost}}. Buka {{shareHost}}/settings, buat token di Settings > API tokens, lalu tempelkan ke bidang di bawah.", "tokenStorageNote": "Disimpan secara lokal di perangkat ini dan hanya dikirim ke {{shareHost}} untuk mengautentikasi unggahan. Pada build web, token ini berbagi penyimpanan browser yang sama dengan data situs lainnya, jadi cabut aksesnya di {{shareHost}} jika perangkat Anda disusupi.", + "tokenUnavailable": "Penerapan ini tidak memiliki server pembagian proyek yang dikonfigurasi, sehingga token API tidak diperlukan.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token Cesium Ion", "cesiumTokenDescription": "Bola dunia 3D (panel tampilan terpisah) menggunakan citra dan medan dunia Cesium Ion, yang memerlukan token akses. Buat akun gratis di ion.cesium.com/tokens, salin token akses default Anda, dan tempelkan di bawah. Tanpa token, tombol bola dunia 3D disembunyikan.", @@ -2297,6 +2299,7 @@ "saveAsEllipsis": "Simpan Sebagai...", "saveAsTemplateEllipsis": "Simpan sebagai templat...", "shareEllipsis": "Bagikan...", + "shareHostUnavailable": "Tidak tersedia: alamat server pembagian pada penerapan ini tidak valid.", "exportHtmlEllipsis": "Ekspor sebagai HTML...", "htmlFile": "HTML", "collaborateEllipsis": "Kolaborasi...", diff --git a/apps/geolibre-desktop/src/i18n/locales/it.json b/apps/geolibre-desktop/src/i18n/locales/it.json index f741e8a1b..59efe0755 100644 --- a/apps/geolibre-desktop/src/i18n/locales/it.json +++ b/apps/geolibre-desktop/src/i18n/locales/it.json @@ -1190,6 +1190,7 @@ "errorInvalidResponse": "Impossibile caricare la galleria (risposta del server non valida).", "errorUnauthorized": "Il tuo token API di {{shareHost}} non è valido o è scaduto. Aggiornalo nelle Impostazioni.", "errorUsernameRequired": "Imposta un nome utente sul tuo account {{shareHost}} prima di caricare i tuoi progetti.", + "errorNotConfigured": "Questo deployment non ha alcun server di condivisione dei progetti configurato.", "retry": "Riprova", "open": "Apri", "openCopy": "Apri una copia", @@ -1793,6 +1794,7 @@ "tokenTitle": "Token API di Share.GeoLibre", "tokenDescription": "Progetto > Condividi usa questo token per caricare le tue mappe su {{shareHost}}. Apri {{shareHost}}/settings, crea un token in Settings > API tokens, quindi incollalo nel campo sottostante.", "tokenStorageNote": "Memorizzato localmente su questo dispositivo e inviato solo a {{shareHost}} per autenticare i caricamenti. Nella build web condivide lo stesso archivio del browser degli altri dati del sito, quindi revocalo su {{shareHost}} se il tuo computer viene compromesso.", + "tokenUnavailable": "Questo deployment non ha alcun server di condivisione dei progetti configurato, quindi non è necessario alcun token API.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token Cesium Ion", "cesiumTokenDescription": "Il globo 3D (un riquadro a schermo diviso) usa le immagini satellitari e il terreno di Cesium Ion, che richiedono un token di accesso. Crea un account gratuito su ion.cesium.com/tokens, copia il tuo token di accesso predefinito e incollalo di seguito. Senza un token, l'interruttore del globo 3D resta nascosto.", @@ -2339,6 +2341,7 @@ "saveAsEllipsis": "Salva con nome...", "saveAsTemplateEllipsis": "Salva come modello...", "shareEllipsis": "Condividi...", + "shareHostUnavailable": "Non disponibile: l'indirizzo del server di condivisione di questo deployment non è valido.", "exportHtmlEllipsis": "Esporta come HTML...", "htmlFile": "HTML", "collaborateEllipsis": "Collabora...", diff --git a/apps/geolibre-desktop/src/i18n/locales/ja.json b/apps/geolibre-desktop/src/i18n/locales/ja.json index 10305268c..d6645482c 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ja.json +++ b/apps/geolibre-desktop/src/i18n/locales/ja.json @@ -1155,6 +1155,7 @@ "errorInvalidResponse": "ギャラリーを読み込めませんでした(サーバーの応答が無効です)。", "errorUnauthorized": "{{shareHost}} のAPIトークンが無効か期限切れです。設定で更新してください。", "errorUsernameRequired": "自分のプロジェクトを読み込む前に {{shareHost}} アカウントでユーザー名を設定してください。", + "errorNotConfigured": "このデプロイにはプロジェクト共有サーバーが設定されていません。", "retry": "再試行", "open": "開く", "openCopy": "コピーを開く", @@ -1753,6 +1754,7 @@ "tokenTitle": "Share.GeoLibre APIトークン", "tokenDescription": "プロジェクト > 共有では、このトークンを使用して地図を{{shareHost}}にアップロードします。{{shareHost}}/settings を開き、設定 > APIトークン でトークンを作成して、下のフィールドに貼り付けてください。", "tokenStorageNote": "このデバイスにローカルで保存され、アップロードの認証のために{{shareHost}}にのみ送信されます。Webビルドでは他のサイトデータと同じブラウザストレージを共有するため、端末が侵害された場合は{{shareHost}}でトークンを失効させてください。", + "tokenUnavailable": "このデプロイにはプロジェクト共有サーバーが設定されていないため、APIトークンは必要ありません。", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ionトークン", "cesiumTokenDescription": "3Dグローブ(分割表示ペイン)はCesium Ionの世界衛星画像と地形データを使用しており、アクセストークンが必要です。ion.cesium.com/tokens で無料アカウントを作成し、デフォルトのアクセストークンをコピーして下に貼り付けてください。トークンがない場合、3Dグローブの切り替えは表示されません。", @@ -2297,6 +2299,7 @@ "saveAsEllipsis": "名前を付けて保存...", "saveAsTemplateEllipsis": "テンプレートとして保存...", "shareEllipsis": "共有...", + "shareHostUnavailable": "利用できません: このデプロイの共有サーバーのアドレスが無効です。", "exportHtmlEllipsis": "HTML としてエクスポート...", "htmlFile": "HTML", "collaborateEllipsis": "共同編集...", diff --git a/apps/geolibre-desktop/src/i18n/locales/ka.json b/apps/geolibre-desktop/src/i18n/locales/ka.json index 7786e707a..a6ac8aef8 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ka.json +++ b/apps/geolibre-desktop/src/i18n/locales/ka.json @@ -1190,6 +1190,7 @@ "errorInvalidResponse": "გალერეის ჩატვირთვა ვერ მოხერხდა (სერვერის არასწორი პასუხი).", "errorUnauthorized": "თქვენი {{shareHost}} API-ტოკენი არასწორია ან ვადაგასულია. განაახლეთ პარამეტრებში.", "errorUsernameRequired": "თქვენი პროექტების ჩატვირთვამდე დააყენეთ მომხმარებლის სახელი {{shareHost}} ანგარიშზე.", + "errorNotConfigured": "ამ განთავსებაში პროექტების გაზიარების სერვერი არ არის კონფიგურირებული.", "retry": "ხელახლა ცდა", "open": "გახსნა", "openCopy": "ასლის გახსნა", @@ -1793,6 +1794,7 @@ "tokenTitle": "Share.GeoLibre-ის API token", "tokenDescription": "„პროექტი > გაზიარება“ იყენებს ამ token-ს თქვენი რუკების {{shareHost}}-ზე ასატვირთად. გახსენით {{shareHost}}/settings, შექმენით token განყოფილებაში „Settings > API tokens“, შემდეგ ჩასვით ქვემოთ ველში.", "tokenStorageNote": "ინახება ლოკალურად ამ მოწყობილობაზე და იგზავნება მხოლოდ {{shareHost}}-ზე ატვირთვების ავთენტიფიკაციისთვის. ვებ-ვერსიაში ის იზიარებს იმავე ბრაუზერის საცავს, რასაც საიტის სხვა მონაცემები, ამიტომ გააუქმეთ ის {{shareHost}}-ზე, თუ თქვენი მანქანა კომპრომეტირებულია.", + "tokenUnavailable": "ამ განთავსებაში პროექტების გაზიარების სერვერი არ არის კონფიგურირებული, ამიტომ API token საჭირო არ არის.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion-ის token", "cesiumTokenDescription": "3D გლობუსი (გაყოფილი ხედის პანელი) იყენებს Cesium Ion-ის მსოფლიო სურათებსა და რელიეფს, რასაც წვდომის token სჭირდება. შექმენით უფასო ანგარიში მისამართზე ion.cesium.com/tokens, დააკოპირეთ თქვენი ნაგულისხმევი წვდომის token და ჩასვით ქვემოთ. token-ის გარეშე 3D გლობუსის გადამრთველი დამალულია.", @@ -2339,6 +2341,7 @@ "saveAsEllipsis": "შენახვა როგორც...", "saveAsTemplateEllipsis": "შაბლონად შენახვა...", "shareEllipsis": "გაზიარება...", + "shareHostUnavailable": "მიუწვდომელია: ამ განთავსების გაზიარების სერვერის მისამართი არასწორია.", "exportHtmlEllipsis": "ექსპორტი HTML-ად...", "htmlFile": "HTML", "collaborateEllipsis": "თანამშრომლობა...", diff --git a/apps/geolibre-desktop/src/i18n/locales/ko.json b/apps/geolibre-desktop/src/i18n/locales/ko.json index 74c114af4..06dd8538e 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ko.json +++ b/apps/geolibre-desktop/src/i18n/locales/ko.json @@ -1155,6 +1155,7 @@ "errorInvalidResponse": "갤러리를 불러올 수 없습니다(잘못된 서버 응답).", "errorUnauthorized": "{{shareHost}} API 토큰이 유효하지 않거나 만료되었습니다. 설정에서 업데이트하세요.", "errorUsernameRequired": "프로젝트를 불러오기 전에 {{shareHost}} 계정에 사용자 이름을 설정하세요.", + "errorNotConfigured": "이 배포에는 프로젝트 공유 서버가 구성되어 있지 않습니다.", "retry": "다시 시도", "open": "열기", "openCopy": "사본 열기", @@ -1753,6 +1754,7 @@ "tokenTitle": "Share.GeoLibre API 토큰", "tokenDescription": "프로젝트 > 공유는 이 토큰을 사용하여 지도를 {{shareHost}}에 업로드합니다. {{shareHost}}/settings를 열고 설정 > API 토큰에서 토큰을 생성한 다음 아래 필드에 붙여넣으세요.", "tokenStorageNote": "이 기기에 로컬로 저장되며 업로드 인증을 위해 {{shareHost}}에만 전송됩니다. 웹 빌드에서는 다른 사이트 데이터와 동일한 브라우저 저장소를 공유하므로, 기기가 침해된 경우 {{shareHost}}에서 토큰을 폐기하세요.", + "tokenUnavailable": "이 배포에는 프로젝트 공유 서버가 구성되어 있지 않으므로 API 토큰이 필요하지 않습니다.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion 토큰", "cesiumTokenDescription": "3D 지구본(분할 화면 패널)은 접근 토큰이 필요한 Cesium Ion 세계 영상과 지형을 사용합니다. ion.cesium.com/tokens에서 무료 계정을 만들고 기본 접근 토큰을 복사하여 아래에 붙여넣으세요. 토큰이 없으면 3D 지구본 전환 버튼이 숨겨집니다.", @@ -2297,6 +2299,7 @@ "saveAsEllipsis": "다른 이름으로 저장...", "saveAsTemplateEllipsis": "템플릿으로 저장...", "shareEllipsis": "공유...", + "shareHostUnavailable": "사용할 수 없음: 이 배포의 공유 서버 주소가 올바르지 않습니다.", "exportHtmlEllipsis": "HTML로 내보내기...", "htmlFile": "HTML", "collaborateEllipsis": "공동 작업...", diff --git a/apps/geolibre-desktop/src/i18n/locales/nl.json b/apps/geolibre-desktop/src/i18n/locales/nl.json index 3aeafa83b..7695f3346 100644 --- a/apps/geolibre-desktop/src/i18n/locales/nl.json +++ b/apps/geolibre-desktop/src/i18n/locales/nl.json @@ -1190,6 +1190,7 @@ "errorInvalidResponse": "Kan de galerij niet laden (ongeldig serverantwoord).", "errorUnauthorized": "Uw {{shareHost}} API-token is ongeldig of verlopen. Werk het bij in Instellingen.", "errorUsernameRequired": "Stel een gebruikersnaam in voor uw {{shareHost}}-account voordat u uw projecten laadt.", + "errorNotConfigured": "Deze implementatie heeft geen server voor het delen van projecten geconfigureerd.", "retry": "Opnieuw proberen", "open": "Openen", "openCopy": "Een kopie openen", @@ -1793,6 +1794,7 @@ "tokenTitle": "Share.GeoLibre API-token", "tokenDescription": "Project > Delen gebruikt dit token om uw kaarten te uploaden naar {{shareHost}}. Open {{shareHost}}/settings, maak een token aan onder Instellingen > API-tokens en plak het vervolgens in het onderstaande veld.", "tokenStorageNote": "Lokaal opgeslagen op dit apparaat en alleen verzonden naar {{shareHost}} om uploads te verifiëren. In de webversie wordt dezelfde browseropslag gebruikt als voor andere sitegegevens; trek het token daarom in op {{shareHost}} als uw machine gecompromitteerd is.", + "tokenUnavailable": "Deze implementatie heeft geen server voor het delen van projecten geconfigureerd, dus er is geen API-token nodig.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion-token", "cesiumTokenDescription": "De 3D-globe (een gesplitst weergavepaneel) gebruikt wereldbeelden en terrein van Cesium Ion, waarvoor een toegangstoken nodig is. Maak een gratis account aan op ion.cesium.com/tokens, kopieer uw standaard toegangstoken en plak het hieronder. Zonder token is de schakelaar voor de 3D-globe verborgen.", @@ -2339,6 +2341,7 @@ "saveAsEllipsis": "Opslaan als...", "saveAsTemplateEllipsis": "Opslaan als sjabloon...", "shareEllipsis": "Delen...", + "shareHostUnavailable": "Niet beschikbaar: het adres van de deelserver van deze implementatie is ongeldig.", "exportHtmlEllipsis": "Exporteren als HTML...", "htmlFile": "HTML", "collaborateEllipsis": "Samenwerken...", diff --git a/apps/geolibre-desktop/src/i18n/locales/pt.json b/apps/geolibre-desktop/src/i18n/locales/pt.json index 3430fc0e2..ef26cec37 100644 --- a/apps/geolibre-desktop/src/i18n/locales/pt.json +++ b/apps/geolibre-desktop/src/i18n/locales/pt.json @@ -1190,6 +1190,7 @@ "errorInvalidResponse": "Não foi possível carregar a galeria (resposta inválida do servidor).", "errorUnauthorized": "Seu token de API do {{shareHost}} é inválido ou expirou. Atualize-o em Configurações.", "errorUsernameRequired": "Defina um nome de usuário na sua conta {{shareHost}} antes de carregar seus projetos.", + "errorNotConfigured": "Esta implantação não tem nenhum servidor de compartilhamento de projetos configurado.", "retry": "Tentar novamente", "open": "Abrir", "openCopy": "Abrir uma cópia", @@ -1793,6 +1794,7 @@ "tokenTitle": "Token de API do Share.GeoLibre", "tokenDescription": "Projeto > Compartilhar usa este token para enviar seus mapas para {{shareHost}}. Abra {{shareHost}}/settings, crie um token em Configurações > Tokens de API e, em seguida, cole-o no campo abaixo.", "tokenStorageNote": "Armazenado localmente neste dispositivo e enviado apenas para {{shareHost}} para autenticar envios. Na versão web, ele compartilha o mesmo armazenamento do navegador que outros dados do site, então revogue-o em {{shareHost}} se sua máquina for comprometida.", + "tokenUnavailable": "Esta implantação não tem nenhum servidor de compartilhamento de projetos configurado, portanto nenhum token de API é necessário.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token do Cesium Ion", "cesiumTokenDescription": "O globo 3D (um painel de visualização dividida) usa imagens e terreno mundiais do Cesium Ion, que precisam de um token de acesso. Crie uma conta gratuita em ion.cesium.com/tokens, copie seu token de acesso padrão e cole-o abaixo. Sem um token, o alternador do globo 3D fica oculto.", @@ -2339,6 +2341,7 @@ "saveAsEllipsis": "Salvar como...", "saveAsTemplateEllipsis": "Salvar como modelo...", "shareEllipsis": "Compartilhar...", + "shareHostUnavailable": "Indisponível: o endereço do servidor de compartilhamento desta implantação não é válido.", "exportHtmlEllipsis": "Exportar como HTML...", "htmlFile": "HTML", "collaborateEllipsis": "Colaborar...", diff --git a/apps/geolibre-desktop/src/i18n/locales/ru.json b/apps/geolibre-desktop/src/i18n/locales/ru.json index f8f650aae..ba08943fe 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ru.json +++ b/apps/geolibre-desktop/src/i18n/locales/ru.json @@ -1260,6 +1260,7 @@ "errorInvalidResponse": "Не удалось загрузить галерею (некорректный ответ сервера).", "errorUnauthorized": "Ваш API-токен {{shareHost}} недействителен или просрочен. Обновите его в Настройках.", "errorUsernameRequired": "Задайте имя пользователя в учётной записи {{shareHost}} перед загрузкой ваших проектов.", + "errorNotConfigured": "В этом развёртывании не настроен сервер для публикации проектов.", "retry": "Повторить", "open": "Открыть", "openCopy": "Открыть копию", @@ -1873,6 +1874,7 @@ "tokenTitle": "API-токен Share.GeoLibre", "tokenDescription": "Раздел Проект > Поделиться использует этот токен для загрузки ваших карт на {{shareHost}}. Откройте {{shareHost}}/settings, создайте токен в разделе Настройки > API-токены, затем вставьте его в поле ниже.", "tokenStorageNote": "Хранится локально на этом устройстве и передаётся только на {{shareHost}} для аутентификации загрузок. В веб-сборке используется то же хранилище браузера, что и для других данных сайта; отзовите токен на {{shareHost}}, если ваше устройство скомпрометировано.", + "tokenUnavailable": "В этом развёртывании не настроен сервер для публикации проектов, поэтому API-токен не нужен.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Токен Cesium Ion", "cesiumTokenDescription": "3D-глобус (панель раздельного вида) использует мировые снимки и рельеф Cesium Ion, для которых требуется токен доступа. Создайте бесплатную учётную запись на ion.cesium.com/tokens, скопируйте свой токен доступа по умолчанию и вставьте его ниже. Без токена переключатель 3D-глобуса скрыт.", @@ -2423,6 +2425,7 @@ "saveAsEllipsis": "Сохранить как...", "saveAsTemplateEllipsis": "Сохранить как шаблон...", "shareEllipsis": "Поделиться...", + "shareHostUnavailable": "Недоступно: адрес сервера публикации в этом развёртывании некорректен.", "exportHtmlEllipsis": "Экспортировать как HTML...", "htmlFile": "HTML", "collaborateEllipsis": "Совместная работа...", diff --git a/apps/geolibre-desktop/src/i18n/locales/tr.json b/apps/geolibre-desktop/src/i18n/locales/tr.json index 9d3c9a461..c804a1795 100644 --- a/apps/geolibre-desktop/src/i18n/locales/tr.json +++ b/apps/geolibre-desktop/src/i18n/locales/tr.json @@ -1190,6 +1190,7 @@ "errorInvalidResponse": "Galeri yüklenemedi (geçersiz sunucu yanıtı).", "errorUnauthorized": "{{shareHost}} API belirteciniz geçersiz veya süresi dolmuş. Ayarlar'da güncelleyin.", "errorUsernameRequired": "Projelerinizi yüklemeden önce {{shareHost}} hesabınızda bir kullanıcı adı belirleyin.", + "errorNotConfigured": "Bu dağıtımda yapılandırılmış bir proje paylaşım sunucusu yok.", "retry": "Yeniden dene", "open": "Aç", "openCopy": "Bir kopyasını aç", @@ -1793,6 +1794,7 @@ "tokenTitle": "Share.GeoLibre API belirteci", "tokenDescription": "Proje > Paylaş, haritalarınızı {{shareHost}} adresine yüklemek için bu belirteci kullanır. {{shareHost}}/settings adresini açın, Ayarlar > API belirteçleri altında bir belirteç oluşturun, ardından aşağıdaki alana yapıştırın.", "tokenStorageNote": "Bu cihazda yerel olarak saklanır ve yalnızca yüklemeleri doğrulamak için {{shareHost}} adresine gönderilir. Web derlemesinde diğer site verileriyle aynı tarayıcı deposunu paylaşır; dolayısıyla makinenizin güvenliği ihlal edilirse {{shareHost}} üzerinden iptal edin.", + "tokenUnavailable": "Bu dağıtımda yapılandırılmış bir proje paylaşım sunucusu yok, bu nedenle API belirtecine gerek yoktur.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion belirteci", "cesiumTokenDescription": "3B küre (bölünmüş görünümlü bir bölme) Cesium Ion dünya görüntüleri ve arazisini kullanır; bunlar bir erişim belirteci gerektirir. ion.cesium.com/tokens adresinde ücretsiz bir hesap oluşturun, varsayılan erişim belirtecinizi kopyalayın ve aşağıya yapıştırın. Belirteç olmadan 3B küre anahtarı gizlenir.", @@ -2339,6 +2341,7 @@ "saveAsEllipsis": "Farklı Kaydet...", "saveAsTemplateEllipsis": "Şablon olarak kaydet...", "shareEllipsis": "Paylaş...", + "shareHostUnavailable": "Kullanılamıyor: bu dağıtımın paylaşım sunucusu adresi geçersiz.", "exportHtmlEllipsis": "HTML Olarak Dışa Aktar...", "htmlFile": "HTML", "collaborateEllipsis": "İş Birliği Yap...", diff --git a/apps/geolibre-desktop/src/i18n/locales/zh.json b/apps/geolibre-desktop/src/i18n/locales/zh.json index 638976963..dba471c4a 100644 --- a/apps/geolibre-desktop/src/i18n/locales/zh.json +++ b/apps/geolibre-desktop/src/i18n/locales/zh.json @@ -1155,6 +1155,7 @@ "errorInvalidResponse": "无法加载图库(服务器响应无效)。", "errorUnauthorized": "您的 {{shareHost}} API 令牌无效或已过期。请在设置中更新它。", "errorUsernameRequired": "在加载您的项目之前,请在您的 {{shareHost}} 账户上设置用户名。", + "errorNotConfigured": "此部署未配置项目共享服务器。", "retry": "重试", "open": "打开", "openCopy": "打开副本", @@ -1753,6 +1754,7 @@ "tokenTitle": "Share.GeoLibre API 令牌", "tokenDescription": "“项目 > 共享”使用此令牌将您的地图上传到 {{shareHost}}。请打开 {{shareHost}}/settings,在“设置 > API 令牌”下创建一个令牌,然后将其粘贴到下方字段中。", "tokenStorageNote": "仅存储在本设备上,并且仅发送到 {{shareHost}} 用于验证上传。在 Web 版本中,它与其他站点数据共用同一浏览器存储,因此如果您的设备遭到入侵,请在 {{shareHost}} 上撤销该令牌。", + "tokenUnavailable": "此部署未配置项目共享服务器,因此无需 API 令牌。", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion 令牌", "cesiumTokenDescription": "3D 地球(分屏视图窗格)使用 Cesium Ion 的世界影像和地形数据,需要访问令牌。请在 ion.cesium.com/tokens 创建免费账户,复制您的默认访问令牌,并粘贴到下方。若无令牌,3D 地球切换开关将被隐藏。", @@ -2297,6 +2299,7 @@ "saveAsEllipsis": "另存为...", "saveAsTemplateEllipsis": "另存为模板...", "shareEllipsis": "共享...", + "shareHostUnavailable": "不可用:此部署的共享服务器地址无效。", "exportHtmlEllipsis": "导出为 HTML...", "htmlFile": "HTML", "collaborateEllipsis": "协作...", diff --git a/apps/geolibre-desktop/src/lib/collab-client.ts b/apps/geolibre-desktop/src/lib/collab-client.ts index 4568d5484..7a5561cc6 100644 --- a/apps/geolibre-desktop/src/lib/collab-client.ts +++ b/apps/geolibre-desktop/src/lib/collab-client.ts @@ -37,7 +37,9 @@ export const COLLAB_URL_ENV = "VITE_GEOLIBRE_COLLAB_URL"; * * Only `wss://` (or `ws://` on loopback for a local relay) is accepted, * mirroring `resolveShareHost`: parse the URL and match the hostname exactly - * so a value like `ws://localhost.evil.com` is rejected. + * so a value like `ws://localhost.evil.com` is rejected. Credentials in the URL + * are rejected regardless of scheme, as `service_url()` in + * `docker/entrypoint.sh` does for the same value. * * @param configured - The raw value; read from the env when omitted. * @param deploymentEnv - Runtime env override, for tests. @@ -53,6 +55,7 @@ export function resolveCollabBaseUrl( const trimmed = value.trim().replace(/\/+$/, ""); try { const url = new URL(trimmed); + if (url.username || url.password) return null; if ( url.protocol === "wss:" || (url.protocol === "ws:" && diff --git a/apps/geolibre-desktop/src/lib/share-fetch.ts b/apps/geolibre-desktop/src/lib/share-fetch.ts index b03cfc951..eac034c1b 100644 --- a/apps/geolibre-desktop/src/lib/share-fetch.ts +++ b/apps/geolibre-desktop/src/lib/share-fetch.ts @@ -35,11 +35,20 @@ export function resetShareFetch(): void { shareFetch = (input, init) => fetch(input, init); } -/** The request URL's host, or null when it cannot be parsed. */ -function requestHost(input: RequestInfo | URL): string | null { +/** + * The request URL's origin, or null when it cannot be parsed. + * + * Origin, not host: the host of `http://maps.example.org` and + * `https://maps.example.org` is identical, so matching on host alone would route + * a plaintext request to a host configured over HTTPS through the CORS-exempt + * native client. + */ +export function requestOrigin(input: RequestInfo | URL): string | null { try { const href = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; - return new URL(href).host; + const origin = new URL(href).origin; + // `new URL("mailto:a@b").origin` is the string "null"; never match that. + return origin && origin !== "null" ? origin : null; } catch { return null; } @@ -49,7 +58,7 @@ function requestHost(input: RequestInfo | URL): string | null { * Route requests to the share host through Tauri's native HTTP client instead of * the WebView's `fetch`, bypassing browser CORS enforcement. Requests to any * other host keep the browser `fetch` unchanged, so the native, CORS-exempt - * client stays scoped to the single share host — which must also be listed in + * client stays scoped to the single share origin — which must also be listed in * the `http:default` capability scope (`src-tauri/capabilities/default.json`). * * The host is resolved from {@link resolveShareBaseUrl} (the configured or @@ -68,18 +77,14 @@ function requestHost(input: RequestInfo | URL): string | null { export async function installNativeShareFetch(): Promise { const baseUrl = resolveShareBaseUrl(); if (!baseUrl) return; - let shareHost: string | null; - try { - shareHost = new URL(baseUrl).host; - } catch { - shareHost = null; - } - if (!shareHost) return; + const shareOrigin = requestOrigin(baseUrl); + if (!shareOrigin) return; const { fetch: tauriFetch } = await import("@tauri-apps/plugin-http"); setShareFetch((input, init) => { - if (requestHost(input) !== shareHost) { - // Not the share host (e.g. a third-party thumbnail or project URL): keep - // the browser fetch, unchanged and outside the native capability scope. + if (requestOrigin(input) !== shareOrigin) { + // Not the share origin (a third-party thumbnail, a project URL, or the same + // host over plaintext): keep the browser fetch, unchanged and outside the + // native capability scope. return fetch(input, init); } return tauriFetch(input, init); diff --git a/apps/geolibre-desktop/src/lib/share-geolibre.ts b/apps/geolibre-desktop/src/lib/share-geolibre.ts index e37da81ce..68a85b752 100644 --- a/apps/geolibre-desktop/src/lib/share-geolibre.ts +++ b/apps/geolibre-desktop/src/lib/share-geolibre.ts @@ -125,14 +125,20 @@ export interface ShareHost { /** * Whether a URL is safe to send a Bearer token to: HTTPS anywhere, or HTTP on - * loopback for local development. + * loopback for local development, and never with credentials in the URL. * * The hostname is matched exactly rather than by prefix — `startsWith( * "http://localhost")` would also accept `http://localhost.evil.com`. A * self-hosted server on a private network therefore needs TLS; see * `docs/getting-started.md`. + * + * Embedded credentials are rejected regardless of scheme, mirroring the + * `service_url()` validator in `docker/entrypoint.sh`: a `https://user:pass@host` + * base would send Basic Auth alongside the Bearer token on every request, and + * the value reaches log output and error messages. */ function isSafeShareUrl(url: URL): boolean { + if (url.username || url.password) return false; if (url.protocol === "https:") return true; return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1"); } @@ -182,23 +188,28 @@ export function resolveShareBaseUrl(configured?: unknown): string | null { return resolveShareHost(configured).baseUrl; } +/** A base URL's host, falling back to the hosted default's when unparseable. */ +function hostOf(baseUrl: string): string { + try { + return new URL(baseUrl).host; + } catch { + return new URL(DEFAULT_SHARE_BASE_URL).host; + } +} + /** * Hostname to name in UI copy — the `{{shareHost}}` interpolation in the message * catalogues ("Sign in to {{shareHost}}") — so a self-hosted deployment reads its * own host instead of share.geolibre.app. * * Falls back to the hosted default's hostname when no usable host is configured. - * The copy that can still render in that state (the Settings token field) - * describes what the token is *for*, and the hosted service is its documented - * default. + * Callers must not render host-bearing copy in that state — see + * `shareHostStatus`; the fallback exists so this never returns an empty string, + * not as a licence to advertise the hosted service on a deployment that opted + * out. */ export function shareHostLabel(): string { - const base = resolveShareBaseUrl() ?? DEFAULT_SHARE_BASE_URL; - try { - return new URL(base).host; - } catch { - return new URL(DEFAULT_SHARE_BASE_URL).host; - } + return hostOf(resolveShareBaseUrl() ?? DEFAULT_SHARE_BASE_URL); } interface ShareProjectResponse { @@ -228,6 +239,9 @@ export async function uploadProjectToShare( throw new Error("No share server is configured for this deployment."); } const base = resolved.replace(/\/+$/, ""); + // Named in the failure messages below so a self-hosted deployment does not + // report an outage at share.geolibre.app. + const hostLabel = hostOf(base); // Defaults to the share fetch, which the desktop build routes through Tauri's // native HTTP client to bypass WebView CORS (see share-fetch.ts). const fetchImpl = options.fetchImpl ?? getShareFetch(); @@ -260,7 +274,7 @@ export async function uploadProjectToShare( throw new Error("Upload timed out. Please try again."); } } - throw new Error("Could not reach share.geolibre.app. Check your internet connection."); + throw new Error(`Could not reach ${hostLabel}. Check your internet connection.`); } if (!response.ok) { @@ -271,7 +285,7 @@ export async function uploadProjectToShare( const payload = (await response.json().catch(() => ({}))) as ShareProjectResponse; const project = payload.project; if (!project?.projectUrl || !project.rawJsonUrl) { - throw new Error("share.geolibre.app returned an unexpected response."); + throw new Error(`${hostLabel} returned an unexpected response.`); } return { username: project.username ?? "", diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 422e97a81..4892594cf 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -176,6 +176,11 @@ def service_url(name, value, schemes, loopback_schemes, loopback_hosts): the boot instead puts the error where an operator will actually see it. """ parsed = urlsplit(value) + # Checked before the loopback shortcut below, so the guarantee holds for every + # accepted value. Both of these are echoed to stdout further down, so a + # credentialed URL would also land in the container logs. + if parsed.username or parsed.password: + raise SystemExit(f"ERROR: {name} must not embed credentials.") if parsed.scheme in loopback_schemes and parsed.hostname in loopback_hosts: return value if parsed.scheme not in schemes or not parsed.netloc: @@ -183,8 +188,6 @@ def service_url(name, value, schemes, loopback_schemes, loopback_hosts): f"ERROR: {name} must be a {schemes[0]}:// URL " f"(or {loopback_schemes[0]}:// on {'/'.join(loopback_hosts)}), not {value!r}." ) - if parsed.username or parsed.password: - raise SystemExit(f"ERROR: {name} must not embed credentials.") return value @@ -213,8 +216,10 @@ with open("/usr/share/nginx/html/geolibre-runtime-config.js", "w") as output: ' if [ -n "${GEOLIBRE_SHARE_URL:-}" ]; then + # Case-insensitive to match the Python validator above and the client's + # resolveShareHost, both of which lowercase before comparing to "off". case "$GEOLIBRE_SHARE_URL" in - off | OFF | Off) echo "Project sharing disabled (GEOLIBRE_SHARE_URL=off)." ;; + [oO][fF][fF]) echo "Project sharing disabled (GEOLIBRE_SHARE_URL=off)." ;; *) echo "Project sharing server: $GEOLIBRE_SHARE_URL" ;; esac fi diff --git a/tests/collab-protocol.test.ts b/tests/collab-protocol.test.ts index 482bf83c2..6e4511d46 100644 --- a/tests/collab-protocol.test.ts +++ b/tests/collab-protocol.test.ts @@ -54,6 +54,11 @@ describe("resolveCollabBaseUrl", () => { ); }); + it("refuses credentials embedded in the URL, on any scheme", () => { + assert.equal(resolveCollabBaseUrl("wss://user:pass@collab.example.org"), null); + assert.equal(resolveCollabBaseUrl("ws://user:pass@127.0.0.1:8787"), null); + }); + it("still validates a deployment-provided value", () => { assert.equal(resolveCollabBaseUrl(undefined, { [COLLAB_URL_ENV]: "ws://relay.corp" }), null); assert.equal(resolveCollabBaseUrl(undefined, { [COLLAB_URL_ENV]: " " }), null); diff --git a/tests/share-fetch.test.ts b/tests/share-fetch.test.ts index 38a493848..b3a14bd86 100644 --- a/tests/share-fetch.test.ts +++ b/tests/share-fetch.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { afterEach, describe, it } from "node:test"; import { getShareFetch, + requestOrigin, resetShareFetch, setShareFetch, } from "../apps/geolibre-desktop/src/lib/share-fetch"; @@ -113,3 +114,41 @@ describe("share fetch override", () => { assert.equal(auth, "Bearer tok"); }); }); + +// installNativeShareFetch scopes the CORS-exempt native client by comparing +// these origins. Matching on host alone would let a plaintext request to a host +// configured over HTTPS through, so the scheme has to be part of the comparison. +describe("requestOrigin", () => { + it("distinguishes schemes on the same host", () => { + assert.equal( + requestOrigin("https://maps.example.org/api/projects"), + "https://maps.example.org", + ); + assert.equal(requestOrigin("http://maps.example.org/api/projects"), "http://maps.example.org"); + assert.notEqual( + requestOrigin("http://maps.example.org/api/projects"), + requestOrigin("https://maps.example.org"), + ); + }); + + it("keeps a non-default port distinct", () => { + assert.equal(requestOrigin("https://maps.example.org:8443/x"), "https://maps.example.org:8443"); + assert.notEqual( + requestOrigin("https://maps.example.org:8443/x"), + requestOrigin("https://maps.example.org/x"), + ); + }); + + it("accepts the Request and URL input shapes", () => { + assert.equal(requestOrigin(new URL("https://maps.example.org/a")), "https://maps.example.org"); + assert.equal( + requestOrigin(new Request("https://maps.example.org/a")), + "https://maps.example.org", + ); + }); + + it("returns null for values that have no real origin", () => { + assert.equal(requestOrigin("not a url"), null); + assert.equal(requestOrigin("mailto:someone@example.org"), null); + }); +}); diff --git a/tests/share-geolibre.test.ts b/tests/share-geolibre.test.ts index 67aa78be3..cf87fbbde 100644 --- a/tests/share-geolibre.test.ts +++ b/tests/share-geolibre.test.ts @@ -97,6 +97,14 @@ describe("resolveShareBaseUrl", () => { assert.equal(resolveShareBaseUrl("not a url"), null); }); + // Mirrors service_url() in docker/entrypoint.sh: a credentialed base would send + // Basic Auth alongside the Bearer token and leak into logs and error messages. + it("refuses credentials embedded in the URL, on any scheme", () => { + assert.equal(resolveShareBaseUrl("https://user:pass@maps.example.org"), null); + assert.equal(resolveShareBaseUrl("https://user@maps.example.org"), null); + assert.equal(resolveShareBaseUrl("http://user:pass@localhost:8000"), null); + }); + it('treats "off" as sharing disabled', () => { assert.equal(resolveShareBaseUrl("off"), null); assert.equal(resolveShareBaseUrl("OFF"), null); From 6b2b465730527749e59f6b63cbc1c7afdf141232 Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 3 Aug 2026 20:49:13 -0400 Subject: [PATCH 3/6] Address Claude review feedback - SettingsDialog.tsx: distinguish the two unusable share-host states. `disabled` keeps "no project sharing server configured"; `invalid` now gets a new `settings.env.tokenHostInvalid` string saying the configured address is not valid, so an operator who typo'd GEOLIBRE_SHARE_URL is not told they forgot to set it. Switched the component to `resolveShareHost()` so it has the status, and translated the new key into all 15 non-English catalogues. Chose a settings-namespaced key over reusing `toolbar.item.shareHostUnavailable` so a later edit to the menu string cannot silently change the Settings copy. --- .../src/components/layout/SettingsDialog.tsx | 15 +++++++++++---- apps/geolibre-desktop/src/i18n/locales/ar.json | 1 + apps/geolibre-desktop/src/i18n/locales/de.json | 1 + apps/geolibre-desktop/src/i18n/locales/en.json | 1 + apps/geolibre-desktop/src/i18n/locales/es.json | 1 + apps/geolibre-desktop/src/i18n/locales/fr.json | 1 + apps/geolibre-desktop/src/i18n/locales/hi.json | 1 + apps/geolibre-desktop/src/i18n/locales/id.json | 1 + apps/geolibre-desktop/src/i18n/locales/it.json | 1 + apps/geolibre-desktop/src/i18n/locales/ja.json | 1 + apps/geolibre-desktop/src/i18n/locales/ka.json | 1 + apps/geolibre-desktop/src/i18n/locales/ko.json | 1 + apps/geolibre-desktop/src/i18n/locales/nl.json | 1 + apps/geolibre-desktop/src/i18n/locales/pt.json | 1 + apps/geolibre-desktop/src/i18n/locales/ru.json | 1 + apps/geolibre-desktop/src/i18n/locales/tr.json | 1 + apps/geolibre-desktop/src/i18n/locales/zh.json | 1 + 17 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx b/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx index e5170a847..926570a2a 100644 --- a/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx +++ b/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx @@ -91,7 +91,7 @@ import type { ThemeMode } from "../../hooks/useThemeMode"; import { isTauri } from "../../lib/is-tauri"; import { THEME_SCHEMES, normalizeHexColor, type ThemeScheme } from "../../lib/theme-schemes"; import { IS_MAS_BUILD } from "../../lib/build-flags"; -import { resolveShareBaseUrl, shareHostLabel } from "../../lib/share-geolibre"; +import { resolveShareHost, shareHostLabel } from "../../lib/share-geolibre"; import { IS_STORE_BUILD, type UpdateNotificationLevel } from "../../lib/updates"; import { DATA_SOURCE_CATALOG, @@ -383,14 +383,21 @@ export function SettingsDialog({ // Derived from the resolved host so a self-hosted deployment links to its own // page; null when the deployment configured no share host, in which case the // description renders without a link rather than pointing at a stranger's site. - const shareBaseUrl = resolveShareBaseUrl(); + const shareHostState = resolveShareHost(); + const shareBaseUrl = shareHostState.baseUrl; const shareHost = shareHostLabel(); const shareSettingsUrl = shareBaseUrl ? `${shareBaseUrl}/settings` : null; // No usable host (sharing turned off, or a configured address that was // rejected) means the token field is dead: it would authenticate against a // server this deployment never talks to. Say so instead of rendering guidance - // that names the public hosted service — the whole point of the opt-out. + // that names the public hosted service — the whole point of the opt-out. The + // two unusable states get different copy: "not configured" would send an + // operator who typo'd the variable looking for one they never set. const shareTokenUsable = shareBaseUrl != null; + const shareTokenUnavailableMessage = + shareHostState.status === "invalid" + ? t("settings.env.tokenHostInvalid") + : t("settings.env.tokenUnavailable"); const { language, options: languageOptions, setLanguage } = useLanguage(); const preferences = useAppStore((s) => s.preferences); const setPreferences = useAppStore((s) => s.setPreferences); @@ -2321,7 +2328,7 @@ export function SettingsDialog({ ) : (

- {t("settings.env.tokenUnavailable")} + {shareTokenUnavailableMessage}

)}
diff --git a/apps/geolibre-desktop/src/i18n/locales/ar.json b/apps/geolibre-desktop/src/i18n/locales/ar.json index dfdecc78c..ee9955259 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ar.json +++ b/apps/geolibre-desktop/src/i18n/locales/ar.json @@ -1954,6 +1954,7 @@ "tokenDescription": "تستخدم قائمة مشروع > مشاركة هذا الرمز لرفع خرائطك إلى {{shareHost}}. افتح {{shareHost}}/settings، وأنشئ رمزًا من الإعدادات > رموز API، ثم الصقه في الحقل أدناه.", "tokenStorageNote": "يُخزن محليًا على هذا الجهاز ويُرسل فقط إلى {{shareHost}} لمصادقة عمليات الرفع. في نسخة الويب يتشارك مساحة تخزين المتصفح نفسها مع بيانات الموقع الأخرى، لذا ألغِه من {{shareHost}} إذا تعرض جهازك للاختراق.", "tokenUnavailable": "لا يوجد خادم لمشاركة المشاريع مُهيّأ في هذا النشر، لذا لا حاجة إلى رمز API.", + "tokenHostInvalid": "عنوان خادم مشاركة المشاريع في هذا النشر غير صالح، لذا لا يمكن استخدام هذا الرمز.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "رمز Cesium Ion", "cesiumTokenDescription": "تستخدم الكرة الأرضية ثلاثية الأبعاد (جزء في العرض المنقسم) صور العالم والتضاريس من Cesium Ion، وهي بيانات تتطلب رمز وصول. أنشئ حسابًا مجانيًا في ion.cesium.com/tokens، وانسخ رمز الوصول الافتراضي الخاص بك، ثم الصقه أدناه. من دون رمز يبقى مفتاح تبديل الكرة الأرضية ثلاثية الأبعاد مخفيًا.", diff --git a/apps/geolibre-desktop/src/i18n/locales/de.json b/apps/geolibre-desktop/src/i18n/locales/de.json index e8bf8ef20..407dda29f 100644 --- a/apps/geolibre-desktop/src/i18n/locales/de.json +++ b/apps/geolibre-desktop/src/i18n/locales/de.json @@ -1795,6 +1795,7 @@ "tokenDescription": "Projekt > Teilen verwendet dieses Token, um Ihre Karten auf {{shareHost}} hochzuladen. Öffnen Sie {{shareHost}}/settings, erstellen Sie ein Token unter Einstellungen > API-Token und fügen Sie es dann in das Feld unten ein.", "tokenStorageNote": "Wird lokal auf diesem Gerät gespeichert und nur an {{shareHost}} gesendet, um Uploads zu authentifizieren. Im Web-Build nutzt es denselben Browser-Speicher wie andere Website-Daten. Widerrufen Sie es daher auf {{shareHost}}, falls Ihr Gerät kompromittiert wurde.", "tokenUnavailable": "In dieser Bereitstellung ist kein Server zum Teilen von Projekten konfiguriert, daher ist kein API-Token erforderlich.", + "tokenHostInvalid": "Die Adresse des Servers zum Teilen von Projekten in dieser Bereitstellung ist ungültig, daher kann dieses Token nicht verwendet werden.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium-Ion-Token", "cesiumTokenDescription": "Der 3D-Globus (ein geteiltes Ansichtsfenster) verwendet Cesium-Ion-Weltbilder und -Gelände, wofür ein Zugriffstoken erforderlich ist. Erstellen Sie ein kostenloses Konto unter ion.cesium.com/tokens, kopieren Sie Ihr Standard-Zugriffstoken und fügen Sie es unten ein. Ohne Token ist der 3D-Globus-Umschalter ausgeblendet.", diff --git a/apps/geolibre-desktop/src/i18n/locales/en.json b/apps/geolibre-desktop/src/i18n/locales/en.json index 4a2c25744..1a15a59cd 100644 --- a/apps/geolibre-desktop/src/i18n/locales/en.json +++ b/apps/geolibre-desktop/src/i18n/locales/en.json @@ -1795,6 +1795,7 @@ "tokenDescription": "Project > Share uses this token to upload your maps to {{shareHost}}. Open {{shareHost}}/settings, create a token under Settings > API tokens, then paste it into the field below.", "tokenStorageNote": "Stored locally on this device and sent only to {{shareHost}} to authenticate uploads. On the web build it shares the same browser storage as other site data, so revoke it on {{shareHost}} if your machine is compromised.", "tokenUnavailable": "This deployment has no project sharing server configured, so no API token is needed.", + "tokenHostInvalid": "This deployment's project sharing server address is not valid, so this token cannot be used.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion token", "cesiumTokenDescription": "The 3D globe (a split-view pane) uses Cesium Ion world imagery and terrain, which need an access token. Create a free account at ion.cesium.com/tokens, copy your default access token, and paste it below. Without a token the 3D globe toggle is hidden.", diff --git a/apps/geolibre-desktop/src/i18n/locales/es.json b/apps/geolibre-desktop/src/i18n/locales/es.json index b4b05918b..df2acc0a4 100644 --- a/apps/geolibre-desktop/src/i18n/locales/es.json +++ b/apps/geolibre-desktop/src/i18n/locales/es.json @@ -1795,6 +1795,7 @@ "tokenDescription": "Proyecto > Compartir usa este token para subir sus mapas a {{shareHost}}. Abra {{shareHost}}/settings, cree un token en Configuración > Tokens de API y luego péguelo en el campo de abajo.", "tokenStorageNote": "Se almacena localmente en este dispositivo y solo se envía a {{shareHost}} para autenticar las subidas. En la compilación web, comparte el mismo almacenamiento del navegador que otros datos del sitio, así que revóquelo en {{shareHost}} si su equipo se ve comprometido.", "tokenUnavailable": "Esta implementación no tiene configurado ningún servidor para compartir proyectos, por lo que no se necesita ningún token de API.", + "tokenHostInvalid": "La dirección del servidor para compartir proyectos de esta implementación no es válida, por lo que este token no se puede usar.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token de Cesium Ion", "cesiumTokenDescription": "El globo 3D (un panel de vista dividida) usa imágenes y terreno mundiales de Cesium Ion, que necesitan un token de acceso. Cree una cuenta gratuita en ion.cesium.com/tokens, copie su token de acceso predeterminado y péguelo a continuación. Sin un token, el interruptor del globo 3D permanece oculto.", diff --git a/apps/geolibre-desktop/src/i18n/locales/fr.json b/apps/geolibre-desktop/src/i18n/locales/fr.json index 266c28e52..9f1befdff 100644 --- a/apps/geolibre-desktop/src/i18n/locales/fr.json +++ b/apps/geolibre-desktop/src/i18n/locales/fr.json @@ -1795,6 +1795,7 @@ "tokenDescription": "Projet > Partager utilise ce jeton pour téléverser vos cartes vers {{shareHost}}. Ouvrez {{shareHost}}/settings, créez un jeton sous Paramètres > Jetons API, puis collez-le dans le champ ci-dessous.", "tokenStorageNote": "Stocké localement sur cet appareil et envoyé uniquement à {{shareHost}} pour authentifier les téléversements. Dans la version web, il partage le même stockage de navigateur que les autres données du site, alors révoquez-le sur {{shareHost}} si votre machine est compromise.", "tokenUnavailable": "Aucun serveur de partage de projets n'est configuré pour ce déploiement, aucun jeton API n'est donc nécessaire.", + "tokenHostInvalid": "L'adresse du serveur de partage de projets de ce déploiement n'est pas valide, ce jeton ne peut donc pas être utilisé.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Jeton Cesium Ion", "cesiumTokenDescription": "Le globe 3D (un volet en vue partagée) utilise l'imagerie mondiale et le relief de Cesium Ion, qui nécessitent un jeton d'accès. Créez un compte gratuit sur ion.cesium.com/tokens, copiez votre jeton d'accès par défaut, puis collez-le ci-dessous. Sans jeton, le bouton du globe 3D est masqué.", diff --git a/apps/geolibre-desktop/src/i18n/locales/hi.json b/apps/geolibre-desktop/src/i18n/locales/hi.json index 7ea20eb8a..de0019b5a 100644 --- a/apps/geolibre-desktop/src/i18n/locales/hi.json +++ b/apps/geolibre-desktop/src/i18n/locales/hi.json @@ -1795,6 +1795,7 @@ "tokenDescription": "Project > Share इस टोकन का उपयोग आपके मानचित्रों को {{shareHost}} पर अपलोड करने के लिए करता है। {{shareHost}}/settings खोलें, Settings > API tokens के अंतर्गत एक टोकन बनाएँ, फिर उसे नीचे दिए गए फ़ील्ड में पेस्ट करें।", "tokenStorageNote": "इस डिवाइस पर स्थानीय रूप से संग्रहीत और अपलोड प्रमाणित करने के लिए केवल {{shareHost}} पर भेजा जाता है। वेब बिल्ड पर यह अन्य साइट डेटा के समान ब्राउज़र स्टोरेज साझा करता है, इसलिए यदि आपकी मशीन से समझौता हो जाए तो इसे {{shareHost}} पर रद्द करें।", "tokenUnavailable": "इस परिनियोजन में कोई प्रोजेक्ट साझाकरण सर्वर कॉन्फ़िगर नहीं है, इसलिए किसी API टोकन की आवश्यकता नहीं है।", + "tokenHostInvalid": "इस परिनियोजन के प्रोजेक्ट साझाकरण सर्वर का पता अमान्य है, इसलिए इस टोकन का उपयोग नहीं किया जा सकता।", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion टोकन", "cesiumTokenDescription": "3D ग्लोब (एक स्प्लिट-व्यू पैनल) Cesium Ion वर्ल्ड इमेजरी और टेरेन का उपयोग करता है, जिसके लिए एक एक्सेस टोकन चाहिए। ion.cesium.com/tokens पर एक मुफ़्त खाता बनाएँ, अपना डिफ़ॉल्ट एक्सेस टोकन कॉपी करें, और उसे नीचे पेस्ट करें। टोकन के बिना 3D ग्लोब टॉगल छिपा रहता है।", diff --git a/apps/geolibre-desktop/src/i18n/locales/id.json b/apps/geolibre-desktop/src/i18n/locales/id.json index be740edd1..58e24b438 100644 --- a/apps/geolibre-desktop/src/i18n/locales/id.json +++ b/apps/geolibre-desktop/src/i18n/locales/id.json @@ -1755,6 +1755,7 @@ "tokenDescription": "Project > Share menggunakan token ini untuk mengunggah peta Anda ke {{shareHost}}. Buka {{shareHost}}/settings, buat token di Settings > API tokens, lalu tempelkan ke bidang di bawah.", "tokenStorageNote": "Disimpan secara lokal di perangkat ini dan hanya dikirim ke {{shareHost}} untuk mengautentikasi unggahan. Pada build web, token ini berbagi penyimpanan browser yang sama dengan data situs lainnya, jadi cabut aksesnya di {{shareHost}} jika perangkat Anda disusupi.", "tokenUnavailable": "Penerapan ini tidak memiliki server pembagian proyek yang dikonfigurasi, sehingga token API tidak diperlukan.", + "tokenHostInvalid": "Alamat server pembagian proyek pada penerapan ini tidak valid, sehingga token ini tidak dapat digunakan.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token Cesium Ion", "cesiumTokenDescription": "Bola dunia 3D (panel tampilan terpisah) menggunakan citra dan medan dunia Cesium Ion, yang memerlukan token akses. Buat akun gratis di ion.cesium.com/tokens, salin token akses default Anda, dan tempelkan di bawah. Tanpa token, tombol bola dunia 3D disembunyikan.", diff --git a/apps/geolibre-desktop/src/i18n/locales/it.json b/apps/geolibre-desktop/src/i18n/locales/it.json index 59efe0755..4eb6b0a6b 100644 --- a/apps/geolibre-desktop/src/i18n/locales/it.json +++ b/apps/geolibre-desktop/src/i18n/locales/it.json @@ -1795,6 +1795,7 @@ "tokenDescription": "Progetto > Condividi usa questo token per caricare le tue mappe su {{shareHost}}. Apri {{shareHost}}/settings, crea un token in Settings > API tokens, quindi incollalo nel campo sottostante.", "tokenStorageNote": "Memorizzato localmente su questo dispositivo e inviato solo a {{shareHost}} per autenticare i caricamenti. Nella build web condivide lo stesso archivio del browser degli altri dati del sito, quindi revocalo su {{shareHost}} se il tuo computer viene compromesso.", "tokenUnavailable": "Questo deployment non ha alcun server di condivisione dei progetti configurato, quindi non è necessario alcun token API.", + "tokenHostInvalid": "L'indirizzo del server di condivisione dei progetti di questo deployment non è valido, quindi questo token non può essere usato.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token Cesium Ion", "cesiumTokenDescription": "Il globo 3D (un riquadro a schermo diviso) usa le immagini satellitari e il terreno di Cesium Ion, che richiedono un token di accesso. Crea un account gratuito su ion.cesium.com/tokens, copia il tuo token di accesso predefinito e incollalo di seguito. Senza un token, l'interruttore del globo 3D resta nascosto.", diff --git a/apps/geolibre-desktop/src/i18n/locales/ja.json b/apps/geolibre-desktop/src/i18n/locales/ja.json index d6645482c..1d178336a 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ja.json +++ b/apps/geolibre-desktop/src/i18n/locales/ja.json @@ -1755,6 +1755,7 @@ "tokenDescription": "プロジェクト > 共有では、このトークンを使用して地図を{{shareHost}}にアップロードします。{{shareHost}}/settings を開き、設定 > APIトークン でトークンを作成して、下のフィールドに貼り付けてください。", "tokenStorageNote": "このデバイスにローカルで保存され、アップロードの認証のために{{shareHost}}にのみ送信されます。Webビルドでは他のサイトデータと同じブラウザストレージを共有するため、端末が侵害された場合は{{shareHost}}でトークンを失効させてください。", "tokenUnavailable": "このデプロイにはプロジェクト共有サーバーが設定されていないため、APIトークンは必要ありません。", + "tokenHostInvalid": "このデプロイのプロジェクト共有サーバーのアドレスが無効なため、このトークンは使用できません。", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ionトークン", "cesiumTokenDescription": "3Dグローブ(分割表示ペイン)はCesium Ionの世界衛星画像と地形データを使用しており、アクセストークンが必要です。ion.cesium.com/tokens で無料アカウントを作成し、デフォルトのアクセストークンをコピーして下に貼り付けてください。トークンがない場合、3Dグローブの切り替えは表示されません。", diff --git a/apps/geolibre-desktop/src/i18n/locales/ka.json b/apps/geolibre-desktop/src/i18n/locales/ka.json index a6ac8aef8..ef5c8e857 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ka.json +++ b/apps/geolibre-desktop/src/i18n/locales/ka.json @@ -1795,6 +1795,7 @@ "tokenDescription": "„პროექტი > გაზიარება“ იყენებს ამ token-ს თქვენი რუკების {{shareHost}}-ზე ასატვირთად. გახსენით {{shareHost}}/settings, შექმენით token განყოფილებაში „Settings > API tokens“, შემდეგ ჩასვით ქვემოთ ველში.", "tokenStorageNote": "ინახება ლოკალურად ამ მოწყობილობაზე და იგზავნება მხოლოდ {{shareHost}}-ზე ატვირთვების ავთენტიფიკაციისთვის. ვებ-ვერსიაში ის იზიარებს იმავე ბრაუზერის საცავს, რასაც საიტის სხვა მონაცემები, ამიტომ გააუქმეთ ის {{shareHost}}-ზე, თუ თქვენი მანქანა კომპრომეტირებულია.", "tokenUnavailable": "ამ განთავსებაში პროექტების გაზიარების სერვერი არ არის კონფიგურირებული, ამიტომ API token საჭირო არ არის.", + "tokenHostInvalid": "ამ განთავსების პროექტების გაზიარების სერვერის მისამართი არასწორია, ამიტომ ამ token-ის გამოყენება შეუძლებელია.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion-ის token", "cesiumTokenDescription": "3D გლობუსი (გაყოფილი ხედის პანელი) იყენებს Cesium Ion-ის მსოფლიო სურათებსა და რელიეფს, რასაც წვდომის token სჭირდება. შექმენით უფასო ანგარიში მისამართზე ion.cesium.com/tokens, დააკოპირეთ თქვენი ნაგულისხმევი წვდომის token და ჩასვით ქვემოთ. token-ის გარეშე 3D გლობუსის გადამრთველი დამალულია.", diff --git a/apps/geolibre-desktop/src/i18n/locales/ko.json b/apps/geolibre-desktop/src/i18n/locales/ko.json index 06dd8538e..8e6f14b25 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ko.json +++ b/apps/geolibre-desktop/src/i18n/locales/ko.json @@ -1755,6 +1755,7 @@ "tokenDescription": "프로젝트 > 공유는 이 토큰을 사용하여 지도를 {{shareHost}}에 업로드합니다. {{shareHost}}/settings를 열고 설정 > API 토큰에서 토큰을 생성한 다음 아래 필드에 붙여넣으세요.", "tokenStorageNote": "이 기기에 로컬로 저장되며 업로드 인증을 위해 {{shareHost}}에만 전송됩니다. 웹 빌드에서는 다른 사이트 데이터와 동일한 브라우저 저장소를 공유하므로, 기기가 침해된 경우 {{shareHost}}에서 토큰을 폐기하세요.", "tokenUnavailable": "이 배포에는 프로젝트 공유 서버가 구성되어 있지 않으므로 API 토큰이 필요하지 않습니다.", + "tokenHostInvalid": "이 배포의 프로젝트 공유 서버 주소가 올바르지 않아 이 토큰을 사용할 수 없습니다.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion 토큰", "cesiumTokenDescription": "3D 지구본(분할 화면 패널)은 접근 토큰이 필요한 Cesium Ion 세계 영상과 지형을 사용합니다. ion.cesium.com/tokens에서 무료 계정을 만들고 기본 접근 토큰을 복사하여 아래에 붙여넣으세요. 토큰이 없으면 3D 지구본 전환 버튼이 숨겨집니다.", diff --git a/apps/geolibre-desktop/src/i18n/locales/nl.json b/apps/geolibre-desktop/src/i18n/locales/nl.json index 7695f3346..7c65a5510 100644 --- a/apps/geolibre-desktop/src/i18n/locales/nl.json +++ b/apps/geolibre-desktop/src/i18n/locales/nl.json @@ -1795,6 +1795,7 @@ "tokenDescription": "Project > Delen gebruikt dit token om uw kaarten te uploaden naar {{shareHost}}. Open {{shareHost}}/settings, maak een token aan onder Instellingen > API-tokens en plak het vervolgens in het onderstaande veld.", "tokenStorageNote": "Lokaal opgeslagen op dit apparaat en alleen verzonden naar {{shareHost}} om uploads te verifiëren. In de webversie wordt dezelfde browseropslag gebruikt als voor andere sitegegevens; trek het token daarom in op {{shareHost}} als uw machine gecompromitteerd is.", "tokenUnavailable": "Deze implementatie heeft geen server voor het delen van projecten geconfigureerd, dus er is geen API-token nodig.", + "tokenHostInvalid": "Het adres van de server voor het delen van projecten in deze implementatie is ongeldig, dus dit token kan niet worden gebruikt.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion-token", "cesiumTokenDescription": "De 3D-globe (een gesplitst weergavepaneel) gebruikt wereldbeelden en terrein van Cesium Ion, waarvoor een toegangstoken nodig is. Maak een gratis account aan op ion.cesium.com/tokens, kopieer uw standaard toegangstoken en plak het hieronder. Zonder token is de schakelaar voor de 3D-globe verborgen.", diff --git a/apps/geolibre-desktop/src/i18n/locales/pt.json b/apps/geolibre-desktop/src/i18n/locales/pt.json index ef26cec37..7dad9fa93 100644 --- a/apps/geolibre-desktop/src/i18n/locales/pt.json +++ b/apps/geolibre-desktop/src/i18n/locales/pt.json @@ -1795,6 +1795,7 @@ "tokenDescription": "Projeto > Compartilhar usa este token para enviar seus mapas para {{shareHost}}. Abra {{shareHost}}/settings, crie um token em Configurações > Tokens de API e, em seguida, cole-o no campo abaixo.", "tokenStorageNote": "Armazenado localmente neste dispositivo e enviado apenas para {{shareHost}} para autenticar envios. Na versão web, ele compartilha o mesmo armazenamento do navegador que outros dados do site, então revogue-o em {{shareHost}} se sua máquina for comprometida.", "tokenUnavailable": "Esta implantação não tem nenhum servidor de compartilhamento de projetos configurado, portanto nenhum token de API é necessário.", + "tokenHostInvalid": "O endereço do servidor de compartilhamento de projetos desta implantação não é válido, portanto este token não pode ser usado.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Token do Cesium Ion", "cesiumTokenDescription": "O globo 3D (um painel de visualização dividida) usa imagens e terreno mundiais do Cesium Ion, que precisam de um token de acesso. Crie uma conta gratuita em ion.cesium.com/tokens, copie seu token de acesso padrão e cole-o abaixo. Sem um token, o alternador do globo 3D fica oculto.", diff --git a/apps/geolibre-desktop/src/i18n/locales/ru.json b/apps/geolibre-desktop/src/i18n/locales/ru.json index ba08943fe..42a9137b0 100644 --- a/apps/geolibre-desktop/src/i18n/locales/ru.json +++ b/apps/geolibre-desktop/src/i18n/locales/ru.json @@ -1875,6 +1875,7 @@ "tokenDescription": "Раздел Проект > Поделиться использует этот токен для загрузки ваших карт на {{shareHost}}. Откройте {{shareHost}}/settings, создайте токен в разделе Настройки > API-токены, затем вставьте его в поле ниже.", "tokenStorageNote": "Хранится локально на этом устройстве и передаётся только на {{shareHost}} для аутентификации загрузок. В веб-сборке используется то же хранилище браузера, что и для других данных сайта; отзовите токен на {{shareHost}}, если ваше устройство скомпрометировано.", "tokenUnavailable": "В этом развёртывании не настроен сервер для публикации проектов, поэтому API-токен не нужен.", + "tokenHostInvalid": "Адрес сервера публикации проектов в этом развёртывании некорректен, поэтому этот токен использовать нельзя.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Токен Cesium Ion", "cesiumTokenDescription": "3D-глобус (панель раздельного вида) использует мировые снимки и рельеф Cesium Ion, для которых требуется токен доступа. Создайте бесплатную учётную запись на ion.cesium.com/tokens, скопируйте свой токен доступа по умолчанию и вставьте его ниже. Без токена переключатель 3D-глобуса скрыт.", diff --git a/apps/geolibre-desktop/src/i18n/locales/tr.json b/apps/geolibre-desktop/src/i18n/locales/tr.json index c804a1795..01ff59549 100644 --- a/apps/geolibre-desktop/src/i18n/locales/tr.json +++ b/apps/geolibre-desktop/src/i18n/locales/tr.json @@ -1795,6 +1795,7 @@ "tokenDescription": "Proje > Paylaş, haritalarınızı {{shareHost}} adresine yüklemek için bu belirteci kullanır. {{shareHost}}/settings adresini açın, Ayarlar > API belirteçleri altında bir belirteç oluşturun, ardından aşağıdaki alana yapıştırın.", "tokenStorageNote": "Bu cihazda yerel olarak saklanır ve yalnızca yüklemeleri doğrulamak için {{shareHost}} adresine gönderilir. Web derlemesinde diğer site verileriyle aynı tarayıcı deposunu paylaşır; dolayısıyla makinenizin güvenliği ihlal edilirse {{shareHost}} üzerinden iptal edin.", "tokenUnavailable": "Bu dağıtımda yapılandırılmış bir proje paylaşım sunucusu yok, bu nedenle API belirtecine gerek yoktur.", + "tokenHostInvalid": "Bu dağıtımın proje paylaşım sunucusu adresi geçersiz, bu nedenle bu belirteç kullanılamaz.", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion belirteci", "cesiumTokenDescription": "3B küre (bölünmüş görünümlü bir bölme) Cesium Ion dünya görüntüleri ve arazisini kullanır; bunlar bir erişim belirteci gerektirir. ion.cesium.com/tokens adresinde ücretsiz bir hesap oluşturun, varsayılan erişim belirtecinizi kopyalayın ve aşağıya yapıştırın. Belirteç olmadan 3B küre anahtarı gizlenir.", diff --git a/apps/geolibre-desktop/src/i18n/locales/zh.json b/apps/geolibre-desktop/src/i18n/locales/zh.json index dba471c4a..05b0196e1 100644 --- a/apps/geolibre-desktop/src/i18n/locales/zh.json +++ b/apps/geolibre-desktop/src/i18n/locales/zh.json @@ -1755,6 +1755,7 @@ "tokenDescription": "“项目 > 共享”使用此令牌将您的地图上传到 {{shareHost}}。请打开 {{shareHost}}/settings,在“设置 > API 令牌”下创建一个令牌,然后将其粘贴到下方字段中。", "tokenStorageNote": "仅存储在本设备上,并且仅发送到 {{shareHost}} 用于验证上传。在 Web 版本中,它与其他站点数据共用同一浏览器存储,因此如果您的设备遭到入侵,请在 {{shareHost}} 上撤销该令牌。", "tokenUnavailable": "此部署未配置项目共享服务器,因此无需 API 令牌。", + "tokenHostInvalid": "此部署的项目共享服务器地址无效,因此无法使用此令牌。", "tokenPlaceholder": "glb_…", "cesiumTokenTitle": "Cesium Ion 令牌", "cesiumTokenDescription": "3D 地球(分屏视图窗格)使用 Cesium Ion 的世界影像和地形数据,需要访问令牌。请在 ion.cesium.com/tokens 创建免费账户,复制您的默认访问令牌,并粘贴到下方。若无令牌,3D 地球切换开关将被隐藏。", From 0e4b1f18d675703aac29ac3f0ee45617dc333636 Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 3 Aug 2026 20:57:46 -0400 Subject: [PATCH 4/6] Address review feedback - share-geolibre.ts: `hostOf` now keeps a base URL's path, so a server hosted under a subpath (`https://example.test/geolibre`) is named the way the links built from the same base resolve. Previously the copy read `example.test` beside a link to `https://example.test/geolibre/settings` (CodeRabbit). Rejecting paths outright was the other option offered, but that would refuse a legitimate subpath deployment. - ShareProjectDialog.tsx: drop the two `settingsUrl &&` checks, dead since the early guard added in 8a879a48 makes it non-null past that point (Claude). - docker/entrypoint.sh: trim the logged values the way the Python block trims the validated ones, so `" off "` logs as disabled instead of printing a "sharing server" line that disagrees with the config actually written (Claude). Added `shareHostLabel` tests covering the subpath case (with and without a trailing slash) and the unusable-host fallback. Re-checked the boot log across `off`/`" off "`/`oFF`/a padded URL, and `sh -n`/`bash -n` on the entrypoint. --- .../components/layout/ShareProjectDialog.tsx | 38 ++++++++-------- .../src/lib/share-geolibre.ts | 14 +++++- docker/entrypoint.sh | 18 +++++--- tests/share-geolibre.test.ts | 43 +++++++++++++++++++ 4 files changed, 85 insertions(+), 28 deletions(-) diff --git a/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx b/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx index 7aa058234..19b728d1d 100644 --- a/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx +++ b/apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx @@ -213,16 +213,14 @@ export function ShareProjectDialog({

{t("share.step1Description", { shareHost })}

- {settingsUrl && ( - - )} +
  • {t("share.step2Title")}

    @@ -299,17 +297,15 @@ export function ShareProjectDialog({ className="space-y-2 rounded-md bg-destructive/10 p-3 text-sm text-destructive" >

    {t("share.usernameRequired", { shareHost })}

    - {settingsUrl && ( - - )} +
  • ) : error ? (

    diff --git a/apps/geolibre-desktop/src/lib/share-geolibre.ts b/apps/geolibre-desktop/src/lib/share-geolibre.ts index 68a85b752..ed9fefef9 100644 --- a/apps/geolibre-desktop/src/lib/share-geolibre.ts +++ b/apps/geolibre-desktop/src/lib/share-geolibre.ts @@ -188,10 +188,20 @@ export function resolveShareBaseUrl(configured?: unknown): string | null { return resolveShareHost(configured).baseUrl; } -/** A base URL's host, falling back to the hosted default's when unparseable. */ +/** + * A base URL's host, plus its path when it has one, falling back to the hosted + * default's host when unparseable. + * + * The path is kept so a server hosted under a subpath + * (`https://example.test/geolibre`) is named the way the links built from the + * same base URL resolve — dropping it would show `example.test` next to a link to + * `https://example.test/geolibre/settings`. + */ function hostOf(baseUrl: string): string { try { - return new URL(baseUrl).host; + const url = new URL(baseUrl); + const path = url.pathname.replace(/\/+$/, ""); + return path ? `${url.host}${path}` : url.host; } catch { return new URL(DEFAULT_SHARE_BASE_URL).host; } diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 4892594cf..3a32ef182 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -215,17 +215,25 @@ with open("/usr/share/nginx/html/geolibre-runtime-config.js", "w") as output: output.write(";\n") ' -if [ -n "${GEOLIBRE_SHARE_URL:-}" ]; then +# Strip surrounding whitespace exactly as the Python block above does, so the boot +# log reports the value that actually landed in the runtime config rather than the +# raw variable (`" off "` is disabled there, and should read as disabled here too). +trim() { + printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' +} + +if [ -n "$(trim "${GEOLIBRE_SHARE_URL:-}")" ]; then + SHARE_URL_LOG=$(trim "$GEOLIBRE_SHARE_URL") # Case-insensitive to match the Python validator above and the client's # resolveShareHost, both of which lowercase before comparing to "off". - case "$GEOLIBRE_SHARE_URL" in + case "$SHARE_URL_LOG" in [oO][fF][fF]) echo "Project sharing disabled (GEOLIBRE_SHARE_URL=off)." ;; - *) echo "Project sharing server: $GEOLIBRE_SHARE_URL" ;; + *) echo "Project sharing server: $SHARE_URL_LOG" ;; esac fi -if [ -n "${GEOLIBRE_COLLAB_URL:-}" ]; then - echo "Collaboration relay: $GEOLIBRE_COLLAB_URL" +if [ -n "$(trim "${GEOLIBRE_COLLAB_URL:-}")" ]; then + echo "Collaboration relay: $(trim "$GEOLIBRE_COLLAB_URL")" fi if [ -n "${GEOLIBRE_EMBED_ORIGINS:-}" ]; then diff --git a/tests/share-geolibre.test.ts b/tests/share-geolibre.test.ts index cf87fbbde..db4a99b56 100644 --- a/tests/share-geolibre.test.ts +++ b/tests/share-geolibre.test.ts @@ -8,6 +8,7 @@ import { resolveShareBaseUrl, resolveShareHost, SHARE_URL_ENV, + shareHostLabel, ShareUploadError, uploadProjectToShare, } from "../apps/geolibre-desktop/src/lib/share-geolibre"; @@ -156,6 +157,48 @@ describe("resolveShareHost", () => { }); }); +describe("shareHostLabel", () => { + function withShareUrl(value: string, run: () => T): T { + (globalThis as { window?: unknown }).window = { + __GEOLIBRE_DEPLOYMENT_ENV__: { [SHARE_URL_ENV]: value }, + }; + try { + return run(); + } finally { + delete (globalThis as { window?: unknown }).window; + } + } + + it("names the host of the configured server", () => { + // Nothing configured resolves to the hosted default. + assert.equal(shareHostLabel(), new URL(DEFAULT_SHARE_BASE_URL).host); + assert.equal( + withShareUrl("https://maps.example.org", () => shareHostLabel()), + "maps.example.org", + ); + }); + + // A server under a subpath must be named the way links built from the same base + // resolve, so the copy and the account-settings link agree. + it("keeps a subpath so the label matches the links built from the base", () => { + assert.equal( + withShareUrl("https://example.test/geolibre", () => shareHostLabel()), + "example.test/geolibre", + ); + assert.equal( + withShareUrl("https://example.test/geolibre/", () => shareHostLabel()), + "example.test/geolibre", + ); + }); + + it("names an unusable host as the hosted default rather than an empty string", () => { + assert.equal( + withShareUrl("off", () => shareHostLabel()), + new URL(DEFAULT_SHARE_BASE_URL).host, + ); + }); +}); + describe("uploadProjectToShare", () => { it("rejects when no token is provided", async () => { await assert.rejects(() => uploadProjectToShare({ ...baseArgs, token: " " }), /token/i); From 2153097260c4022eba4a64df8c5a25a0147fa86d Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 3 Aug 2026 21:06:20 -0400 Subject: [PATCH 5/6] Address Claude review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker/nginx.conf + entrypoint.sh: allow a self-hosted collaboration relay in the CSP. `connect-src` has a bare `https:` (so any share host works) but no bare `wss:` — only `wss://collab.geolibre.app` was listed. A container started with `-e GEOLIBRE_COLLAB_URL=wss://relay.example.org` (the example this PR added to the docs) passed validation, advertised collaboration in the UI, and then had its WebSocket blocked by the browser. The entrypoint now substitutes that relay's origin into a `__GEOLIBRE_COLLAB_CONNECT_SRC__` placeholder while rendering the nginx template, alongside the existing sidecar-token substitution. - SettingsDialog.tsx: drop the unreachable `` fallback for `tokenLink`; the branch requires `shareBaseUrl`, which `shareSettingsUrl` is derived from. - docs: note that the CSP is handled automatically for the web/Docker path, in contrast to the desktop build's manual `connect-src` edit. Verified the render step over four inputs: unset, a wss relay, a relay with a port and path (origin only, path stripped — CSP source expressions take no path), and a loopback ws relay. With nothing configured the emitted `connect-src` is byte-identical to the one on `main`. --- .../src/components/layout/SettingsDialog.tsx | 8 ++++---- docker/entrypoint.sh | 20 ++++++++++++++++++- docker/nginx.conf | 8 +++++++- docs/collaboration.md | 6 +++++- docs/getting-started.md | 5 +++++ 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx b/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx index 926570a2a..bb814600d 100644 --- a/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx +++ b/apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx @@ -2300,15 +2300,15 @@ export function SettingsDialog({ i18nKey="settings.env.tokenDescription" values={{ shareHost }} components={{ - tokenLink: shareSettingsUrl ? ( + // Non-null here: this branch requires shareBaseUrl, + // which is what shareSettingsUrl is derived from. + tokenLink: ( - ) : ( - ), }} /> diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 3a32ef182..9cb7249d4 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -248,10 +248,28 @@ fi # surprises). python -c ' import os +from urllib.parse import urlsplit + token = os.environ["GEOLIBRE_SIDECAR_TOKEN"] + +# A self-hosted relay has to be allowed in connect-src or the browser blocks its +# WebSocket: the directive has a bare "https:" (so any share host works) but no +# bare "wss:". Only the origin is inserted -- CSP source expressions do not take a +# path, and the value was already validated above. +collab = os.environ.get("GEOLIBRE_COLLAB_URL", "").strip() +# Carries its own leading space so the header is byte-identical to the template +# when no relay is configured. +collab_src = "" +if collab: + parsed = urlsplit(collab) + if parsed.scheme and parsed.netloc: + collab_src = f" {parsed.scheme}://{parsed.netloc}" + src = open("/etc/nginx/nginx.conf.template").read() open("/etc/nginx/conf.d/default.conf", "w").write( - src.replace("__GEOLIBRE_SIDECAR_TOKEN__", token) + src.replace("__GEOLIBRE_SIDECAR_TOKEN__", token).replace( + "__GEOLIBRE_COLLAB_CONNECT_SRC__", collab_src + ) ) ' diff --git a/docker/nginx.conf b/docker/nginx.conf index ede09587a..a27e6474f 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -77,6 +77,12 @@ server { # maplibre-gl-vector's DuckDB-WASM + geojson-vt/vt-pbf (Add Vector # Layer) loaded via dynamic import()/importScripts(), and /pyodide/ # covers the Pyodide vector engine. + # __GEOLIBRE_COLLAB_CONNECT_SRC__ is replaced at boot by entrypoint.sh with + # the origin of GEOLIBRE_COLLAB_URL (empty when unset), so a self-hosted + # relay is reachable without editing this file. connect-src has a bare + # `https:` but no bare `wss:`, so a custom relay would otherwise pass the + # entrypoint's validation, be advertised in the UI, and then have its + # WebSocket blocked here. # When adding or removing a *named* host from any CSP directive (e.g. a # collaboration server in connect-src, or a new CDN in script-src), # mirror the change in the Tauri CSP in @@ -92,7 +98,7 @@ server { # launched JupyterLab server in the Notebook panel. That is desktop-only # and intentionally NOT mirrored here: the web build embeds the # same-origin self-hosted JupyterLite site, already covered by 'self'. - add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https: data: blob: http://127.0.0.1:* http://localhost:* wss://collab.geolibre.app ws://127.0.0.1:* ws://localhost:*; img-src 'self' data: blob: https:; media-src 'self' blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' blob: 'unsafe-eval' 'wasm-unsafe-eval' https://cdn.jsdelivr.net/npm/ https://cdn.jsdelivr.net/pyodide/ https://accounts.google.com; child-src 'self' https://accounts.google.com https://www.google.com; frame-src 'self' https://accounts.google.com https://www.google.com; worker-src blob: 'self'" always; + add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https: data: blob: http://127.0.0.1:* http://localhost:* wss://collab.geolibre.app__GEOLIBRE_COLLAB_CONNECT_SRC__ ws://127.0.0.1:* ws://localhost:*; img-src 'self' data: blob: https:; media-src 'self' blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' blob: 'unsafe-eval' 'wasm-unsafe-eval' https://cdn.jsdelivr.net/npm/ https://cdn.jsdelivr.net/pyodide/ https://accounts.google.com; child-src 'self' https://accounts.google.com https://www.google.com; frame-src 'self' https://accounts.google.com https://www.google.com; worker-src blob: 'self'" always; } # The service worker has a stable filename, so it must always revalidate; diff --git a/docs/collaboration.md b/docs/collaboration.md index 90387d1c1..8a2cdb307 100644 --- a/docs/collaboration.md +++ b/docs/collaboration.md @@ -207,7 +207,11 @@ In the Docker image the same setting is available at **container runtime** as `geolibre-runtime-config.js`, and `resolveCollabBaseUrl()` prefers that over the build-time variable — so a prebuilt image can be pointed at a self-hosted relay without a rebuild. A value that is not `wss://` (or `ws://` on loopback) fails the -container boot rather than silently leaving collaboration dark. See +container boot rather than silently leaving collaboration dark. The entrypoint also +substitutes the relay's origin into the nginx CSP's `connect-src` +(`__GEOLIBRE_COLLAB_CONNECT_SRC__` in `docker/nginx.conf`), since that directive +has no bare `wss:` — so unlike the desktop build below, the web/Docker path needs +no manual CSP edit. See [Run with Docker](getting-started.md#self-hosted-sharing-and-collaboration-servers). > **Self-hosting note:** the desktop CSP pins `wss://collab.geolibre.app` (plus diff --git a/docs/getting-started.md b/docs/getting-started.md index fc77f2d89..a21cd1530 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -295,6 +295,11 @@ restarting it with different values — no rebuild. (The equivalent build arguments, `VITE_GEOLIBRE_SHARE_URL` and `VITE_GEOLIBRE_COLLAB_URL`, exist for baking a default into your own image.) +When `GEOLIBRE_COLLAB_URL` is set, the entrypoint also adds that relay's origin to +the container's `Content-Security-Policy` `connect-src`, so the browser is allowed +to open the WebSocket. (The directive has a bare `https:`, which covers any share +server, but no bare `wss:`.) No manual edit of `docker/nginx.conf` is needed. + Both must use TLS — `https://` for the share server, `wss://` for the relay — because the app sends your API token to the share server with every request. Plaintext is accepted only on `localhost` / `127.0.0.1` for local development, so From 2e8ecb4641c7b5b000a916c3836d2c72befa64dc Mon Sep 17 00:00:00 2001 From: giswqs Date: Mon, 3 Aug 2026 21:15:47 -0400 Subject: [PATCH 6/6] Address Claude review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Harden the value that now reaches the nginx CSP. - docker/entrypoint.sh: `service_url()` rejects a netloc containing anything outside `[A-Za-z0-9.\-:\[\]]`. `urlsplit()` puts everything up to the next `/`, `?` or `#` into netloc — quotes and semicolons included — and 21530972 started substituting that unescaped into the double-quoted `add_header Content-Security-Policy` value, so `wss://x"; add_header X-Pwned "1` could have injected nginx directives at boot. - The check runs *before* the loopback early-return, which is load-bearing: `urlsplit('ws://localhost:8080"; …')` reports hostname `localhost`, so the payload would otherwise have matched the loopback allowlist and returned early. (Same ordering lesson as the credentials check.) - The substitution site re-validates the composed origin with `re.fullmatch` and exits rather than emitting anything that is not a plain origin, so a future edit loosening the validator cannot reach the config. - Added the missing `import re` to the nginx-render block. Verified: four injection shapes (quote-escape, the loopback-shaped variant, a bare `;`, and an embedded newline) are rejected by both the validator and the render step, with no `X-Pwned` header reaching the output; `wss://relay.example.org`, `:8443`, `ws://127.0.0.1:8787`, and `wss://[::1]:8443` still pass. With nothing configured the emitted `connect-src` is still byte-identical to `main`. --- docker/entrypoint.sh | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 9cb7249d4..5c2027da2 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -131,6 +131,7 @@ fi python -c ' import json import os +import re from urllib.parse import urlsplit deployment = {} @@ -176,11 +177,26 @@ def service_url(name, value, schemes, loopback_schemes, loopback_hosts): the boot instead puts the error where an operator will actually see it. """ parsed = urlsplit(value) - # Checked before the loopback shortcut below, so the guarantee holds for every - # accepted value. Both of these are echoed to stdout further down, so a - # credentialed URL would also land in the container logs. + # Both checks below run before the loopback shortcut, so their guarantees hold + # for every accepted value. That ordering is load-bearing: urlsplit() parses + # ws://localhost:8080"; ... with hostname "localhost", which would match the + # loopback allowlist while netloc still carried the rest. + # + # Credentials: both values are echoed to stdout further down, so a credentialed + # URL would also land in the container logs. if parsed.username or parsed.password: raise SystemExit(f"ERROR: {name} must not embed credentials.") + # Character set: netloc is substituted unescaped into the double-quoted CSP + # add_header value in nginx.conf.template, so anything outside a hostname, + # port, or IPv6 literal could break out of that string and inject nginx + # directives. urlsplit() puts everything up to the next /, ? or # into netloc, + # quotes and semicolons included. Same discipline as GEOLIBRE_TRUSTED_PROXIES + # (parsed through ipaddress) and GEOLIBRE_AI_PROXY_URL (no path/query/fragment). + if re.search(r"[^A-Za-z0-9.\-:\[\]]", parsed.netloc): + raise SystemExit( + f"ERROR: {name} host may contain only letters, digits, dots, hyphens, " + f"colons, and brackets, not {parsed.netloc!r}." + ) if parsed.scheme in loopback_schemes and parsed.hostname in loopback_hosts: return value if parsed.scheme not in schemes or not parsed.netloc: @@ -248,6 +264,7 @@ fi # surprises). python -c ' import os +import re from urllib.parse import urlsplit token = os.environ["GEOLIBRE_SIDECAR_TOKEN"] @@ -262,8 +279,12 @@ collab = os.environ.get("GEOLIBRE_COLLAB_URL", "").strip() collab_src = "" if collab: parsed = urlsplit(collab) - if parsed.scheme and parsed.netloc: - collab_src = f" {parsed.scheme}://{parsed.netloc}" + origin = f"{parsed.scheme}://{parsed.netloc}" + # Re-checked here rather than trusting service_url(): this string goes into a + # quoted nginx directive, so it must be a plain origin or nothing at all. + if not re.fullmatch(r"[A-Za-z]+://[A-Za-z0-9.\-:\[\]]+", origin): + raise SystemExit(f"ERROR: GEOLIBRE_COLLAB_URL is not a plain origin: {collab!r}.") + collab_src = f" {origin}" src = open("/etc/nginx/nginx.conf.template").read() open("/etc/nginx/conf.d/default.conf", "w").write(