diff --git a/.env.example b/.env.example index 82483f1a0..2fde9116b 100644 --- a/.env.example +++ b/.env.example @@ -170,6 +170,16 @@ REQUEST_ANALYTICS_MAX_ROWS=100000 # model to another (e.g. after a fallback). Off by default. # FREELLMAPI_CONTEXT_HANDOFF=on_model_switch +# Manual application update checker. Set to off to hide it from Settings and +# prevent Git discovery or outbound update-check requests. Enabled by default. +# FREELLMAPI_UPDATE_CHECK=off +# Optional token used only for update checks against GitHub. The checker is +# anonymous by default; use a narrowly scoped token only if higher rate limits +# are needed. Generic GITHUB_TOKEN values are intentionally ignored. +# FREELLMAPI_UPDATE_GITHUB_TOKEN= +# FREELLMAPI_COMMIT_SHA and FREELLMAPI_INSTALL_METHOD are injected by official +# Docker and desktop builds and normally should not be set in .env. + # Comma-separated extra origins allowed to call the API from a browser. # localhost:5173, 127.0.0.1:5173, [::1]:5173 are allowed by default for the # Vite dev dashboard. Only set this if you serve the dashboard from a diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ff3c56b98..bc44b6d78 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -62,6 +62,8 @@ jobs: context: . file: ./Dockerfile platforms: ${{ matrix.platform }} + build-args: | + FREELLMAPI_COMMIT_SHA=${{ github.sha }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }} cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }} diff --git a/Dockerfile b/Dockerfile index 67bb9a893..1d70ac6ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,12 @@ RUN npm prune --omit=dev FROM ${NODE_IMAGE} AS runtime WORKDIR /app +ARG FREELLMAPI_COMMIT_SHA + ENV NODE_ENV=production ENV PORT=3001 +ENV FREELLMAPI_COMMIT_SHA=${FREELLMAPI_COMMIT_SHA} +ENV FREELLMAPI_INSTALL_METHOD=docker COPY --from=build --chown=node:node /app/package.json /app/package-lock.json ./ COPY --from=build --chown=node:node /app/node_modules ./node_modules diff --git a/client/src/components/settings-dialog.tsx b/client/src/components/settings-dialog.tsx index e4157186b..7a7ee649c 100644 --- a/client/src/components/settings-dialog.tsx +++ b/client/src/components/settings-dialog.tsx @@ -1,15 +1,21 @@ import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react' import { + AlertTriangle, Check, + CheckCircle2, ChevronsUpDown, + ExternalLink, FlaskConical, Gauge, Info, Loader2, Monitor, Moon, + RefreshCw, Search, SlidersHorizontal, + Sparkles, + SquareTerminal, Sun, Wrench, X, @@ -545,7 +551,7 @@ function PreviewSection({ state }: { state: CompressionState }) { ) } -function GeneralSection() { +function GeneralSection({ active }: { active: boolean }) { const { t } = useI18n() const { theme, setTheme } = useTheme() @@ -572,10 +578,318 @@ function GeneralSection() { /> )} /> + ) } +type UpdateStatus = 'idle' | 'current' | 'available' | 'ahead' | 'diverged' | 'unknown' | 'unsupported' | 'disabled' +type CheckedUpdateStatus = Exclude +type Installation = 'source' | 'docker' | 'desktop' | 'unknown' + +interface UpdateStatusInfo { + status: UpdateStatus + installation: Installation + localSha: string | null + lastChecked: string | null +} + +interface UpdateCheckInfo { + status: CheckedUpdateStatus + installation: Installation + localSha: string | null + checkedAt: string + remoteSha?: string + remoteDate?: string + remoteMessage?: string + changes?: Array<{ + sha: string + message: string + date?: string + }> +} + +function formatDateTime(value: string | null | undefined, locale: Locale) { + if (!value) return null + const date = new Date(value) + if (Number.isNaN(date.getTime())) return null + try { + return new Intl.DateTimeFormat(locale, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(date) + } catch { + return null + } +} + +function UpdateChecker({ active }: { active: boolean }) { + const { locale, t } = useI18n() + const [statusInfo, setStatusInfo] = useState(null) + const [checkResult, setCheckResult] = useState(null) + const [loadingStatus, setLoadingStatus] = useState(true) + const [checking, setChecking] = useState(false) + const [error, setError] = useState(false) + const [dialogOpen, setDialogOpen] = useState(false) + const [activity, setActivity] = useState([]) + + useEffect(() => { + if (!active) return + let cancelled = false + apiFetch('/api/update/status') + .then(result => { + if (!cancelled) setStatusInfo(result) + }) + .catch(() => { + if (!cancelled) setError(true) + }) + .finally(() => { + if (!cancelled) setLoadingStatus(false) + }) + return () => { cancelled = true } + }, [active]) + + async function check() { + setChecking(true) + setError(false) + setCheckResult(null) + setActivity([ + statusInfo?.localSha + ? `${t('settings.currentVersion')} ${statusInfo.localSha}` + : t('settings.notGitInstall'), + t('keys.checking'), + ]) + try { + const result = await apiFetch('/api/update/check') + setCheckResult(result) + setStatusInfo({ + status: result.status, + installation: result.installation, + localSha: result.localSha, + lastChecked: result.checkedAt, + }) + const resultMessage = result.status === 'available' + ? t('settings.updateAvailable') + : result.status === 'current' + ? t('settings.upToDate') + : result.status === 'ahead' + ? t('settings.buildAhead') + : result.status === 'diverged' + ? t('settings.buildDiverged') + : result.status === 'unknown' + ? t('settings.buildUnknown') + : t('settings.notGitInstall') + setActivity(current => [current[0], resultMessage]) + } catch { + setError(true) + setActivity(current => current.slice(0, 1)) + } finally { + setChecking(false) + } + } + + function openChecker() { + setDialogOpen(true) + void check() + } + + const info = checkResult + const currentSha = info?.localSha ?? statusInfo?.localSha + const lastChecked = formatDateTime(checkResult?.checkedAt, locale) + const remoteDate = formatDateTime(checkResult?.remoteDate, locale) + const statusMessage = info?.status === 'current' + ? t('settings.upToDate') + : info?.status === 'ahead' + ? t('settings.buildAhead') + : info?.status === 'diverged' + ? t('settings.buildDiverged') + : info?.status === 'unknown' + ? t('settings.buildUnknown') + : info?.status === 'unsupported' + ? t('settings.notGitInstall') + : null + + if ((loadingStatus && !statusInfo) || statusInfo?.status === 'disabled') return null + + return ( +
+
+
+

{t('settings.updatesTitle')}

+ {currentSha ? ( +

+ {t('settings.currentVersion')}{' '} + {currentSha} +

+ ) : ( +

+ {error ? t('settings.checkFailed') : t('settings.notGitInstall')} +

+ )} +
+ +
+ +

{t('settings.contactsGithub')}

+ + + +
+
+
+ +
+
+ {t('settings.updatesTitle')} + {currentSha && ( +

+ {t('settings.currentVersion')} {currentSha} +

+ )} +
+
+ + + +
+ +
+ {activity.length > 0 && ( +
+
+ + {checking ? t('keys.checking') : t('settings.updatesTitle')} +
+
+ {activity.map((entry, index) => ( +

+ > + {entry} +

+ ))} +
+
+ )} + + {info?.status === 'available' && ( +
+
+ +
+

{t('settings.updateAvailable')}

+ {info.remoteMessage && ( +

{info.remoteMessage}

+ )} + {(info.remoteSha || remoteDate) && ( +

+ {[info.remoteSha, remoteDate].filter(Boolean).join(' · ')} +

+ )} +
+
+ + {info.changes && info.changes.length > 0 && ( +
+

+ + {t('settings.changelog')} +

+
    + {info.changes.map(change => { + const changeDate = formatDateTime(change.date, locale) + return ( +
  1. + +
    +

    {change.message}

    +

    + {[change.sha, changeDate].filter(Boolean).join(' · ')} +

    +
    +
  2. + ) + })} +
+
+ )} + + {info.installation === 'docker' && ( +
+ + docker compose pull && docker compose up -d +
+ )} + {info.installation === 'source' && ( +
+
+ + git fetch https://github.com/tashfeenahmed/freellmapi.git main && git merge --ff-only FETCH_HEAD && npm ci && npm run build +
+

{t('settings.restartAfterUpdate')}

+
+ )} + {(info.installation === 'desktop' || info.installation === 'unknown') && ( + + {t('settings.openReleases')} + + + )} +
+ )} + + {statusMessage && ( +
+ {info?.status === 'current' + ? + : } +

{statusMessage}

+
+ )} + + {error && ( +
+ +

{t('settings.checkFailed')}

+
+ )} + +
+
+ {lastChecked &&

{t('premium.lastChecked', { when: lastChecked })}

} +
+ +
+
+
+
+
+ ) +} + export function SettingsDialog({ open, onOpenChange, @@ -634,7 +948,7 @@ export function SettingsDialog({ {/* min-height keeps the popup from resizing as sections are switched. */}
- {section === 'general' && } + {section === 'general' && } {loading && (
diff --git a/client/src/i18n/locales/am.json b/client/src/i18n/locales/am.json index f3c150182..9844fff37 100644 --- a/client/src/i18n/locales/am.json +++ b/client/src/i18n/locales/am.json @@ -124,7 +124,20 @@ "compressionPreview": "ቀጥታ ቅድመ-እይታ", "compressionPreviewDescription": "አቅራቢ ምን እንደሚቀበል ለማየት የጥያቄ አካልን በሂደቱ ውስጥ ይላኩ።", "compressionPreviewPlaceholder": "የጥያቄ JSON ወይም ተራ ጽሑፍ ይለጥፉ", - "compressionRunPreview": "ቅድመ-እይታ አሂድ" + "compressionRunPreview": "ቅድመ-እይታ አሂድ", + "updatesTitle": "ዝማኔዎች", + "currentVersion": "የአሁኑ ግንባታ:", + "updateAvailable": "ዝማኔ አለ", + "changelog": "ምን ተለውጧል", + "upToDate": "ወቅታዊ ነው", + "buildAhead": "ይህ ግንባታ ከይፋዊው main ቀድሟል።", + "buildDiverged": "ይህ ግንባታ ከይፋዊው main ተለይቷል።", + "buildUnknown": "ይህ ብጁ ግንባታ ከይፋዊው main ጋር ሊነጻጸር አይችልም።", + "notGitInstall": "ይህ ጭነት የcommit መረጃን አያካትትም።", + "checkFailed": "ዝማኔዎችን መፈተሽ አልተቻለም።", + "contactsGithub": "ፍተሻዎች በእጅ የሚከናወኑ ሲሆን GitHubን ያገናኛሉ።", + "openReleases": "የGitHub ልቀቶችን ክፈት", + "restartAfterUpdate": "ግንባታው ከተጠናቀቀ በኋላ FreeLLMAPIን እንደገና ያስጀምሩ።" }, "models": { "title": "ሞዴሎች", diff --git a/client/src/i18n/locales/ar.json b/client/src/i18n/locales/ar.json index 190539e88..0e6dd6313 100644 --- a/client/src/i18n/locales/ar.json +++ b/client/src/i18n/locales/ar.json @@ -124,7 +124,20 @@ "compressionPreview": "معاينة مباشرة", "compressionPreviewDescription": "أرسل نص طلب عبر المسار لترى ما سيصل إلى المزوّد.", "compressionPreviewPlaceholder": "الصق JSON الطلب أو نصًا عاديًا", - "compressionRunPreview": "تشغيل المعاينة" + "compressionRunPreview": "تشغيل المعاينة", + "updatesTitle": "التحديثات", + "currentVersion": "البنية الحالية:", + "updateAvailable": "يتوفر تحديث", + "changelog": "ما الذي تغيّر", + "upToDate": "محدّث", + "buildAhead": "هذه البنية متقدمة على الفرع main الرسمي.", + "buildDiverged": "تباعدت هذه البنية عن الفرع main الرسمي.", + "buildUnknown": "لا يمكن مقارنة هذه البنية المخصصة بالفرع main الرسمي.", + "notGitInstall": "لا يتضمن هذا التثبيت معلومات الالتزام.", + "checkFailed": "تعذر التحقق من وجود تحديثات.", + "contactsGithub": "تُجرى عمليات التحقق يدويًا وتتصل بـ GitHub.", + "openReleases": "فتح إصدارات GitHub", + "restartAfterUpdate": "أعد تشغيل FreeLLMAPI بعد اكتمال البنية." }, "models": { "title": "نماذج", diff --git a/client/src/i18n/locales/az.json b/client/src/i18n/locales/az.json index 0fac80730..9842e3f9e 100644 --- a/client/src/i18n/locales/az.json +++ b/client/src/i18n/locales/az.json @@ -124,7 +124,20 @@ "compressionPreview": "Canlı önbaxış", "compressionPreviewDescription": "Sorğu gövdəsini prosesdən keçirin və təchizatçının nə alacağını görün.", "compressionPreviewPlaceholder": "Sorğu JSON və ya sadə mətn yapışdırın", - "compressionRunPreview": "Önbaxışı işə sal" + "compressionRunPreview": "Önbaxışı işə sal", + "updatesTitle": "Yeniləmələr", + "currentVersion": "Cari quruluş:", + "updateAvailable": "Yeniləmə mövcuddur", + "changelog": "Nələr dəyişib", + "upToDate": "Ən son versiyadır", + "buildAhead": "Bu quruluş rəsmi main budağından irəlidədir.", + "buildDiverged": "Bu quruluş rəsmi main budağından ayrılıb.", + "buildUnknown": "Bu xüsusi quruluşu rəsmi main budağı ilə müqayisə etmək mümkün deyil.", + "notGitInstall": "Bu quraşdırmada commit məlumatı yoxdur.", + "checkFailed": "Yeniləmələri yoxlamaq mümkün olmadı.", + "contactsGithub": "Yoxlamalar əl ilə başladılır və GitHub-a qoşulur.", + "openReleases": "GitHub buraxılışlarını aç", + "restartAfterUpdate": "Quruluş tamamlandıqdan sonra FreeLLMAPI-ni yenidən başladın." }, "models": { "title": "Modellər", diff --git a/client/src/i18n/locales/bg.json b/client/src/i18n/locales/bg.json index 1d80d223a..113ebe8ca 100644 --- a/client/src/i18n/locales/bg.json +++ b/client/src/i18n/locales/bg.json @@ -124,7 +124,20 @@ "compressionPreview": "Преглед на живо", "compressionPreviewDescription": "Прекарайте тяло на заявка през веригата, за да видите какво би получил доставчикът.", "compressionPreviewPlaceholder": "Поставете JSON на заявка или обикновен текст", - "compressionRunPreview": "Изпълни преглед" + "compressionRunPreview": "Изпълни преглед", + "updatesTitle": "Актуализации", + "currentVersion": "Текуща компилация:", + "updateAvailable": "Налична е актуализация", + "changelog": "Какво се промени", + "upToDate": "Актуална версия", + "buildAhead": "Тази компилация е пред официалния клон main.", + "buildDiverged": "Тази компилация се е отклонила от официалния клон main.", + "buildUnknown": "Тази персонализирана компилация не може да бъде сравнена с официалния клон main.", + "notGitInstall": "Тази инсталация не съдържа информация за commit.", + "checkFailed": "Актуализациите не можаха да бъдат проверени.", + "contactsGithub": "Проверките се стартират ръчно и се свързват с GitHub.", + "openReleases": "Отвори изданията в GitHub", + "restartAfterUpdate": "Рестартирайте FreeLLMAPI след завършване на компилацията." }, "models": { "title": "Модели", diff --git a/client/src/i18n/locales/bn.json b/client/src/i18n/locales/bn.json index 758eeb94f..5ca3c779e 100644 --- a/client/src/i18n/locales/bn.json +++ b/client/src/i18n/locales/bn.json @@ -124,7 +124,20 @@ "compressionPreview": "লাইভ প্রিভিউ", "compressionPreviewDescription": "প্রদানকারী কী পাবে তা দেখতে একটি অনুরোধ বডি পাইপলাইনের মধ্য দিয়ে পাঠান।", "compressionPreviewPlaceholder": "অনুরোধের JSON বা সাধারণ টেক্সট পেস্ট করুন", - "compressionRunPreview": "প্রিভিউ চালান" + "compressionRunPreview": "প্রিভিউ চালান", + "updatesTitle": "আপডেটসমূহ", + "currentVersion": "বর্তমান বিল্ড:", + "updateAvailable": "আপডেট উপলব্ধ", + "changelog": "কী পরিবর্তন হয়েছে", + "upToDate": "হালনাগাদ আছে", + "buildAhead": "এই বিল্ডটি অফিসিয়াল main-এর চেয়ে এগিয়ে আছে।", + "buildDiverged": "এই বিল্ডটি অফিসিয়াল main থেকে বিচ্যুত হয়েছে।", + "buildUnknown": "এই কাস্টম বিল্ডটির সঙ্গে অফিসিয়াল main-এর তুলনা করা যায় না।", + "notGitInstall": "এই ইনস্টলেশনে কমিটের তথ্য নেই।", + "checkFailed": "আপডেট পরীক্ষা করা যায়নি।", + "contactsGithub": "পরীক্ষাগুলি ম্যানুয়ালি করা হয় এবং GitHub-এর সঙ্গে যোগাযোগ করে।", + "openReleases": "GitHub রিলিজ খুলুন", + "restartAfterUpdate": "বিল্ড সম্পূর্ণ হওয়ার পর FreeLLMAPI পুনরায় চালু করুন।" }, "models": { "title": "মডেল", diff --git a/client/src/i18n/locales/cs.json b/client/src/i18n/locales/cs.json index 9141a344b..ea456a316 100644 --- a/client/src/i18n/locales/cs.json +++ b/client/src/i18n/locales/cs.json @@ -124,7 +124,20 @@ "compressionPreview": "Živý náhled", "compressionPreviewDescription": "Pošlete tělo požadavku zpracovacím řetězcem a uvidíte, co by obdržel poskytovatel.", "compressionPreviewPlaceholder": "Vložte JSON požadavku nebo prostý text", - "compressionRunPreview": "Spustit náhled" + "compressionRunPreview": "Spustit náhled", + "updatesTitle": "Aktualizace", + "currentVersion": "Aktuální sestavení:", + "updateAvailable": "Je dostupná aktualizace", + "changelog": "Co se změnilo", + "upToDate": "Vše je aktuální", + "buildAhead": "Toto sestavení je před oficiální větví main.", + "buildDiverged": "Toto sestavení se odchýlilo od oficiální větve main.", + "buildUnknown": "Toto vlastní sestavení nelze porovnat s oficiální větví main.", + "notGitInstall": "Tato instalace neobsahuje informace o commitu.", + "checkFailed": "Aktualizace se nepodařilo zkontrolovat.", + "contactsGithub": "Kontroly se spouštějí ručně a kontaktují GitHub.", + "openReleases": "Otevřít vydání na GitHubu", + "restartAfterUpdate": "Po dokončení sestavení restartujte FreeLLMAPI." }, "models": { "title": "Modely", diff --git a/client/src/i18n/locales/da.json b/client/src/i18n/locales/da.json index 552391009..afb8c6bb4 100644 --- a/client/src/i18n/locales/da.json +++ b/client/src/i18n/locales/da.json @@ -124,7 +124,20 @@ "compressionPreview": "Live-forhåndsvisning", "compressionPreviewDescription": "Send indholdet af en forespørgsel gennem pipelinen for at se, hvad en udbyder ville modtage.", "compressionPreviewPlaceholder": "Indsæt forespørgsels-JSON eller almindelig tekst", - "compressionRunPreview": "Kør forhåndsvisning" + "compressionRunPreview": "Kør forhåndsvisning", + "updatesTitle": "Opdateringer", + "currentVersion": "Nuværende build:", + "updateAvailable": "Opdatering tilgængelig", + "changelog": "Hvad er ændret", + "upToDate": "Opdateret", + "buildAhead": "Denne build er foran den officielle main-gren.", + "buildDiverged": "Denne build er divergeret fra den officielle main-gren.", + "buildUnknown": "Denne tilpassede build kan ikke sammenlignes med den officielle main-gren.", + "notGitInstall": "Denne installation indeholder ikke commit-oplysninger.", + "checkFailed": "Det var ikke muligt at søge efter opdateringer.", + "contactsGithub": "Tjek udføres manuelt og kontakter GitHub.", + "openReleases": "Åbn GitHub-udgivelser", + "restartAfterUpdate": "Genstart FreeLLMAPI, når buildet er færdigt." }, "models": { "title": "Modeller", diff --git a/client/src/i18n/locales/de.json b/client/src/i18n/locales/de.json index 6e8d371bb..0736b401c 100644 --- a/client/src/i18n/locales/de.json +++ b/client/src/i18n/locales/de.json @@ -124,7 +124,20 @@ "compressionPreview": "Live-Vorschau", "compressionPreviewDescription": "Schicken Sie einen Anfragetext durch die Pipeline, um zu sehen, was ein Anbieter erhalten würde.", "compressionPreviewPlaceholder": "Anfrage-JSON oder einfachen Text einfügen", - "compressionRunPreview": "Vorschau ausführen" + "compressionRunPreview": "Vorschau ausführen", + "updatesTitle": "Aktualisierungen", + "currentVersion": "Aktueller Build:", + "updateAvailable": "Aktualisierung verfügbar", + "changelog": "Was ist neu", + "upToDate": "Auf dem neuesten Stand", + "buildAhead": "Dieser Build ist dem offiziellen main-Branch voraus.", + "buildDiverged": "Dieser Build ist vom offiziellen main-Branch abgewichen.", + "buildUnknown": "Dieser benutzerdefinierte Build kann nicht mit dem offiziellen main-Branch verglichen werden.", + "notGitInstall": "Diese Installation enthält keine Commit-Informationen.", + "checkFailed": "Aktualisierungen konnten nicht geprüft werden.", + "contactsGithub": "Prüfungen erfolgen manuell und stellen eine Verbindung zu GitHub her.", + "openReleases": "GitHub-Releases öffnen", + "restartAfterUpdate": "FreeLLMAPI nach Abschluss des Builds neu starten." }, "models": { "title": "Modelle", diff --git a/client/src/i18n/locales/el.json b/client/src/i18n/locales/el.json index 6537dc8f2..59b1eda55 100644 --- a/client/src/i18n/locales/el.json +++ b/client/src/i18n/locales/el.json @@ -124,7 +124,20 @@ "compressionPreview": "Ζωντανή προεπισκόπηση", "compressionPreviewDescription": "Στείλτε ένα σώμα αιτήματος μέσα από τη ροή για να δείτε τι θα λάμβανε ένας πάροχος.", "compressionPreviewPlaceholder": "Επικολλήστε JSON αιτήματος ή απλό κείμενο", - "compressionRunPreview": "Εκτέλεση προεπισκόπησης" + "compressionRunPreview": "Εκτέλεση προεπισκόπησης", + "updatesTitle": "Ενημερώσεις", + "currentVersion": "Τρέχον build:", + "updateAvailable": "Διαθέσιμη ενημέρωση", + "changelog": "Τι άλλαξε", + "upToDate": "Ενημερωμένο", + "buildAhead": "Αυτό το build είναι μπροστά από το επίσημο main.", + "buildDiverged": "Αυτό το build έχει αποκλίνει από το επίσημο main.", + "buildUnknown": "Αυτό το προσαρμοσμένο build δεν μπορεί να συγκριθεί με το επίσημο main.", + "notGitInstall": "Αυτή η εγκατάσταση δεν περιλαμβάνει πληροφορίες commit.", + "checkFailed": "Δεν ήταν δυνατός ο έλεγχος για ενημερώσεις.", + "contactsGithub": "Οι έλεγχοι γίνονται χειροκίνητα και επικοινωνούν με το GitHub.", + "openReleases": "Άνοιγμα εκδόσεων GitHub", + "restartAfterUpdate": "Επανεκκινήστε το FreeLLMAPI μετά την ολοκλήρωση του build." }, "models": { "title": "Μοντέλα", diff --git a/client/src/i18n/locales/en.json b/client/src/i18n/locales/en.json index dc1bbb345..2e02be3e5 100644 --- a/client/src/i18n/locales/en.json +++ b/client/src/i18n/locales/en.json @@ -124,7 +124,20 @@ "compressionPreview": "Live preview", "compressionPreviewDescription": "Send a request body through the pipeline to see what a provider would receive.", "compressionPreviewPlaceholder": "Paste request JSON or plain text", - "compressionRunPreview": "Run preview" + "compressionRunPreview": "Run preview", + "updatesTitle": "Updates", + "currentVersion": "Current build:", + "updateAvailable": "Update available", + "changelog": "What's changed", + "upToDate": "Up to date", + "buildAhead": "This build is ahead of official main.", + "buildDiverged": "This build has diverged from official main.", + "buildUnknown": "This custom build cannot be compared with official main.", + "notGitInstall": "This installation does not include commit information.", + "checkFailed": "Could not check for updates.", + "contactsGithub": "Checks are manual and contact GitHub.", + "openReleases": "Open GitHub Releases", + "restartAfterUpdate": "Restart FreeLLMAPI after the build completes." }, "models": { "title": "Models", diff --git a/client/src/i18n/locales/es.json b/client/src/i18n/locales/es.json index 2cbb81a28..cd425cdce 100644 --- a/client/src/i18n/locales/es.json +++ b/client/src/i18n/locales/es.json @@ -124,7 +124,20 @@ "compressionPreview": "Vista previa en vivo", "compressionPreviewDescription": "Envía un cuerpo de solicitud por el pipeline para ver lo que recibiría un proveedor.", "compressionPreviewPlaceholder": "Pega JSON de la solicitud o texto sin formato", - "compressionRunPreview": "Ejecutar vista previa" + "compressionRunPreview": "Ejecutar vista previa", + "updatesTitle": "Actualizaciones", + "currentVersion": "Compilación actual:", + "updateAvailable": "Actualización disponible", + "changelog": "Qué ha cambiado", + "upToDate": "Al día", + "buildAhead": "Esta compilación está por delante de la rama main oficial.", + "buildDiverged": "Esta compilación ha divergido de la rama main oficial.", + "buildUnknown": "Esta compilación personalizada no se puede comparar con la rama main oficial.", + "notGitInstall": "Esta instalación no incluye información de commits.", + "checkFailed": "No se pudo comprobar si hay actualizaciones.", + "contactsGithub": "Las comprobaciones se realizan manualmente y contactan con GitHub.", + "openReleases": "Abrir lanzamientos de GitHub", + "restartAfterUpdate": "Reinicia FreeLLMAPI cuando finalice la compilación." }, "models": { "title": "Modelos", diff --git a/client/src/i18n/locales/fa.json b/client/src/i18n/locales/fa.json index 73fb3ca4f..4bdc9331b 100644 --- a/client/src/i18n/locales/fa.json +++ b/client/src/i18n/locales/fa.json @@ -124,7 +124,20 @@ "compressionPreview": "پیش‌نمایش زنده", "compressionPreviewDescription": "بدنه یک درخواست را از خط پردازش عبور دهید تا ببینید ارائه‌دهنده چه چیزی دریافت می‌کند.", "compressionPreviewPlaceholder": "JSON درخواست یا متن ساده را جای‌گذاری کنید", - "compressionRunPreview": "اجرای پیش‌نمایش" + "compressionRunPreview": "اجرای پیش‌نمایش", + "updatesTitle": "به‌روزرسانی‌ها", + "currentVersion": "بیلد فعلی:", + "updateAvailable": "به‌روزرسانی در دسترس است", + "changelog": "چه چیزهایی تغییر کرده است", + "upToDate": "به‌روز است", + "buildAhead": "این بیلد از شاخهٔ رسمی main جلوتر است.", + "buildDiverged": "این بیلد از شاخهٔ رسمی main واگرا شده است.", + "buildUnknown": "این بیلد سفارشی را نمی‌توان با شاخهٔ رسمی main مقایسه کرد.", + "notGitInstall": "این نصب شامل اطلاعات commit نیست.", + "checkFailed": "بررسی به‌روزرسانی‌ها ممکن نبود.", + "contactsGithub": "بررسی‌ها دستی هستند و با GitHub ارتباط برقرار می‌کنند.", + "openReleases": "باز کردن نسخه‌های منتشرشده در GitHub", + "restartAfterUpdate": "پس از تکمیل بیلد، FreeLLMAPI را راه‌اندازی مجدد کنید." }, "models": { "title": "مدل ها", diff --git a/client/src/i18n/locales/fi.json b/client/src/i18n/locales/fi.json index b39d0f7eb..1c4b4bcaa 100644 --- a/client/src/i18n/locales/fi.json +++ b/client/src/i18n/locales/fi.json @@ -124,7 +124,20 @@ "compressionPreview": "Live-esikatselu", "compressionPreviewDescription": "Lähetä pyynnön runko putken läpi nähdäksesi, mitä palveluntarjoaja saisi.", "compressionPreviewPlaceholder": "Liitä pyynnön JSON tai pelkkä teksti", - "compressionRunPreview": "Suorita esikatselu" + "compressionRunPreview": "Suorita esikatselu", + "updatesTitle": "Päivitykset", + "currentVersion": "Nykyinen koontiversio:", + "updateAvailable": "Päivitys saatavilla", + "changelog": "Mikä muuttui", + "upToDate": "Ajan tasalla", + "buildAhead": "Tämä koontiversio on virallista main-haaraa edellä.", + "buildDiverged": "Tämä koontiversio on erkaantunut virallisesta main-haarasta.", + "buildUnknown": "Tätä mukautettua koontiversiota ei voi verrata viralliseen main-haaraan.", + "notGitInstall": "Tämä asennus ei sisällä commit-tietoja.", + "checkFailed": "Päivitysten tarkistaminen epäonnistui.", + "contactsGithub": "Tarkistukset tehdään manuaalisesti ja ne ottavat yhteyden GitHubiin.", + "openReleases": "Avaa GitHub-julkaisut", + "restartAfterUpdate": "Käynnistä FreeLLMAPI uudelleen koonnin valmistuttua." }, "models": { "title": "Mallit", diff --git a/client/src/i18n/locales/fr.json b/client/src/i18n/locales/fr.json index ddf015244..fe6672792 100644 --- a/client/src/i18n/locales/fr.json +++ b/client/src/i18n/locales/fr.json @@ -124,7 +124,20 @@ "compressionPreview": "Aperçu en direct", "compressionPreviewDescription": "Envoyez un corps de requête dans le pipeline pour voir ce qu'un fournisseur recevrait.", "compressionPreviewPlaceholder": "Collez le JSON de la requête ou du texte brut", - "compressionRunPreview": "Lancer l'aperçu" + "compressionRunPreview": "Lancer l'aperçu", + "updatesTitle": "Mises à jour", + "currentVersion": "Build actuel :", + "updateAvailable": "Une mise à jour est disponible", + "changelog": "Nouveautés", + "upToDate": "À jour", + "buildAhead": "Ce build est en avance sur la branche main officielle.", + "buildDiverged": "Ce build a divergé de la branche main officielle.", + "buildUnknown": "Ce build personnalisé ne peut pas être comparé à la branche main officielle.", + "notGitInstall": "Cette installation ne contient aucune information de commit.", + "checkFailed": "Impossible de rechercher les mises à jour.", + "contactsGithub": "Les vérifications sont effectuées manuellement et contactent GitHub.", + "openReleases": "Ouvrir les versions GitHub", + "restartAfterUpdate": "Redémarrez FreeLLMAPI une fois le build terminé." }, "models": { "title": "Modèles", diff --git a/client/src/i18n/locales/gu.json b/client/src/i18n/locales/gu.json index df52bb78b..e4d7cf383 100644 --- a/client/src/i18n/locales/gu.json +++ b/client/src/i18n/locales/gu.json @@ -124,7 +124,20 @@ "compressionPreview": "લાઇવ પ્રીવ્યૂ", "compressionPreviewDescription": "પ્રદાતાને શું મળશે તે જોવા માટે વિનંતીનું બોડી પાઇપલાઇનમાંથી પસાર કરો.", "compressionPreviewPlaceholder": "વિનંતીનું JSON અથવા સાદો ટેક્સ્ટ પેસ્ટ કરો", - "compressionRunPreview": "પ્રીવ્યૂ ચલાવો" + "compressionRunPreview": "પ્રીવ્યૂ ચલાવો", + "updatesTitle": "અપડેટ્સ", + "currentVersion": "વર્તમાન બિલ્ડ:", + "updateAvailable": "અપડેટ ઉપલબ્ધ છે", + "changelog": "શું બદલાયું છે", + "upToDate": "અદ્યતન છે", + "buildAhead": "આ બિલ્ડ અધિકૃત main કરતાં આગળ છે.", + "buildDiverged": "આ બિલ્ડ અધિકૃત mainથી વિખૂટું પડ્યું છે.", + "buildUnknown": "આ કસ્ટમ બિલ્ડની અધિકૃત main સાથે સરખામણી કરી શકાતી નથી.", + "notGitInstall": "આ ઇન્સ્ટોલેશનમાં કમિટની માહિતી શામેલ નથી.", + "checkFailed": "અપડેટ્સ તપાસી શકાયા નથી.", + "contactsGithub": "તપાસો મેન્યુઅલી કરવામાં આવે છે અને GitHubનો સંપર્ક કરે છે.", + "openReleases": "GitHub રિલીઝ ખોલો", + "restartAfterUpdate": "બિલ્ડ પૂર્ણ થયા પછી FreeLLMAPI ફરી શરૂ કરો." }, "models": { "title": "મોડલ્સ", diff --git a/client/src/i18n/locales/ha.json b/client/src/i18n/locales/ha.json index 11b980d1e..682cd584f 100644 --- a/client/src/i18n/locales/ha.json +++ b/client/src/i18n/locales/ha.json @@ -124,7 +124,20 @@ "compressionPreview": "Samfoti kai tsaye", "compressionPreviewDescription": "Turo jikin buƙata ta tsarin domin ganin abin da mai bayarwa zai karɓa.", "compressionPreviewPlaceholder": "Manna JSON na buƙata ko rubutu kawai", - "compressionRunPreview": "Gudanar da samfoti" + "compressionRunPreview": "Gudanar da samfoti", + "updatesTitle": "Sabuntawa", + "currentVersion": "Build na yanzu:", + "updateAvailable": "Akwai sabuntawa", + "changelog": "Abin da ya canza", + "upToDate": "An sabunta", + "buildAhead": "Wannan build ɗin yana gaba da main na hukuma.", + "buildDiverged": "Wannan build ɗin ya rabu da main na hukuma.", + "buildUnknown": "Ba za a iya kwatanta wannan build na musamman da main na hukuma ba.", + "notGitInstall": "Wannan shigarwar ba ta ƙunshi bayanan commit ba.", + "checkFailed": "Ba a iya bincika sabuntawa ba.", + "contactsGithub": "Ana yin dubawa da hannu kuma ana tuntuɓar GitHub.", + "openReleases": "Buɗe sakin GitHub", + "restartAfterUpdate": "Sake kunna FreeLLMAPI bayan an gama build ɗin." }, "models": { "title": "Samfura", diff --git a/client/src/i18n/locales/he.json b/client/src/i18n/locales/he.json index d29e625ce..181f25c73 100644 --- a/client/src/i18n/locales/he.json +++ b/client/src/i18n/locales/he.json @@ -124,7 +124,20 @@ "compressionPreview": "תצוגה מקדימה חיה", "compressionPreviewDescription": "שלח גוף בקשה דרך הצינור כדי לראות מה הספק יקבל.", "compressionPreviewPlaceholder": "הדבק JSON של בקשה או טקסט רגיל", - "compressionRunPreview": "הרץ תצוגה מקדימה" + "compressionRunPreview": "הרץ תצוגה מקדימה", + "updatesTitle": "עדכונים", + "currentVersion": "גרסה נוכחית:", + "updateAvailable": "עדכון זמין", + "changelog": "מה השתנה", + "upToDate": "מעודכן", + "buildAhead": "גרסה זו מקדימה את ענף main הרשמי.", + "buildDiverged": "גרסה זו התפצלה מענף main הרשמי.", + "buildUnknown": "לא ניתן להשוות גרסה מותאמת אישית זו לענף main הרשמי.", + "notGitInstall": "התקנה זו אינה כוללת מידע על קומיטים.", + "checkFailed": "לא ניתן היה לבדוק אם קיימים עדכונים.", + "contactsGithub": "הבדיקות מתבצעות ידנית ויוצרות קשר עם GitHub.", + "openReleases": "פתיחת מהדורות GitHub", + "restartAfterUpdate": "הפעל מחדש את FreeLLMAPI לאחר השלמת הבנייה." }, "models": { "title": "דגמים", diff --git a/client/src/i18n/locales/hi.json b/client/src/i18n/locales/hi.json index d39cad32b..c3e466cb3 100644 --- a/client/src/i18n/locales/hi.json +++ b/client/src/i18n/locales/hi.json @@ -124,7 +124,20 @@ "compressionPreview": "लाइव पूर्वावलोकन", "compressionPreviewDescription": "यह देखने के लिए कि प्रदाता को क्या मिलेगा, किसी अनुरोध बॉडी को पाइपलाइन से गुज़ारें।", "compressionPreviewPlaceholder": "अनुरोध JSON या सादा टेक्स्ट पेस्ट करें", - "compressionRunPreview": "पूर्वावलोकन चलाएँ" + "compressionRunPreview": "पूर्वावलोकन चलाएँ", + "updatesTitle": "अपडेट", + "currentVersion": "वर्तमान बिल्ड:", + "updateAvailable": "अपडेट उपलब्ध है", + "changelog": "क्या बदला है", + "upToDate": "नवीनतम संस्करण", + "buildAhead": "यह बिल्ड आधिकारिक main शाखा से आगे है।", + "buildDiverged": "यह बिल्ड आधिकारिक main शाखा से अलग हो गया है।", + "buildUnknown": "इस कस्टम बिल्ड की तुलना आधिकारिक main शाखा से नहीं की जा सकती।", + "notGitInstall": "इस इंस्टॉलेशन में कमिट की जानकारी शामिल नहीं है।", + "checkFailed": "अपडेट की जाँच नहीं की जा सकी।", + "contactsGithub": "जाँच मैन्युअल रूप से होती है और GitHub से संपर्क करती है।", + "openReleases": "GitHub रिलीज़ खोलें", + "restartAfterUpdate": "बिल्ड पूरा होने के बाद FreeLLMAPI को पुनः आरंभ करें।" }, "models": { "title": "मॉडल", diff --git a/client/src/i18n/locales/hr.json b/client/src/i18n/locales/hr.json index 3f1bb63a6..7d437c653 100644 --- a/client/src/i18n/locales/hr.json +++ b/client/src/i18n/locales/hr.json @@ -124,7 +124,20 @@ "compressionPreview": "Pregled uživo", "compressionPreviewDescription": "Pošaljite tijelo zahtjeva kroz lanac obrade da vidite što bi pružatelj primio.", "compressionPreviewPlaceholder": "Zalijepite JSON zahtjeva ili običan tekst", - "compressionRunPreview": "Pokreni pregled" + "compressionRunPreview": "Pokreni pregled", + "updatesTitle": "Ažuriranja", + "currentVersion": "Trenutačna verzija:", + "updateAvailable": "Dostupno je ažuriranje", + "changelog": "Što se promijenilo", + "upToDate": "Ažurno", + "buildAhead": "Ova je verzija ispred službene grane main.", + "buildDiverged": "Ova se verzija odvojila od službene grane main.", + "buildUnknown": "Ovu prilagođenu verziju nije moguće usporediti sa službenom granom main.", + "notGitInstall": "Ova instalacija ne sadrži informacije o commitovima.", + "checkFailed": "Nije moguće provjeriti ažuriranja.", + "contactsGithub": "Provjere se pokreću ručno i kontaktiraju GitHub.", + "openReleases": "Otvori izdanja na GitHubu", + "restartAfterUpdate": "Ponovno pokrenite FreeLLMAPI nakon dovršetka izgradnje." }, "models": { "title": "Modeli", diff --git a/client/src/i18n/locales/hu.json b/client/src/i18n/locales/hu.json index e6fb2a57a..d7b0decf0 100644 --- a/client/src/i18n/locales/hu.json +++ b/client/src/i18n/locales/hu.json @@ -124,7 +124,20 @@ "compressionPreview": "Élő előnézet", "compressionPreviewDescription": "Küldjön át egy kéréstörzset a folyamaton, hogy lássa, mit kapna egy szolgáltató.", "compressionPreviewPlaceholder": "Illessze be a kérés JSON-jét vagy egyszerű szöveget", - "compressionRunPreview": "Előnézet futtatása" + "compressionRunPreview": "Előnézet futtatása", + "updatesTitle": "Frissítések", + "currentVersion": "Jelenlegi build:", + "updateAvailable": "Frissítés érhető el", + "changelog": "Mi változott", + "upToDate": "Naprakész", + "buildAhead": "Ez a build megelőzi a hivatalos main ágat.", + "buildDiverged": "Ez a build eltért a hivatalos main ágtól.", + "buildUnknown": "Ez az egyéni build nem hasonlítható össze a hivatalos main ággal.", + "notGitInstall": "Ez a telepítés nem tartalmaz commitinformációkat.", + "checkFailed": "Nem sikerült ellenőrizni a frissítéseket.", + "contactsGithub": "Az ellenőrzések manuálisak, és kapcsolatba lépnek a GitHubbal.", + "openReleases": "GitHub-kiadások megnyitása", + "restartAfterUpdate": "A build befejezése után indítsa újra a FreeLLMAPI-t." }, "models": { "title": "Modellek", diff --git a/client/src/i18n/locales/id.json b/client/src/i18n/locales/id.json index 96e0259c2..9c7704fed 100644 --- a/client/src/i18n/locales/id.json +++ b/client/src/i18n/locales/id.json @@ -124,7 +124,20 @@ "compressionPreview": "Pratinjau langsung", "compressionPreviewDescription": "Kirim body permintaan melalui alur kompresi untuk melihat apa yang akan diterima penyedia.", "compressionPreviewPlaceholder": "Tempel JSON permintaan atau teks biasa", - "compressionRunPreview": "Jalankan pratinjau" + "compressionRunPreview": "Jalankan pratinjau", + "updatesTitle": "Pembaruan", + "currentVersion": "Build saat ini:", + "updateAvailable": "Pembaruan tersedia", + "changelog": "Yang berubah", + "upToDate": "Sudah terbaru", + "buildAhead": "Build ini lebih maju dari cabang main resmi.", + "buildDiverged": "Build ini telah menyimpang dari cabang main resmi.", + "buildUnknown": "Build khusus ini tidak dapat dibandingkan dengan cabang main resmi.", + "notGitInstall": "Instalasi ini tidak menyertakan informasi commit.", + "checkFailed": "Tidak dapat memeriksa pembaruan.", + "contactsGithub": "Pemeriksaan dilakukan secara manual dan menghubungi GitHub.", + "openReleases": "Buka Rilis GitHub", + "restartAfterUpdate": "Mulai ulang FreeLLMAPI setelah proses build selesai." }, "models": { "title": "Model", diff --git a/client/src/i18n/locales/ig.json b/client/src/i18n/locales/ig.json index 005260d5f..154cca86a 100644 --- a/client/src/i18n/locales/ig.json +++ b/client/src/i18n/locales/ig.json @@ -124,7 +124,20 @@ "compressionPreview": "Nlele ozugbo", "compressionPreviewDescription": "Zipu ozi arịrịọ site na usoro ahụ ka ị hụ ihe onye na-enye ga-anata.", "compressionPreviewPlaceholder": "Tinye JSON arịrịọ ma ọ bụ ederede nkịtị", - "compressionRunPreview": "Mee nlele" + "compressionRunPreview": "Mee nlele", + "updatesTitle": "Mmelite", + "currentVersion": "Build dị ugbu a:", + "updateAvailable": "Mmelite dị", + "changelog": "Ihe gbanwere", + "upToDate": "Ọ dị ọhụrụ", + "buildAhead": "Build a dị n'ihu alaka main nke iwu kwadoro.", + "buildDiverged": "Build a esiwo na alaka main nke iwu kwadoro kewapụ.", + "buildUnknown": "Enweghị ike iji build omenala a tụnyere alaka main nke iwu kwadoro.", + "notGitInstall": "Nwụnye a enweghị ozi commit.", + "checkFailed": "Enweghị ike ịlele mmelite.", + "contactsGithub": "A na-eji aka eme nyocha ndị a, ha na-akpọtụrụ GitHub.", + "openReleases": "Mepee GitHub Releases", + "restartAfterUpdate": "Malitegharịa FreeLLMAPI mgbe build ahụ gwụchara." }, "models": { "title": "Ụdị", diff --git a/client/src/i18n/locales/it.json b/client/src/i18n/locales/it.json index ff597f1cc..9dc9bf134 100644 --- a/client/src/i18n/locales/it.json +++ b/client/src/i18n/locales/it.json @@ -124,7 +124,20 @@ "compressionPreview": "Anteprima live", "compressionPreviewDescription": "Invia un corpo di richiesta attraverso la pipeline per vedere che cosa riceverebbe un fornitore.", "compressionPreviewPlaceholder": "Incolla il JSON della richiesta o testo semplice", - "compressionRunPreview": "Esegui anteprima" + "compressionRunPreview": "Esegui anteprima", + "updatesTitle": "Aggiornamenti", + "currentVersion": "Build corrente:", + "updateAvailable": "Aggiornamento disponibile", + "changelog": "Novità", + "upToDate": "Aggiornato", + "buildAhead": "Questa build è più avanti del ramo main ufficiale.", + "buildDiverged": "Questa build si è discostata dal ramo main ufficiale.", + "buildUnknown": "Questa build personalizzata non può essere confrontata con il ramo main ufficiale.", + "notGitInstall": "Questa installazione non include informazioni sui commit.", + "checkFailed": "Impossibile verificare la disponibilità di aggiornamenti.", + "contactsGithub": "I controlli sono manuali e contattano GitHub.", + "openReleases": "Apri le release di GitHub", + "restartAfterUpdate": "Riavvia FreeLLMAPI al termine della build." }, "models": { "title": "Modelli", diff --git a/client/src/i18n/locales/ja.json b/client/src/i18n/locales/ja.json index 2f15b9c91..c4f0b18c3 100644 --- a/client/src/i18n/locales/ja.json +++ b/client/src/i18n/locales/ja.json @@ -124,7 +124,20 @@ "compressionPreview": "ライブプレビュー", "compressionPreviewDescription": "リクエストのボディをパイプラインに通して、プロバイダーが受け取る内容を確認します。", "compressionPreviewPlaceholder": "リクエストのJSONまたはプレーンテキストを貼り付け", - "compressionRunPreview": "プレビューを実行" + "compressionRunPreview": "プレビューを実行", + "updatesTitle": "アップデート", + "currentVersion": "現在のビルド:", + "updateAvailable": "アップデートがあります", + "changelog": "変更内容", + "upToDate": "最新版です", + "buildAhead": "このビルドは公式のmainより先行しています。", + "buildDiverged": "このビルドは公式のmainから分岐しています。", + "buildUnknown": "このカスタムビルドは公式のmainと比較できません。", + "notGitInstall": "このインストールにはコミット情報が含まれていません。", + "checkFailed": "アップデートを確認できませんでした。", + "contactsGithub": "確認は手動で行われ、GitHubに接続します。", + "openReleases": "GitHub Releasesを開く", + "restartAfterUpdate": "ビルド完了後にFreeLLMAPIを再起動してください。" }, "models": { "title": "モデル", diff --git a/client/src/i18n/locales/ka.json b/client/src/i18n/locales/ka.json index 4f443aa61..aaf5b07df 100644 --- a/client/src/i18n/locales/ka.json +++ b/client/src/i18n/locales/ka.json @@ -124,7 +124,20 @@ "compressionPreview": "ცოცხალი გადახედვა", "compressionPreviewDescription": "გაატარეთ მოთხოვნის სხეული კონვეიერში, რომ ნახოთ, რას მიიღებდა პროვაიდერი.", "compressionPreviewPlaceholder": "ჩასვით მოთხოვნის JSON ან უბრალო ტექსტი", - "compressionRunPreview": "გადახედვის გაშვება" + "compressionRunPreview": "გადახედვის გაშვება", + "updatesTitle": "განახლებები", + "currentVersion": "მიმდინარე ბილდი:", + "updateAvailable": "განახლება ხელმისაწვდომია", + "changelog": "რა შეიცვალა", + "upToDate": "განახლებულია", + "buildAhead": "ეს ბილდი ოფიციალურ main შტოს უსწრებს.", + "buildDiverged": "ეს ბილდი ოფიციალურ main შტოს ასცდა.", + "buildUnknown": "ამ მორგებული ბილდის შედარება ოფიციალურ main შტოსთან შეუძლებელია.", + "notGitInstall": "ეს ინსტალაცია კომიტების შესახებ ინფორმაციას არ შეიცავს.", + "checkFailed": "განახლებების შემოწმება ვერ მოხერხდა.", + "contactsGithub": "შემოწმება ხელით სრულდება და GitHub-ს უკავშირდება.", + "openReleases": "GitHub-ის რელიზების გახსნა", + "restartAfterUpdate": "ბილდის დასრულების შემდეგ გადატვირთეთ FreeLLMAPI." }, "models": { "title": "მოდელები", diff --git a/client/src/i18n/locales/km.json b/client/src/i18n/locales/km.json index 2a00233ed..dc3ce7fc6 100644 --- a/client/src/i18n/locales/km.json +++ b/client/src/i18n/locales/km.json @@ -124,7 +124,20 @@ "compressionPreview": "មើលជាមុនផ្ទាល់", "compressionPreviewDescription": "បញ្ជូនតួសំណើឆ្លងកាត់ដំណើរការបង្ហាប់ ដើម្បីមើលថាអ្នកផ្តល់សេវានឹងទទួលបានអ្វី។", "compressionPreviewPlaceholder": "បិទភ្ជាប់ JSON សំណើ ឬអត្ថបទធម្មតា", - "compressionRunPreview": "ដំណើរការមើលជាមុន" + "compressionRunPreview": "ដំណើរការមើលជាមុន", + "updatesTitle": "ការអាប់ដេត", + "currentVersion": "ប៊ីលបច្ចុប្បន្ន៖", + "updateAvailable": "មានការអាប់ដេត", + "changelog": "អ្វីដែលបានផ្លាស់ប្តូរ", + "upToDate": "ទាន់សម័យ", + "buildAhead": "ប៊ីលនេះនាំមុខសាខា main ផ្លូវការ។", + "buildDiverged": "ប៊ីលនេះបានបែកចេញពីសាខា main ផ្លូវការ។", + "buildUnknown": "មិនអាចប្រៀបធៀបប៊ីលផ្ទាល់ខ្លួននេះជាមួយសាខា main ផ្លូវការបានទេ។", + "notGitInstall": "ការដំឡើងនេះមិនមានព័ត៌មាន commit ទេ។", + "checkFailed": "មិនអាចពិនិត្យរកការអាប់ដេតបានទេ។", + "contactsGithub": "ការពិនិត្យធ្វើឡើងដោយដៃ និងភ្ជាប់ទៅ GitHub។", + "openReleases": "បើក GitHub Releases", + "restartAfterUpdate": "ចាប់ផ្តើម FreeLLMAPI ឡើងវិញ បន្ទាប់ពីការបង្កើតបានបញ្ចប់។" }, "models": { "title": "ម៉ូដែល", diff --git a/client/src/i18n/locales/kn.json b/client/src/i18n/locales/kn.json index 148369e64..62c44c525 100644 --- a/client/src/i18n/locales/kn.json +++ b/client/src/i18n/locales/kn.json @@ -124,7 +124,20 @@ "compressionPreview": "ಲೈವ್ ಪೂರ್ವವೀಕ್ಷಣೆ", "compressionPreviewDescription": "ಒದಗಿಸುವವರಿಗೆ ಏನು ತಲುಪುತ್ತದೆ ಎಂದು ನೋಡಲು ವಿನಂತಿಯ ಬಾಡಿಯನ್ನು ಪೈಪ್‌ಲೈನ್ ಮೂಲಕ ಕಳುಹಿಸಿ.", "compressionPreviewPlaceholder": "ವಿನಂತಿಯ JSON ಅಥವಾ ಸರಳ ಪಠ್ಯವನ್ನು ಅಂಟಿಸಿ", - "compressionRunPreview": "ಪೂರ್ವವೀಕ್ಷಣೆ ರನ್ ಮಾಡಿ" + "compressionRunPreview": "ಪೂರ್ವವೀಕ್ಷಣೆ ರನ್ ಮಾಡಿ", + "updatesTitle": "ನವೀಕರಣಗಳು", + "currentVersion": "ಪ್ರಸ್ತುತ ಬಿಲ್ಡ್:", + "updateAvailable": "ನವೀಕರಣ ಲಭ್ಯವಿದೆ", + "changelog": "ಏನು ಬದಲಾಗಿದೆ", + "upToDate": "ನವೀಕೃತವಾಗಿದೆ", + "buildAhead": "ಈ ಬಿಲ್ಡ್ ಅಧಿಕೃತ main ಶಾಖೆಗಿಂತ ಮುಂದಿದೆ.", + "buildDiverged": "ಈ ಬಿಲ್ಡ್ ಅಧಿಕೃತ main ಶಾಖೆಯಿಂದ ಬೇರೆಯಾಗಿದೆ.", + "buildUnknown": "ಈ ಕಸ್ಟಮ್ ಬಿಲ್ಡ್ ಅನ್ನು ಅಧಿಕೃತ main ಶಾಖೆಯೊಂದಿಗೆ ಹೋಲಿಸಲಾಗುವುದಿಲ್ಲ.", + "notGitInstall": "ಈ ಸ್ಥಾಪನೆಯು ಕಮಿಟ್ ಮಾಹಿತಿಯನ್ನು ಒಳಗೊಂಡಿಲ್ಲ.", + "checkFailed": "ನವೀಕರಣಗಳಿಗಾಗಿ ಪರಿಶೀಲಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ.", + "contactsGithub": "ಪರಿಶೀಲನೆಗಳನ್ನು ಹಸ್ತಚಾಲಿತವಾಗಿ ಮಾಡಲಾಗುತ್ತದೆ ಮತ್ತು ಅವು GitHub ಅನ್ನು ಸಂಪರ್ಕಿಸುತ್ತವೆ.", + "openReleases": "GitHub ಬಿಡುಗಡೆಗಳನ್ನು ತೆರೆಯಿರಿ", + "restartAfterUpdate": "ಬಿಲ್ಡ್ ಪೂರ್ಣಗೊಂಡ ನಂತರ FreeLLMAPI ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಿ." }, "models": { "title": "ಮಾದರಿಗಳು", diff --git a/client/src/i18n/locales/ko.json b/client/src/i18n/locales/ko.json index 25780a5f5..3b1f654e2 100644 --- a/client/src/i18n/locales/ko.json +++ b/client/src/i18n/locales/ko.json @@ -124,7 +124,20 @@ "compressionPreview": "실시간 미리보기", "compressionPreviewDescription": "요청 본문을 파이프라인에 통과시켜 공급자가 받게 될 내용을 확인합니다.", "compressionPreviewPlaceholder": "요청 JSON 또는 일반 텍스트를 붙여넣으세요", - "compressionRunPreview": "미리보기 실행" + "compressionRunPreview": "미리보기 실행", + "updatesTitle": "업데이트", + "currentVersion": "현재 빌드:", + "updateAvailable": "업데이트 사용 가능", + "changelog": "변경 사항", + "upToDate": "최신 상태", + "buildAhead": "이 빌드는 공식 main 브랜치보다 앞서 있습니다.", + "buildDiverged": "이 빌드는 공식 main 브랜치에서 분기되었습니다.", + "buildUnknown": "이 사용자 지정 빌드는 공식 main 브랜치와 비교할 수 없습니다.", + "notGitInstall": "이 설치에는 커밋 정보가 포함되어 있지 않습니다.", + "checkFailed": "업데이트를 확인할 수 없습니다.", + "contactsGithub": "확인은 수동으로 진행되며 GitHub에 연결합니다.", + "openReleases": "GitHub 릴리스 열기", + "restartAfterUpdate": "빌드가 완료되면 FreeLLMAPI를 다시 시작하세요." }, "models": { "title": "모델", diff --git a/client/src/i18n/locales/lt.json b/client/src/i18n/locales/lt.json index a619df585..6e2c84fe5 100644 --- a/client/src/i18n/locales/lt.json +++ b/client/src/i18n/locales/lt.json @@ -124,7 +124,20 @@ "compressionPreview": "Tiesioginė peržiūra", "compressionPreviewDescription": "Praleiskite užklausos turinį per apdorojimo grandinę ir pamatysite, ką gautų tiekėjas.", "compressionPreviewPlaceholder": "Įklijuokite užklausos JSON arba paprastą tekstą", - "compressionRunPreview": "Vykdyti peržiūrą" + "compressionRunPreview": "Vykdyti peržiūrą", + "updatesTitle": "Naujiniai", + "currentVersion": "Dabartinė komponavimo versija:", + "updateAvailable": "Yra naujinys", + "changelog": "Kas pasikeitė", + "upToDate": "Atnaujinta", + "buildAhead": "Ši komponavimo versija yra priekyje oficialios main šakos.", + "buildDiverged": "Ši komponavimo versija nukrypo nuo oficialios main šakos.", + "buildUnknown": "Šios tinkintos komponavimo versijos negalima palyginti su oficialia main šaka.", + "notGitInstall": "Šiame diegime nėra informacijos apie Git įrašus.", + "checkFailed": "Nepavyko patikrinti, ar yra naujinių.", + "contactsGithub": "Tikrinimas atliekamas rankiniu būdu ir susisiekiama su GitHub.", + "openReleases": "Atverti GitHub leidimus", + "restartAfterUpdate": "Baigę komponuoti paleiskite FreeLLMAPI iš naujo." }, "models": { "title": "Modeliai", diff --git a/client/src/i18n/locales/ml.json b/client/src/i18n/locales/ml.json index 8d30e980e..e56055d82 100644 --- a/client/src/i18n/locales/ml.json +++ b/client/src/i18n/locales/ml.json @@ -124,7 +124,20 @@ "compressionPreview": "ലൈവ് പ്രിവ്യൂ", "compressionPreviewDescription": "ദാതാവിന് എന്ത് ലഭിക്കുമെന്ന് കാണാൻ ഒരു അഭ്യർത്ഥന ബോഡി പൈപ്പ്‌ലൈനിലൂടെ അയയ്ക്കുക.", "compressionPreviewPlaceholder": "അഭ്യർത്ഥന JSON അല്ലെങ്കിൽ സാധാരണ ടെക്സ്റ്റ് പേസ്റ്റ് ചെയ്യുക", - "compressionRunPreview": "പ്രിവ്യൂ പ്രവർത്തിപ്പിക്കുക" + "compressionRunPreview": "പ്രിവ്യൂ പ്രവർത്തിപ്പിക്കുക", + "updatesTitle": "അപ്‌ഡേറ്റുകൾ", + "currentVersion": "നിലവിലെ ബിൽഡ്:", + "updateAvailable": "അപ്‌ഡേറ്റ് ലഭ്യമാണ്", + "changelog": "എന്താണ് മാറിയത്", + "upToDate": "കാലികമാണ്", + "buildAhead": "ഈ ബിൽഡ് ഔദ്യോഗിക main ശാഖയേക്കാൾ മുന്നിലാണ്.", + "buildDiverged": "ഈ ബിൽഡ് ഔദ്യോഗിക main ശാഖയിൽ നിന്ന് വ്യതിചലിച്ചിരിക്കുന്നു.", + "buildUnknown": "ഈ കസ്റ്റം ബിൽഡിനെ ഔദ്യോഗിക main ശാഖയുമായി താരതമ്യം ചെയ്യാനാകില്ല.", + "notGitInstall": "ഈ ഇൻസ്റ്റാളേഷനിൽ കമ്മിറ്റ് വിവരങ്ങൾ ഉൾപ്പെട്ടിട്ടില്ല.", + "checkFailed": "അപ്‌ഡേറ്റുകൾ പരിശോധിക്കാനായില്ല.", + "contactsGithub": "പരിശോധനകൾ മാനുവലായാണ് നടത്തുന്നത്; അവ GitHub-മായി ബന്ധപ്പെടുന്നു.", + "openReleases": "GitHub റിലീസുകൾ തുറക്കുക", + "restartAfterUpdate": "ബിൽഡ് പൂർത്തിയായ ശേഷം FreeLLMAPI പുനരാരംഭിക്കുക." }, "models": { "title": "മോഡലുകൾ", diff --git a/client/src/i18n/locales/mr.json b/client/src/i18n/locales/mr.json index 7db49378f..06b884ab7 100644 --- a/client/src/i18n/locales/mr.json +++ b/client/src/i18n/locales/mr.json @@ -124,7 +124,20 @@ "compressionPreview": "लाइव्ह पूर्वावलोकन", "compressionPreviewDescription": "प्रदात्याला काय मिळेल हे पाहण्यासाठी विनंतीचा बॉडी पाइपलाइनमधून पाठवा.", "compressionPreviewPlaceholder": "विनंतीचे JSON किंवा साधा मजकूर पेस्ट करा", - "compressionRunPreview": "पूर्वावलोकन चालवा" + "compressionRunPreview": "पूर्वावलोकन चालवा", + "updatesTitle": "अपडेट्स", + "currentVersion": "सध्याचा बिल्ड:", + "updateAvailable": "अपडेट उपलब्ध आहे", + "changelog": "काय बदलले", + "upToDate": "अद्ययावत", + "buildAhead": "हा बिल्ड अधिकृत main शाखेच्या पुढे आहे.", + "buildDiverged": "हा बिल्ड अधिकृत main शाखेपासून वेगळा झाला आहे.", + "buildUnknown": "या कस्टम बिल्डची अधिकृत main शाखेशी तुलना करता येत नाही.", + "notGitInstall": "या इंस्टॉलेशनमध्ये कमिटची माहिती समाविष्ट नाही.", + "checkFailed": "अपडेट्स तपासता आले नाहीत.", + "contactsGithub": "तपासणी स्वतः सुरू करावी लागते आणि ती GitHub शी संपर्क साधते.", + "openReleases": "GitHub रिलीजेस उघडा", + "restartAfterUpdate": "बिल्ड पूर्ण झाल्यानंतर FreeLLMAPI पुन्हा सुरू करा." }, "models": { "title": "मॉडेल्स", diff --git a/client/src/i18n/locales/ms.json b/client/src/i18n/locales/ms.json index 68fe1e016..c54ad8b04 100644 --- a/client/src/i18n/locales/ms.json +++ b/client/src/i18n/locales/ms.json @@ -124,7 +124,20 @@ "compressionPreview": "Pratonton langsung", "compressionPreviewDescription": "Hantar badan permintaan melalui saluran pemampatan untuk melihat apa yang akan diterima oleh penyedia.", "compressionPreviewPlaceholder": "Tampal JSON permintaan atau teks biasa", - "compressionRunPreview": "Jalankan pratonton" + "compressionRunPreview": "Jalankan pratonton", + "updatesTitle": "Kemas kini", + "currentVersion": "Binaan semasa:", + "updateAvailable": "Kemas kini tersedia", + "changelog": "Perubahan", + "upToDate": "Sudah dikemas kini", + "buildAhead": "Binaan ini mendahului cabang main rasmi.", + "buildDiverged": "Binaan ini telah menyimpang daripada cabang main rasmi.", + "buildUnknown": "Binaan tersuai ini tidak dapat dibandingkan dengan cabang main rasmi.", + "notGitInstall": "Pemasangan ini tidak menyertakan maklumat komit.", + "checkFailed": "Tidak dapat menyemak kemas kini.", + "contactsGithub": "Semakan dijalankan secara manual dan menghubungi GitHub.", + "openReleases": "Buka Keluaran GitHub", + "restartAfterUpdate": "Mulakan semula FreeLLMAPI selepas binaan selesai." }, "models": { "title": "Model", diff --git a/client/src/i18n/locales/my.json b/client/src/i18n/locales/my.json index 6575a6c8f..d6891dd2d 100644 --- a/client/src/i18n/locales/my.json +++ b/client/src/i18n/locales/my.json @@ -124,7 +124,20 @@ "compressionPreview": "တိုက်ရိုက် အစမ်းကြည့်ရှုမှု", "compressionPreviewDescription": "ပံ့ပိုးသူ မည်သည့်အရာ လက်ခံရရှိမည်ကို ကြည့်ရန် တောင်းဆိုမှု အကြောင်းအရာကို လုပ်ငန်းစဉ်ထဲ ဖြတ်သန်း ပို့ကြည့်ပါ။", "compressionPreviewPlaceholder": "တောင်းဆိုမှု JSON သို့မဟုတ် စာသားရိုးရိုးကို ကူးထည့်ပါ", - "compressionRunPreview": "အစမ်းကြည့်ရန်" + "compressionRunPreview": "အစမ်းကြည့်ရန်", + "updatesTitle": "အပ်ဒိတ်များ", + "currentVersion": "လက်ရှိ build:", + "updateAvailable": "အပ်ဒိတ် ရရှိနိုင်သည်", + "changelog": "ပြောင်းလဲထားသည့်အရာများ", + "upToDate": "နောက်ဆုံးပေါ် ဖြစ်သည်", + "buildAhead": "ဤ build သည် တရားဝင် main ထက် ရှေ့ရောက်နေသည်။", + "buildDiverged": "ဤ build သည် တရားဝင် main မှ ကွဲထွက်သွားသည်။", + "buildUnknown": "ဤစိတ်ကြိုက် build ကို တရားဝင် main နှင့် နှိုင်းယှဉ်၍ မရပါ။", + "notGitInstall": "ဤထည့်သွင်းမှုတွင် commit အချက်အလက် မပါဝင်ပါ။", + "checkFailed": "အပ်ဒိတ်များကို စစ်ဆေး၍ မရပါ။", + "contactsGithub": "စစ်ဆေးမှုများကို ကိုယ်တိုင်လုပ်ဆောင်ရပြီး GitHub သို့ ဆက်သွယ်သည်။", + "openReleases": "GitHub Releases ကို ဖွင့်ရန်", + "restartAfterUpdate": "build ပြီးဆုံးပြီးနောက် FreeLLMAPI ကို ပြန်လည်စတင်ပါ။" }, "models": { "title": "မော်ဒယ်များ", diff --git a/client/src/i18n/locales/ne.json b/client/src/i18n/locales/ne.json index 32e24539a..0d1547fee 100644 --- a/client/src/i18n/locales/ne.json +++ b/client/src/i18n/locales/ne.json @@ -124,7 +124,20 @@ "compressionPreview": "प्रत्यक्ष पूर्वावलोकन", "compressionPreviewDescription": "प्रदायकले के प्राप्त गर्नेछ हेर्न अनुरोधको बडी पाइपलाइनमार्फत पठाउनुहोस्।", "compressionPreviewPlaceholder": "अनुरोध JSON वा सादा पाठ टाँस्नुहोस्", - "compressionRunPreview": "पूर्वावलोकन चलाउनुहोस्" + "compressionRunPreview": "पूर्वावलोकन चलाउनुहोस्", + "updatesTitle": "अद्यावधिकहरू", + "currentVersion": "हालको बिल्ड:", + "updateAvailable": "अद्यावधिक उपलब्ध छ", + "changelog": "के परिवर्तन भयो", + "upToDate": "अद्यावधिक छ", + "buildAhead": "यो बिल्ड आधिकारिक main शाखाभन्दा अगाडि छ।", + "buildDiverged": "यो बिल्ड आधिकारिक main शाखाबाट अलग भएको छ।", + "buildUnknown": "यो अनुकूलित बिल्डलाई आधिकारिक main शाखासँग तुलना गर्न सकिँदैन।", + "notGitInstall": "यो स्थापनामा commit जानकारी समावेश छैन।", + "checkFailed": "अद्यावधिकहरू जाँच गर्न सकिएन।", + "contactsGithub": "जाँचहरू म्यानुअल रूपमा गरिन्छन् र GitHub सँग सम्पर्क गर्छन्।", + "openReleases": "GitHub रिलीजहरू खोल्नुहोस्", + "restartAfterUpdate": "बिल्ड पूरा भएपछि FreeLLMAPI पुनः सुरु गर्नुहोस्।" }, "models": { "title": "नमूनाहरू", diff --git a/client/src/i18n/locales/nl.json b/client/src/i18n/locales/nl.json index b19c569d9..e22029e6b 100644 --- a/client/src/i18n/locales/nl.json +++ b/client/src/i18n/locales/nl.json @@ -124,7 +124,20 @@ "compressionPreview": "Live voorbeeld", "compressionPreviewDescription": "Stuur een verzoekinhoud door de pipeline om te zien wat een aanbieder zou ontvangen.", "compressionPreviewPlaceholder": "Plak verzoek-JSON of platte tekst", - "compressionRunPreview": "Voorbeeld uitvoeren" + "compressionRunPreview": "Voorbeeld uitvoeren", + "updatesTitle": "Updates", + "currentVersion": "Huidige build:", + "updateAvailable": "Update beschikbaar", + "changelog": "Wat is er veranderd", + "upToDate": "Up-to-date", + "buildAhead": "Deze build loopt voor op de officiële main-branch.", + "buildDiverged": "Deze build is afgeweken van de officiële main-branch.", + "buildUnknown": "Deze aangepaste build kan niet worden vergeleken met de officiële main-branch.", + "notGitInstall": "Deze installatie bevat geen commitinformatie.", + "checkFailed": "Kan niet controleren op updates.", + "contactsGithub": "Controles worden handmatig uitgevoerd en maken contact met GitHub.", + "openReleases": "GitHub Releases openen", + "restartAfterUpdate": "Start FreeLLMAPI opnieuw nadat de build is voltooid." }, "models": { "title": "Modellen", diff --git a/client/src/i18n/locales/no.json b/client/src/i18n/locales/no.json index 2d803a8f8..c4c39b889 100644 --- a/client/src/i18n/locales/no.json +++ b/client/src/i18n/locales/no.json @@ -124,7 +124,20 @@ "compressionPreview": "Direkte forhåndsvisning", "compressionPreviewDescription": "Send en forespørselstekst gjennom pipelinen for å se hva en leverandør ville motta.", "compressionPreviewPlaceholder": "Lim inn forespørsels-JSON eller ren tekst", - "compressionRunPreview": "Kjør forhåndsvisning" + "compressionRunPreview": "Kjør forhåndsvisning", + "updatesTitle": "Oppdateringer", + "currentVersion": "Nåværende bygg:", + "updateAvailable": "Oppdatering tilgjengelig", + "changelog": "Hva er endret", + "upToDate": "Oppdatert", + "buildAhead": "Dette bygget ligger foran den offisielle main-grenen.", + "buildDiverged": "Dette bygget har skilt lag med den offisielle main-grenen.", + "buildUnknown": "Dette egendefinerte bygget kan ikke sammenlignes med den offisielle main-grenen.", + "notGitInstall": "Denne installasjonen inneholder ikke commit-informasjon.", + "checkFailed": "Kunne ikke se etter oppdateringer.", + "contactsGithub": "Oppdateringssjekker kjøres manuelt og kontakter GitHub.", + "openReleases": "Åpne GitHub Releases", + "restartAfterUpdate": "Start FreeLLMAPI på nytt etter at byggingen er fullført." }, "models": { "title": "Modeller", diff --git a/client/src/i18n/locales/or.json b/client/src/i18n/locales/or.json index 588f6a3f7..c1cd6cec1 100644 --- a/client/src/i18n/locales/or.json +++ b/client/src/i18n/locales/or.json @@ -124,7 +124,20 @@ "compressionPreview": "ଲାଇଭ୍ ପୂର୍ବାବଲୋକନ", "compressionPreviewDescription": "ପ୍ରଦାନକାରୀ କ'ଣ ପାଇବେ ତାହା ଦେଖିବାକୁ ଏକ ଅନୁରୋଧ ବଡି ପାଇପଲାଇନ ମଧ୍ୟ ଦେଇ ପଠାନ୍ତୁ।", "compressionPreviewPlaceholder": "ଅନୁରୋଧ JSON କିମ୍ବା ସାଧା ଟେକ୍ସଟ୍ ପେଷ୍ଟ କରନ୍ତୁ", - "compressionRunPreview": "ପୂର୍ବାବଲୋକନ ଚଲାନ୍ତୁ" + "compressionRunPreview": "ପୂର୍ବାବଲୋକନ ଚଲାନ୍ତୁ", + "updatesTitle": "ଅପଡେଟ୍‌ଗୁଡ଼ିକ", + "currentVersion": "ବର୍ତ୍ତମାନର ବିଲ୍ଡ:", + "updateAvailable": "ଅପଡେଟ୍ ଉପଲବ୍ଧ", + "changelog": "କ’ଣ ପରିବର୍ତ୍ତନ ହୋଇଛି", + "upToDate": "ଅଦ୍ୟତନ ଅଛି", + "buildAhead": "ଏହି ବିଲ୍ଡଟି ଅଧିକାରିକ main ଶାଖାଠାରୁ ଆଗରେ ଅଛି।", + "buildDiverged": "ଏହି ବିଲ୍ଡଟି ଅଧିକାରିକ main ଶାଖାଠାରୁ ଅଲଗା ହୋଇଯାଇଛି।", + "buildUnknown": "ଏହି କଷ୍ଟମ୍ ବିଲ୍ଡକୁ ଅଧିକାରିକ main ଶାଖା ସହିତ ତୁଳନା କରାଯାଇପାରିବ ନାହିଁ।", + "notGitInstall": "ଏହି ଇନ୍‌ଷ୍ଟଲେସନ୍‌ରେ commit ସୂଚନା ନାହିଁ।", + "checkFailed": "ଅପଡେଟ୍ ଯାଞ୍ଚ କରାଯାଇପାରିଲା ନାହିଁ।", + "contactsGithub": "ଯାଞ୍ଚଗୁଡ଼ିକ ମାନୁଆଲ୍ ଭାବେ କରାଯାଏ ଏବଂ GitHub ସହିତ ସଂଯୋଗ କରେ।", + "openReleases": "GitHub Releases ଖୋଲନ୍ତୁ", + "restartAfterUpdate": "ବିଲ୍ଡ ସମ୍ପୂର୍ଣ୍ଣ ହେବା ପରେ FreeLLMAPI ପୁନଃ ଚାଲୁ କରନ୍ତୁ।" }, "models": { "title": "ମଡେଲଗୁଡିକ", diff --git a/client/src/i18n/locales/pa.json b/client/src/i18n/locales/pa.json index 7a254df6e..666e19a69 100644 --- a/client/src/i18n/locales/pa.json +++ b/client/src/i18n/locales/pa.json @@ -124,7 +124,20 @@ "compressionPreview": "ਲਾਈਵ ਝਲਕ", "compressionPreviewDescription": "ਇਹ ਵੇਖਣ ਲਈ ਕਿ ਪ੍ਰਦਾਤਾ ਨੂੰ ਕੀ ਮਿਲੇਗਾ, ਬੇਨਤੀ ਦਾ ਬਾਡੀ ਪਾਈਪਲਾਈਨ ਵਿੱਚੋਂ ਲੰਘਾਓ।", "compressionPreviewPlaceholder": "ਬੇਨਤੀ JSON ਜਾਂ ਸਾਦਾ ਟੈਕਸਟ ਪੇਸਟ ਕਰੋ", - "compressionRunPreview": "ਝਲਕ ਚਲਾਓ" + "compressionRunPreview": "ਝਲਕ ਚਲਾਓ", + "updatesTitle": "ਅੱਪਡੇਟ", + "currentVersion": "ਮੌਜੂਦਾ ਬਿਲਡ:", + "updateAvailable": "ਅੱਪਡੇਟ ਉਪਲਬਧ ਹੈ", + "changelog": "ਕੀ ਬਦਲਿਆ ਹੈ", + "upToDate": "ਨਵੀਨਤਮ ਹੈ", + "buildAhead": "ਇਹ ਬਿਲਡ ਅਧਿਕਾਰਤ main ਸ਼ਾਖਾ ਤੋਂ ਅੱਗੇ ਹੈ।", + "buildDiverged": "ਇਹ ਬਿਲਡ ਅਧਿਕਾਰਤ main ਸ਼ਾਖਾ ਤੋਂ ਵੱਖ ਹੋ ਗਿਆ ਹੈ।", + "buildUnknown": "ਇਸ ਕਸਟਮ ਬਿਲਡ ਦੀ ਤੁਲਨਾ ਅਧਿਕਾਰਤ main ਸ਼ਾਖਾ ਨਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।", + "notGitInstall": "ਇਸ ਇੰਸਟਾਲੇਸ਼ਨ ਵਿੱਚ commit ਜਾਣਕਾਰੀ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।", + "checkFailed": "ਅੱਪਡੇਟਾਂ ਦੀ ਜਾਂਚ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ।", + "contactsGithub": "ਜਾਂਚਾਂ ਹੱਥੀਂ ਚਲਾਈਆਂ ਜਾਂਦੀਆਂ ਹਨ ਅਤੇ GitHub ਨਾਲ ਸੰਪਰਕ ਕਰਦੀਆਂ ਹਨ।", + "openReleases": "GitHub Releases ਖੋਲ੍ਹੋ", + "restartAfterUpdate": "ਬਿਲਡ ਪੂਰਾ ਹੋਣ ਤੋਂ ਬਾਅਦ FreeLLMAPI ਨੂੰ ਮੁੜ ਚਾਲੂ ਕਰੋ।" }, "models": { "title": "ਮਾਡਲ", diff --git a/client/src/i18n/locales/pl.json b/client/src/i18n/locales/pl.json index 0b409eabf..218cd9179 100644 --- a/client/src/i18n/locales/pl.json +++ b/client/src/i18n/locales/pl.json @@ -124,7 +124,20 @@ "compressionPreview": "Podgląd na żywo", "compressionPreviewDescription": "Prześlij treść żądania przez potok, aby zobaczyć, co otrzymałby dostawca.", "compressionPreviewPlaceholder": "Wklej JSON żądania lub zwykły tekst", - "compressionRunPreview": "Uruchom podgląd" + "compressionRunPreview": "Uruchom podgląd", + "updatesTitle": "Aktualizacje", + "currentVersion": "Bieżąca kompilacja:", + "updateAvailable": "Dostępna aktualizacja", + "changelog": "Co się zmieniło", + "upToDate": "Aktualne", + "buildAhead": "Ta kompilacja wyprzedza oficjalną gałąź main.", + "buildDiverged": "Ta kompilacja odbiega od oficjalnej gałęzi main.", + "buildUnknown": "Tej niestandardowej kompilacji nie można porównać z oficjalną gałęzią main.", + "notGitInstall": "Ta instalacja nie zawiera informacji o commicie.", + "checkFailed": "Nie udało się sprawdzić dostępności aktualizacji.", + "contactsGithub": "Sprawdzanie jest uruchamiane ręcznie i nawiązuje połączenie z GitHubem.", + "openReleases": "Otwórz wydania w GitHubie", + "restartAfterUpdate": "Uruchom ponownie FreeLLMAPI po zakończeniu kompilacji." }, "models": { "title": "Modele", diff --git a/client/src/i18n/locales/pt-BR.json b/client/src/i18n/locales/pt-BR.json index be3008fdc..ff5fa11c3 100644 --- a/client/src/i18n/locales/pt-BR.json +++ b/client/src/i18n/locales/pt-BR.json @@ -124,7 +124,20 @@ "compressionPreview": "Prévia ao vivo", "compressionPreviewDescription": "Envie um corpo de solicitação pelo pipeline para ver o que um provedor receberia.", "compressionPreviewPlaceholder": "Cole o JSON da solicitação ou texto simples", - "compressionRunPreview": "Executar prévia" + "compressionRunPreview": "Executar prévia", + "updatesTitle": "Atualizações", + "currentVersion": "Build atual:", + "updateAvailable": "Atualização disponível", + "changelog": "O que mudou", + "upToDate": "Atualizado", + "buildAhead": "Esta build está à frente da branch main oficial.", + "buildDiverged": "Esta build divergiu da branch main oficial.", + "buildUnknown": "Esta build personalizada não pode ser comparada à branch main oficial.", + "notGitInstall": "Esta instalação não inclui informações do commit.", + "checkFailed": "Não foi possível verificar se há atualizações.", + "contactsGithub": "As verificações são manuais e acessam o GitHub.", + "openReleases": "Abrir releases no GitHub", + "restartAfterUpdate": "Reinicie o FreeLLMAPI após a conclusão da build." }, "models": { "title": "Modelos", diff --git a/client/src/i18n/locales/pt-PT.json b/client/src/i18n/locales/pt-PT.json index 8de6b5e81..05ae901ca 100644 --- a/client/src/i18n/locales/pt-PT.json +++ b/client/src/i18n/locales/pt-PT.json @@ -124,7 +124,20 @@ "compressionPreview": "Pré-visualização em direto", "compressionPreviewDescription": "Envie um corpo de pedido pelo pipeline para ver o que um fornecedor receberia.", "compressionPreviewPlaceholder": "Cole o JSON do pedido ou texto simples", - "compressionRunPreview": "Executar pré-visualização" + "compressionRunPreview": "Executar pré-visualização", + "updatesTitle": "Atualizações", + "currentVersion": "Compilação atual:", + "updateAvailable": "Atualização disponível", + "changelog": "O que mudou", + "upToDate": "Atualizado", + "buildAhead": "Esta compilação está à frente do ramo main oficial.", + "buildDiverged": "Esta compilação divergiu do ramo main oficial.", + "buildUnknown": "Esta compilação personalizada não pode ser comparada com o ramo main oficial.", + "notGitInstall": "Esta instalação não inclui informações do commit.", + "checkFailed": "Não foi possível procurar atualizações.", + "contactsGithub": "As verificações são manuais e contactam o GitHub.", + "openReleases": "Abrir lançamentos no GitHub", + "restartAfterUpdate": "Reinicie o FreeLLMAPI após a conclusão da compilação." }, "models": { "title": "Modelos", diff --git a/client/src/i18n/locales/ro.json b/client/src/i18n/locales/ro.json index 967e49e0d..d8cc40e88 100644 --- a/client/src/i18n/locales/ro.json +++ b/client/src/i18n/locales/ro.json @@ -124,7 +124,20 @@ "compressionPreview": "Previzualizare live", "compressionPreviewDescription": "Trimite corpul unei cereri prin lanțul de procesare pentru a vedea ce ar primi un furnizor.", "compressionPreviewPlaceholder": "Lipește JSON-ul cererii sau text simplu", - "compressionRunPreview": "Rulează previzualizarea" + "compressionRunPreview": "Rulează previzualizarea", + "updatesTitle": "Actualizări", + "currentVersion": "Versiunea curentă:", + "updateAvailable": "Actualizare disponibilă", + "changelog": "Ce s-a schimbat", + "upToDate": "La zi", + "buildAhead": "Această versiune este înaintea ramurii main oficiale.", + "buildDiverged": "Această versiune a deviat de la ramura main oficială.", + "buildUnknown": "Această versiune personalizată nu poate fi comparată cu ramura main oficială.", + "notGitInstall": "Această instalare nu include informații despre commit.", + "checkFailed": "Nu s-au putut verifica actualizările.", + "contactsGithub": "Verificările sunt inițiate manual și contactează GitHub.", + "openReleases": "Deschide versiunile GitHub", + "restartAfterUpdate": "Repornește FreeLLMAPI după finalizarea compilării." }, "models": { "title": "Modele", diff --git a/client/src/i18n/locales/ru.json b/client/src/i18n/locales/ru.json index 1375c7138..a3bbf6b5d 100644 --- a/client/src/i18n/locales/ru.json +++ b/client/src/i18n/locales/ru.json @@ -124,7 +124,20 @@ "compressionPreview": "Живой предпросмотр", "compressionPreviewDescription": "Пропустите тело запроса через конвейер, чтобы увидеть, что получит поставщик.", "compressionPreviewPlaceholder": "Вставьте JSON запроса или обычный текст", - "compressionRunPreview": "Запустить предпросмотр" + "compressionRunPreview": "Запустить предпросмотр", + "updatesTitle": "Обновления", + "currentVersion": "Текущая сборка:", + "updateAvailable": "Доступно обновление", + "changelog": "Что изменилось", + "upToDate": "Установлена актуальная версия", + "buildAhead": "Эта сборка опережает официальную ветку main.", + "buildDiverged": "История этой сборки разошлась с официальной веткой main.", + "buildUnknown": "Эту пользовательскую сборку невозможно сравнить с официальной веткой main.", + "notGitInstall": "Эта установка не содержит сведений о коммите.", + "checkFailed": "Не удалось проверить наличие обновлений.", + "contactsGithub": "Проверка запускается вручную и обращается к GitHub.", + "openReleases": "Открыть релизы на GitHub", + "restartAfterUpdate": "Перезапустите FreeLLMAPI после завершения сборки." }, "models": { "title": "Модели", diff --git a/client/src/i18n/locales/si.json b/client/src/i18n/locales/si.json index e75a1d616..893f0b066 100644 --- a/client/src/i18n/locales/si.json +++ b/client/src/i18n/locales/si.json @@ -124,7 +124,20 @@ "compressionPreview": "සජීවී පෙරදසුන", "compressionPreviewDescription": "සපයන්නෙකුට ලැබෙන්නේ කුමක්දැයි බැලීමට ඉල්ලීම් ශරීරයක් නල මාර්ගය හරහා යවන්න.", "compressionPreviewPlaceholder": "ඉල්ලීමේ JSON හෝ සරල පෙළ අලවන්න", - "compressionRunPreview": "පෙරදසුන ධාවනය කරන්න" + "compressionRunPreview": "පෙරදසුන ධාවනය කරන්න", + "updatesTitle": "යාවත්කාලීන", + "currentVersion": "වත්මන් ගොඩනැගීම:", + "updateAvailable": "යාවත්කාලීනයක් තිබේ", + "changelog": "වෙනස් වූ දේ", + "upToDate": "යාවත්කාලීනයි", + "buildAhead": "මෙම ගොඩනැගීම නිල main ශාඛාවට වඩා ඉදිරියෙන් ඇත.", + "buildDiverged": "මෙම ගොඩනැගීම නිල main ශාඛාවෙන් අපගමනය වී ඇත.", + "buildUnknown": "මෙම අභිරුචි ගොඩනැගීම නිල main ශාඛාව සමඟ සැසඳිය නොහැක.", + "notGitInstall": "මෙම ස්ථාපනයේ commit තොරතුරු ඇතුළත් නොවේ.", + "checkFailed": "යාවත්කාලීන පරීක්ෂා කිරීමට නොහැකි විය.", + "contactsGithub": "පරීක්ෂා කිරීම් අතින් ක්‍රියාත්මක කරන අතර GitHub සමඟ සම්බන්ධ වේ.", + "openReleases": "GitHub Releases විවෘත කරන්න", + "restartAfterUpdate": "ගොඩනැගීම සම්පූර්ණ වූ පසු FreeLLMAPI නැවත ආරම්භ කරන්න." }, "models": { "title": "ආකෘති", diff --git a/client/src/i18n/locales/sk.json b/client/src/i18n/locales/sk.json index 4c026baac..d880f5796 100644 --- a/client/src/i18n/locales/sk.json +++ b/client/src/i18n/locales/sk.json @@ -124,7 +124,20 @@ "compressionPreview": "Živý náhľad", "compressionPreviewDescription": "Pošlite telo požiadavky spracovateľským reťazcom a uvidíte, čo by dostal poskytovateľ.", "compressionPreviewPlaceholder": "Vložte JSON požiadavky alebo obyčajný text", - "compressionRunPreview": "Spustiť náhľad" + "compressionRunPreview": "Spustiť náhľad", + "updatesTitle": "Aktualizácie", + "currentVersion": "Aktuálna zostava:", + "updateAvailable": "K dispozícii je aktualizácia", + "changelog": "Čo sa zmenilo", + "upToDate": "Aktuálne", + "buildAhead": "Táto zostava je pred oficiálnou vetvou main.", + "buildDiverged": "Táto zostava sa odchýlila od oficiálnej vetvy main.", + "buildUnknown": "Túto vlastnú zostavu nemožno porovnať s oficiálnou vetvou main.", + "notGitInstall": "Táto inštalácia neobsahuje informácie o commite.", + "checkFailed": "Nepodarilo sa skontrolovať aktualizácie.", + "contactsGithub": "Kontroly sa vykonávajú manuálne a pripájajú sa ku GitHubu.", + "openReleases": "Otvoriť vydania na GitHube", + "restartAfterUpdate": "Reštartujte FreeLLMAPI po dokončení zostavovania." }, "models": { "title": "Modely", diff --git a/client/src/i18n/locales/sr.json b/client/src/i18n/locales/sr.json index 022e7693c..ff948c872 100644 --- a/client/src/i18n/locales/sr.json +++ b/client/src/i18n/locales/sr.json @@ -124,7 +124,20 @@ "compressionPreview": "Преглед уживо", "compressionPreviewDescription": "Пошаљите тело захтева кроз ланац обраде да видите шта би провајдер примио.", "compressionPreviewPlaceholder": "Налепи JSON захтева или обичан текст", - "compressionRunPreview": "Покрени преглед" + "compressionRunPreview": "Покрени преглед", + "updatesTitle": "Ажурирања", + "currentVersion": "Тренутна верзија:", + "updateAvailable": "Доступно је ажурирање", + "changelog": "Шта је промењено", + "upToDate": "Ажурно", + "buildAhead": "Ова верзија је испред званичне гране main.", + "buildDiverged": "Ова верзија се разишла са званичном граном main.", + "buildUnknown": "Ова прилагођена верзија се не може упоредити са званичном граном main.", + "notGitInstall": "Ова инсталација не садржи информације о комиту.", + "checkFailed": "Провера ажурирања није успела.", + "contactsGithub": "Провере се покрећу ручно и повезују се са GitHub-ом.", + "openReleases": "Отвори GitHub издања", + "restartAfterUpdate": "Поново покрените FreeLLMAPI након завршетка израде." }, "models": { "title": "Модели", diff --git a/client/src/i18n/locales/sv.json b/client/src/i18n/locales/sv.json index 5a4f0967b..f1098a6ae 100644 --- a/client/src/i18n/locales/sv.json +++ b/client/src/i18n/locales/sv.json @@ -124,7 +124,20 @@ "compressionPreview": "Live-förhandsvisning", "compressionPreviewDescription": "Skicka en förfrågningskropp genom pipelinen för att se vad en leverantör skulle få.", "compressionPreviewPlaceholder": "Klistra in förfrågnings-JSON eller vanlig text", - "compressionRunPreview": "Kör förhandsvisning" + "compressionRunPreview": "Kör förhandsvisning", + "updatesTitle": "Uppdateringar", + "currentVersion": "Aktuell version:", + "updateAvailable": "Uppdatering tillgänglig", + "changelog": "Vad har ändrats", + "upToDate": "Uppdaterad", + "buildAhead": "Den här versionen ligger före officiella main.", + "buildDiverged": "Den här versionen har avvikit från officiella main.", + "buildUnknown": "Den här anpassade versionen kan inte jämföras med officiella main.", + "notGitInstall": "Den här installationen innehåller ingen commit-information.", + "checkFailed": "Det gick inte att söka efter uppdateringar.", + "contactsGithub": "Kontroller görs manuellt och kontaktar GitHub.", + "openReleases": "Öppna GitHub Releases", + "restartAfterUpdate": "Starta om FreeLLMAPI när bygget är klart." }, "models": { "title": "Modeller", diff --git a/client/src/i18n/locales/sw.json b/client/src/i18n/locales/sw.json index 7b705a5f7..5e9d49586 100644 --- a/client/src/i18n/locales/sw.json +++ b/client/src/i18n/locales/sw.json @@ -124,7 +124,20 @@ "compressionPreview": "Hakikisho la moja kwa moja", "compressionPreviewDescription": "Pitisha mwili wa ombi kwenye mfumo ili uone kile ambacho mtoa huduma angepokea.", "compressionPreviewPlaceholder": "Bandika JSON ya ombi au maandishi wazi", - "compressionRunPreview": "Endesha hakikisho" + "compressionRunPreview": "Endesha hakikisho", + "updatesTitle": "Masasisho", + "currentVersion": "Toleo la sasa:", + "updateAvailable": "Sasisho linapatikana", + "changelog": "Kilichobadilika", + "upToDate": "Imesasishwa", + "buildAhead": "Toleo hili liko mbele ya tawi rasmi la main.", + "buildDiverged": "Toleo hili limejitenga na tawi rasmi la main.", + "buildUnknown": "Toleo hili maalum haliwezi kulinganishwa na tawi rasmi la main.", + "notGitInstall": "Usakinishaji huu hauna taarifa za commit.", + "checkFailed": "Imeshindikana kuangalia masasisho.", + "contactsGithub": "Ukaguzi hufanywa mwenyewe na huwasiliana na GitHub.", + "openReleases": "Fungua Matoleo ya GitHub", + "restartAfterUpdate": "Anzisha upya FreeLLMAPI baada ya ujenzi kukamilika." }, "models": { "title": "Mifano", diff --git a/client/src/i18n/locales/ta.json b/client/src/i18n/locales/ta.json index 493673dde..95cf0229c 100644 --- a/client/src/i18n/locales/ta.json +++ b/client/src/i18n/locales/ta.json @@ -124,7 +124,20 @@ "compressionPreview": "நேரடி முன்னோட்டம்", "compressionPreviewDescription": "வழங்குபவருக்கு என்ன கிடைக்கும் என்று பார்க்க ஒரு கோரிக்கை பாடியைப் பைப்லைன் வழியாக அனுப்பவும்.", "compressionPreviewPlaceholder": "கோரிக்கை JSON அல்லது சாதாரண உரையை ஒட்டவும்", - "compressionRunPreview": "முன்னோட்டத்தை இயக்கு" + "compressionRunPreview": "முன்னோட்டத்தை இயக்கு", + "updatesTitle": "புதுப்பிப்புகள்", + "currentVersion": "தற்போதைய உருவாக்கம்:", + "updateAvailable": "புதுப்பிப்பு கிடைக்கிறது", + "changelog": "என்ன மாற்றப்பட்டுள்ளது", + "upToDate": "புதுப்பித்த நிலையில் உள்ளது", + "buildAhead": "இந்த உருவாக்கம் அதிகாரப்பூர்வ main-ஐ விட முன்னிலையில் உள்ளது.", + "buildDiverged": "இந்த உருவாக்கம் அதிகாரப்பூர்வ main-இலிருந்து வேறுபட்டுள்ளது.", + "buildUnknown": "இந்தத் தனிப்பயன் உருவாக்கத்தை அதிகாரப்பூர்வ main உடன் ஒப்பிட முடியாது.", + "notGitInstall": "இந்த நிறுவலில் commit தகவல் இல்லை.", + "checkFailed": "புதுப்பிப்புகளைச் சரிபார்க்க முடியவில்லை.", + "contactsGithub": "சரிபார்ப்புகள் கைமுறையாகச் செய்யப்படுகின்றன மற்றும் GitHub-ஐத் தொடர்புகொள்கின்றன.", + "openReleases": "GitHub வெளியீடுகளைத் திற", + "restartAfterUpdate": "உருவாக்கம் முடிந்ததும் FreeLLMAPI-ஐ மறுதொடக்கம் செய்யவும்." }, "models": { "title": "மாதிரிகள்", diff --git a/client/src/i18n/locales/te.json b/client/src/i18n/locales/te.json index 104b27a93..dbc84a3f3 100644 --- a/client/src/i18n/locales/te.json +++ b/client/src/i18n/locales/te.json @@ -124,7 +124,20 @@ "compressionPreview": "లైవ్ ప్రివ్యూ", "compressionPreviewDescription": "ప్రొవైడర్‌కు ఏమి అందుతుందో చూడటానికి ఒక అభ్యర్థన బాడీని పైప్‌లైన్ ద్వారా పంపండి.", "compressionPreviewPlaceholder": "అభ్యర్థన JSON లేదా సాదా టెక్స్ట్ అతికించండి", - "compressionRunPreview": "ప్రివ్యూ అమలు చేయండి" + "compressionRunPreview": "ప్రివ్యూ అమలు చేయండి", + "updatesTitle": "నవీకరణలు", + "currentVersion": "ప్రస్తుత బిల్డ్:", + "updateAvailable": "నవీకరణ అందుబాటులో ఉంది", + "changelog": "ఏమి మారింది", + "upToDate": "తాజా స్థితిలో ఉంది", + "buildAhead": "ఈ బిల్డ్ అధికారిక main కంటే ముందుంది.", + "buildDiverged": "ఈ బిల్డ్ అధికారిక main నుండి వేరుగా మారింది.", + "buildUnknown": "ఈ అనుకూల బిల్డ్‌ను అధికారిక mainతో పోల్చడం సాధ్యం కాదు.", + "notGitInstall": "ఈ ఇన్‌స్టాలేషన్‌లో commit సమాచారం లేదు.", + "checkFailed": "నవీకరణల కోసం తనిఖీ చేయడం సాధ్యం కాలేదు.", + "contactsGithub": "తనిఖీలు మాన్యువల్‌గా జరుగుతాయి మరియు GitHubను సంప్రదిస్తాయి.", + "openReleases": "GitHub విడుదలలను తెరవండి", + "restartAfterUpdate": "బిల్డ్ పూర్తయిన తర్వాత FreeLLMAPIని పునఃప్రారంభించండి." }, "models": { "title": "మోడల్స్", diff --git a/client/src/i18n/locales/th.json b/client/src/i18n/locales/th.json index e5c65e531..7bcb62f32 100644 --- a/client/src/i18n/locales/th.json +++ b/client/src/i18n/locales/th.json @@ -124,7 +124,20 @@ "compressionPreview": "ตัวอย่างสด", "compressionPreviewDescription": "ส่งเนื้อหาคำขอผ่านไปป์ไลน์เพื่อดูว่าผู้ให้บริการจะได้รับอะไร", "compressionPreviewPlaceholder": "วาง JSON ของคำขอหรือข้อความธรรมดา", - "compressionRunPreview": "รันตัวอย่าง" + "compressionRunPreview": "รันตัวอย่าง", + "updatesTitle": "การอัปเดต", + "currentVersion": "บิลด์ปัจจุบัน:", + "updateAvailable": "มีการอัปเดต", + "changelog": "มีอะไรเปลี่ยนแปลง", + "upToDate": "เป็นเวอร์ชันล่าสุด", + "buildAhead": "บิลด์นี้ล้ำหน้า main อย่างเป็นทางการ", + "buildDiverged": "บิลด์นี้แยกออกจาก main อย่างเป็นทางการ", + "buildUnknown": "ไม่สามารถเปรียบเทียบบิลด์ที่ปรับแต่งนี้กับ main อย่างเป็นทางการได้", + "notGitInstall": "การติดตั้งนี้ไม่มีข้อมูล commit", + "checkFailed": "ไม่สามารถตรวจสอบการอัปเดตได้", + "contactsGithub": "การตรวจสอบเป็นแบบดำเนินการเองและมีการติดต่อ GitHub", + "openReleases": "เปิด GitHub Releases", + "restartAfterUpdate": "รีสตาร์ต FreeLLMAPI หลังจากบิลด์เสร็จสิ้น" }, "models": { "title": "โมเดล", diff --git a/client/src/i18n/locales/tl.json b/client/src/i18n/locales/tl.json index 46127fbc3..04caaa6b6 100644 --- a/client/src/i18n/locales/tl.json +++ b/client/src/i18n/locales/tl.json @@ -124,7 +124,20 @@ "compressionPreview": "Live preview", "compressionPreviewDescription": "Ipadala ang request body sa pipeline para makita kung ano ang matatanggap ng provider.", "compressionPreviewPlaceholder": "I-paste ang request JSON o plain text", - "compressionRunPreview": "Patakbuhin ang preview" + "compressionRunPreview": "Patakbuhin ang preview", + "updatesTitle": "Mga Update", + "currentVersion": "Kasalukuyang build:", + "updateAvailable": "May available na update", + "changelog": "Ano ang nagbago", + "upToDate": "Napapanahon", + "buildAhead": "Nauuna ang build na ito sa opisyal na main.", + "buildDiverged": "Lumihis ang build na ito sa opisyal na main.", + "buildUnknown": "Hindi maihahambing ang custom build na ito sa opisyal na main.", + "notGitInstall": "Walang impormasyon ng commit ang installation na ito.", + "checkFailed": "Hindi masuri kung may mga update.", + "contactsGithub": "Manu-mano ang mga pagsusuri at kumokonekta ang mga ito sa GitHub.", + "openReleases": "Buksan ang GitHub Releases", + "restartAfterUpdate": "I-restart ang FreeLLMAPI kapag natapos ang build." }, "models": { "title": "Mga Modelo", diff --git a/client/src/i18n/locales/tr.json b/client/src/i18n/locales/tr.json index 0abf480fe..a7d76c0b0 100644 --- a/client/src/i18n/locales/tr.json +++ b/client/src/i18n/locales/tr.json @@ -124,7 +124,20 @@ "compressionPreview": "Canlı önizleme", "compressionPreviewDescription": "Bir istek gövdesini hattan geçirerek sağlayıcının ne alacağını görün.", "compressionPreviewPlaceholder": "İstek JSON'u veya düz metin yapıştırın", - "compressionRunPreview": "Önizlemeyi çalıştır" + "compressionRunPreview": "Önizlemeyi çalıştır", + "updatesTitle": "Güncellemeler", + "currentVersion": "Mevcut derleme:", + "updateAvailable": "Güncelleme mevcut", + "changelog": "Neler değişti", + "upToDate": "Güncel", + "buildAhead": "Bu derleme resmi main dalının ilerisinde.", + "buildDiverged": "Bu derleme resmi main dalından ayrışmış.", + "buildUnknown": "Bu özel derleme resmi main dalıyla karşılaştırılamıyor.", + "notGitInstall": "Bu kurulum commit bilgisi içermiyor.", + "checkFailed": "Güncellemeler denetlenemedi.", + "contactsGithub": "Denetimler elle yapılır ve GitHub ile iletişim kurar.", + "openReleases": "GitHub Sürümlerini Aç", + "restartAfterUpdate": "Derleme tamamlandıktan sonra FreeLLMAPI'yi yeniden başlatın." }, "models": { "title": "Modeller", diff --git a/client/src/i18n/locales/uk.json b/client/src/i18n/locales/uk.json index 42147bc45..34e3f123b 100644 --- a/client/src/i18n/locales/uk.json +++ b/client/src/i18n/locales/uk.json @@ -124,7 +124,20 @@ "compressionPreview": "Попередній перегляд наживо", "compressionPreviewDescription": "Пропустіть тіло запиту через конвеєр, щоб побачити, що отримає провайдер.", "compressionPreviewPlaceholder": "Вставте JSON запиту або звичайний текст", - "compressionRunPreview": "Запустити перегляд" + "compressionRunPreview": "Запустити перегляд", + "updatesTitle": "Оновлення", + "currentVersion": "Поточна збірка:", + "updateAvailable": "Доступне оновлення", + "changelog": "Що змінилося", + "upToDate": "Актуальна версія", + "buildAhead": "Ця збірка випереджає офіційну гілку main.", + "buildDiverged": "Ця збірка розійшлася з офіційною гілкою main.", + "buildUnknown": "Цю користувацьку збірку неможливо порівняти з офіційною гілкою main.", + "notGitInstall": "Ця інсталяція не містить інформації про commit.", + "checkFailed": "Не вдалося перевірити наявність оновлень.", + "contactsGithub": "Перевірки запускаються вручну та звертаються до GitHub.", + "openReleases": "Відкрити релізи GitHub", + "restartAfterUpdate": "Перезапустіть FreeLLMAPI після завершення збірки." }, "models": { "title": "Моделі", diff --git a/client/src/i18n/locales/ur.json b/client/src/i18n/locales/ur.json index 2ccb56c7b..9220726ae 100644 --- a/client/src/i18n/locales/ur.json +++ b/client/src/i18n/locales/ur.json @@ -124,7 +124,20 @@ "compressionPreview": "لائیو پیش منظر", "compressionPreviewDescription": "درخواست کی باڈی پائپ لائن سے گزاریں تاکہ دیکھ سکیں کہ فراہم کنندہ کو کیا ملے گا۔", "compressionPreviewPlaceholder": "درخواست کا JSON یا سادہ متن پیسٹ کریں", - "compressionRunPreview": "پیش منظر چلائیں" + "compressionRunPreview": "پیش منظر چلائیں", + "updatesTitle": "اپ ڈیٹس", + "currentVersion": "موجودہ بلڈ:", + "updateAvailable": "اپ ڈیٹ دستیاب ہے", + "changelog": "کیا بدلا ہے", + "upToDate": "تازہ ترین", + "buildAhead": "یہ بلڈ آفیشل main سے آگے ہے۔", + "buildDiverged": "یہ بلڈ آفیشل main سے مختلف ہو گیا ہے۔", + "buildUnknown": "اس کسٹم بلڈ کا آفیشل main سے موازنہ نہیں کیا جا سکتا۔", + "notGitInstall": "اس انسٹالیشن میں commit کی معلومات شامل نہیں ہیں۔", + "checkFailed": "اپ ڈیٹس کی جانچ نہیں ہو سکی۔", + "contactsGithub": "جانچ دستی طور پر کی جاتی ہے اور GitHub سے رابطہ کرتی ہے۔", + "openReleases": "GitHub ریلیزز کھولیں", + "restartAfterUpdate": "بلڈ مکمل ہونے کے بعد FreeLLMAPI کو دوبارہ شروع کریں۔" }, "models": { "title": "ماڈلز", diff --git a/client/src/i18n/locales/uz.json b/client/src/i18n/locales/uz.json index 190dc8c32..21aa38525 100644 --- a/client/src/i18n/locales/uz.json +++ b/client/src/i18n/locales/uz.json @@ -124,7 +124,20 @@ "compressionPreview": "Jonli oldindan ko'rish", "compressionPreviewDescription": "So'rov tanasini jarayondan o'tkazing va provayder nimani olishini ko'ring.", "compressionPreviewPlaceholder": "So'rov JSON yoki oddiy matnni joylashtiring", - "compressionRunPreview": "Oldindan ko'rishni ishga tushirish" + "compressionRunPreview": "Oldindan ko'rishni ishga tushirish", + "updatesTitle": "Yangilanishlar", + "currentVersion": "Joriy build:", + "updateAvailable": "Yangilanish mavjud", + "changelog": "Nimalar o'zgardi", + "upToDate": "Eng so'nggi versiya", + "buildAhead": "Bu build rasmiy main tarmog'idan oldinda.", + "buildDiverged": "Bu build rasmiy main tarmog'idan ajralib ketgan.", + "buildUnknown": "Bu maxsus buildni rasmiy main tarmog'i bilan taqqoslab bo'lmaydi.", + "notGitInstall": "Bu o'rnatishda commit ma'lumoti mavjud emas.", + "checkFailed": "Yangilanishlarni tekshirib bo'lmadi.", + "contactsGithub": "Tekshiruvlar qo'lda bajariladi va GitHub bilan bog'lanadi.", + "openReleases": "GitHub relizlarini ochish", + "restartAfterUpdate": "Build tugagach, FreeLLMAPI'ni qayta ishga tushiring." }, "models": { "title": "Modellar", diff --git a/client/src/i18n/locales/vi.json b/client/src/i18n/locales/vi.json index c51a0e7e9..f6f627c02 100644 --- a/client/src/i18n/locales/vi.json +++ b/client/src/i18n/locales/vi.json @@ -124,7 +124,20 @@ "compressionPreview": "Xem trước trực tiếp", "compressionPreviewDescription": "Gửi nội dung yêu cầu qua quy trình nén để xem nhà cung cấp sẽ nhận được gì.", "compressionPreviewPlaceholder": "Dán JSON yêu cầu hoặc văn bản thuần", - "compressionRunPreview": "Chạy xem trước" + "compressionRunPreview": "Chạy xem trước", + "updatesTitle": "Bản cập nhật", + "currentVersion": "Bản dựng hiện tại:", + "updateAvailable": "Có bản cập nhật", + "changelog": "Có gì thay đổi", + "upToDate": "Đã cập nhật", + "buildAhead": "Bản dựng này đi trước nhánh main chính thức.", + "buildDiverged": "Bản dựng này đã phân kỳ khỏi nhánh main chính thức.", + "buildUnknown": "Không thể so sánh bản dựng tùy chỉnh này với nhánh main chính thức.", + "notGitInstall": "Bản cài đặt này không có thông tin commit.", + "checkFailed": "Không thể kiểm tra bản cập nhật.", + "contactsGithub": "Việc kiểm tra được thực hiện thủ công và có kết nối với GitHub.", + "openReleases": "Mở GitHub Releases", + "restartAfterUpdate": "Khởi động lại FreeLLMAPI sau khi hoàn tất bản dựng." }, "models": { "title": "Người mẫu", diff --git a/client/src/i18n/locales/yo.json b/client/src/i18n/locales/yo.json index 571e7b6f1..37cac5581 100644 --- a/client/src/i18n/locales/yo.json +++ b/client/src/i18n/locales/yo.json @@ -124,7 +124,20 @@ "compressionPreview": "Awotẹlẹ laaye", "compressionPreviewDescription": "Rán ara ìbéèrè kan gba ìlànà náà kọjá láti rí ohun tí olupese yóò gbà.", "compressionPreviewPlaceholder": "Lẹ JSON ibeere tabi ọrọ lasan", - "compressionRunPreview": "Ṣiṣe awotẹlẹ" + "compressionRunPreview": "Ṣiṣe awotẹlẹ", + "updatesTitle": "Àwọn ìmúdójúìwọ̀n", + "currentVersion": "Ẹ̀yà ìkọ́lé lọ́wọ́lọ́wọ́:", + "updateAvailable": "Ìmúdójúìwọ̀n wà", + "changelog": "Ohun tí ó yí padà", + "upToDate": "Ó ti mú dójú ìwọ̀n", + "buildAhead": "Ẹ̀yà ìkọ́lé yìí ti lọ síwájú main àṣẹ.", + "buildDiverged": "Ẹ̀yà ìkọ́lé yìí ti yà kúrò ní main àṣẹ.", + "buildUnknown": "A kò lè fi ẹ̀yà ìkọ́lé àkànṣe yìí wé main àṣẹ.", + "notGitInstall": "Fífìmọ́lẹ̀ yìí kò ní àlàyé commit.", + "checkFailed": "A kò lè ṣàyẹ̀wò àwọn ìmúdójúìwọ̀n.", + "contactsGithub": "Ọwọ́ ni a fi ń ṣe àwọn àyẹ̀wò, wọ́n sì ń kan sí GitHub.", + "openReleases": "Ṣí àwọn ìtújáde GitHub", + "restartAfterUpdate": "Tun FreeLLMAPI bẹ̀rẹ̀ lẹ́yìn tí ìkọ́lé bá parí." }, "models": { "title": "Awọn awoṣe", diff --git a/client/src/i18n/locales/zh-CN.json b/client/src/i18n/locales/zh-CN.json index 0343da823..bc0322776 100644 --- a/client/src/i18n/locales/zh-CN.json +++ b/client/src/i18n/locales/zh-CN.json @@ -124,7 +124,20 @@ "compressionPreview": "实时预览", "compressionPreviewDescription": "将请求体送入压缩流程,查看提供方实际会收到的内容。", "compressionPreviewPlaceholder": "粘贴请求 JSON 或纯文本", - "compressionRunPreview": "运行预览" + "compressionRunPreview": "运行预览", + "updatesTitle": "更新", + "currentVersion": "当前构建:", + "updateAvailable": "有可用更新", + "changelog": "更新内容", + "upToDate": "已是最新版本", + "buildAhead": "此构建领先于官方 main 分支。", + "buildDiverged": "此构建已偏离官方 main 分支。", + "buildUnknown": "无法将此自定义构建与官方 main 分支进行比较。", + "notGitInstall": "此安装不包含 commit 信息。", + "checkFailed": "无法检查更新。", + "contactsGithub": "检查需手动执行,并会连接 GitHub。", + "openReleases": "打开 GitHub Releases", + "restartAfterUpdate": "构建完成后重启 FreeLLMAPI。" }, "models": { "title": "模型", diff --git a/client/src/i18n/locales/zh-TW.json b/client/src/i18n/locales/zh-TW.json index b7a28436a..a479369fd 100644 --- a/client/src/i18n/locales/zh-TW.json +++ b/client/src/i18n/locales/zh-TW.json @@ -124,7 +124,20 @@ "compressionPreview": "即時預覽", "compressionPreviewDescription": "將請求主體送入流程,檢視提供者實際會收到的內容。", "compressionPreviewPlaceholder": "貼上請求 JSON 或純文字", - "compressionRunPreview": "執行預覽" + "compressionRunPreview": "執行預覽", + "updatesTitle": "更新", + "currentVersion": "目前建置:", + "updateAvailable": "有可用更新", + "changelog": "更新內容", + "upToDate": "已是最新版本", + "buildAhead": "此建置領先官方 main 分支。", + "buildDiverged": "此建置已偏離官方 main 分支。", + "buildUnknown": "無法將此自訂建置與官方 main 分支進行比較。", + "notGitInstall": "此安裝不包含 commit 資訊。", + "checkFailed": "無法檢查更新。", + "contactsGithub": "檢查需手動執行,並會連線至 GitHub。", + "openReleases": "開啟 GitHub Releases", + "restartAfterUpdate": "建置完成後重新啟動 FreeLLMAPI。" }, "models": { "title": "型號", diff --git a/desktop/scripts/bundle-server.mjs b/desktop/scripts/bundle-server.mjs index da43a7c56..6e7c1199d 100644 --- a/desktop/scripts/bundle-server.mjs +++ b/desktop/scripts/bundle-server.mjs @@ -7,6 +7,8 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const githubSha = process.env.GITHUB_SHA; +const hasBuildIdentity = /^[0-9a-f]{40}$/i.test(githubSha ?? ''); await build({ entryPoints: [path.resolve(__dirname, '../src/server-host.ts')], @@ -16,6 +18,10 @@ await build({ target: 'node20', outfile: path.resolve(__dirname, '../build/server.mjs'), external: ['better-sqlite3'], + define: { + 'process.env.FREELLMAPI_COMMIT_SHA': JSON.stringify(hasBuildIdentity ? githubSha : ''), + 'process.env.FREELLMAPI_INSTALL_METHOD': JSON.stringify(hasBuildIdentity ? 'desktop' : ''), + }, // Some inlined CJS deps (express internals) reference `require` at runtime; // give the ESM bundle a working one. banner: { diff --git a/server/src/__tests__/routes/auth.test.ts b/server/src/__tests__/routes/auth.test.ts index 9a43d1775..abb45fc86 100644 --- a/server/src/__tests__/routes/auth.test.ts +++ b/server/src/__tests__/routes/auth.test.ts @@ -38,6 +38,7 @@ describe('Dashboard auth (#35)', () => { expect((await call(app, 'GET', '/api/keys')).status).toBe(401); expect((await call(app, 'GET', '/api/fallback')).status).toBe(401); expect((await call(app, 'GET', '/api/settings/api-key')).status).toBe(401); + expect((await call(app, 'GET', '/api/update/status')).status).toBe(401); }); it('leaves /api/ping and the /v1 proxy reachable without a dashboard session', async () => { diff --git a/server/src/__tests__/routes/update.test.ts b/server/src/__tests__/routes/update.test.ts new file mode 100644 index 000000000..912d0d6ac --- /dev/null +++ b/server/src/__tests__/routes/update.test.ts @@ -0,0 +1,490 @@ +import { describe, it, expect, vi } from 'vitest'; +import express from 'express'; +import type { Express } from 'express'; +import http from 'node:http'; +import { createUpdateRouter } from '../../routes/update.js'; + +const LOCAL_SHA = '0123456789abcdef0123456789abcdef01234567'; +const REMOTE_SHA = '89abcdef0123456789abcdef0123456789abcdef'; +const NOW = Date.parse('2026-07-28T14:00:00.000Z'); + +function httpGet(app: Express, path: string): Promise<{ status: number; body: any }> { + return new Promise((resolve, reject) => { + const server = app.listen(0, () => { + const address = server.address(); + if (!address || typeof address === 'string') return reject(new Error('No test server address')); + const req = http.request( + { hostname: '127.0.0.1', port: address.port, path, method: 'GET' }, + (res) => { + let data = ''; + res.on('data', chunk => { data += chunk; }); + res.on('end', () => { + server.close(); + resolve({ status: res.statusCode!, body: JSON.parse(data) }); + }); + }, + ); + req.on('error', (error) => { server.close(); reject(error); }); + req.end(); + }); + }); +} + +function response(body: unknown, status = 200): Response { + return new Response(body === undefined ? undefined : JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); +} + +function atomResponse(commits: Array<{ sha: string; message: string; date?: string }>, status = 200): Response { + const entries = commits.map(commit => ` + + tag:github.com,2008:Grit::Commit/${commit.sha} + ${commit.message} + ${commit.date ? `${commit.date}` : ''} + `).join(''); + return new Response(`${entries}`, { + status, + headers: { 'Content-Type': 'application/atom+xml' }, + }); +} + +function compareBody(status: 'identical' | 'ahead' | 'behind' | 'diverged', message = 'remote commit') { + const commit = { + sha: status === 'identical' ? LOCAL_SHA : REMOTE_SHA, + commit: { + message, + committer: { date: '2026-07-28T10:00:00Z' }, + }, + }; + return { + status, + commits: status === 'identical' || status === 'behind' ? [] : [commit], + base_commit: commit, + merge_base_commit: commit, + }; +} + +function createTestApp(overrides: Parameters[0] = {}) { + const app = express(); + const fetchMock = overrides.fetch ?? vi.fn(async () => response(compareBody('identical'))); + const execMock = overrides.execFileSync ?? vi.fn(() => `${LOCAL_SHA}\n`); + const logger = overrides.logger ?? { error: vi.fn() }; + app.use('/api/update', createUpdateRouter({ + env: {}, + cwd: '/worktree/server', + now: () => NOW, + ...overrides, + fetch: fetchMock, + execFileSync: execMock, + logger, + })); + return { app, fetchMock, execMock, logger }; +} + +describe('Update API', () => { + describe('GET /api/update/check', () => { + it.each([ + ['identical', 'current'], + ['ahead', 'available'], + ['behind', 'ahead'], + ['diverged', 'diverged'], + ] as const)('maps GitHub %s to %s', async (githubStatus, expectedStatus) => { + const fetchMock = vi.fn(async () => response(compareBody(githubStatus))); + const { app } = createTestApp({ fetch: fetchMock }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(200); + expect(result.body).toMatchObject({ + status: expectedStatus, + installation: 'source', + localSha: LOCAL_SHA.slice(0, 7), + remoteSha: (githubStatus === 'identical' ? LOCAL_SHA : REMOTE_SHA).slice(0, 7), + remoteMessage: 'remote commit', + remoteDate: '2026-07-28T10:00:00Z', + checkedAt: '2026-07-28T14:00:00.000Z', + }); + expect(fetchMock).toHaveBeenCalledWith( + `https://api.github.com/repos/tashfeenahmed/freellmapi/compare/${LOCAL_SHA}...main`, + expect.any(Object), + ); + }); + + it('prefers a validated packaged-build identity and truncates commit messages', async () => { + const longMessage = 'x'.repeat(250); + const fetchMock = vi.fn(async () => response(compareBody('ahead', longMessage))); + const execMock = vi.fn(() => `${REMOTE_SHA}\n`); + const { app } = createTestApp({ + env: { + FREELLMAPI_COMMIT_SHA: LOCAL_SHA.toUpperCase(), + FREELLMAPI_INSTALL_METHOD: 'docker', + }, + fetch: fetchMock, + execFileSync: execMock, + }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.body.installation).toBe('docker'); + expect(result.body.remoteMessage).toHaveLength(200); + expect(execMock).not.toHaveBeenCalled(); + }); + + it('uses an explicitly configured update-check token for authenticated rate limits', async () => { + const fetchMock = vi.fn(async () => response(compareBody('identical'))); + const { app } = createTestApp({ + env: { FREELLMAPI_UPDATE_GITHUB_TOKEN: 'test-token' }, + fetch: fetchMock, + }); + + await httpGet(app, '/api/update/check'); + + expect(fetchMock).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ + headers: expect.objectContaining({ Authorization: 'Bearer test-token' }), + })); + }); + + it('does not borrow an ambient GitHub token', async () => { + const fetchMock = vi.fn(async () => response(compareBody('identical'))); + const { app } = createTestApp({ + env: { GITHUB_TOKEN: 'ambient-token' }, + fetch: fetchMock, + }); + + await httpGet(app, '/api/update/check'); + + const request = fetchMock.mock.calls[0][1]; + expect(request?.headers).not.toHaveProperty('Authorization'); + }); + + it('reports disabled without resolving Git identity or making a network request', async () => { + const fetchMock = vi.fn(); + const execMock = vi.fn(() => `${LOCAL_SHA}\n`); + const { app, logger } = createTestApp({ + env: { FREELLMAPI_UPDATE_CHECK: ' OFF ' }, + fetch: fetchMock, + execFileSync: execMock, + }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(200); + expect(result.body).toEqual({ + status: 'disabled', + installation: 'unknown', + localSha: null, + checkedAt: '2026-07-28T14:00:00.000Z', + }); + expect(execMock).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('falls back to the public Atom feed when the anonymous API limit is exhausted', async () => { + const newestSha = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + const middleSha = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + const fetchMock = vi.fn() + .mockResolvedValueOnce(response({ message: 'API rate limit exceeded' }, 403)) + .mockResolvedValueOnce(atomResponse([ + { sha: newestSha, message: 'Newest & safest', date: '2026-07-28T13:00:00Z' }, + { sha: middleSha, message: 'Middle commit', date: '2026-07-28T12:00:00Z' }, + { sha: LOCAL_SHA, message: 'Installed commit', date: '2026-07-28T11:00:00Z' }, + ])); + const { app } = createTestApp({ fetch: fetchMock }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(200); + expect(result.body).toMatchObject({ + status: 'available', + remoteSha: newestSha.slice(0, 7), + remoteMessage: 'Newest & safest', + remoteDate: '2026-07-28T13:00:00Z', + changes: [ + { sha: newestSha.slice(0, 7), message: 'Newest & safest', date: '2026-07-28T13:00:00Z' }, + { sha: middleSha.slice(0, 7), message: 'Middle commit', date: '2026-07-28T12:00:00Z' }, + ], + }); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'https://github.com/tashfeenahmed/freellmapi/commits/main.atom', + expect.any(Object), + ); + }); + + it('uses the Atom fallback to recognize the current build', async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(response(undefined, 401)) + .mockResolvedValueOnce(atomResponse([ + { sha: LOCAL_SHA, message: 'Current commit', date: '2026-07-28T13:00:00Z' }, + ])); + const { app } = createTestApp({ fetch: fetchMock }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(200); + expect(result.body.status).toBe('current'); + expect(result.body.changes).toBeUndefined(); + }); + + it('returns at most eight changes in newest-first order with optional dates', async () => { + const commits = Array.from({ length: 10 }, (_, index) => { + const number = index + 1; + return { + sha: number.toString(16).padStart(40, '0'), + commit: { + message: `subject ${number}\nignored body`, + ...(number === 9 ? {} : { committer: { date: `2026-07-${number.toString().padStart(2, '0')}T10:00:00Z` } }), + }, + }; + }); + const fetchMock = vi.fn(async () => response({ + ...compareBody('ahead'), + commits, + })); + const { app } = createTestApp({ fetch: fetchMock }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(200); + expect(result.body.changes).toHaveLength(8); + expect(result.body.changes.map((change: { message: string }) => change.message)).toEqual([ + 'subject 10', + 'subject 9', + 'subject 8', + 'subject 7', + 'subject 6', + 'subject 5', + 'subject 4', + 'subject 3', + ]); + expect(result.body.changes[0]).toEqual({ + sha: commits[9].sha.slice(0, 7), + message: 'subject 10', + date: '2026-07-10T10:00:00Z', + }); + expect(result.body.changes[1]).toEqual({ + sha: commits[8].sha.slice(0, 7), + message: 'subject 9', + }); + }); + + it('caps change subjects at 160 characters', async () => { + const fetchMock = vi.fn(async () => response(compareBody( + 'ahead', + `${'x'.repeat(200)}\nignored body`, + ))); + const { app } = createTestApp({ fetch: fetchMock }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(200); + expect(result.body.changes).toEqual([{ + sha: REMOTE_SHA.slice(0, 7), + message: 'x'.repeat(160), + date: '2026-07-28T10:00:00Z', + }]); + }); + + it('rejects malformed commits selected for the changelog', async () => { + const validCommit = compareBody('ahead').commits[0]; + const fetchMock = vi.fn(async () => response({ + ...compareBody('ahead'), + commits: [ + { sha: LOCAL_SHA, commit: { message: 42 } }, + validCommit, + ], + })); + const logger = { error: vi.fn() }; + const { app } = createTestApp({ fetch: fetchMock, logger }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(502); + expect(result.body).toEqual({ + error: { message: 'Unable to check for updates', type: 'upstream_error' }, + }); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('resolves Git identity lazily from the current working directory', async () => { + const execMock = vi.fn(() => `${LOCAL_SHA}\n`); + const { app } = createTestApp({ + env: { FREELLMAPI_COMMIT_SHA: 'invalid' }, + execFileSync: execMock, + }); + expect(execMock).not.toHaveBeenCalled(); + + await httpGet(app, '/api/update/status'); + + expect(execMock).toHaveBeenCalledWith('git', ['rev-parse', 'HEAD'], { + cwd: '/worktree/server', + encoding: 'utf8', + timeout: 5_000, + }); + }); + + it('returns unknown for an unrecognized commit without parsing the 404 body', async () => { + const fetchMock = vi.fn(async () => response({ private: 'upstream detail' }, 404)); + const { app } = createTestApp({ fetch: fetchMock }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(200); + expect(result.body).toEqual({ + status: 'unknown', + installation: 'source', + localSha: LOCAL_SHA.slice(0, 7), + checkedAt: '2026-07-28T14:00:00.000Z', + }); + }); + + it('returns unsupported without a network request when no identity is available', async () => { + const fetchMock = vi.fn(); + const execMock = vi.fn(() => { throw new Error('not a repository'); }); + const { app } = createTestApp({ fetch: fetchMock, execFileSync: execMock }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(200); + expect(result.body).toEqual({ + status: 'unsupported', + installation: 'unknown', + localSha: null, + checkedAt: '2026-07-28T14:00:00.000Z', + }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('preserves the install method for a self-built Docker image without Git metadata', async () => { + const fetchMock = vi.fn(); + const execMock = vi.fn(() => `${LOCAL_SHA}\n`); + const { app } = createTestApp({ + env: { FREELLMAPI_INSTALL_METHOD: 'docker' }, + fetch: fetchMock, + execFileSync: execMock, + }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.body).toMatchObject({ + status: 'unsupported', + installation: 'docker', + localSha: null, + }); + expect(execMock).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it.each([ + ['network failure', vi.fn(async () => { throw new Error('secret network detail'); })], + ['non-2xx response', vi.fn(async () => response({ message: 'upstream detail' }, 500))], + ['malformed response', vi.fn(async () => response({ status: 'ahead', commits: [{ sha: 'not-a-sha' }] }))], + ])('returns a stable 502 for %s and logs details', async (_name, fetchMock) => { + const logger = { error: vi.fn() }; + const { app } = createTestApp({ fetch: fetchMock, logger }); + + const result = await httpGet(app, '/api/update/check'); + + expect(result.status).toBe(502); + expect(result.body).toEqual({ + error: { message: 'Unable to check for updates', type: 'upstream_error' }, + }); + expect(JSON.stringify(result.body)).not.toMatch(/secret|upstream detail|not-a-sha/); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('caches successful semantic results for five minutes but not errors', async () => { + let currentTime = NOW; + const fetchMock = vi.fn() + .mockResolvedValueOnce(response(compareBody('ahead'))) + .mockRejectedValueOnce(new Error('temporary failure')) + .mockResolvedValueOnce(response(compareBody('identical'))); + const { app } = createTestApp({ fetch: fetchMock, now: () => currentTime }); + + expect((await httpGet(app, '/api/update/check')).body.status).toBe('available'); + currentTime += 299_999; + expect((await httpGet(app, '/api/update/check')).body.status).toBe('available'); + expect(fetchMock).toHaveBeenCalledTimes(1); + + currentTime += 1; + expect((await httpGet(app, '/api/update/check')).status).toBe(502); + expect((await httpGet(app, '/api/update/check')).body.status).toBe('current'); + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it('coalesces concurrent in-flight checks', async () => { + let resolveFetch!: (value: Response) => void; + const fetchMock = vi.fn(() => new Promise((resolve) => { resolveFetch = resolve; })); + const { app } = createTestApp({ fetch: fetchMock }); + + const first = httpGet(app, '/api/update/check'); + const second = httpGet(app, '/api/update/check'); + await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce()); + resolveFetch(response(compareBody('ahead'))); + + expect((await first).body.status).toBe('available'); + expect((await second).body.status).toBe('available'); + expect(fetchMock).toHaveBeenCalledOnce(); + }); + }); + + describe('GET /api/update/status', () => { + it('reports disabled without resolving Git identity or making a network request', async () => { + const fetchMock = vi.fn(); + const execMock = vi.fn(() => `${LOCAL_SHA}\n`); + const { app } = createTestApp({ + env: { FREELLMAPI_UPDATE_CHECK: 'off' }, + fetch: fetchMock, + execFileSync: execMock, + }); + + expect((await httpGet(app, '/api/update/status')).body).toEqual({ + status: 'disabled', + installation: 'unknown', + localSha: null, + lastChecked: null, + }); + expect(execMock).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('returns idle before a supported installation has checked', async () => { + const { app } = createTestApp(); + + expect((await httpGet(app, '/api/update/status')).body).toEqual({ + status: 'idle', + installation: 'source', + localSha: LOCAL_SHA.slice(0, 7), + lastChecked: null, + }); + }); + + it('returns unsupported for an unknown installation', async () => { + const { app } = createTestApp({ execFileSync: vi.fn(() => 'invalid') }); + + expect((await httpGet(app, '/api/update/status')).body).toEqual({ + status: 'unsupported', + installation: 'unknown', + localSha: null, + lastChecked: null, + }); + }); + + it('returns the cached semantic state and last checked time without fetching', async () => { + const fetchMock = vi.fn(async () => response(compareBody('diverged'))); + const { app } = createTestApp({ fetch: fetchMock }); + await httpGet(app, '/api/update/check'); + + expect((await httpGet(app, '/api/update/status')).body).toEqual({ + status: 'diverged', + installation: 'source', + localSha: LOCAL_SHA.slice(0, 7), + lastChecked: '2026-07-28T14:00:00.000Z', + }); + expect(fetchMock).toHaveBeenCalledOnce(); + }); + }); +}); diff --git a/server/src/app.ts b/server/src/app.ts index c48cbaf38..0e425fb8b 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -26,6 +26,7 @@ import { statusRouter, providersRouter } from './routes/status.js'; import { geminiRouter } from './routes/gemini.js'; import { ollamaRouter } from './routes/ollama.js'; import { urlTokenRouter } from './routes/url-tokens.js'; +import { updateRouter } from './routes/update.js'; import { requireAuth } from './middleware/requireAuth.js'; import { createProxyRateLimiter } from './middleware/rateLimit.js'; import { errorHandler } from './middleware/errorHandler.js'; @@ -114,6 +115,7 @@ export function createApp(config?: Config) { app.use('/api/premium', requireAuth, premiumRouter); app.use('/api/cache', requireAuth, cacheRouter); app.use('/api/compression', requireAuth, compressionRouter); + app.use('/api/update', requireAuth, updateRouter); // Static, unauthenticated API reference: GET /v1/docs (viewer) and // GET /v1/openapi.json (spec). Mounted before the rate limiter so the docs diff --git a/server/src/routes/update.ts b/server/src/routes/update.ts new file mode 100644 index 000000000..ece79dc76 --- /dev/null +++ b/server/src/routes/update.ts @@ -0,0 +1,416 @@ +import { execFileSync as nodeExecFileSync } from 'node:child_process'; +import { Router } from 'express'; +import type { Request, Response } from 'express'; + +const REPOSITORY = 'tashfeenahmed/freellmapi'; +const CACHE_TTL_MS = 5 * 60 * 1000; +const SHA_PATTERN = /^[0-9a-f]{40}$/i; +const COMPARE_STATUSES = ['identical', 'ahead', 'behind', 'diverged'] as const; +const MAX_CHANGES = 8; +const MAX_CHANGE_MESSAGE_LENGTH = 160; +const MAX_ATOM_FEED_LENGTH = 1_000_000; + +type Installation = 'source' | 'docker' | 'desktop' | 'unknown'; +type CheckStatus = 'current' | 'available' | 'ahead' | 'diverged' | 'unknown' | 'unsupported' | 'disabled'; +type CompareStatus = (typeof COMPARE_STATUSES)[number]; + +interface Identity { + sha: string | null; + installation: Installation; +} + +interface CheckResult { + status: CheckStatus; + installation: Installation; + localSha: string | null; + checkedAt: string; + remoteSha?: string; + remoteMessage?: string; + remoteDate?: string; + changes?: Change[]; +} + +interface Change { + sha: string; + message: string; + date?: string; +} + +interface CachedResult { + result: CheckResult; + cachedAt: number; +} + +type ExecFileSync = ( + file: string, + args: string[], + options: { cwd: string; encoding: 'utf8'; timeout: number }, +) => string | Buffer; + +export interface UpdateRouterOptions { + fetch?: typeof globalThis.fetch; + execFileSync?: ExecFileSync; + env?: NodeJS.ProcessEnv; + cwd?: string; + now?: () => number; + logger?: Pick; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function validSha(value: unknown): string | null { + return typeof value === 'string' && SHA_PATTERN.test(value.trim()) + ? value.trim().toLowerCase() + : null; +} + +function compareStatus(value: unknown): CompareStatus | null { + return typeof value === 'string' && (COMPARE_STATUSES as readonly string[]).includes(value) + ? value as CompareStatus + : null; +} + +function validDate(value: unknown): value is string { + return typeof value === 'string' && value.length <= 64 && !Number.isNaN(Date.parse(value)); +} + +function mapCompareStatus(status: CompareStatus): Exclude { + switch (status) { + case 'identical': return 'current'; + case 'ahead': return 'available'; + case 'behind': return 'ahead'; + case 'diverged': return 'diverged'; + } +} + +function remoteCommit(body: Record, status: CompareStatus): Record | null { + if (!Array.isArray(body.commits)) { + throw new Error('GitHub compare response has no commits array'); + } + + const lastCommit = body.commits.at(-1); + const candidate = lastCommit + ?? (status === 'behind' ? body.merge_base_commit : body.base_commit); + + if (candidate === undefined || candidate === null) return null; + if (!isRecord(candidate)) throw new Error('GitHub compare response has an invalid commit'); + return candidate; +} + +function parseRemoteMetadata( + body: Record, + status: CompareStatus, + localSha: string, +): Pick { + const candidate = remoteCommit(body, status); + if (!candidate) { + return status === 'identical' ? { remoteSha: localSha.slice(0, 7) } : {}; + } + + const sha = validSha(candidate.sha); + if (!sha) throw new Error('GitHub compare response has an invalid commit SHA'); + + const metadata: Pick = { + remoteSha: sha.slice(0, 7), + }; + if (candidate.commit === undefined || candidate.commit === null) return metadata; + if (!isRecord(candidate.commit)) throw new Error('GitHub compare response has invalid commit metadata'); + + if (candidate.commit.message !== undefined) { + if (typeof candidate.commit.message !== 'string') { + throw new Error('GitHub compare response has an invalid commit message'); + } + metadata.remoteMessage = candidate.commit.message.slice(0, 200); + } + + const committer = candidate.commit.committer; + if (committer !== undefined && committer !== null) { + if (!isRecord(committer)) throw new Error('GitHub compare response has an invalid committer'); + if (committer.date !== undefined && committer.date !== null) { + if (!validDate(committer.date)) { + throw new Error('GitHub compare response has an invalid commit date'); + } + metadata.remoteDate = committer.date; + } + } + + return metadata; +} + +function parseChanges(body: Record, status: CompareStatus): Change[] | undefined { + if (status !== 'ahead') return undefined; + if (!Array.isArray(body.commits)) { + throw new Error('GitHub compare response has no commits array'); + } + + return body.commits.slice(-MAX_CHANGES).reverse().map((candidate: unknown) => { + if (!isRecord(candidate)) throw new Error('GitHub compare response has an invalid commit'); + const sha = validSha(candidate.sha); + if (!sha) throw new Error('GitHub compare response has an invalid commit SHA'); + if (!isRecord(candidate.commit)) { + throw new Error('GitHub compare response has invalid commit metadata'); + } + if (typeof candidate.commit.message !== 'string') { + throw new Error('GitHub compare response has an invalid commit message'); + } + + const change: Change = { + sha: sha.slice(0, 7), + message: candidate.commit.message + .slice(0, MAX_CHANGE_MESSAGE_LENGTH) + .split(/[\r\n]/, 1)[0], + }; + const committer = candidate.commit.committer; + if (committer !== undefined && committer !== null) { + if (!isRecord(committer)) throw new Error('GitHub compare response has an invalid committer'); + if (committer.date !== undefined && committer.date !== null) { + if (!validDate(committer.date)) { + throw new Error('GitHub compare response has an invalid commit date'); + } + change.date = committer.date; + } + } + return change; + }); +} + +function decodeXmlText(value: string): string { + const named: Record = { + amp: '&', + apos: "'", + gt: '>', + lt: '<', + quot: '"', + }; + return value.replace(/&(?:#(\d+)|#x([0-9a-f]+)|(amp|apos|gt|lt|quot));/gi, (entity, decimal, hex, name) => { + if (decimal) return String.fromCodePoint(Number.parseInt(decimal, 10)); + if (hex) return String.fromCodePoint(Number.parseInt(hex, 16)); + return named[String(name).toLowerCase()] ?? entity; + }); +} + +function parseAtomFallback( + xml: string, + localSha: string, + baseResult: Pick, +): CheckResult { + if (xml.length > MAX_ATOM_FEED_LENGTH) throw new Error('GitHub Atom feed is too large'); + + const commits: Array = []; + for (const match of xml.matchAll(/([\s\S]*?)<\/entry>/gi)) { + const entry = match[1]; + const sha = entry.match(/[^<]*Commit\/([0-9a-f]{40})<\/id>/i)?.[1]?.toLowerCase(); + const title = entry.match(/\s*([\s\S]*?)\s*<\/title>/i)?.[1]; + const updated = entry.match(/<updated>\s*([^<]+)\s*<\/updated>/i)?.[1]?.trim(); + if (!sha || title === undefined || (updated !== undefined && !validDate(updated))) { + throw new Error('GitHub Atom feed has an invalid commit'); + } + + commits.push({ + fullSha: sha, + sha: sha.slice(0, 7), + message: decodeXmlText(title).replace(/\s+/g, ' ').trim().slice(0, MAX_CHANGE_MESSAGE_LENGTH), + ...(updated ? { date: updated } : {}), + }); + } + + if (commits.length === 0 || commits[0].message.length === 0) { + throw new Error('GitHub Atom feed has no commits'); + } + + const latest = commits[0]; + const localIndex = commits.findIndex(commit => commit.fullSha === localSha); + const status: CheckStatus = localIndex === 0 + ? 'current' + : localIndex > 0 + ? 'available' + : 'unknown'; + + return { + status, + ...baseResult, + remoteSha: latest.sha, + remoteMessage: latest.message, + ...(latest.date ? { remoteDate: latest.date } : {}), + ...(status === 'available' + ? { changes: commits.slice(0, localIndex).slice(0, MAX_CHANGES).map(({ fullSha: _fullSha, ...change }) => change) } + : {}), + }; +} + +export function createUpdateRouter(options: UpdateRouterOptions = {}): Router { + const router = Router(); + const env = options.env ?? process.env; + const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init)); + const execFile = options.execFileSync ?? ((file, args, execOptions) => ( + nodeExecFileSync(file, args, execOptions) + )); + const now = options.now ?? Date.now; + const logger = options.logger ?? console; + const updateCheckDisabled = env.FREELLMAPI_UPDATE_CHECK?.trim().toLowerCase() === 'off'; + const githubToken = env.FREELLMAPI_UPDATE_GITHUB_TOKEN?.trim(); + + let identity: Identity | undefined; + let cache: CachedResult | null = null; + let inFlight: Promise<CheckResult> | null = null; + + function resolveIdentity(): Identity { + if (identity) return identity; + + const environmentSha = validSha(env.FREELLMAPI_COMMIT_SHA); + const installMethod = env.FREELLMAPI_INSTALL_METHOD; + if (installMethod === 'docker' || installMethod === 'desktop') { + identity = { sha: environmentSha, installation: installMethod }; + return identity; + } + + try { + const output = execFile('git', ['rev-parse', 'HEAD'], { + cwd: options.cwd ?? process.cwd(), + encoding: 'utf8', + timeout: 5_000, + }); + const gitSha = validSha(output.toString()); + if (gitSha) { + identity = { sha: gitSha, installation: 'source' }; + return identity; + } + } catch { + // A packaged or custom build may not have Git metadata. + } + + identity = { sha: null, installation: 'unknown' }; + return identity; + } + + async function performCheck(currentIdentity: Identity): Promise<CheckResult> { + const checkedAtMs = now(); + const baseResult = { + installation: currentIdentity.installation, + localSha: currentIdentity.sha!.slice(0, 7), + checkedAt: new Date(checkedAtMs).toISOString(), + }; + const url = `https://api.github.com/repos/${REPOSITORY}/compare/${currentIdentity.sha}...main`; + const response = await fetchImpl(url, { + headers: { + Accept: 'application/vnd.github+json', + 'User-Agent': 'freellmapi-update-checker', + 'X-GitHub-Api-Version': '2022-11-28', + ...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}), + }, + signal: AbortSignal.timeout(10_000), + }); + + if (response.status === 404) { + return { status: 'unknown', ...baseResult }; + } + if (response.status === 401 || response.status === 403 || response.status === 429) { + const atomResponse = await fetchImpl(`https://github.com/${REPOSITORY}/commits/main.atom`, { + headers: { + Accept: 'application/atom+xml', + 'User-Agent': 'freellmapi-update-checker', + }, + signal: AbortSignal.timeout(10_000), + }); + if (!atomResponse.ok) { + throw new Error(`GitHub Atom request returned HTTP ${atomResponse.status}`); + } + return parseAtomFallback(await atomResponse.text(), currentIdentity.sha!, baseResult); + } + if (!response.ok) { + throw new Error(`GitHub compare request returned HTTP ${response.status}`); + } + + const body: unknown = await response.json(); + if (!isRecord(body)) throw new Error('GitHub compare response is not an object'); + const upstreamStatus = compareStatus(body.status); + if (!upstreamStatus) throw new Error('GitHub compare response has an invalid status'); + + return { + status: mapCompareStatus(upstreamStatus), + ...baseResult, + ...parseRemoteMetadata(body, upstreamStatus, currentIdentity.sha!), + ...(upstreamStatus === 'ahead' ? { changes: parseChanges(body, upstreamStatus) } : {}), + }; + } + + function check(currentIdentity: Identity): Promise<CheckResult> { + const currentTime = now(); + if (cache && currentTime - cache.cachedAt < CACHE_TTL_MS) { + return Promise.resolve(cache.result); + } + if (inFlight) return inFlight; + + const request = performCheck(currentIdentity) + .then((result) => { + cache = { result, cachedAt: now() }; + return result; + }) + .catch((error: unknown) => { + logger.error('[update] GitHub compare check failed', error); + throw error; + }) + .finally(() => { + if (inFlight === request) inFlight = null; + }); + inFlight = request; + return request; + } + + router.get('/check', async (_req: Request, res: Response) => { + if (updateCheckDisabled) { + return res.json({ + status: 'disabled', + installation: 'unknown', + localSha: null, + checkedAt: new Date(now()).toISOString(), + } satisfies CheckResult); + } + + const currentIdentity = resolveIdentity(); + if (!currentIdentity.sha) { + return res.json({ + status: 'unsupported', + installation: currentIdentity.installation, + localSha: null, + checkedAt: new Date(now()).toISOString(), + } satisfies CheckResult); + } + + try { + return res.json(await check(currentIdentity)); + } catch { + return res.status(502).json({ + error: { + message: 'Unable to check for updates', + type: 'upstream_error', + }, + }); + } + }); + + router.get('/status', (_req: Request, res: Response) => { + if (updateCheckDisabled) { + return res.json({ + status: 'disabled', + installation: 'unknown', + localSha: null, + lastChecked: null, + }); + } + + const currentIdentity = resolveIdentity(); + res.json({ + status: currentIdentity.sha ? (cache?.result.status ?? 'idle') : 'unsupported', + installation: currentIdentity.installation, + localSha: currentIdentity.sha?.slice(0, 7) ?? null, + lastChecked: cache?.result.checkedAt ?? null, + }); + }); + + return router; +} + +export const updateRouter = createUpdateRouter();