From 2668ff71101220b4f004d390b6098b1a8051a116 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:31:19 +0200 Subject: [PATCH 001/368] Fix Step 3 mobile layout, X consent navigation and app display messages --- .github/workflows/ci.yml | 2 + frontend/app/page.tsx | 4 +- frontend/components/FoodSearchPlaceholder.tsx | 15 +- frontend/components/XamanLoginPanel.tsx | 52 +- frontend/config/auth-ui-copy.json | 464 ++++++++++++++++++ frontend/lib/authUi.ts | 21 + frontend/lib/foodSearchReadiness.ts | 36 ++ tools/tests/account_erasure_ui.test.mjs | 6 +- tools/tests/auth_ui.test.mjs | 37 ++ tools/tests/backend_wake_navigation.test.mjs | 3 + tools/tests/food_logging_ui.test.mjs | 11 +- tools/tests/food_search_readiness.test.mjs | 21 + tools/tests/helpers/auth_ui.mjs | 16 + tools/tests/xaman_login_start_retry.test.mjs | 11 +- tools/tests/xaman_logout_request.test.mjs | 2 + .../calorietoken-site-style/LEESMIJ.md | 20 +- .../assets/blog-timeline.js | 11 + .../assets/content-data.json | 2 +- .../assets/content-language.js | 4 +- .../assets/discovery.css | 18 +- .../assets/refinements.css | 32 ++ .../assets/refinements.js | 27 +- .../calorietoken-site-style.php | 4 +- 23 files changed, 774 insertions(+), 45 deletions(-) create mode 100644 frontend/config/auth-ui-copy.json create mode 100644 frontend/lib/authUi.ts create mode 100644 frontend/lib/foodSearchReadiness.ts create mode 100644 tools/tests/auth_ui.test.mjs create mode 100644 tools/tests/food_search_readiness.test.mjs create mode 100644 tools/tests/helpers/auth_ui.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd2ca8bd..237cfd6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -315,6 +315,8 @@ jobs: - name: Test embedded login and private account controls run: >- node --test + tools/tests/auth_ui.test.mjs + tools/tests/food_search_readiness.test.mjs tools/tests/auth_callback_return.test.mjs tools/tests/account_data_import_ui.test.mjs tools/tests/account_data_export_validation.test.mjs diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index c6692e33..7cedb7f6 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -7,10 +7,10 @@ import { TestnetEntry } from "@/components/TestnetEntry"; export default function Home() { return ( -
+
-
+
diff --git a/frontend/components/FoodSearchPlaceholder.tsx b/frontend/components/FoodSearchPlaceholder.tsx index 28a02a39..eec36d27 100644 --- a/frontend/components/FoodSearchPlaceholder.tsx +++ b/frontend/components/FoodSearchPlaceholder.tsx @@ -1,5 +1,6 @@ "use client"; +import { createFoodSearchReadiness } from "@/lib/foodSearchReadiness"; import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { EmptyState } from "@/components/EmptyState"; import { ErrorBanner } from "@/components/ErrorBanner"; @@ -169,6 +170,18 @@ export function FoodSearchPlaceholder() { const displayInteger = (value: number) => display.enabled ? numbers.integer.format(Number.isFinite(value) ? Math.round(value) : 0) : formatInteger(value); const displayPercentage = (value: number) => display.enabled ? numbers.percentage.format(value) : String(value); + const searchReadinessRef = useRef | null>(null); + useEffect(() => { + // Anonymous visitors get the same startup preparation as returning visitors. + // This only reads public health; no sign-in or provider search is started. + const readiness = createFoodSearchReadiness(); + searchReadinessRef.current = readiness; + void readiness.prepare().catch(() => { /* The search action presents any remaining error. */ }); + return () => { + readiness.dispose(); + if (searchReadinessRef.current === readiness) searchReadinessRef.current = null; + }; + }, []); const searchRequestIdRef = useRef(0); const searchAbortControllerRef = useRef(null); const searchInFlightRef = useRef(false); @@ -379,7 +392,7 @@ export function FoodSearchPlaceholder() { ); try { - await waitForBackendReady(BACKEND_WAKE_BASE_URL, controller.signal); + await (searchReadinessRef.current?.prepare(controller.signal) ?? waitForBackendReady(BACKEND_WAKE_BASE_URL, controller.signal)); setSearchStatus("Searching foods. This can take up to 45 seconds."); const response = await backendRequest( diff --git a/frontend/components/XamanLoginPanel.tsx b/frontend/components/XamanLoginPanel.tsx index 0fd692c5..bec15521 100644 --- a/frontend/components/XamanLoginPanel.tsx +++ b/frontend/components/XamanLoginPanel.tsx @@ -13,6 +13,7 @@ import { waitForBackendReady, } from "@/lib/backendRequest"; import { resolveLocale } from "@/lib/locales"; +import { getAuthUi, translateAuthMessage } from "@/lib/authUi"; type MeResponse = { user_id: string; @@ -949,6 +950,7 @@ export function XamanLoginPanel() { const [loginSurfaceMode, setLoginSurfaceMode] = useState("checking"); const [displayLocale, setDisplayLocale] = useState(initialLocale); + const { copy: authCopy, locale: authLocale, direction: authDirection } = getAuthUi(displayLocale); const loginAbortController = useRef(null); const parentOrigin = useRef(null); const embeddedRequestId = useRef(""); @@ -1550,6 +1552,8 @@ export function XamanLoginPanel() { return (

- {currentUser ? "Connected account" : "Optional account access"} + {currentUser ? authCopy.connectedAccount : authCopy.optionalAccount}

- {currentUser ? "You’re signed in" : "Sign in with Xaman"} + {currentUser ? authCopy.signedIn : authCopy.signIn}

{currentUser ? loginSurfaceMode === "embedded" - ? "Your website and CalorieApp sessions are connected in this browser." - : "Your CalorieApp session is active in this browser." - : "Connect securely to save, review, and manage your personal food log."} + ? authCopy.sessionsConnected + : authCopy.sessionActive + : authCopy.signInDescription}

{currentUser ? ( ) : null}
@@ -1595,12 +1599,12 @@ export function XamanLoginPanel() { role="note" className="mt-4 rounded-2xl border border-amber-300/80 bg-amber-50 px-3.5 py-3 text-xs leading-relaxed text-amber-950" > - On your phone:{" "} + {authCopy.onPhone}{" "} {loginSurfaceMode === "embedded" - ? "Sign once in Xaman, then tap Close or use Back. This page finishes both sign-ins automatically." + ? authCopy.phoneInstructions : loginSurfaceMode === "standalone" - ? "Continue on CalorieToken.net to sign in to the website and CalorieApp together." - : "Connecting this view to the secure CalorieToken.net sign-in page."} + ? authCopy.websiteInstructions + : authCopy.connectingView}
) : null} @@ -1610,13 +1614,13 @@ export function XamanLoginPanel() {

{loginSurfaceMode === "embedded" - ? "Website + CalorieApp" - : "CalorieApp session"} + ? authCopy.websiteAndApp + : authCopy.appSession}

{loginSurfaceMode === "embedded" - ? "Log out of CalorieToken.net and CalorieApp on this device." - : "Log out on this device when you’re finished."} + ? authCopy.logoutBoth + : authCopy.logoutDevice}

@@ -1633,10 +1637,10 @@ export function XamanLoginPanel() { - Account tools + {authCopy.accountTools} - Export and privacy options + {authCopy.privacyOptions} - Continue on CalorieToken.net + {authCopy.continueWebsite} ) : loginSurfaceMode === "embedded" && !isLoading ? ( {isLoading - ? "Preparing Xaman..." + ? authCopy.preparingXaman : loginSurfaceMode === "checking" - ? "Connecting secure sign-in..." - : "Continue in Xaman"} + ? authCopy.connectingLogin + : authCopy.continueXaman} )} @@ -1724,13 +1728,13 @@ export function XamanLoginPanel() { aria-live="polite" className="mt-3 rounded-lg bg-white/70 px-3 py-2 text-xs leading-relaxed text-brand-secondary" > - {loginStatus} + {translateAuthMessage(loginStatus, authCopy)}

) : null} {error && (

- {error} + {translateAuthMessage(error, authCopy)}

)} @@ -1740,7 +1744,7 @@ export function XamanLoginPanel() { aria-live="polite" className="mt-3 rounded-lg bg-green-50 px-3 py-2 text-xs text-green-800" > - {successNotice} + {translateAuthMessage(successNotice, authCopy)}

)} diff --git a/frontend/config/auth-ui-copy.json b/frontend/config/auth-ui-copy.json new file mode 100644 index 00000000..a33e345a --- /dev/null +++ b/frontend/config/auth-ui-copy.json @@ -0,0 +1,464 @@ +{ + "en": { + "connectedAccount": "Connected account", + "optionalAccount": "Optional account access", + "signedIn": "You’re signed in", + "signIn": "Sign in with Xaman", + "sessionsConnected": "Your website and CalorieApp sessions are connected in this browser.", + "sessionActive": "Your CalorieApp session is active in this browser.", + "signInDescription": "Connect securely to save, review, and manage your personal food log.", + "connected": "Connected", + "onPhone": "On your phone:", + "phoneInstructions": "Sign once in Xaman, then tap Close or use Back. This page finishes both sign-ins automatically.", + "websiteInstructions": "Continue on CalorieToken.net to sign in to the website and CalorieApp together.", + "connectingView": "Connecting this view to the secure CalorieToken.net sign-in page.", + "websiteAndApp": "Website + CalorieApp", + "appSession": "CalorieApp session", + "logoutBoth": "Log out of CalorieToken.net and CalorieApp on this device.", + "logoutDevice": "Log out on this device when you’re finished.", + "loggingOut": "Logging out...", + "logout": "Log out", + "accountTools": "Account tools", + "privacyOptions": "Export and privacy options", + "continueWebsite": "Continue on CalorieToken.net", + "continueXaman": "Continue in Xaman", + "preparingXaman": "Preparing Xaman...", + "connectingLogin": "Connecting secure sign-in...", + "restoring": "Restoring the Xaman sign-in started from this tab...", + "complete": "Sign-in completed. You can continue safely in this browser.", + "restoreFailed": "The earlier Xaman sign-in could not be restored. Start again from this tab.", + "preparing": "Preparing Xaman and starting CalorieApp securely in the background...", + "starting": "Xaman is ready. Starting the secure CalorieApp service. This can take about a minute. Keep this page open.", + "retrying": "Xaman is ready. CalorieApp is still starting and will retry automatically. Keep this page open.", + "busy": "Xaman is ready. CalorieApp is temporarily busy and will retry automatically. Keep this page open.", + "activating": "Activating CalorieApp in this browser...", + "reconnecting": "WordPress is signed in. Refreshing the secure CalorieApp connection automatically...", + "signedBoth": "Signed in to WordPress and CalorieApp in this browser.", + "languageMismatch": "The WordPress sign-in language context did not match this page.", + "responseMismatch": "The WordPress sign-in response did not match this CalorieApp request.", + "finishFailed": "WordPress is signed in, but CalorieApp could not finish. Please try again.", + "prepareFailed": "CalorieApp could not prepare its session. WordPress sign-in was not changed.", + "logoutFailed": "Unable to log out right now. Please try again.", + "serviceSlow": "The CalorieApp service is taking longer than expected to start. Please wait a few minutes before trying again; repeated refreshes can slow startup." + }, + "nl": { + "connectedAccount": "Verbonden account", + "optionalAccount": "Optioneel inloggen", + "signedIn": "Je bent ingelogd", + "signIn": "Inloggen met Xaman", + "sessionsConnected": "Je website- en CalorieApp-sessie zijn in deze browser verbonden.", + "sessionActive": "Je CalorieApp-sessie is actief in deze browser.", + "signInDescription": "Log veilig in om je eigen eetdagboek op te slaan, te bekijken en te beheren.", + "connected": "Verbonden", + "onPhone": "Op je telefoon:", + "phoneInstructions": "Onderteken eenmaal in Xaman en tik daarna op Sluiten of gebruik Terug. Deze pagina rondt beide aanmeldingen automatisch af.", + "websiteInstructions": "Ga verder op CalorieToken.net om bij de website en CalorieApp samen in te loggen.", + "connectingView": "Deze weergave wordt verbonden met de beveiligde inlogpagina van CalorieToken.net.", + "websiteAndApp": "Website + CalorieApp", + "appSession": "CalorieApp-sessie", + "logoutBoth": "Log op dit apparaat uit bij CalorieToken.net en CalorieApp.", + "logoutDevice": "Log uit op dit apparaat wanneer je klaar bent.", + "loggingOut": "Uitloggen…", + "logout": "Uitloggen", + "accountTools": "Accountbeheer", + "privacyOptions": "Export- en privacyopties", + "continueWebsite": "Verder op CalorieToken.net", + "continueXaman": "Verder in Xaman", + "preparingXaman": "Xaman voorbereiden…", + "connectingLogin": "Veilig inloggen verbinden…", + "restoring": "De Xaman-aanmelding van dit tabblad herstellen…", + "complete": "Inloggen voltooid. Je kunt verder in deze browser.", + "restoreFailed": "De eerdere Xaman-aanmelding kon niet worden hersteld. Begin opnieuw vanuit dit tabblad.", + "preparing": "Xaman voorbereiden en CalorieApp veilig op de achtergrond starten…", + "starting": "Xaman is klaar. CalorieApp wordt gestart. Dit kan ongeveer een minuut duren. Houd deze pagina open.", + "retrying": "Xaman is klaar. CalorieApp start nog en probeert het automatisch opnieuw. Houd deze pagina open.", + "busy": "Xaman is klaar. CalorieApp is tijdelijk bezet en probeert het automatisch opnieuw. Houd deze pagina open.", + "activating": "CalorieApp activeren in deze browser…", + "reconnecting": "WordPress is ingelogd. De beveiligde CalorieApp-verbinding wordt automatisch hersteld…", + "signedBoth": "In deze browser ingelogd bij WordPress en CalorieApp.", + "languageMismatch": "De taalcontext van de WordPress-aanmelding komt niet overeen met deze pagina.", + "responseMismatch": "Het WordPress-inlogantwoord past niet bij dit CalorieApp-verzoek.", + "finishFailed": "WordPress is ingelogd, maar CalorieApp kon niet afronden. Probeer opnieuw.", + "prepareFailed": "CalorieApp kon de sessie niet voorbereiden. Je WordPress-aanmelding is niet gewijzigd.", + "logoutFailed": "Uitloggen lukt nu niet. Probeer opnieuw.", + "serviceSlow": "Het opstarten van CalorieApp duurt langer dan verwacht. Wacht enkele minuten voordat je het opnieuw probeert; herhaald vernieuwen kan het opstarten vertragen." + }, + "es": { + "connectedAccount": "Cuenta conectada", + "optionalAccount": "Acceso opcional a la cuenta", + "signedIn": "Has iniciado sesión", + "signIn": "Iniciar sesión con Xaman", + "sessionsConnected": "Tus sesiones del sitio y de CalorieApp están conectadas en este navegador.", + "sessionActive": "Tu sesión de CalorieApp está activa en este navegador.", + "signInDescription": "Conéctate de forma segura para guardar, consultar y gestionar tu diario de alimentación.", + "connected": "Conectado", + "onPhone": "En tu teléfono:", + "phoneInstructions": "Firma una vez en Xaman y pulsa Cerrar o Atrás. Esta página completa ambos inicios de sesión automáticamente.", + "websiteInstructions": "Continúa en CalorieToken.net para iniciar sesión en el sitio y en CalorieApp a la vez.", + "connectingView": "Conectando esta vista con la página segura de acceso de CalorieToken.net.", + "websiteAndApp": "Sitio web + CalorieApp", + "appSession": "Sesión de CalorieApp", + "logoutBoth": "Cierra sesión en CalorieToken.net y CalorieApp en este dispositivo.", + "logoutDevice": "Cierra sesión en este dispositivo cuando termines.", + "loggingOut": "Cerrando sesión…", + "logout": "Cerrar sesión", + "accountTools": "Herramientas de cuenta", + "privacyOptions": "Opciones de exportación y privacidad", + "continueWebsite": "Continuar en CalorieToken.net", + "continueXaman": "Continuar en Xaman", + "preparingXaman": "Preparando Xaman…", + "connectingLogin": "Conectando el acceso seguro…", + "restoring": "Restaurando el acceso con Xaman iniciado en esta pestaña…", + "complete": "Acceso completado. Puedes continuar en este navegador.", + "restoreFailed": "No se pudo restaurar el acceso anterior con Xaman. Empieza de nuevo desde esta pestaña.", + "preparing": "Preparando Xaman e iniciando CalorieApp de forma segura en segundo plano…", + "starting": "Xaman está listo. CalorieApp se está iniciando. Puede tardar un minuto. Mantén esta página abierta.", + "retrying": "Xaman está listo. CalorieApp sigue iniciándose y reintentará automáticamente. Mantén esta página abierta.", + "busy": "Xaman está listo. CalorieApp está ocupado y reintentará automáticamente. Mantén esta página abierta.", + "activating": "Activando CalorieApp en este navegador…", + "reconnecting": "WordPress tiene sesión iniciada. Restaurando la conexión segura con CalorieApp automáticamente…", + "signedBoth": "Has iniciado sesión en WordPress y CalorieApp en este navegador.", + "languageMismatch": "El idioma del acceso de WordPress no coincide con esta página.", + "responseMismatch": "La respuesta de acceso de WordPress no coincide con esta solicitud de CalorieApp.", + "finishFailed": "WordPress tiene sesión iniciada, pero CalorieApp no pudo completarla. Inténtalo de nuevo.", + "prepareFailed": "CalorieApp no pudo preparar su sesión. El acceso a WordPress no cambió.", + "logoutFailed": "No se puede cerrar sesión ahora. Inténtalo de nuevo.", + "serviceSlow": "CalorieApp tarda más de lo esperado en iniciarse. Espera unos minutos antes de volver a intentarlo; actualizar repetidamente puede retrasar el inicio." + }, + "fr": { + "connectedAccount": "Compte connecté", + "optionalAccount": "Connexion facultative", + "signedIn": "Vous êtes connecté", + "signIn": "Se connecter avec Xaman", + "sessionsConnected": "Vos sessions du site et de CalorieApp sont connectées dans ce navigateur.", + "sessionActive": "Votre session CalorieApp est active dans ce navigateur.", + "signInDescription": "Connectez-vous pour enregistrer, consulter et gérer votre journal alimentaire personnel.", + "connected": "Connecté", + "onPhone": "Sur votre téléphone :", + "phoneInstructions": "Signez une fois dans Xaman, puis appuyez sur Fermer ou Retour. Cette page termine automatiquement les deux connexions.", + "websiteInstructions": "Continuez sur CalorieToken.net pour vous connecter au site et à CalorieApp ensemble.", + "connectingView": "Connexion de cette vue à la page de connexion sécurisée de CalorieToken.net.", + "websiteAndApp": "Site web + CalorieApp", + "appSession": "Session CalorieApp", + "logoutBoth": "Déconnectez CalorieToken.net et CalorieApp sur cet appareil.", + "logoutDevice": "Déconnectez-vous de cet appareil quand vous avez terminé.", + "loggingOut": "Déconnexion…", + "logout": "Se déconnecter", + "accountTools": "Outils du compte", + "privacyOptions": "Options d’exportation et de confidentialité", + "continueWebsite": "Continuer sur CalorieToken.net", + "continueXaman": "Continuer dans Xaman", + "preparingXaman": "Préparation de Xaman…", + "connectingLogin": "Connexion sécurisée en cours…", + "restoring": "Restauration de la connexion Xaman de cet onglet…", + "complete": "Connexion terminée. Vous pouvez continuer dans ce navigateur.", + "restoreFailed": "Impossible de restaurer la connexion Xaman précédente. Recommencez depuis cet onglet.", + "preparing": "Préparation de Xaman et démarrage sécurisé de CalorieApp en arrière-plan…", + "starting": "Xaman est prêt. CalorieApp démarre. Cela peut prendre environ une minute. Gardez cette page ouverte.", + "retrying": "Xaman est prêt. CalorieApp démarre encore et réessaiera automatiquement. Gardez cette page ouverte.", + "busy": "Xaman est prêt. CalorieApp est occupé et réessaiera automatiquement. Gardez cette page ouverte.", + "activating": "Activation de CalorieApp dans ce navigateur…", + "reconnecting": "WordPress est connecté. Rétablissement automatique de la connexion sécurisée à CalorieApp…", + "signedBoth": "Connecté à WordPress et CalorieApp dans ce navigateur.", + "languageMismatch": "La langue de la connexion WordPress ne correspond pas à cette page.", + "responseMismatch": "La réponse de connexion WordPress ne correspond pas à cette demande CalorieApp.", + "finishFailed": "WordPress est connecté, mais CalorieApp n’a pas pu terminer. Réessayez.", + "prepareFailed": "CalorieApp n’a pas pu préparer sa session. La connexion WordPress reste inchangée.", + "logoutFailed": "Impossible de se déconnecter maintenant. Réessayez.", + "serviceSlow": "CalorieApp met plus de temps que prévu à démarrer. Patientez quelques minutes avant de réessayer ; des actualisations répétées peuvent ralentir le démarrage." + }, + "pt": { + "connectedAccount": "Conta conectada", + "optionalAccount": "Acesso opcional à conta", + "signedIn": "Sessão iniciada", + "signIn": "Entrar com Xaman", + "sessionsConnected": "As sessões do site e do CalorieApp estão conectadas neste navegador.", + "sessionActive": "Sua sessão do CalorieApp está ativa neste navegador.", + "signInDescription": "Entre com segurança para salvar, consultar e gerenciar seu diário alimentar.", + "connected": "Conectado", + "onPhone": "No seu telefone:", + "phoneInstructions": "Assine uma vez no Xaman e toque em Fechar ou Voltar. Esta página conclui os dois acessos automaticamente.", + "websiteInstructions": "Continue no CalorieToken.net para entrar no site e no CalorieApp juntos.", + "connectingView": "Conectando esta tela à página segura de acesso do CalorieToken.net.", + "websiteAndApp": "Site + CalorieApp", + "appSession": "Sessão CalorieApp", + "logoutBoth": "Saia do CalorieToken.net e do CalorieApp neste dispositivo.", + "logoutDevice": "Saia deste dispositivo quando terminar.", + "loggingOut": "Saindo…", + "logout": "Sair", + "accountTools": "Ferramentas da conta", + "privacyOptions": "Opções de exportação e privacidade", + "continueWebsite": "Continuar no CalorieToken.net", + "continueXaman": "Continuar no Xaman", + "preparingXaman": "Preparando Xaman…", + "connectingLogin": "Conectando o acesso seguro…", + "restoring": "Restaurando o acesso com Xaman iniciado nesta aba…", + "complete": "Acesso concluído. Você pode continuar neste navegador.", + "restoreFailed": "Não foi possível restaurar o acesso anterior com Xaman. Recomece nesta aba.", + "preparing": "Preparando Xaman e iniciando CalorieApp com segurança em segundo plano…", + "starting": "Xaman está pronto. CalorieApp está iniciando. Pode levar um minuto. Mantenha esta página aberta.", + "retrying": "Xaman está pronto. CalorieApp ainda está iniciando e tentará novamente. Mantenha esta página aberta.", + "busy": "Xaman está pronto. CalorieApp está ocupado e tentará novamente. Mantenha esta página aberta.", + "activating": "Ativando CalorieApp neste navegador…", + "reconnecting": "WordPress está conectado. Restaurando automaticamente a conexão segura do CalorieApp…", + "signedBoth": "Conectado ao WordPress e ao CalorieApp neste navegador.", + "languageMismatch": "O idioma do acesso do WordPress não corresponde a esta página.", + "responseMismatch": "A resposta de acesso do WordPress não corresponde a esta solicitação do CalorieApp.", + "finishFailed": "WordPress está conectado, mas CalorieApp não conseguiu concluir. Tente novamente.", + "prepareFailed": "CalorieApp não conseguiu preparar a sessão. O acesso ao WordPress não foi alterado.", + "logoutFailed": "Não foi possível sair agora. Tente novamente.", + "serviceSlow": "O CalorieApp está a demorar mais do que o previsto a iniciar. Aguarde alguns minutos antes de tentar novamente; atualizar repetidamente pode atrasar o início." + }, + "zh-Hans": { + "connectedAccount": "已连接的账户", + "optionalAccount": "可选账户登录", + "signedIn": "您已登录", + "signIn": "使用 Xaman 登录", + "sessionsConnected": "网站和 CalorieApp 会话已在此浏览器中连接。", + "sessionActive": "您的 CalorieApp 会话在此浏览器中处于活动状态。", + "signInDescription": "安全登录以保存、查看和管理个人饮食记录。", + "connected": "已连接", + "onPhone": "在手机上:", + "phoneInstructions": "在 Xaman 中签名一次,然后点关闭或返回。本页会自动完成两处登录。", + "websiteInstructions": "前往 CalorieToken.net,同时登录网站和 CalorieApp。", + "connectingView": "正在连接 CalorieToken.net 安全登录页面。", + "websiteAndApp": "网站 + CalorieApp", + "appSession": "CalorieApp 会话", + "logoutBoth": "在此设备上退出 CalorieToken.net 和 CalorieApp。", + "logoutDevice": "完成后请在此设备上退出登录。", + "loggingOut": "正在退出…", + "logout": "退出登录", + "accountTools": "账户工具", + "privacyOptions": "导出和隐私选项", + "continueWebsite": "前往 CalorieToken.net", + "continueXaman": "在 Xaman 中继续", + "preparingXaman": "正在准备 Xaman…", + "connectingLogin": "正在连接安全登录…", + "restoring": "正在恢复此标签页发起的 Xaman 登录…", + "complete": "登录完成。您可以在此浏览器中继续。", + "restoreFailed": "无法恢复之前的 Xaman 登录。请从此标签页重新开始。", + "preparing": "正在准备 Xaman 并在后台安全启动 CalorieApp…", + "starting": "Xaman 已就绪。CalorieApp 正在启动,可能需要约一分钟。请保持本页打开。", + "retrying": "Xaman 已就绪。CalorieApp 仍在启动,会自动重试。请保持本页打开。", + "busy": "Xaman 已就绪。CalorieApp 暂时繁忙,会自动重试。请保持本页打开。", + "activating": "正在此浏览器中激活 CalorieApp…", + "reconnecting": "WordPress 已登录。正在自动恢复 CalorieApp 安全连接…", + "signedBoth": "已在此浏览器中登录 WordPress 和 CalorieApp。", + "languageMismatch": "WordPress 登录语言与此页面不匹配。", + "responseMismatch": "WordPress 登录响应与此 CalorieApp 请求不匹配。", + "finishFailed": "WordPress 已登录,但 CalorieApp 未能完成。请重试。", + "prepareFailed": "CalorieApp 无法准备会话。WordPress 登录未改变。", + "logoutFailed": "目前无法退出登录。请重试。", + "serviceSlow": "CalorieApp 服务启动时间超出预期。请等待几分钟再重试;反复刷新可能会减慢启动速度。" + }, + "id": { + "connectedAccount": "Akun terhubung", + "optionalAccount": "Akses akun opsional", + "signedIn": "Anda sudah masuk", + "signIn": "Masuk dengan Xaman", + "sessionsConnected": "Sesi situs dan CalorieApp Anda terhubung di browser ini.", + "sessionActive": "Sesi CalorieApp Anda aktif di browser ini.", + "signInDescription": "Masuk dengan aman untuk menyimpan, melihat, dan mengelola catatan makanan pribadi.", + "connected": "Terhubung", + "onPhone": "Di ponsel Anda:", + "phoneInstructions": "Tanda tangani sekali di Xaman, lalu ketuk Tutup atau Kembali. Halaman ini menyelesaikan kedua proses masuk secara otomatis.", + "websiteInstructions": "Lanjutkan di CalorieToken.net untuk masuk ke situs dan CalorieApp bersama.", + "connectingView": "Menghubungkan tampilan ini ke halaman masuk aman CalorieToken.net.", + "websiteAndApp": "Situs + CalorieApp", + "appSession": "Sesi CalorieApp", + "logoutBoth": "Keluar dari CalorieToken.net dan CalorieApp di perangkat ini.", + "logoutDevice": "Keluar dari perangkat ini setelah selesai.", + "loggingOut": "Keluar…", + "logout": "Keluar", + "accountTools": "Alat akun", + "privacyOptions": "Opsi ekspor dan privasi", + "continueWebsite": "Lanjutkan di CalorieToken.net", + "continueXaman": "Lanjutkan di Xaman", + "preparingXaman": "Menyiapkan Xaman…", + "connectingLogin": "Menghubungkan masuk aman…", + "restoring": "Memulihkan proses masuk Xaman dari tab ini…", + "complete": "Berhasil masuk. Anda dapat melanjutkan di browser ini.", + "restoreFailed": "Proses masuk Xaman sebelumnya tidak dapat dipulihkan. Mulai lagi dari tab ini.", + "preparing": "Menyiapkan Xaman dan memulai CalorieApp dengan aman di latar belakang…", + "starting": "Xaman siap. CalorieApp sedang dimulai, mungkin sekitar satu menit. Biarkan halaman ini terbuka.", + "retrying": "Xaman siap. CalorieApp masih dimulai dan akan mencoba lagi otomatis. Biarkan halaman terbuka.", + "busy": "Xaman siap. CalorieApp sedang sibuk dan akan mencoba lagi otomatis. Biarkan halaman terbuka.", + "activating": "Mengaktifkan CalorieApp di browser ini…", + "reconnecting": "WordPress sudah masuk. Memulihkan koneksi aman CalorieApp secara otomatis…", + "signedBoth": "Sudah masuk ke WordPress dan CalorieApp di browser ini.", + "languageMismatch": "Konteks bahasa masuk WordPress tidak cocok dengan halaman ini.", + "responseMismatch": "Respons masuk WordPress tidak cocok dengan permintaan CalorieApp ini.", + "finishFailed": "WordPress sudah masuk, tetapi CalorieApp belum selesai. Coba lagi.", + "prepareFailed": "CalorieApp tidak dapat menyiapkan sesi. Proses masuk WordPress tidak berubah.", + "logoutFailed": "Tidak dapat keluar sekarang. Coba lagi.", + "serviceSlow": "Layanan CalorieApp memerlukan waktu lebih lama dari perkiraan untuk dimulai. Tunggu beberapa menit sebelum mencoba lagi; memuat ulang berulang kali dapat memperlambat prosesnya." + }, + "hi": { + "connectedAccount": "जुड़ा हुआ खाता", + "optionalAccount": "वैकल्पिक खाता प्रवेश", + "signedIn": "आप लॉग इन हैं", + "signIn": "Xaman से लॉग इन करें", + "sessionsConnected": "इस ब्राउज़र में वेबसाइट और CalorieApp सत्र जुड़े हैं।", + "sessionActive": "इस ब्राउज़र में आपका CalorieApp सत्र सक्रिय है।", + "signInDescription": "अपनी भोजन डायरी सहेजने, देखने और प्रबंधित करने के लिए सुरक्षित लॉग इन करें।", + "connected": "जुड़ा हुआ", + "onPhone": "अपने फ़ोन पर:", + "phoneInstructions": "Xaman में एक बार हस्ताक्षर करें, फिर बंद करें या वापस जाएँ। यह पेज दोनों लॉग इन अपने आप पूरे करता है।", + "websiteInstructions": "वेबसाइट और CalorieApp में साथ लॉग इन करने के लिए CalorieToken.net पर जाएँ।", + "connectingView": "CalorieToken.net के सुरक्षित लॉग इन पेज से जुड़ रहा है।", + "websiteAndApp": "वेबसाइट + CalorieApp", + "appSession": "CalorieApp सत्र", + "logoutBoth": "इस डिवाइस पर CalorieToken.net और CalorieApp से लॉग आउट करें।", + "logoutDevice": "काम पूरा होने पर इस डिवाइस से लॉग आउट करें।", + "loggingOut": "लॉग आउट हो रहा है…", + "logout": "लॉग आउट करें", + "accountTools": "खाता उपकरण", + "privacyOptions": "निर्यात और गोपनीयता विकल्प", + "continueWebsite": "CalorieToken.net पर आगे बढ़ें", + "continueXaman": "Xaman में आगे बढ़ें", + "preparingXaman": "Xaman तैयार हो रहा है…", + "connectingLogin": "सुरक्षित लॉग इन से जुड़ रहा है…", + "restoring": "इस टैब से शुरू हुआ Xaman लॉग इन बहाल हो रहा है…", + "complete": "लॉग इन पूरा हुआ। आप इस ब्राउज़र में आगे बढ़ सकते हैं।", + "restoreFailed": "पिछला Xaman लॉग इन बहाल नहीं हुआ। इस टैब से दोबारा शुरू करें।", + "preparing": "Xaman तैयार करके CalorieApp सुरक्षित रूप से पृष्ठभूमि में शुरू हो रहा है…", + "starting": "Xaman तैयार है। CalorieApp शुरू हो रहा है। लगभग एक मिनट लग सकता है। पेज खुला रखें।", + "retrying": "Xaman तैयार है। CalorieApp अभी शुरू हो रहा है और अपने आप फिर कोशिश करेगा। पेज खुला रखें।", + "busy": "Xaman तैयार है। CalorieApp अभी व्यस्त है और अपने आप फिर कोशिश करेगा। पेज खुला रखें।", + "activating": "इस ब्राउज़र में CalorieApp सक्रिय हो रहा है…", + "reconnecting": "WordPress में लॉग इन है। CalorieApp का सुरक्षित कनेक्शन अपने आप बहाल हो रहा है…", + "signedBoth": "इस ब्राउज़र में WordPress और CalorieApp में लॉग इन है।", + "languageMismatch": "WordPress लॉग इन की भाषा इस पेज से मेल नहीं खाती।", + "responseMismatch": "WordPress का लॉग इन उत्तर इस CalorieApp अनुरोध से मेल नहीं खाता।", + "finishFailed": "WordPress में लॉग इन है, लेकिन CalorieApp पूरा नहीं हो सका। फिर कोशिश करें।", + "prepareFailed": "CalorieApp सत्र तैयार नहीं कर सका। WordPress लॉग इन नहीं बदला।", + "logoutFailed": "अभी लॉग आउट नहीं हो सका। फिर कोशिश करें।", + "serviceSlow": "CalorieApp सेवा शुरू होने में अपेक्षा से अधिक समय ले रही है। फिर कोशिश करने से पहले कुछ मिनट रुकें; बार-बार रीफ़्रेश करने से शुरू होने में और समय लग सकता है।" + }, + "bn": { + "connectedAccount": "সংযুক্ত অ্যাকাউন্ট", + "optionalAccount": "ঐচ্ছিক অ্যাকাউন্ট প্রবেশ", + "signedIn": "আপনি লগ ইন করেছেন", + "signIn": "Xaman দিয়ে লগ ইন করুন", + "sessionsConnected": "এই ব্রাউজারে ওয়েবসাইট ও CalorieApp সেশন সংযুক্ত।", + "sessionActive": "এই ব্রাউজারে আপনার CalorieApp সেশন সক্রিয়।", + "signInDescription": "নিজের খাবারের ডায়েরি সংরক্ষণ, দেখা ও পরিচালনা করতে নিরাপদে লগ ইন করুন।", + "connected": "সংযুক্ত", + "onPhone": "আপনার ফোনে:", + "phoneInstructions": "Xaman-এ একবার স্বাক্ষর করুন, তারপর বন্ধ করুন বা ফিরে যান। এই পৃষ্ঠা স্বয়ংক্রিয়ভাবে দুটি লগ ইন সম্পূর্ণ করে।", + "websiteInstructions": "ওয়েবসাইট ও CalorieApp-এ একসঙ্গে লগ ইন করতে CalorieToken.net-এ যান।", + "connectingView": "CalorieToken.net-এর নিরাপদ লগ ইন পৃষ্ঠার সঙ্গে সংযোগ হচ্ছে।", + "websiteAndApp": "ওয়েবসাইট + CalorieApp", + "appSession": "CalorieApp সেশন", + "logoutBoth": "এই ডিভাইসে CalorieToken.net ও CalorieApp থেকে লগ আউট করুন।", + "logoutDevice": "কাজ শেষ হলে এই ডিভাইস থেকে লগ আউট করুন।", + "loggingOut": "লগ আউট হচ্ছে…", + "logout": "লগ আউট করুন", + "accountTools": "অ্যাকাউন্টের সরঞ্জাম", + "privacyOptions": "রপ্তানি ও গোপনীয়তার বিকল্প", + "continueWebsite": "CalorieToken.net-এ এগিয়ে যান", + "continueXaman": "Xaman-এ এগিয়ে যান", + "preparingXaman": "Xaman প্রস্তুত হচ্ছে…", + "connectingLogin": "নিরাপদ লগ ইন সংযোগ হচ্ছে…", + "restoring": "এই ট্যাব থেকে শুরু করা Xaman লগ ইন পুনরুদ্ধার হচ্ছে…", + "complete": "লগ ইন সম্পূর্ণ। আপনি এই ব্রাউজারে এগিয়ে যেতে পারেন।", + "restoreFailed": "আগের Xaman লগ ইন পুনরুদ্ধার করা যায়নি। এই ট্যাব থেকে আবার শুরু করুন।", + "preparing": "Xaman প্রস্তুত করে পটভূমিতে নিরাপদে CalorieApp চালু হচ্ছে…", + "starting": "Xaman প্রস্তুত। CalorieApp চালু হচ্ছে, প্রায় এক মিনিট লাগতে পারে। পৃষ্ঠাটি খোলা রাখুন।", + "retrying": "Xaman প্রস্তুত। CalorieApp এখনও চালু হচ্ছে এবং স্বয়ংক্রিয়ভাবে আবার চেষ্টা করবে। পৃষ্ঠাটি খোলা রাখুন।", + "busy": "Xaman প্রস্তুত। CalorieApp সাময়িকভাবে ব্যস্ত এবং স্বয়ংক্রিয়ভাবে আবার চেষ্টা করবে। পৃষ্ঠাটি খোলা রাখুন।", + "activating": "এই ব্রাউজারে CalorieApp সক্রিয় হচ্ছে…", + "reconnecting": "WordPress-এ লগ ইন হয়েছে। CalorieApp-এর নিরাপদ সংযোগ স্বয়ংক্রিয়ভাবে পুনরুদ্ধার হচ্ছে…", + "signedBoth": "এই ব্রাউজারে WordPress ও CalorieApp-এ লগ ইন হয়েছে।", + "languageMismatch": "WordPress লগ ইনের ভাষা এই পৃষ্ঠার সঙ্গে মিলছে না।", + "responseMismatch": "WordPress লগ ইনের উত্তর এই CalorieApp অনুরোধের সঙ্গে মিলছে না।", + "finishFailed": "WordPress-এ লগ ইন হয়েছে, কিন্তু CalorieApp সম্পূর্ণ করতে পারেনি। আবার চেষ্টা করুন।", + "prepareFailed": "CalorieApp সেশন প্রস্তুত করতে পারেনি। WordPress লগ ইন পরিবর্তন হয়নি।", + "logoutFailed": "এখন লগ আউট করা যায়নি। আবার চেষ্টা করুন।", + "serviceSlow": "CalorieApp চালু হতে প্রত্যাশার চেয়ে বেশি সময় লাগছে। আবার চেষ্টা করার আগে কয়েক মিনিট অপেক্ষা করুন; বারবার রিফ্রেশ করলে চালু হতে আরও সময় লাগতে পারে।" + }, + "ar": { + "connectedAccount": "الحساب المتصل", + "optionalAccount": "تسجيل الدخول اختياري", + "signedIn": "تم تسجيل دخولك", + "signIn": "تسجيل الدخول باستخدام Xaman", + "sessionsConnected": "جلسات الموقع وCalorieApp متصلة في هذا المتصفح.", + "sessionActive": "جلسة CalorieApp نشطة في هذا المتصفح.", + "signInDescription": "سجّل الدخول بأمان لحفظ سجل طعامك الشخصي وعرضه وإدارته.", + "connected": "متصل", + "onPhone": "على هاتفك:", + "phoneInstructions": "وقّع مرة واحدة في Xaman، ثم اضغط إغلاق أو رجوع. تُكمل هذه الصفحة عمليتي الدخول تلقائيًا.", + "websiteInstructions": "تابع على CalorieToken.net للدخول إلى الموقع وCalorieApp معًا.", + "connectingView": "جارٍ الاتصال بصفحة الدخول الآمنة على CalorieToken.net.", + "websiteAndApp": "الموقع + CalorieApp", + "appSession": "جلسة CalorieApp", + "logoutBoth": "سجّل الخروج من CalorieToken.net وCalorieApp على هذا الجهاز.", + "logoutDevice": "سجّل الخروج من هذا الجهاز عند الانتهاء.", + "loggingOut": "جارٍ تسجيل الخروج…", + "logout": "تسجيل الخروج", + "accountTools": "أدوات الحساب", + "privacyOptions": "خيارات التصدير والخصوصية", + "continueWebsite": "المتابعة على CalorieToken.net", + "continueXaman": "المتابعة في Xaman", + "preparingXaman": "جارٍ تجهيز Xaman…", + "connectingLogin": "جارٍ الاتصال بتسجيل الدخول الآمن…", + "restoring": "جارٍ استعادة تسجيل الدخول إلى Xaman من علامة التبويب هذه…", + "complete": "اكتمل تسجيل الدخول. يمكنك المتابعة في هذا المتصفح.", + "restoreFailed": "تعذرت استعادة تسجيل الدخول السابق إلى Xaman. ابدأ مجددًا من علامة التبويب هذه.", + "preparing": "جارٍ تجهيز Xaman وتشغيل CalorieApp بأمان في الخلفية…", + "starting": "Xaman جاهز. يجري تشغيل CalorieApp وقد يستغرق نحو دقيقة. أبقِ الصفحة مفتوحة.", + "retrying": "Xaman جاهز. لا يزال CalorieApp يبدأ وسيعيد المحاولة تلقائيًا. أبقِ الصفحة مفتوحة.", + "busy": "Xaman جاهز. CalorieApp مشغول مؤقتًا وسيعيد المحاولة تلقائيًا. أبقِ الصفحة مفتوحة.", + "activating": "جارٍ تفعيل CalorieApp في هذا المتصفح…", + "reconnecting": "تم الدخول إلى WordPress. جارٍ استعادة اتصال CalorieApp الآمن تلقائيًا…", + "signedBoth": "تم الدخول إلى WordPress وCalorieApp في هذا المتصفح.", + "languageMismatch": "لغة تسجيل الدخول إلى WordPress لا تطابق هذه الصفحة.", + "responseMismatch": "استجابة الدخول إلى WordPress لا تطابق طلب CalorieApp هذا.", + "finishFailed": "تم الدخول إلى WordPress، لكن CalorieApp لم يكتمل. حاول مجددًا.", + "prepareFailed": "تعذر تجهيز جلسة CalorieApp. لم يتغير تسجيل الدخول إلى WordPress.", + "logoutFailed": "تعذر تسجيل الخروج الآن. حاول مجددًا.", + "serviceSlow": "تستغرق خدمة CalorieApp وقتًا أطول من المتوقع لبدء التشغيل. انتظر بضع دقائق قبل المحاولة مجددًا؛ قد يؤدي التحديث المتكرر إلى إبطاء بدء التشغيل." + }, + "ur": { + "connectedAccount": "منسلک اکاؤنٹ", + "optionalAccount": "اختیاری اکاؤنٹ رسائی", + "signedIn": "آپ لاگ ان ہیں", + "signIn": "Xaman سے لاگ ان کریں", + "sessionsConnected": "اس براؤزر میں ویب سائٹ اور CalorieApp کے سیشن منسلک ہیں۔", + "sessionActive": "اس براؤزر میں آپ کا CalorieApp سیشن فعال ہے۔", + "signInDescription": "اپنی خوراک کی ڈائری محفوظ کرنے، دیکھنے اور منظم کرنے کے لیے محفوظ طریقے سے لاگ ان کریں۔", + "connected": "منسلک", + "onPhone": "اپنے فون پر:", + "phoneInstructions": "Xaman میں ایک بار دستخط کریں، پھر بند کریں یا واپس جائیں۔ یہ صفحہ دونوں لاگ ان خود مکمل کرتا ہے۔", + "websiteInstructions": "ویب سائٹ اور CalorieApp میں اکٹھے لاگ ان کرنے کے لیے CalorieToken.net پر جائیں۔", + "connectingView": "CalorieToken.net کے محفوظ لاگ ان صفحے سے رابطہ ہو رہا ہے۔", + "websiteAndApp": "ویب سائٹ + CalorieApp", + "appSession": "CalorieApp سیشن", + "logoutBoth": "اس آلے پر CalorieToken.net اور CalorieApp سے لاگ آؤٹ کریں۔", + "logoutDevice": "کام مکمل ہونے پر اس آلے سے لاگ آؤٹ کریں۔", + "loggingOut": "لاگ آؤٹ ہو رہا ہے…", + "logout": "لاگ آؤٹ کریں", + "accountTools": "اکاؤنٹ کے اوزار", + "privacyOptions": "برآمد اور رازداری کے اختیارات", + "continueWebsite": "CalorieToken.net پر جاری رکھیں", + "continueXaman": "Xaman میں جاری رکھیں", + "preparingXaman": "Xaman تیار ہو رہا ہے…", + "connectingLogin": "محفوظ لاگ ان سے رابطہ ہو رہا ہے…", + "restoring": "اس ٹیب سے شروع کیا گیا Xaman لاگ ان بحال ہو رہا ہے…", + "complete": "لاگ ان مکمل ہوا۔ آپ اس براؤزر میں جاری رکھ سکتے ہیں۔", + "restoreFailed": "پچھلا Xaman لاگ ان بحال نہیں ہو سکا۔ اسی ٹیب سے دوبارہ شروع کریں۔", + "preparing": "Xaman کی تیاری اور پس منظر میں CalorieApp محفوظ طور پر شروع ہو رہا ہے…", + "starting": "Xaman تیار ہے۔ CalorieApp شروع ہو رہا ہے، تقریباً ایک منٹ لگ سکتا ہے۔ صفحہ کھلا رکھیں۔", + "retrying": "Xaman تیار ہے۔ CalorieApp ابھی شروع ہو رہا ہے اور خود دوبارہ کوشش کرے گا۔ صفحہ کھلا رکھیں۔", + "busy": "Xaman تیار ہے۔ CalorieApp عارضی طور پر مصروف ہے اور خود دوبارہ کوشش کرے گا۔ صفحہ کھلا رکھیں۔", + "activating": "اس براؤزر میں CalorieApp فعال ہو رہا ہے…", + "reconnecting": "WordPress میں لاگ ان ہے۔ CalorieApp کا محفوظ رابطہ خود بحال ہو رہا ہے…", + "signedBoth": "اس براؤزر میں WordPress اور CalorieApp میں لاگ ان ہے۔", + "languageMismatch": "WordPress لاگ ان کی زبان اس صفحے سے مطابقت نہیں رکھتی۔", + "responseMismatch": "WordPress لاگ ان کا جواب اس CalorieApp درخواست سے مطابقت نہیں رکھتا۔", + "finishFailed": "WordPress میں لاگ ان ہے لیکن CalorieApp مکمل نہیں ہو سکا۔ دوبارہ کوشش کریں۔", + "prepareFailed": "CalorieApp سیشن تیار نہیں کر سکا۔ WordPress لاگ ان تبدیل نہیں ہوا۔", + "logoutFailed": "ابھی لاگ آؤٹ نہیں ہو سکا۔ دوبارہ کوشش کریں۔", + "serviceSlow": "CalorieApp سروس شروع ہونے میں توقع سے زیادہ وقت لے رہی ہے۔ دوبارہ کوشش کرنے سے پہلے چند منٹ انتظار کریں؛ بار بار ریفریش کرنے سے آغاز میں مزید دیر ہو سکتی ہے۔" + } +} diff --git a/frontend/lib/authUi.ts b/frontend/lib/authUi.ts new file mode 100644 index 00000000..fa1a8552 --- /dev/null +++ b/frontend/lib/authUi.ts @@ -0,0 +1,21 @@ +import translations from "@/config/auth-ui-copy.json"; +import { localeDirection, resolveLocale } from "@/lib/locales"; + +export type AuthUiCopy = typeof translations.en; +export function getAuthUi(value?: string | null) { + const locale = resolveLocale(value); + return { locale, direction: localeDirection(locale), copy: translations[locale as keyof typeof translations] ?? translations.en }; +} +const messages = new Map( + (Object.keys(translations.en) as (keyof AuthUiCopy)[]).map(key => [translations.en[key], key]), +); +messages.set("Sign-in completed. Your session was restored in this browser.", "complete"); +messages.set("WordPress signed in. Restoring CalorieApp in this browser...", "reconnecting"); +messages.set("Could not log out of both sessions. Please try again.", "logoutFailed"); + +export function translateAuthMessage(message: string, copy: AuthUiCopy): string { + // Display only: the protocol locale, request state and server messages stay intact. + // Only exact first-party messages belong to this catalogue. + const key = messages.get(message); + return key ? copy[key] : message; +} diff --git a/frontend/lib/foodSearchReadiness.ts b/frontend/lib/foodSearchReadiness.ts new file mode 100644 index 00000000..d6b57069 --- /dev/null +++ b/frontend/lib/foodSearchReadiness.ts @@ -0,0 +1,36 @@ +import { BACKEND_WAKE_BASE_URL, waitForBackendReady } from "@/lib/backendRequest"; + +const READY_TTL_MS = 60_000; + +/** A page-scoped public health probe, shared by preparation and the first search. */ +export function createFoodSearchReadiness() { + let readyUntil = 0; + let pending: Promise | null = null; + let controller: AbortController | null = null; + let disposed = false; + + function prepare(signal?: AbortSignal): Promise { + if (disposed || signal?.aborted) { + return Promise.reject(signal?.reason ?? new Error("Food search preparation cancelled")); + } + if (Date.now() < readyUntil) return Promise.resolve(); + if (!pending) { + controller = new AbortController(); + pending = waitForBackendReady(BACKEND_WAKE_BASE_URL, controller.signal) + .then(() => { if (!disposed) readyUntil = Date.now() + READY_TTL_MS; }) + .finally(() => { pending = null; }); + } + const preparation = pending; + if (!signal) return preparation; + return new Promise((resolve, reject) => { + const onAbort = () => reject(signal.reason ?? new Error("Food search cancelled")); + signal.addEventListener("abort", onAbort, { once: true }); + preparation.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort)); + if (signal.aborted) onAbort(); + }); + } + return { + prepare, + dispose() { disposed = true; readyUntil = 0; controller?.abort(); }, + }; +} diff --git a/tools/tests/account_erasure_ui.test.mjs b/tools/tests/account_erasure_ui.test.mjs index c8873fa3..7a124b89 100644 --- a/tools/tests/account_erasure_ui.test.mjs +++ b/tools/tests/account_erasure_ui.test.mjs @@ -1,3 +1,4 @@ +import { authUi } from "./helpers/auth_ui.mjs"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; import { readFile } from "node:fs/promises"; @@ -68,6 +69,7 @@ async function loadComponentModule(overrides = {}) { exports: module.exports, module, require(specifier) { + if (specifier === "@/lib/authUi") return authUi; if (specifier === "react") { return overrides.react ?? {}; } @@ -343,8 +345,8 @@ test("account tools stay available but collapsed below the primary app", async ( assert.match(panel, /
{ + const copy=authUi.getAuthUi('es').copy; + assert.equal(authUi.translateAuthMessage(authCopy.en.preparing,copy),copy.preparing); + assert.equal(authUi.translateAuthMessage(authCopy.en.serviceSlow,copy),copy.serviceSlow); + assert.equal(authUi.translateAuthMessage('WordPress signed in. Restoring CalorieApp in this browser...',copy),copy.reconnecting); + const unknown='Provider detail {locale}';assert.equal(authUi.translateAuthMessage(unknown,copy),unknown); + assert.equal(authUi.getAuthUi('unknown').locale,'en'); +}); diff --git a/tools/tests/backend_wake_navigation.test.mjs b/tools/tests/backend_wake_navigation.test.mjs index 4e2deacb..9c98ea5d 100644 --- a/tools/tests/backend_wake_navigation.test.mjs +++ b/tools/tests/backend_wake_navigation.test.mjs @@ -1,3 +1,4 @@ +import { authUi } from "./helpers/auth_ui.mjs"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; import { readFile } from "node:fs/promises"; @@ -24,6 +25,7 @@ function harness() { module, exports: module.exports, URL, Number, JSON, process: { env: {} }, Date: { now: () => now }, window: { sessionStorage: storage }, require(name) { + if (name === "@/lib/authUi") return authUi; if (name === "@/lib/backendRequest") return { BACKEND_WAKE_BASE_URL: "/api/backend" }; if (name === "@/lib/locales") return { resolveLocale: value => value || "en" }; if (name === "react" || name === "react/jsx-runtime" || name.startsWith("@/components/")) return {}; @@ -128,6 +130,7 @@ test("the actual login control resumes once after return and only after a truste document: { referrer: `${origin}/index.php/calorieapp/`, body: { scrollHeight: 100 }, documentElement: { scrollHeight: 100 } }, process: { env: { NODE_ENV: "production" } }, require(name) { + if (name === "@/lib/authUi") return authUi; if (name === "react") return react; if (name === "react/jsx-runtime") return { jsx, jsxs: jsx, Fragment: "fragment" }; if (name === "@/lib/locales") return { resolveLocale: value => value || "en" }; diff --git a/tools/tests/food_logging_ui.test.mjs b/tools/tests/food_logging_ui.test.mjs index 3c5bd402..f55dbe1b 100644 --- a/tools/tests/food_logging_ui.test.mjs +++ b/tools/tests/food_logging_ui.test.mjs @@ -11,14 +11,14 @@ const localeRegistry = JSON.parse(await readFile(new URL("../../frontend/config/ const usdaReference = JSON.parse(await readFile(new URL("../../frontend/data/usda-reference-foods.json", import.meta.url), "utf8")); const usdaCopy = JSON.parse(await readFile(new URL("../../frontend/config/usda-reference-copy.json", import.meta.url), "utf8")); const foodUiCopy = JSON.parse(await readFile(new URL("../../frontend/config/food-ui-copy.json", import.meta.url), "utf8")); -async function loadLibrary(name, imports) { +async function loadLibrary(name, imports, globals = {}) { const source = await readFile(new URL(`../../frontend/lib/${name}.ts`, import.meta.url), "utf8"); const compiled = typescript.transpileModule(source, { compilerOptions: { module: typescript.ModuleKind.CommonJS, target: typescript.ScriptTarget.ES2022 }, }).outputText; const module = { exports: {} }; vm.runInNewContext(compiled, { - module, exports: module.exports, + module, exports: module.exports, ...globals, require(specifier) { if (Object.hasOwn(imports, specifier)) return imports[specifier]; throw new Error(`Unexpected filter import: ${specifier}`); @@ -72,6 +72,12 @@ async function harness(componentName = "FoodSearchPlaceholder", postResponse, lo let now = Date.parse("2026-09-10T12:00:00Z"), timerId = 0, warmups = 0; const timers = new Map(); class ClockDate extends Date { static now() { return now; } } + const readiness = await loadLibrary("foodSearchReadiness", { + "@/lib/backendRequest": { + BACKEND_WAKE_BASE_URL: "https://backend.example", + waitForBackendReady: async () => { warmups++; if (warmupResponse) await warmupResponse(); }, + }, + }, { AbortController, Date: ClockDate }); let confirmAnswer = false; const document = { body: {}, activeElement: null, documentElement: { lang: "en" } }; document.activeElement = document.body; @@ -129,6 +135,7 @@ async function harness(componentName = "FoodSearchPlaceholder", postResponse, lo if (specifier === "@/components/authEvents") return { AUTH_STATE_CHANGED_EVENT: AUTH_EVENT }; if (specifier === "@/lib/foodLogFilter") return foodLogFilter; if (specifier === "@/lib/foodUi") return foodUi; + if (specifier === "@/lib/foodSearchReadiness") return readiness; if (specifier === "@/lib/foodSearchAvailability") return { foodSearchRetryAt: (status, header) => searchAvailability.foodSearchRetryAt(status, header, now), }; diff --git a/tools/tests/food_search_readiness.test.mjs b/tools/tests/food_search_readiness.test.mjs new file mode 100644 index 00000000..5dcab200 --- /dev/null +++ b/tools/tests/food_search_readiness.test.mjs @@ -0,0 +1,21 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import {createRequire} from 'node:module'; +import vm from 'node:vm'; +const require=createRequire(new URL('../../frontend/package.json',import.meta.url)),ts=require('typescript'); +const compiled=ts.transpileModule(readFileSync(new URL('../../frontend/lib/foodSearchReadiness.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText; +function fixture(){ + let now=100000,calls=[];const module={exports:{}};class Clock extends Date{static now(){return now;}} + vm.runInNewContext(compiled,{module,exports:module.exports,Date:Clock,AbortController,require(name){assert.equal(name,'@/lib/backendRequest');return {BACKEND_WAKE_BASE_URL:'https://backend.example',waitForBackendReady(url,signal){assert.equal(url,'https://backend.example');return new Promise((resolve,reject)=>{calls.push({signal,resolve,reject});signal.addEventListener('abort',()=>reject(new Error('aborted')),{once:true});});}};}}); + return {api:module.exports.createFoodSearchReadiness(),calls,advance(ms){now+=ms;}}; +} +test('Preparation and first search share one public probe; readiness expires',async()=>{ + const h=fixture(),warm=h.api.prepare(),search=h.api.prepare();assert.equal(h.calls.length,1);h.calls[0].resolve();await Promise.all([warm,search]);await h.api.prepare();assert.equal(h.calls.length,1);h.advance(61000);const fresh=h.api.prepare();assert.equal(h.calls.length,2);h.calls[1].resolve();await fresh; +}); +test('Cancelling a search preserves page preparation and rejects the cancelled caller',async()=>{ + const h=fixture(),warm=h.api.prepare(),controller=new AbortController(),search=h.api.prepare(controller.signal);controller.abort();await assert.rejects(search);assert.equal(h.calls[0].signal.aborted,false);h.calls[0].resolve();await warm;await h.api.prepare();assert.equal(h.calls.length,1); +}); +test('Failed readiness is not cached and unmount stops the outstanding probe',async()=>{ + const h=fixture(),first=h.api.prepare();h.calls[0].reject(new Error('not ready'));await assert.rejects(first);const retry=h.api.prepare();assert.equal(h.calls.length,2);h.api.dispose();await assert.rejects(retry);assert.equal(h.calls[1].signal.aborted,true);await assert.rejects(h.api.prepare());assert.equal(h.calls.length,2); +}); diff --git a/tools/tests/helpers/auth_ui.mjs b/tools/tests/helpers/auth_ui.mjs new file mode 100644 index 00000000..052224ed --- /dev/null +++ b/tools/tests/helpers/auth_ui.mjs @@ -0,0 +1,16 @@ +import {readFileSync} from 'node:fs'; +import {createRequire} from 'node:module'; +import vm from 'node:vm'; +const require=createRequire(new URL('../../../frontend/package.json',import.meta.url)); +const ts=require('typescript'); +function load(path,imports){ + const source=readFileSync(new URL('../../../frontend/'+path,import.meta.url),'utf8'); + const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText; + const module={exports:{}}; + vm.runInNewContext(code,{module,exports:module.exports,require:name=>{if(Object.hasOwn(imports,name))return imports[name];throw new Error('Unexpected auth display import '+name);}}); + return module.exports; +} +export const authCopy=JSON.parse(readFileSync(new URL('../../../frontend/config/auth-ui-copy.json',import.meta.url),'utf8')); +const registry=JSON.parse(readFileSync(new URL('../../../frontend/config/locales.json',import.meta.url),'utf8')); +const locales=load('lib/locales.ts',{'@/config/locales.json':{default:registry}}); +export const authUi=load('lib/authUi.ts',{'@/config/auth-ui-copy.json':{default:authCopy},'@/lib/locales':locales}); diff --git a/tools/tests/xaman_login_start_retry.test.mjs b/tools/tests/xaman_login_start_retry.test.mjs index f1ba5989..eccdaa0d 100644 --- a/tools/tests/xaman_login_start_retry.test.mjs +++ b/tools/tests/xaman_login_start_retry.test.mjs @@ -1,3 +1,4 @@ +import { authUi } from "./helpers/auth_ui.mjs"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; import { readFile } from "node:fs/promises"; @@ -43,6 +44,7 @@ test("login surface fails closed until an embedded parent is trusted", async () exports: module.exports, process: { env: {} }, require(specifier) { + if (specifier === "@/lib/authUi") return authUi; if (specifier === "react") { return {}; } @@ -87,7 +89,8 @@ test("login surface fails closed until an embedded parent is trusted", async () assert.match(source, /type:\s*"calorieapp:bridge:initialized"/); assert.match(source, /event\.data\?\.type === "calorieapp:logout"/); assert.match(source, /type:\s*"calorieapp:logout:request"/); - assert.match(source, /Log out of CalorieToken\.net and CalorieApp on this device\./); + assert.match(source, /authCopy\.logoutBoth/); + assert.match(authUi.getAuthUi("en").copy.logoutBoth, /Log out of CalorieToken\.net and CalorieApp on this device\./); assert.match(source, /requestCalorieAppLogout\(\)/); }); @@ -108,6 +111,7 @@ test("logout retries the cookie-clearing endpoint after an interrupted response" exports: module.exports, process: { env: {} }, require(specifier) { + if (specifier === "@/lib/authUi") return authUi; if (specifier === "react") { return {}; } @@ -211,6 +215,7 @@ test("login start retries transport errors and transient responses", async () => exports: module.exports, process: { env: {} }, require(specifier) { + if (specifier === "@/lib/authUi") return authUi; if (specifier === "react") { return {}; } @@ -318,6 +323,7 @@ test("embedded login wakes the backend before creating login state", async () => exports: module.exports, process: { env: {} }, require(specifier) { + if (specifier === "@/lib/authUi") return authUi; if (specifier === "react") { return {}; } @@ -409,6 +415,7 @@ test("embedded login does not report progress after cancellation", async () => { exports: module.exports, process: { env: {} }, require(specifier) { + if (specifier === "@/lib/authUi") return authUi; if (specifier === "react") { return {}; } @@ -542,6 +549,7 @@ test("login status polling slows down by age, failures, and Retry-After", async exports: module.exports, process: { env: {} }, require(specifier) { + if (specifier === "@/lib/authUi") return authUi; if (specifier === "react") { return {}; } @@ -716,6 +724,7 @@ test("embedded completion recovers safely without replaying one-time codes", asy exports: module.exports, process: { env: {} }, require(specifier) { + if (specifier === "@/lib/authUi") return authUi; if (specifier === "react") { return {}; } diff --git a/tools/tests/xaman_logout_request.test.mjs b/tools/tests/xaman_logout_request.test.mjs index 57baf395..ead9bb85 100644 --- a/tools/tests/xaman_logout_request.test.mjs +++ b/tools/tests/xaman_logout_request.test.mjs @@ -1,3 +1,4 @@ +import { authUi } from "./helpers/auth_ui.mjs"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; import { readFile } from "node:fs/promises"; @@ -39,6 +40,7 @@ async function loadModule( exports: module.exports, process: { env: {} }, require(specifier) { + if (specifier === "@/lib/authUi") return authUi; if (specifier === "react") { return {}; } diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index a8c637bc..0c67ed5d 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,10 +1,26 @@ -# CalorieToken Site Style 1.4.3 +# CalorieToken Site Style 1.4.4 Eén complete vervangende ZIP, 10 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.3.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.3** actief is en leeg de websitecache. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.4.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.4** actief is en leeg de websitecache. Laat XummLogin, Identity Bridge en Content Workbench staan. + +## Correctieronde 1.4.4 — 29 screenshots en negen opmerkingen + +- Home-menu en logovlak krijgen een ondoorzichtige achtergrond. De bestaande Home-titelbanner blijft behouden. +- De consolidatiewallet gebruikt de grijze papierkaart, groene titelstrook en afgeronde onderkant van de bestaande walletpresentatie. Adres, explorerlink en volledige toelichting blijven staan. +- De acht roadmaponderdelen krijgen op mobiel dezelfde volledige breedte. De appverwijzing past binnen de tijdlijn en is geen afwijkende ingesloten kaart meer. +- De X-toestemmingsknop kan niet meer tegelijk de profiel-link openen. Complianz blijft de toestemming afhandelen. Cookie-instellingen staan ook in de footer. De echte X-timeline is op 10 september na toestemming op de huidige website gezien. +- Extra ruimte tussen Contact-team en XPMarket; dezelfde kleurrijke afsluitstrook onder iedere herkende footer. +- Op mobiel staan de zwevende knoppen in één compacte rij. Het CalorieApp-menu opent via het herkenbare appicoon. Footer en formulieren krijgen onderruimte; bediening verdwijnt tijdens de cookiemelding. +- Aanvullende vertaling van de getoonde Home-teksten, XPMarket-labels, X-kop en winkelstappen. Dit vertaalt geen providergegevens of tekst in afbeeldingen. + +**Afzonderlijke appupdate:** de bijbehorende GitHub-wijziging bevat 40 inlogteksten en meldingen in elf talen, minder geneste mobiele witruimte en vroegere openbare zoekvoorbereiding. Voorbereiding en de eerste zoekactie delen één gezondheidscontrole. Er wordt niets automatisch gezocht of aangemeld. Deze appwijzigingen vereisen een Render-uitrol; het uploaden van deze ZIP voert die niet uit. + +**Resterende acceptatie:** upload deze versie en controleer de zichtbare pagina's op de telefoon. De productiebuild en gerichte softwarecontroles zijn geen mobiele live acceptatie. Een terugkerende anonieme zoekfout is op de huidige app gereproduceerd; vroegere voorbereiding heft een hosting- of bronstoring niet op. Verdere Render-diagnose/uitrol was in deze ronde door de automatische toegangscontrole geblokkeerd. Een volledige taalcontrole van historische CMS-inhoud en de echte Xaman/Testnet-telefoonproef blijven open. + +Onderstaande oudere release-notities bewaren de voorgeschiedenis; bovenstaande versie is leidend. ## Gerichte correctie 1.4.3 — echt app-adres diff --git a/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js b/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js index 6869a685..cbf76d2b 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js +++ b/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js @@ -120,6 +120,17 @@ document.head.appendChild(script); } window.CalorieTokenBlogTimeline = {refresh: refresh}; + // Complianz inserts its button inside the profile anchor. Keep the native + // consent handler, but prevent that click from also navigating away to X. + document.addEventListener('click', function (event) { + if (!allowed() || !(event.target instanceof window.Element)) return; + var button = event.target.closest('button.cmplz-accept-service[data-service="twitter"]'); + var link = button && button.closest('a.twitter-timeline'); + if (link && profile(link) && link.closest('[data-brz-custom-id="amfuxnhsfmkknesyuldlbdorcvqsardaetus"]')) { + event.preventDefault(); + window.setTimeout(refresh, 0); + } + }, true); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', refresh, {once: true}); else refresh(); window.addEventListener('load', refresh, {once: true}); ['cmplz_cookie_warning_loaded', 'cmplz_status_change', 'cmplz_status_change_service','cmplz_service_status_change','cmplz_enable_service','cmplz_enable_category'].forEach(function (name) { diff --git a/wordpress-plugins/calorietoken-site-style/assets/content-data.json b/wordpress-plugins/calorietoken-site-style/assets/content-data.json index 24eb60c9..03a2f5f5 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/content-data.json +++ b/wordpress-plugins/calorietoken-site-style/assets/content-data.json @@ -1 +1 @@ -{"schema":1,"entries":[{"source":"Find food products, explore nutrition and keep your own food diary.","translations":{"zh-Hans":"查找食品、了解营养信息并记录自己的饮食日记。","hi":"खाद्य उत्पाद खोजें, पोषण संबंधी जानकारी देखें और अपनी भोजन डायरी रखें।","es":"Busca alimentos, consulta su información nutricional y lleva tu propio diario de alimentación.","ar":"ابحث عن المنتجات الغذائية، واطّلع على معلوماتها الغذائية، واحتفظ بسجل طعامك الخاص.","fr":"Recherchez des aliments, consultez leurs informations nutritionnelles et tenez votre propre journal alimentaire.","bn":"খাদ্যপণ্য খুঁজুন, পুষ্টির তথ্য দেখুন এবং নিজের খাবারের ডায়েরি রাখুন।","pt":"Encontre alimentos, consulte informações nutricionais e mantenha o seu próprio diário alimentar.","id":"Cari produk makanan, lihat informasi gizi, dan buat catatan makanan pribadi Anda.","ur":"غذائی مصنوعات تلاش کریں، غذائیت کی معلومات دیکھیں اور اپنی خوراک کی ڈائری رکھیں۔","nl":"Zoek voedingsproducten, bekijk voedingswaarden en houd je eigen eetdagboek bij."},"pages":["*"]},{"source":"Open CalorieApp","translations":{"zh-Hans":"打开 CalorieApp","hi":"CalorieApp खोलें","es":"Abrir CalorieApp","ar":"فتح CalorieApp","fr":"Ouvrir CalorieApp","bn":"CalorieApp খুলুন","pt":"Abrir CalorieApp","id":"Buka CalorieApp","ur":"CalorieApp کھولیں","nl":"CalorieApp openen"},"pages":[1205,4205,7880]},{"source":"CalorieApp is available above.","translations":{"zh-Hans":"CalorieApp 位于本页上方。","hi":"CalorieApp इस पृष्ठ पर ऊपर उपलब्ध है।","es":"CalorieApp está disponible más arriba en esta página.","ar":"يتوفر CalorieApp في أعلى هذه الصفحة.","fr":"CalorieApp est disponible plus haut sur cette page.","bn":"এই পৃষ্ঠার উপরে CalorieApp রয়েছে।","pt":"O CalorieApp está disponível acima nesta página.","id":"CalorieApp tersedia di bagian atas halaman ini.","ur":"CalorieApp اس صفحے پر اوپر موجود ہے۔","nl":"CalorieApp staat hierboven op deze pagina."},"pages":["*"]},{"source":"Product data from {off} (database licence: {odbl}).","translations":{"zh-Hans":"产品数据来自 {off}(数据库许可协议:{odbl})。","hi":"उत्पाद डेटा का स्रोत: {off} (डेटाबेस लाइसेंस: {odbl})।","es":"Datos de productos de {off} (licencia de la base de datos: {odbl}).","ar":"بيانات المنتجات من {off} (ترخيص قاعدة البيانات: {odbl}).","fr":"Données produits issues de {off} (licence de la base de données : {odbl}).","bn":"পণ্যের ডেটার উৎস: {off} (ডেটাবেসের লাইসেন্স: {odbl})।","pt":"Dados de produtos de {off} (licença da base de dados: {odbl}).","id":"Data produk dari {off} (lisensi basis data: {odbl}).","ur":"مصنوعات کے ڈیٹا کا ذریعہ: {off} (ڈیٹابیس کا لائسنس: {odbl})۔","nl":"Productgegevens van {off} (databaselicentie: {odbl})."},"pages":["*"]},{"source":"Reference foods: {usda} · {cc0}.","translations":{"zh-Hans":"参考食品数据:{usda} · {cc0}。","hi":"संदर्भ के लिए खाद्य डेटा: {usda} · {cc0}।","es":"Alimentos de referencia: {usda} · {cc0}.","ar":"أغذية مرجعية: {usda} · {cc0}.","fr":"Aliments de référence : {usda} · {cc0}.","bn":"তথ্যসূত্র হিসেবে দেওয়া খাদ্য: {usda} · {cc0}।","pt":"Alimentos de referência: {usda} · {cc0}.","id":"Makanan referensi: {usda} · {cc0}.","ur":"حوالے کے لیے غذاؤں کا ڈیٹا: {usda} · {cc0}۔","nl":"Voedingsmiddelen ter referentie: {usda} · {cc0}."},"pages":["*"]},{"source":"Home","translations":{"nl":"Home","zh-Hans":"首页","hi":"होम","es":"Inicio","ar":"الرئيسية","fr":"Accueil","bn":"হোম","pt":"Início","id":"Beranda","ur":"ہوم"},"pages":["*"]},{"source":"Whitepaper","translations":{"nl":"Whitepaper","zh-Hans":"白皮书","hi":"श्वेतपत्र","es":"Libro blanco","ar":"الورقة البيضاء","fr":"Livre blanc","bn":"শ্বেতপত্র","pt":"Livro branco","id":"Whitepaper","ur":"وائٹ پیپر"},"pages":["*"]},{"source":"CAL & Crypto","translations":{"nl":"CAL & Crypto","zh-Hans":"CAL & Crypto","hi":"CAL & Crypto","es":"CAL & Crypto","ar":"CAL & Crypto","fr":"CAL & Crypto","bn":"CAL & Crypto","pt":"CAL & Crypto","id":"CAL & Crypto","ur":"CAL & Crypto"},"pages":[1205,4205,6855,7880]},{"source":"Trustline","translations":{"nl":"Trustline","zh-Hans":"信任线","hi":"ट्रस्टलाइन","es":"Línea de confianza","ar":"خط الثقة","fr":"Ligne de confiance","bn":"ট্রাস্টলাইন","pt":"Linha de confiança","id":"Trustline","ur":"ٹرسٹ لائن"},"pages":["*"]},{"source":"Tokenomics update","translations":{"nl":"Tokenomics-update","zh-Hans":"代币经济更新","hi":"टोकन अर्थव्यवस्था अपडेट","es":"Actualización de tokenómica","ar":"تحديث اقتصاد الرمز","fr":"Mise à jour de la tokenomique","bn":"টোকেন অর্থনীতি আপডেট","pt":"Atualização da tokenomia","id":"Pembaruan tokenomik","ur":"ٹوکن معاشیات کی تازہ کاری"},"pages":["*"]},{"source":"Roadmap","translations":{"nl":"Routekaart","zh-Hans":"路线图","hi":"रोडमैप","es":"Hoja de ruta","ar":"خارطة الطريق","fr":"Feuille de route","bn":"রোডম্যাপ","pt":"Roteiro","id":"Peta jalan","ur":"روڈمیپ"},"pages":["*"]},{"source":"Richlist","translations":{"nl":"Houderslijst","zh-Hans":"持有者排名","hi":"धारकों की सूची","es":"Lista de titulares","ar":"قائمة الحائزين","fr":"Liste des détenteurs","bn":"ধারকদের তালিকা","pt":"Lista de detentores","id":"Daftar pemegang","ur":"ہولڈرز کی فہرست"},"pages":["*"]},{"source":"Blog","translations":{"nl":"Blog","zh-Hans":"博客","hi":"ब्लॉग","es":"Blog","ar":"المدونة","fr":"Blog","bn":"ব্লগ","pt":"Blog","id":"Blog","ur":"بلاگ"},"pages":["*"]},{"source":"Contact","translations":{"nl":"Contact","zh-Hans":"联系","hi":"संपर्क","es":"Contacto","ar":"اتصل بنا","fr":"Contact","bn":"যোগাযোগ","pt":"Contacto","id":"Kontak","ur":"رابطہ"},"pages":["*"]},{"source":"FAQ","translations":{"nl":"Veelgestelde vragen","zh-Hans":"常见问题","hi":"अक्सर पूछे जाने वाले प्रश्न","es":"Preguntas frecuentes","ar":"الأسئلة الشائعة","fr":"Questions fréquentes","bn":"সাধারণ প্রশ্ন","pt":"Perguntas frequentes","id":"Pertanyaan umum","ur":"اکثر پوچھے گئے سوالات"},"pages":["*"]},{"source":"Donate","translations":{"nl":"Doneren","zh-Hans":"捐赠","hi":"दान करें","es":"Donar","ar":"تبرع","fr":"Faire un don","bn":"অনুদান দিন","pt":"Doar","id":"Donasi","ur":"عطیہ دیں"},"pages":["*"]},{"source":"Merch&NFTs","translations":{"nl":"Merch & NFT’s","zh-Hans":"商品与 NFT","hi":"उत्पाद और NFT","es":"Productos y NFT","ar":"المنتجات وNFT","fr":"Produits et NFT","bn":"পণ্য ও NFT","pt":"Produtos e NFT","id":"Produk & NFT","ur":"مصنوعات اور NFT"},"pages":["*"]},{"source":"Privacy Policy","translations":{"nl":"Privacybeleid","zh-Hans":"隐私政策","hi":"गोपनीयता नीति","es":"Política de privacidad","ar":"سياسة الخصوصية","fr":"Politique de confidentialité","bn":"গোপনীয়তা নীতি","pt":"Política de privacidade","id":"Kebijakan privasi","ur":"رازداری کی پالیسی"},"pages":["*"]},{"source":"Terms & Conditions","translations":{"nl":"Algemene voorwaarden","zh-Hans":"条款与条件","hi":"नियम और शर्तें","es":"Términos y condiciones","ar":"الشروط والأحكام","fr":"Conditions générales","bn":"শর্তাবলি","pt":"Termos e condições","id":"Syarat dan ketentuan","ur":"شرائط و ضوابط"},"pages":["*"]},{"source":"Copy issuer","translations":{"zh-Hans":"复制发行方地址","hi":"जारीकर्ता का पता कॉपी करें","es":"Copiar emisor","ar":"نسخ عنوان المُصدر","fr":"Copier l’émetteur","bn":"ইস্যুকারীর ঠিকানা কপি করুন","pt":"Copiar emissor","id":"Salin penerbit","ur":"جاری کنندہ کا پتہ کاپی کریں","nl":"Issuer kopiëren"},"pages":[1205]},{"source":"Copy currency code","translations":{"zh-Hans":"复制货币代码","hi":"मुद्रा कोड कॉपी करें","es":"Copiar código de moneda","ar":"نسخ رمز العملة","fr":"Copier le code de devise","bn":"মুদ্রার কোড কপি করুন","pt":"Copiar código da moeda","id":"Salin kode mata uang","ur":"کرنسی کوڈ کاپی کریں","nl":"Valutacode kopiëren"},"pages":[1205]},{"source":"Copied issuer.","translations":{"zh-Hans":"已复制发行方地址。","hi":"जारीकर्ता का पता कॉपी हो गया।","es":"Emisor copiado.","ar":"تم نسخ عنوان المُصدر.","fr":"Émetteur copié.","bn":"ইস্যুকারীর ঠিকানা কপি হয়েছে।","pt":"Emissor copiado.","id":"Penerbit disalin.","ur":"جاری کنندہ کا پتہ کاپی ہو گیا۔","nl":"Issuer gekopieerd."},"pages":[1205]},{"source":"Copied currency code.","translations":{"zh-Hans":"已复制货币代码。","hi":"मुद्रा कोड कॉपी हो गया।","es":"Código de moneda copiado.","ar":"تم نسخ رمز العملة.","fr":"Code de devise copié.","bn":"মুদ্রার কোড কপি হয়েছে।","pt":"Código da moeda copiado.","id":"Kode mata uang disalin.","ur":"کرنسی کوڈ کاپی ہو گیا۔","nl":"Valutacode gekopieerd."},"pages":[1205]},{"source":"Select the issuer above to copy it.","translations":{"zh-Hans":"请选择上方的发行方地址进行复制。","hi":"कॉपी करने के लिए ऊपर दिया गया जारीकर्ता का पता चुनें।","es":"Selecciona el emisor de arriba para copiarlo.","ar":"حدّد عنوان المُصدر أعلاه لنسخه.","fr":"Sélectionnez l’émetteur ci-dessus pour le copier.","bn":"কপি করতে উপরের ইস্যুকারীর ঠিকানা নির্বাচন করুন।","pt":"Selecione o emissor acima para o copiar.","id":"Pilih penerbit di atas untuk menyalinnya.","ur":"کاپی کرنے کے لیے اوپر دیا گیا جاری کنندہ کا پتہ منتخب کریں۔","nl":"Selecteer de issuer hierboven om deze te kopiëren."},"pages":[1205]},{"source":"Select the currency code above to copy it.","translations":{"zh-Hans":"请选择上方的货币代码进行复制。","hi":"कॉपी करने के लिए ऊपर दिया गया मुद्रा कोड चुनें।","es":"Selecciona el código de moneda de arriba para copiarlo.","ar":"حدّد رمز العملة أعلاه لنسخه.","fr":"Sélectionnez le code de devise ci-dessus pour le copier.","bn":"কপি করতে উপরের মুদ্রার কোড নির্বাচন করুন।","pt":"Selecione o código da moeda acima para o copiar.","id":"Pilih kode mata uang di atas untuk menyalinnya.","ur":"کاپی کرنے کے لیے اوپر دیا گیا کرنسی کوڈ منتخب کریں۔","nl":"Selecteer de valutacode hierboven om deze te kopiëren."},"pages":[1205]},{"source":"Set CAL trustline in Xaman","translations":{"zh-Hans":"在 Xaman 中设置 CAL 信任线","hi":"Xaman में CAL ट्रस्टलाइन सेट करें","es":"Configurar línea de confianza CAL en Xaman","ar":"إعداد خط ثقة ⁦CAL⁩ في ⁦Xaman⁩","fr":"Configurer la ligne de confiance CAL dans Xaman","bn":"Xaman-এ CAL ট্রাস্টলাইন সেট করুন","pt":"Configurar linha de confiança CAL no Xaman","id":"Atur trustline CAL di Xaman","ur":"⁦Xaman⁩ میں ⁦CAL⁩ ٹرسٹ لائن قائم کریں","nl":"CAL-trustline instellen in Xaman"},"pages":[1205]},{"source":"Create a CAL trustline request and review it in Xaman","translations":{"zh-Hans":"创建 CAL 信任线请求并在 Xaman 中核对","hi":"CAL ट्रस्टलाइन अनुरोध बनाएं और Xaman में उसकी जाँच करें","es":"Crear una solicitud de línea de confianza CAL y revisarla en Xaman","ar":"إنشاء طلب خط ثقة ⁦CAL⁩ ومراجعته في ⁦Xaman⁩","fr":"Créer une demande de ligne de confiance CAL et la vérifier dans Xaman","bn":"CAL ট্রাস্টলাইনের অনুরোধ তৈরি করুন এবং Xaman-এ যাচাই করুন","pt":"Criar um pedido de linha de confiança CAL e verificá-lo no Xaman","id":"Buat permintaan trustline CAL dan periksa di Xaman","ur":"⁦CAL⁩ ٹرسٹ لائن کی درخواست بنائیں اور ⁦Xaman⁩ میں اس کی جانچ کریں","nl":"Maak een CAL-trustlineverzoek aan en controleer het in Xaman"},"pages":[1205]},{"source":"Before you continue","translations":{"zh-Hans":"继续之前","hi":"आगे बढ़ने से पहले","es":"Antes de continuar","ar":"قبل المتابعة","fr":"Avant de continuer","bn":"এগিয়ে যাওয়ার আগে","pt":"Antes de continuar","id":"Sebelum melanjutkan","ur":"آگے بڑھنے سے پہلے","nl":"Voordat je verdergaat"},"pages":[1205]},{"source":"A CAL trustline belongs to your XRPL wallet. Check the issuer and currency code in the request before signing. Food search and nutrition information in CalorieApp do not require a CAL trustline.","translations":{"zh-Hans":"CAL 信任线属于你的 XRPL 钱包。签署前,请核对请求中的发行方地址和货币代码。使用 CalorieApp 搜索食品和查看营养信息不需要 CAL 信任线。","hi":"CAL ट्रस्टलाइन आपके XRPL वॉलेट से जुड़ी होती है। हस्ताक्षर करने से पहले अनुरोध में जारीकर्ता का पता और मुद्रा कोड जाँचें। CalorieApp में खाद्य खोज और पोषण संबंधी जानकारी के लिए CAL ट्रस्टलाइन की आवश्यकता नहीं है।","es":"Una línea de confianza CAL pertenece a tu cartera XRPL. Comprueba el emisor y el código de moneda en la solicitud antes de firmar. La búsqueda de alimentos y la información nutricional de CalorieApp no requieren una línea de confianza CAL.","ar":"يرتبط خط ثقة ⁦CAL⁩ بمحفظتك على ⁦XRPL⁩. تحقّق من عنوان المُصدر ورمز العملة في الطلب قبل التوقيع. لا يتطلب البحث عن الأغذية وعرض المعلومات الغذائية في ⁦CalorieApp⁩ خط ثقة ⁦CAL⁩.","fr":"Une ligne de confiance CAL appartient à votre portefeuille XRPL. Vérifiez l’émetteur et le code de devise dans la demande avant de signer. La recherche d’aliments et les informations nutritionnelles dans CalorieApp ne nécessitent pas de ligne de confiance CAL.","bn":"CAL ট্রাস্টলাইন আপনার XRPL ওয়ালেটের সঙ্গে যুক্ত থাকে। স্বাক্ষর করার আগে অনুরোধের ইস্যুকারীর ঠিকানা ও মুদ্রার কোড যাচাই করুন। CalorieApp-এ খাবার খোঁজা এবং পুষ্টির তথ্য দেখার জন্য CAL ট্রাস্টলাইন প্রয়োজন হয় না।","pt":"Uma linha de confiança CAL pertence à sua carteira XRPL. Verifique o emissor e o código da moeda no pedido antes de assinar. A pesquisa de alimentos e as informações nutricionais na CalorieApp não exigem uma linha de confiança CAL.","id":"Trustline CAL terhubung ke dompet XRPL Anda. Periksa penerbit dan kode mata uang dalam permintaan sebelum menandatangani. Pencarian makanan dan informasi gizi di CalorieApp tidak memerlukan trustline CAL.","ur":"⁦CAL⁩ ٹرسٹ لائن آپ کے ⁦XRPL⁩ والیٹ سے منسلک ہوتی ہے۔ دستخط کرنے سے پہلے درخواست میں جاری کنندہ کا پتہ اور کرنسی کوڈ چیک کریں۔ ⁦CalorieApp⁩ میں کھانے کی تلاش اور غذائی معلومات کے لیے ⁦CAL⁩ ٹرسٹ لائن کی ضرورت نہیں ہے۔","nl":"Een CAL-trustline hoort bij je XRPL-wallet. Controleer de issuer en de valutacode in het verzoek voordat je ondertekent. Voor het zoeken naar voedingsmiddelen en het bekijken van voedingsinformatie in CalorieApp is geen CAL-trustline nodig."},"pages":[1205]},{"source":"Open XRP Toolkit","translations":{"zh-Hans":"打开 XRP Toolkit","hi":"XRP Toolkit खोलें","es":"Abrir XRP Toolkit","ar":"فتح ⁦XRP Toolkit⁩","fr":"Ouvrir XRP Toolkit","bn":"XRP Toolkit খুলুন","pt":"Abrir XRP Toolkit","id":"Buka XRP Toolkit","ur":"⁦XRP Toolkit⁩ کھولیں","nl":"XRP Toolkit openen"},"pages":[1205]},{"source":"CalorieToken holders. Scroll sideways to see the complete table.","translations":{"zh-Hans":"CalorieToken 持有者。左右滚动以查看完整表格。","hi":"CalorieToken धारक। पूरी तालिका देखने के लिए दाएँ या बाएँ स्क्रॉल करें।","es":"Titulares de CalorieToken. Desplázate horizontalmente para ver la tabla completa.","ar":"حاملو CalorieToken. مرّر أفقياً لرؤية الجدول كاملاً.","fr":"Détenteurs de CalorieToken. Faites défiler horizontalement pour voir le tableau complet.","bn":"CalorieToken ধারকরা। পুরো সারণিটি দেখতে পাশে স্ক্রল করুন।","pt":"Detentores de CalorieToken. Desloque horizontalmente para ver a tabela completa.","id":"Pemegang CalorieToken. Gulir ke samping untuk melihat seluruh tabel.","ur":"CalorieToken کے حاملین۔ مکمل جدول دیکھنے کے لیے دائیں یا بائیں اسکرول کریں۔","nl":"CalorieToken-houders. Scrol horizontaal om de hele tabel te bekijken."},"pages":[3243]},{"source":"My position","translations":{"zh-Hans":"我的排名","hi":"मेरा स्थान","es":"Mi posición","ar":"ترتيبي","fr":"Ma position","bn":"আমার অবস্থান","pt":"A minha posição","id":"Posisi saya","ur":"میری پوزیشن","nl":"Mijn positie"},"pages":[3243]},{"source":"Go to my Richlist position","translations":{"zh-Hans":"跳转到我在持有者排名中的位置","hi":"धारकों की सूची में मेरे स्थान पर जाएँ","es":"Ir a mi posición en la lista de titulares","ar":"انتقل إلى ترتيبي في قائمة الحاملين","fr":"Aller à ma position dans la liste des détenteurs","bn":"ধারকদের তালিকায় আমার অবস্থানে যান","pt":"Ir para a minha posição na lista de detentores","id":"Buka posisi saya dalam daftar pemegang","ur":"حاملین کی فہرست میں میری پوزیشن پر جائیں","nl":"Ga naar mijn positie in de Richlist"},"pages":[3243]},{"source":"Airdrops completed","translations":{"zh-Hans":"空投已完成","hi":"एयरड्रॉप पूरे हो चुके हैं","es":"Airdrops completados","ar":"اكتملت عمليات التوزيع المجاني","fr":"Airdrops terminés","bn":"এয়ারড্রপ সম্পন্ন হয়েছে","pt":"Airdrops concluídos","id":"Airdrop telah selesai","ur":"ایئرڈراپس مکمل ہو چکے ہیں","nl":"Airdrops afgerond"},"pages":[1209]},{"source":"All airdrops are completed, as confirmed by the project on 2026-09-09. The published chart is retained as historical context; earlier references to future airdrops no longer apply.","translations":{"zh-Hans":"项目方于 2026-09-09 确认,所有空投均已完成。保留已发布图表作为历史资料;其中关于未来空投的说明已不再适用。","hi":"परियोजना ने 2026-09-09 को पुष्टि की कि सभी एयरड्रॉप पूरे हो चुके हैं। प्रकाशित चार्ट ऐतिहासिक संदर्भ के लिए रखा गया है; भविष्य के एयरड्रॉप के पुराने उल्लेख अब लागू नहीं होते।","es":"El proyecto confirmó el 2026-09-09 que todos los airdrops han finalizado. El gráfico publicado se conserva como referencia histórica; las menciones anteriores a futuros airdrops ya no se aplican.","ar":"أكد المشروع بتاريخ 2026-09-09 اكتمال جميع عمليات التوزيع المجاني (airdrop). يُحتفظ بالمخطط المنشور كسياق تاريخي؛ ولم تعد الإشارات السابقة إلى عمليات توزيع مستقبلية سارية.","fr":"Le projet a confirmé le 2026-09-09 que tous les airdrops sont terminés. Le graphique publié est conservé à titre historique ; les anciennes mentions de futurs airdrops ne sont plus applicables.","bn":"প্রকল্পটি 2026-09-09 তারিখে নিশ্চিত করেছে যে সব এয়ারড্রপ সম্পন্ন হয়েছে। প্রকাশিত চার্টটি ঐতিহাসিক তথ্য হিসেবে রাখা হয়েছে; ভবিষ্যৎ এয়ারড্রপের পুরোনো উল্লেখ আর প্রযোজ্য নয়।","pt":"O projeto confirmou em 2026-09-09 que todos os airdrops foram concluídos. O gráfico publicado é mantido como referência histórica; as menções anteriores a futuros airdrops já não se aplicam.","id":"Proyek mengonfirmasi pada 2026-09-09 bahwa semua airdrop telah selesai. Grafik yang diterbitkan dipertahankan sebagai konteks historis; keterangan lama tentang airdrop mendatang tidak lagi berlaku.","ur":"منصوبے نے 2026-09-09 کو تصدیق کی کہ تمام ایئرڈراپس مکمل ہو چکے ہیں۔ شائع شدہ چارٹ تاریخی حوالے کے لیے برقرار ہے؛ آئندہ ایئرڈراپس کے پرانے حوالے اب لاگو نہیں ہوتے۔","nl":"Alle airdrops zijn afgerond, zoals door het project bevestigd op 2026-09-09. De gepubliceerde grafiek blijft als historische toelichting staan; eerdere verwijzingen naar toekomstige airdrops zijn niet meer van toepassing."},"pages":[1209]},{"source":"A remaining giveaway allocation will be stated after distributions, internal transfers and liquidity positions have been reconciled.","translations":{"zh-Hans":"在核对分发、内部转账和流动性头寸后,将公布剩余赠送额度。","hi":"वितरण, आंतरिक हस्तांतरण और लिक्विडिटी पोज़िशन का मिलान करने के बाद शेष गिवअवे आवंटन बताया जाएगा।","es":"La asignación restante para sorteos y regalos se indicará tras conciliar las distribuciones, las transferencias internas y las posiciones de liquidez.","ar":"سيُذكر المخصص المتبقي للهدايا بعد مطابقة التوزيعات والتحويلات الداخلية ومراكز السيولة.","fr":"Le solde alloué aux cadeaux sera indiqué après rapprochement des distributions, des transferts internes et des positions de liquidité.","bn":"বিতরণ, অভ্যন্তরীণ স্থানান্তর এবং লিকুইডিটি পজিশন মিলিয়ে দেখার পর গিভঅ্যাওয়ের অবশিষ্ট বরাদ্দ জানানো হবে।","pt":"A alocação restante para brindes será indicada após a reconciliação das distribuições, transferências internas e posições de liquidez.","id":"Sisa alokasi giveaway akan dinyatakan setelah distribusi, transfer internal, dan posisi likuiditas direkonsiliasi.","ur":"تقسیم، داخلی منتقلیوں اور لیکویڈیٹی پوزیشنوں کا حساب ملانے کے بعد تحائف کے لیے باقی مختص مقدار بتائی جائے گی۔","nl":"Het resterende giveawaybudget wordt vermeld nadat uitdelingen, interne overboekingen en liquiditeitsposities met elkaar zijn afgestemd."},"pages":[1209]},{"source":"Consolidation wallet","translations":{"zh-Hans":"归集钱包","hi":"समेकन वॉलेट","es":"Billetera de consolidación","ar":"محفظة التجميع","fr":"Portefeuille de consolidation","bn":"একত্রীকরণ ওয়ালেট","pt":"Carteira de consolidação","id":"Dompet konsolidasi","ur":"یکجا فنڈز کا والیٹ","nl":"Consolidatiewallet"},"pages":[1209]},{"source":"Project wallet for consolidated CalorieToken funds. Its balance is not a statement of the remaining giveaway allocation.","translations":{"zh-Hans":"用于归集 CalorieToken 项目资金的钱包。其余额不代表剩余赠送额度。","hi":"CalorieToken परियोजना की समेकित निधियों का वॉलेट। इसका बैलेंस शेष गिवअवे आवंटन नहीं दर्शाता।","es":"Billetera del proyecto para fondos consolidados de CalorieToken. Su saldo no indica la asignación restante para sorteos y regalos.","ar":"محفظة المشروع لتجميع أموال CalorieToken. لا يمثل رصيدها المخصص المتبقي للهدايا.","fr":"Portefeuille du projet destiné aux fonds consolidés de CalorieToken. Son solde ne représente pas le montant restant alloué aux cadeaux.","bn":"CalorieToken প্রকল্পের একত্রিত তহবিলের ওয়ালেট। এর ব্যালেন্স গিভঅ্যাওয়ের অবশিষ্ট বরাদ্দ নির্দেশ করে না।","pt":"Carteira do projeto para fundos consolidados de CalorieToken. O seu saldo não representa a alocação restante para brindes.","id":"Dompet proyek untuk dana CalorieToken yang dikonsolidasikan. Saldonya tidak menyatakan sisa alokasi giveaway.","ur":"CalorieToken منصوبے کے یکجا فنڈز کا والیٹ۔ اس کا بیلنس تحائف کے لیے باقی مختص مقدار کو ظاہر نہیں کرتا۔","nl":"Projectwallet voor samengebrachte CalorieToken-fondsen. Het saldo geeft niet het resterende giveawaybudget aan."},"pages":[1209]},{"source":"At the ledger check on 2026-09-09, this wallet held a liquidity position in the CAL/XRP AMM. The project therefore has a financial interest in this pool.","translations":{"zh-Hans":"在 2026-09-09 的账本核查中,此钱包持有 CAL/XRP AMM 的流动性头寸。因此,项目在该资金池中具有经济利益。","hi":"2026-09-09 की लेजर जाँच के समय इस वॉलेट की CAL/XRP AMM में लिक्विडिटी पोज़िशन थी। इसलिए इस पूल में परियोजना का वित्तीय हित है।","es":"En la comprobación del ledger del 2026-09-09, esta billetera mantenía una posición de liquidez en el AMM CAL/XRP. Por tanto, el proyecto tiene un interés financiero en este pool.","ar":"عند فحص السجل بتاريخ 2026-09-09، كانت لهذه المحفظة حصة سيولة في مجمع CAL/XRP AMM. ولذلك للمشروع مصلحة مالية في هذا المجمع.","fr":"Lors de la vérification du registre le 2026-09-09, ce portefeuille détenait une position de liquidité dans l’AMM CAL/XRP. Le projet a donc un intérêt financier dans ce pool.","bn":"2026-09-09 তারিখে লেজার যাচাইয়ের সময় এই ওয়ালেটের CAL/XRP AMM-এ লিকুইডিটি পজিশন ছিল। তাই এই পুলে প্রকল্পটির আর্থিক স্বার্থ রয়েছে।","pt":"Na verificação do ledger em 2026-09-09, esta carteira mantinha uma posição de liquidez no AMM CAL/XRP. Por isso, o projeto tem um interesse financeiro neste pool.","id":"Pada pemeriksaan ledger tanggal 2026-09-09, dompet ini memiliki posisi likuiditas di AMM CAL/XRP. Karena itu, proyek memiliki kepentingan finansial dalam pool ini.","ur":"2026-09-09 کی لیجر جانچ کے وقت اس والیٹ کی CAL/XRP AMM میں لیکویڈیٹی پوزیشن تھی۔ اس لیے اس پول میں منصوبے کا مالی مفاد ہے۔","nl":"Bij de ledgercontrole op 2026-09-09 had deze wallet een liquiditeitspositie in de CAL/XRP-AMM. Het project heeft daarmee een financieel belang in deze pool."},"pages":[1209]},{"source":"View wallet on XPMarket (new tab)","translations":{"zh-Hans":"在 XPMarket 查看钱包(新标签页)","hi":"XPMarket पर वॉलेट देखें (नया टैब)","es":"Ver billetera en XPMarket (nueva pestaña)","ar":"عرض المحفظة على XPMarket (علامة تبويب جديدة)","fr":"Voir le portefeuille sur XPMarket (nouvel onglet)","bn":"XPMarket-এ ওয়ালেট দেখুন (নতুন ট্যাব)","pt":"Ver carteira no XPMarket (nova aba)","id":"Lihat dompet di XPMarket (tab baru)","ur":"XPMarket پر والیٹ دیکھیں (نیا ٹیب)","nl":"Bekijk wallet op XPMarket (nieuw tabblad)"},"pages":[1209]},{"source":"Page shortcuts","translations":{"zh-Hans":"页面快捷操作","hi":"पेज शॉर्टकट","es":"Accesos de página","ar":"اختصارات الصفحة","fr":"Raccourcis de la page","bn":"পৃষ্ঠার শর্টকাট","pt":"Atalhos da página","id":"Pintasan halaman","ur":"صفحے کے شارٹ کٹس","nl":"Paginasnelkoppelingen"},"pages":["*"]},{"source":"Go to Home","translations":{"zh-Hans":"返回首页","hi":"होम पर जाएँ","es":"Ir al inicio","ar":"إلى الصفحة الرئيسية","fr":"Aller à l’accueil","bn":"হোমে যান","pt":"Ir para o início","id":"Ke beranda","ur":"ہوم پر جائیں","nl":"Naar de homepage"},"pages":["*"]},{"source":"Back to top","translations":{"zh-Hans":"返回顶部","hi":"ऊपर लौटें","es":"Volver arriba","ar":"إلى أعلى الصفحة","fr":"Revenir en haut","bn":"উপরে ফিরুন","pt":"Voltar ao topo","id":"Kembali ke atas","ur":"اوپر واپس جائیں","nl":"Naar boven"},"pages":["*"]},{"source":"Go to bottom","translations":{"zh-Hans":"前往底部","hi":"नीचे जाएँ","es":"Ir abajo","ar":"إلى أسفل الصفحة","fr":"Aller en bas","bn":"নিচে যান","pt":"Ir para o fim","id":"Ke bagian bawah","ur":"صفحے کے نیچے جائیں","nl":"Naar beneden"},"pages":["*"]},{"source":"XRPL buying and selling guide","translations":{"zh-Hans":"XRPL 买卖指南","hi":"XRPL खरीदने और बेचने की मार्गदर्शिका","es":"Guía de compra y venta en XRPL","ar":"دليل الشراء والبيع على XRPL","fr":"Guide d’achat et de vente sur XRPL","bn":"XRPL কেনাবেচার নির্দেশিকা","pt":"Guia de compra e venda na XRPL","id":"Panduan jual beli XRPL","ur":"XRPL خرید و فروخت کی رہنمائی","nl":"Koop- en verkoopgids voor XRPL"},"pages":[4205]},{"source":"Buy or sell CAL in four clear steps","translations":{"zh-Hans":"四步买入或卖出 CAL","hi":"चार स्पष्ट चरणों में CAL खरीदें या बेचें","es":"Compra o vende CAL en cuatro pasos claros","ar":"اشترِ أو بِع CAL في أربع خطوات واضحة","fr":"Acheter ou vendre CAL en quatre étapes claires","bn":"চারটি স্পষ্ট ধাপে CAL কিনুন বা বিক্রি করুন","pt":"Compre ou venda CAL em quatro passos claros","id":"Beli atau jual CAL dalam empat langkah jelas","ur":"چار واضح مراحل میں CAL خریدیں یا بیچیں","nl":"Koop of verkoop CAL in vier duidelijke stappen"},"pages":[4205]},{"source":"CAL trades on the XRP Ledger DEX. Verify the complete issuer and currency code, not just the name or logo.","translations":{"zh-Hans":"CAL 在 XRP Ledger DEX 上交易。请核对完整发行方地址和货币代码,而不只是名称或标志。","hi":"CAL का कारोबार XRP Ledger DEX पर होता है। केवल नाम या लोगो नहीं, पूरा जारीकर्ता पता और मुद्रा कोड जाँचें।","es":"CAL se negocia en el DEX de XRP Ledger. Comprueba el emisor completo y el código de moneda, no solo el nombre o el logotipo.","ar":"يُتداول CAL على منصة XRP Ledger اللامركزية. تحقّق من عنوان المُصدر الكامل ورمز العملة، وليس الاسم أو الشعار فقط.","fr":"CAL se négocie sur le DEX du XRP Ledger. Vérifiez l’adresse complète de l’émetteur et le code de devise, pas seulement le nom ou le logo.","bn":"XRP Ledger DEX-এ CAL লেনদেন হয়। শুধু নাম বা লোগো নয়, সম্পূর্ণ ইস্যুকারীর ঠিকানা ও মুদ্রার কোড যাচাই করুন।","pt":"CAL é negociado na DEX do XRP Ledger. Verifique o endereço completo do emissor e o código da moeda, não apenas o nome ou o logótipo.","id":"CAL diperdagangkan di XRP Ledger DEX. Verifikasi alamat penerbit lengkap dan kode mata uang, bukan hanya nama atau logo.","ur":"CAL کی تجارت XRP Ledger DEX پر ہوتی ہے۔ صرف نام یا لوگو نہیں، جاری کنندہ کا مکمل پتہ اور کرنسی کوڈ جانچیں۔","nl":"CAL wordt verhandeld op de XRP Ledger DEX. Controleer het volledige issueradres en de valutacode, niet alleen de naam of het logo."},"pages":[4205]},{"source":"Asset","translations":{"zh-Hans":"资产","hi":"संपत्ति","es":"Activo","ar":"الأصل","fr":"Actif","bn":"সম্পদ","pt":"Ativo","id":"Aset","ur":"اثاثہ","nl":"Token"},"pages":[4205]},{"source":"Issuer","translations":{"zh-Hans":"发行方","hi":"जारीकर्ता","es":"Emisor","ar":"المُصدر","fr":"Émetteur","bn":"ইস্যুকারী","pt":"Emissor","id":"Penerbit","ur":"جاری کنندہ","nl":"Issuer"},"pages":[4205]},{"source":"Currency code","translations":{"zh-Hans":"货币代码","hi":"मुद्रा कोड","es":"Código de moneda","ar":"رمز العملة","fr":"Code de devise","bn":"মুদ্রার কোড","pt":"Código da moeda","id":"Kode mata uang","ur":"کرنسی کوڈ","nl":"Valutacode"},"pages":[4205]},{"source":"Set up an XRPL wallet","translations":{"zh-Hans":"设置 XRPL 钱包","hi":"XRPL वॉलेट सेट करें","es":"Configura una billetera XRPL","ar":"إعداد محفظة XRPL","fr":"Configurer un portefeuille XRPL","bn":"XRPL ওয়ালেট সেট করুন","pt":"Configure uma carteira XRPL","id":"Siapkan dompet XRPL","ur":"XRPL والیٹ بنائیں","nl":"Maak een XRPL-wallet aan"},"pages":[4205]},{"source":"Install Xaman from its official source. Keep recovery information offline; never enter it on this website or a trading page.","translations":{"zh-Hans":"从官方来源安装 Xaman。离线保存恢复信息,切勿在本网站或交易页面输入。","hi":"आधिकारिक स्रोत से Xaman इंस्टॉल करें। रिकवरी जानकारी ऑफलाइन रखें; इसे इस वेबसाइट या किसी ट्रेडिंग पेज पर कभी न डालें।","es":"Instala Xaman desde su fuente oficial. Guarda los datos de recuperación fuera de línea; nunca los introduzcas en esta web ni en una página de trading.","ar":"ثبّت Xaman من مصدره الرسمي. احفظ معلومات الاسترداد دون اتصال؛ ولا تدخلها أبداً في هذا الموقع أو صفحات التداول.","fr":"Installez Xaman depuis sa source officielle. Conservez les informations de récupération hors ligne ; ne les saisissez jamais sur ce site ou une page de trading.","bn":"অফিশিয়াল উৎস থেকে Xaman ইনস্টল করুন। পুনরুদ্ধারের তথ্য অফলাইনে রাখুন; এই ওয়েবসাইট বা কোনো ট্রেডিং পেজে কখনো লিখবেন না।","pt":"Instale o Xaman a partir da fonte oficial. Guarde os dados de recuperação offline; nunca os introduza neste site ou numa página de negociação.","id":"Instal Xaman dari sumber resminya. Simpan informasi pemulihan secara offline; jangan pernah memasukkannya di situs ini atau halaman perdagangan.","ur":"Xaman اس کے سرکاری ذریعے سے انسٹال کریں۔ بحالی کی معلومات آف لائن رکھیں؛ انہیں اس ویب سائٹ یا تجارتی صفحے پر کبھی درج نہ کریں۔","nl":"Installeer Xaman via de officiële bron. Bewaar herstelgegevens offline; voer ze nooit op deze website of een handelspagina in."},"pages":[4205]},{"source":"Visit Xaman","translations":{"zh-Hans":"访问 Xaman","hi":"Xaman पर जाएँ","es":"Visitar Xaman","ar":"زيارة Xaman","fr":"Visiter Xaman","bn":"Xaman দেখুন","pt":"Visitar Xaman","id":"Kunjungi Xaman","ur":"Xaman دیکھیں","nl":"Bezoek Xaman"},"pages":[4205]},{"source":"Fund your wallet with XRP","translations":{"zh-Hans":"向钱包存入 XRP","hi":"वॉलेट में XRP डालें","es":"Ingresa XRP en tu billetera","ar":"تمويل محفظتك بعملة XRP","fr":"Alimenter votre portefeuille en XRP","bn":"ওয়ালেটে XRP যোগ করুন","pt":"Adicione XRP à sua carteira","id":"Isi dompet Anda dengan XRP","ur":"والیٹ میں XRP شامل کریں","nl":"Zet XRP op je wallet"},"pages":[4205]},{"source":"Xaman lists independent providers by country and currency. Check eligibility, identity checks, rates and fees; availability varies by region. Already hold BTC, ETH or another supported coin? AllChainBridge / SWFT is another possible route to XRP across networks. See section 3 below. Select the source coin and network, choose XRP on XRP Ledger as the destination, and check the available wallets, route, fees and any destination tag with SWFT. CAL is temporarily unavailable there.","translations":{"zh-Hans":"Xaman 按国家和货币列出独立服务商。请核查资格、身份验证、汇率和费用;可用性因地区而异。 已有 BTC、ETH 或其他受支持的币?AllChainBridge / SWFT 也是跨网络兑换 XRP 的一种可能途径,见下方第 3 节。选择来源币和网络,将 XRP Ledger 上的 XRP 设为目标,并在 SWFT 检查可用钱包、路径、费用及所需的目标标签。CAL 暂时不可用。","hi":"Xaman देश और मुद्रा के अनुसार स्वतंत्र प्रदाता दिखाता है। पात्रता, पहचान जाँच, दरें और शुल्क जाँचें; उपलब्धता क्षेत्र के अनुसार बदलती है। क्या आपके पास BTC, ETH या कोई समर्थित कॉइन है? AllChainBridge / SWFT अलग नेटवर्क से XRP पाने का एक और संभावित रास्ता है। नीचे भाग 3 देखें। स्रोत कॉइन और नेटवर्क चुनें, गंतव्य में XRP Ledger का XRP चुनें और SWFT में उपलब्ध वॉलेट, मार्ग, शुल्क तथा आवश्यक डेस्टिनेशन टैग जाँचें। वहाँ CAL अस्थायी रूप से उपलब्ध नहीं है।","es":"Xaman muestra proveedores independientes por país y moneda. Comprueba requisitos, verificación de identidad, tipos de cambio y comisiones; la disponibilidad varía por región. ¿Ya tienes BTC, ETH u otra moneda compatible? AllChainBridge / SWFT es otra posible vía hacia XRP entre redes; consulta la sección 3. Elige moneda y red de origen, XRP en XRP Ledger como destino y comprueba en SWFT carteras disponibles, ruta, costes y posible etiqueta de destino. CAL no está disponible allí temporalmente.","ar":"يعرض Xaman مزودين مستقلين حسب البلد والعملة. تحقّق من الأهلية وفحوص الهوية والأسعار والرسوم؛ فالتوفر يختلف حسب المنطقة. هل لديك BTC أو ETH أو عملة مدعومة أخرى؟ يتيح AllChainBridge / SWFT مسارًا محتملًا آخر إلى XRP بين الشبكات؛ انظر القسم 3 أدناه. اختر عملة المصدر وشبكتها، ثم XRP على XRP Ledger كوجهة، وتحقق لدى SWFT من المحافظ والمسار والرسوم وأي وسم وجهة مطلوب. CAL غير متاح هناك مؤقتًا.","fr":"Xaman présente des prestataires indépendants par pays et devise. Vérifiez l’éligibilité, les contrôles d’identité, les taux et les frais ; la disponibilité varie selon les régions. Vous détenez déjà du BTC, de l’ETH ou une autre monnaie prise en charge ? AllChainBridge / SWFT est une autre voie possible vers XRP entre réseaux ; voir la section 3. Choisissez la monnaie et le réseau source, puis XRP sur XRP Ledger comme destination. Vérifiez chez SWFT les portefeuilles, le parcours, les frais et l’éventuel tag de destination. CAL y est temporairement indisponible.","bn":"Xaman দেশ ও মুদ্রা অনুযায়ী স্বাধীন সেবাদাতা দেখায়। যোগ্যতা, পরিচয় যাচাই, হার ও ফি পরীক্ষা করুন; অঞ্চলভেদে উপলভ্যতা বদলায়। ইতিমধ্যে BTC, ETH বা অন্য সমর্থিত কয়েন আছে? AllChainBridge / SWFT বিভিন্ন নেটওয়ার্ক থেকে XRP পাওয়ার আরেকটি সম্ভাব্য পথ; নিচের ৩ নম্বর অংশ দেখুন। উৎস কয়েন ও নেটওয়ার্ক বেছে নিন, গন্তব্য হিসেবে XRP Ledger-এর XRP দিন এবং SWFT-এ উপলব্ধ ওয়ালেট, পথ, ফি ও প্রয়োজনীয় ডেস্টিনেশন ট্যাগ যাচাই করুন। সেখানে CAL সাময়িকভাবে অনুপলব্ধ।","pt":"O Xaman apresenta prestadores independentes por país e moeda. Verifique elegibilidade, identificação, taxas de câmbio e comissões; a disponibilidade varia por região. Já tem BTC, ETH ou outra moeda compatível? AllChainBridge / SWFT é outra possível via para XRP entre redes; veja a secção 3. Escolha a moeda e rede de origem e XRP na XRP Ledger como destino. Confirme no SWFT as carteiras disponíveis, o percurso, as taxas e uma eventual etiqueta de destino. CAL está temporariamente indisponível ali.","id":"Xaman menampilkan penyedia independen berdasarkan negara dan mata uang. Periksa kelayakan, verifikasi identitas, kurs, dan biaya; ketersediaan berbeda menurut wilayah. Sudah memiliki BTC, ETH, atau koin lain yang didukung? AllChainBridge / SWFT adalah jalur lain yang mungkin tersedia menuju XRP lintas jaringan; lihat bagian 3. Pilih koin dan jaringan asal, XRP di XRP Ledger sebagai tujuan, lalu periksa dompet yang tersedia, rute, biaya, dan tag tujuan yang diperlukan di SWFT. CAL sementara tidak tersedia di sana.","ur":"Xaman ملک اور کرنسی کے مطابق آزاد فراہم کنندگان دکھاتا ہے۔ اہلیت، شناخت کی جانچ، شرح اور فیس دیکھیں؛ دستیابی علاقے کے لحاظ سے بدلتی ہے۔ کیا آپ کے پاس BTC، ETH یا کوئی معاون کوائن ہے؟ AllChainBridge / SWFT مختلف نیٹ ورکس سے XRP حاصل کرنے کا ایک اور ممکنہ راستہ ہے؛ نیچے حصہ 3 دیکھیں۔ اصل کوائن اور نیٹ ورک منتخب کریں، منزل میں XRP Ledger کا XRP چنیں اور SWFT پر دستیاب والیٹس، راستہ، فیس اور مطلوبہ ڈیسٹینیشن ٹیگ دیکھیں۔ وہاں CAL عارضی طور پر دستیاب نہیں۔","nl":"Xaman toont onafhankelijke aanbieders per land en valuta. Controleer toelating, identiteitscontroles, koersen en kosten; beschikbaarheid verschilt per regio. Heb je al BTC, ETH of een andere ondersteunde munt? AllChainBridge / SWFT biedt ook een mogelijke route naar XRP tussen netwerken. Zie sectie 3 hieronder. Kies de bronmunt en het netwerk, kies XRP op XRP Ledger als bestemming en controleer bij SWFT de beschikbare wallets, route, kosten en een eventuele destination tag. CAL is daar tijdelijk niet beschikbaar."},"pages":[4205]},{"source":"Find options in Xaman","translations":{"zh-Hans":"在 Xaman 查看选项","hi":"Xaman में विकल्प देखें","es":"Ver opciones en Xaman","ar":"عرض الخيارات في Xaman","fr":"Voir les options dans Xaman","bn":"Xaman-এ বিকল্প দেখুন","pt":"Ver opções no Xaman","id":"Lihat pilihan di Xaman","ur":"Xaman میں اختیارات دیکھیں","nl":"Bekijk mogelijkheden in Xaman"},"pages":[4205]},{"source":"Keep enough XRP for the reserve and network fees shown by your wallet. Reserve requirements can change.","translations":{"zh-Hans":"保留足够的 XRP,以满足钱包显示的储备金和网络费用要求。储备金要求可能变化。","hi":"वॉलेट में दिखाए गए रिज़र्व और नेटवर्क शुल्क के लिए पर्याप्त XRP रखें। रिज़र्व की आवश्यकताएँ बदल सकती हैं।","es":"Mantén suficiente XRP para la reserva y las comisiones de red que indica tu billetera. Los requisitos de reserva pueden cambiar.","ar":"احتفظ بما يكفي من XRP للاحتياطي ورسوم الشبكة التي تعرضها محفظتك. قد تتغير متطلبات الاحتياطي.","fr":"Gardez assez de XRP pour la réserve et les frais de réseau indiqués par votre portefeuille. Les exigences de réserve peuvent changer.","bn":"ওয়ালেটে দেখানো রিজার্ভ ও নেটওয়ার্ক ফির জন্য পর্যাপ্ত XRP রাখুন। রিজার্ভের শর্ত বদলাতে পারে।","pt":"Mantenha XRP suficiente para a reserva e as taxas de rede indicadas pela carteira. Os requisitos de reserva podem mudar.","id":"Sisakan cukup XRP untuk cadangan dan biaya jaringan yang ditampilkan dompet Anda. Persyaratan cadangan dapat berubah.","ur":"والیٹ میں دکھائے گئے ریزرو اور نیٹ ورک فیس کے لیے کافی XRP رکھیں۔ ریزرو کی ضروریات بدل سکتی ہیں۔","nl":"Houd genoeg XRP aan voor de reserve en netwerkkosten die je wallet toont. Reservevereisten kunnen veranderen."},"pages":[4205]},{"source":"XRPL reserve information","translations":{"zh-Hans":"XRPL 储备金说明","hi":"XRPL रिज़र्व की जानकारी","es":"Información sobre reservas XRPL","ar":"معلومات احتياطي XRPL","fr":"Informations sur les réserves XRPL","bn":"XRPL রিজার্ভের তথ্য","pt":"Informações sobre reservas XRPL","id":"Informasi cadangan XRPL","ur":"XRPL ریزرو کی معلومات","nl":"Uitleg over XRPL-reserves"},"pages":[4205]},{"source":"Add the exact CAL trustline","translations":{"zh-Hans":"添加准确的 CAL 信任线","hi":"सही CAL ट्रस्टलाइन जोड़ें","es":"Añade la línea de confianza CAL exacta","ar":"إضافة خط الثقة الصحيح لعملة CAL","fr":"Ajouter la ligne de confiance CAL exacte","bn":"সঠিক CAL ট্রাস্টলাইন যোগ করুন","pt":"Adicione a linha de confiança CAL exata","id":"Tambahkan trustline CAL yang tepat","ur":"درست CAL ٹرسٹ لائن شامل کریں","nl":"Voeg de juiste CAL-trustline toe"},"pages":[4205]},{"source":"To receive CAL, review its issuer and currency code before approving the trustline in Xaman.","translations":{"zh-Hans":"接收 CAL 前,请核对发行方地址和货币代码,再在 Xaman 中批准信任线。","hi":"CAL प्राप्त करने के लिए Xaman में ट्रस्टलाइन स्वीकृत करने से पहले जारीकर्ता और मुद्रा कोड जाँचें।","es":"Para recibir CAL, revisa su emisor y código de moneda antes de aprobar la línea de confianza en Xaman.","ar":"لاستلام CAL، راجع المُصدر ورمز العملة قبل الموافقة على خط الثقة في Xaman.","fr":"Pour recevoir CAL, vérifiez son émetteur et son code de devise avant d’approuver la ligne de confiance dans Xaman.","bn":"CAL পেতে Xaman-এ ট্রাস্টলাইন অনুমোদনের আগে ইস্যুকারী ও মুদ্রার কোড যাচাই করুন।","pt":"Para receber CAL, verifique o emissor e o código da moeda antes de aprovar a linha de confiança no Xaman.","id":"Untuk menerima CAL, periksa penerbit dan kode mata uangnya sebelum menyetujui trustline di Xaman.","ur":"CAL وصول کرنے کے لیے Xaman میں ٹرسٹ لائن منظور کرنے سے پہلے جاری کنندہ اور کرنسی کوڈ جانچیں۔","nl":"Controleer voor het ontvangen van CAL de issuer en valutacode voordat je de trustline in Xaman goedkeurt."},"pages":[4205]},{"source":"Open CAL trustline guide","translations":{"zh-Hans":"打开 CAL 信任线指南","hi":"CAL ट्रस्टलाइन मार्गदर्शिका खोलें","es":"Abrir guía de línea de confianza CAL","ar":"فتح دليل خط الثقة CAL","fr":"Ouvrir le guide de ligne de confiance CAL","bn":"CAL ট্রাস্টলাইন নির্দেশিকা খুলুন","pt":"Abrir guia da linha de confiança CAL","id":"Buka panduan trustline CAL","ur":"CAL ٹرسٹ لائن کی رہنمائی کھولیں","nl":"Open de CAL-trustlinegids"},"pages":[4205]},{"source":"Choose buy or sell","translations":{"zh-Hans":"选择买入或卖出","hi":"खरीदना या बेचना चुनें","es":"Elige comprar o vender","ar":"اختيار الشراء أو البيع","fr":"Choisir l’achat ou la vente","bn":"কেনা বা বিক্রি বেছে নিন","pt":"Escolha comprar ou vender","id":"Pilih beli atau jual","ur":"خریدنا یا بیچنا منتخب کریں","nl":"Kies kopen of verkopen"},"pages":[4205]},{"source":"On XPMarket, check what you pay and receive, the issuers, route, fees, liquidity and price impact. Review the final request in your wallet.","translations":{"zh-Hans":"在 XPMarket 核对支付和接收的资产、发行方、路径、费用、流动性和价格影响。在钱包中审核最终请求。","hi":"XPMarket पर भुगतान और प्राप्ति की संपत्ति, जारीकर्ता, रूट, शुल्क, लिक्विडिटी और मूल्य प्रभाव जाँचें। अंतिम अनुरोध अपने वॉलेट में जाँचें।","es":"En XPMarket, comprueba lo que pagas y recibes, los emisores, la ruta, las comisiones, la liquidez y el impacto en el precio. Revisa la solicitud final en tu billetera.","ar":"في XPMarket، تحقّق مما تدفعه وتستلمه والمُصدرين والمسار والرسوم والسيولة وتأثير السعر. راجع الطلب النهائي في محفظتك.","fr":"Sur XPMarket, vérifiez ce que vous payez et recevez, les émetteurs, le parcours, les frais, la liquidité et l’impact sur le prix. Vérifiez la demande finale dans votre portefeuille.","bn":"XPMarket-এ কী দিচ্ছেন ও পাচ্ছেন, ইস্যুকারী, রুট, ফি, লিকুইডিটি এবং দামের প্রভাব যাচাই করুন। চূড়ান্ত অনুরোধ ওয়ালেটে যাচাই করুন।","pt":"Na XPMarket, confira o que paga e recebe, os emissores, a rota, as taxas, a liquidez e o impacto no preço. Reveja o pedido final na carteira.","id":"Di XPMarket, periksa apa yang dibayar dan diterima, penerbit, rute, biaya, likuiditas, dan dampak harga. Tinjau permintaan akhir di dompet Anda.","ur":"XPMarket پر ادائیگی اور وصولی کے اثاثے، جاری کنندگان، راستہ، فیس، لیکویڈیٹی اور قیمت پر اثر جانچیں۔ حتمی درخواست والیٹ میں دیکھیں۔","nl":"Controleer op XPMarket wat je betaalt en ontvangt, de issuers, route, kosten, liquiditeit en prijsimpact. Bekijk het definitieve verzoek in je wallet."},"pages":[4205]},{"source":"Open CAL on XPMarket","translations":{"zh-Hans":"在 XPMarket 打开 CAL","hi":"XPMarket पर CAL खोलें","es":"Abrir CAL en XPMarket","ar":"فتح CAL على XPMarket","fr":"Ouvrir CAL sur XPMarket","bn":"XPMarket-এ CAL খুলুন","pt":"Abrir CAL na XPMarket","id":"Buka CAL di XPMarket","ur":"XPMarket پر CAL کھولیں","nl":"Open CAL op XPMarket"},"pages":[4205]},{"source":"To sell, select CAL as the asset you pay and check the asset you receive. A received token may need its own trustline. Converting proceeds to money in a bank account is a separate provider service.","translations":{"zh-Hans":"卖出时,选择 CAL 作为支付资产,并核对接收的资产。接收的代币可能需要单独的信任线。将所得兑换为银行账户中的资金是服务商提供的另一项服务。","hi":"बेचने के लिए भुगतान वाली संपत्ति के रूप में CAL चुनें और मिलने वाली संपत्ति जाँचें। प्राप्त होने वाले टोकन के लिए अलग ट्रस्टलाइन की आवश्यकता हो सकती है। प्राप्त राशि को बैंक खाते में पैसे के रूप में बदलना प्रदाता की अलग सेवा है।","es":"Para vender, selecciona CAL como el activo que pagas y comprueba el activo que recibes. El token recibido puede necesitar su propia línea de confianza. Convertir lo obtenido en dinero en una cuenta bancaria es un servicio independiente del proveedor.","ar":"للبيع، اختر CAL بوصفه الأصل الذي تدفعه وتحقّق من الأصل الذي تستلمه. قد يحتاج الرمز المستلم إلى خط ثقة خاص به. تحويل العائد إلى أموال في حساب مصرفي خدمة منفصلة يقدمها المزود.","fr":"Pour vendre, choisissez CAL comme actif à payer et vérifiez l’actif que vous recevez. Le jeton reçu peut nécessiter sa propre ligne de confiance. Convertir le produit de la vente en argent sur un compte bancaire est un service distinct du prestataire.","bn":"বিক্রি করতে যে সম্পদ দিয়ে পরিশোধ করবেন সেটি হিসেবে CAL বেছে নিন এবং যে সম্পদ পাবেন সেটি যাচাই করুন। প্রাপ্ত টোকেনের নিজস্ব ট্রাস্টলাইন প্রয়োজন হতে পারে। প্রাপ্ত সম্পদ ব্যাংক হিসাবে অর্থে রূপান্তর করা সেবাদাতার আলাদা সেবা।","pt":"Para vender, selecione CAL como o ativo que paga e verifique o ativo que recebe. O token recebido pode precisar de uma linha de confiança própria. Converter o resultado da venda em dinheiro numa conta bancária é um serviço separado do prestador.","id":"Untuk menjual, pilih CAL sebagai aset yang Anda bayarkan dan periksa aset yang diterima. Token yang diterima mungkin memerlukan trustline sendiri. Mengubah hasil penjualan menjadi uang di rekening bank adalah layanan penyedia yang terpisah.","ur":"بیچنے کے لیے ادائیگی کے اثاثے کے طور پر CAL منتخب کریں اور وصول ہونے والا اثاثہ جانچیں۔ وصول ہونے والے ٹوکن کو اپنی ٹرسٹ لائن درکار ہو سکتی ہے۔ حاصل رقم کو بینک اکاؤنٹ میں پیسوں میں تبدیل کرنا فراہم کنندہ کی الگ سروس ہے۔","nl":"Kies bij verkopen CAL als de token waarmee je betaalt en controleer wat je ontvangt. Voor een ontvangen token kan een eigen trustline nodig zijn. De opbrengst omzetten naar geld op een bankrekening is een afzonderlijke dienst van een aanbieder."},"pages":[4205]},{"source":"Trading through external services","translations":{"zh-Hans":"通过外部服务交易","hi":"बाहरी सेवाओं के माध्यम से कारोबार","es":"Operar mediante servicios externos","ar":"التداول عبر خدمات خارجية","fr":"Négocier via des services externes","bn":"বাইরের সেবার মাধ্যমে লেনদেন","pt":"Negociação através de serviços externos","id":"Perdagangan melalui layanan eksternal","ur":"بیرونی خدمات کے ذریعے تجارت","nl":"Handelen via externe diensten"},"pages":[4205]},{"source":"This website provides guidance and external links. Trading takes place with the selected external service and your wallet.","translations":{"zh-Hans":"本网站提供指南和外部链接。交易通过选定的外部服务及您的钱包进行。","hi":"यह वेबसाइट मार्गदर्शन और बाहरी लिंक देती है। कारोबार चुनी गई बाहरी सेवा और आपके वॉलेट के माध्यम से होता है।","es":"Esta web ofrece orientación y enlaces externos. Las operaciones se realizan con el servicio externo elegido y tu billetera.","ar":"يوفّر هذا الموقع إرشادات وروابط خارجية. يتم التداول عبر الخدمة الخارجية المختارة ومحفظتك.","fr":"Ce site fournit des conseils pratiques et des liens externes. Les opérations passent par le service externe choisi et votre portefeuille.","bn":"এই ওয়েবসাইট নির্দেশনা ও বাইরের লিংক দেয়। নির্বাচিত বাইরের সেবা এবং আপনার ওয়ালেটের মাধ্যমে লেনদেন হয়।","pt":"Este site fornece orientação e links externos. A negociação ocorre através do serviço externo escolhido e da sua carteira.","id":"Situs ini menyediakan panduan dan tautan eksternal. Perdagangan berlangsung melalui layanan eksternal pilihan Anda dan dompet Anda.","ur":"یہ ویب سائٹ رہنمائی اور بیرونی روابط دیتی ہے۔ تجارت منتخب بیرونی سروس اور آپ کے والیٹ کے ذریعے ہوتی ہے۔","nl":"Deze website biedt uitleg en externe links. Handelen gebeurt via de gekozen externe dienst en je wallet."},"pages":[4205]},{"source":"Before you trade","translations":{"zh-Hans":"交易前须知","hi":"कारोबार से पहले","es":"Antes de operar","ar":"قبل التداول","fr":"Avant de négocier","bn":"লেনদেনের আগে","pt":"Antes de negociar","id":"Sebelum berdagang","ur":"تجارت سے پہلے","nl":"Voordat je handelt"},"pages":[4205]},{"source":"Trading can cause loss. Project-related wallets may provide liquidity and have a financial interest. Quotes, liquidity and execution are not guaranteed. This guide is not investment advice.","translations":{"zh-Hans":"交易可能造成损失。项目关联钱包可能提供流动性并具有经济利益。报价、流动性和成交均无保证。本指南不构成投资建议。","hi":"कारोबार में नुकसान हो सकता है। परियोजना से जुड़े वॉलेट लिक्विडिटी दे सकते हैं और उनका वित्तीय हित हो सकता है। कोट, लिक्विडिटी और निष्पादन की गारंटी नहीं है। यह निवेश सलाह नहीं है।","es":"Operar puede causar pérdidas. Las billeteras vinculadas al proyecto pueden aportar liquidez y tener un interés financiero. Las cotizaciones, la liquidez y la ejecución no están garantizadas. Esta guía no es asesoramiento de inversión.","ar":"قد يؤدي التداول إلى خسارة. قد توفّر محافظ مرتبطة بالمشروع السيولة وتكون لها مصلحة مالية. الأسعار والسيولة والتنفيذ غير مضمونة. هذا الدليل ليس نصيحة استثمارية.","fr":"Le trading peut entraîner des pertes. Des portefeuilles liés au projet peuvent fournir de la liquidité et avoir un intérêt financier. Les cotations, la liquidité et l’exécution ne sont pas garanties. Ce guide n’est pas un conseil en investissement.","bn":"লেনদেনে ক্ষতি হতে পারে। প্রকল্প-সংশ্লিষ্ট ওয়ালেট লিকুইডিটি দিতে পারে এবং তাদের আর্থিক স্বার্থ থাকতে পারে। দর, লিকুইডিটি ও লেনদেন সম্পাদনের নিশ্চয়তা নেই। এটি বিনিয়োগের পরামর্শ নয়।","pt":"A negociação pode causar perdas. Carteiras ligadas ao projeto podem fornecer liquidez e ter um interesse financeiro. Cotações, liquidez e execução não são garantidas. Este guia não é aconselhamento de investimento.","id":"Perdagangan dapat menyebabkan kerugian. Dompet terkait proyek mungkin menyediakan likuiditas dan memiliki kepentingan finansial. Harga, likuiditas, dan eksekusi tidak dijamin. Panduan ini bukan saran investasi.","ur":"تجارت میں نقصان ہو سکتا ہے۔ منصوبے سے متعلق والیٹ لیکویڈیٹی فراہم کر سکتے ہیں اور ان کا مالی مفاد ہو سکتا ہے۔ قیمت، لیکویڈیٹی اور عمل درآمد کی ضمانت نہیں۔ یہ سرمایہ کاری کا مشورہ نہیں ہے۔","nl":"Handelen kan verlies opleveren. Projectgerelateerde wallets kunnen liquiditeit leveren en een financieel belang hebben. Koersen, liquiditeit en uitvoering zijn niet gegarandeerd. Deze gids is geen beleggingsadvies."},"pages":[4205]},{"source":"CalorieApp food search and food logging do not require buying CAL.","translations":{"zh-Hans":"使用 CalorieApp 搜索食物和记录饮食无需购买 CAL。","hi":"CalorieApp में भोजन खोजने और दर्ज करने के लिए CAL खरीदना आवश्यक नहीं है।","es":"Buscar alimentos y registrarlos en CalorieApp no requiere comprar CAL.","ar":"لا يتطلب البحث عن الطعام وتسجيله في CalorieApp شراء CAL.","fr":"La recherche et le journal alimentaires de CalorieApp ne nécessitent pas l’achat de CAL.","bn":"CalorieApp-এ খাবার খুঁজতে বা লিখে রাখতে CAL কেনার প্রয়োজন নেই।","pt":"A pesquisa e o registo de alimentos na CalorieApp não exigem a compra de CAL.","id":"Pencarian dan pencatatan makanan di CalorieApp tidak memerlukan pembelian CAL.","ur":"CalorieApp میں خوراک تلاش کرنے اور درج کرنے کے لیے CAL خریدنا ضروری نہیں۔","nl":"Voor voedsel zoeken en je voedingslogboek in CalorieApp hoef je geen CAL te kopen."},"pages":[4205]},{"source":"Other trading interfaces","translations":{"zh-Hans":"其他交易界面","hi":"अन्य ट्रेडिंग इंटरफ़ेस","es":"Otras interfaces de negociación","ar":"واجهات تداول أخرى","fr":"Autres interfaces de négociation","bn":"অন্যান্য ট্রেডিং ইন্টারফেস","pt":"Outras interfaces de negociação","id":"Antarmuka perdagangan lainnya","ur":"دیگر تجارتی انٹرفیس","nl":"Andere handelsinterfaces"},"pages":[4205]},{"source":"Use the interface you already know and verify the same issuer and currency code.","translations":{"zh-Hans":"使用您熟悉的交易界面,并核对同一发行方地址和货币代码。","hi":"जिस इंटरफ़ेस से आप परिचित हैं उसका उपयोग करें और वही जारीकर्ता पता तथा मुद्रा कोड सत्यापित करें।","es":"Usa la interfaz que ya conoces y verifica la misma dirección del emisor y el código de moneda.","ar":"استخدم الواجهة التي تعرفها وتحقّق من عنوان المُصدر ورمز العملة نفسيهما.","fr":"Utilisez l’interface que vous connaissez et vérifiez la même adresse d’émetteur et le même code de devise.","bn":"আপনার পরিচিত ইন্টারফেস ব্যবহার করুন এবং একই ইস্যুকারীর ঠিকানা ও মুদ্রার কোড যাচাই করুন।","pt":"Use a interface que já conhece e verifique o mesmo endereço do emissor e o mesmo código da moeda.","id":"Gunakan antarmuka yang sudah Anda kenal dan verifikasi alamat penerbit serta kode mata uang yang sama.","ur":"وہ انٹرفیس استعمال کریں جس سے آپ واقف ہیں اور اسی جاری کنندہ کے پتے اور کرنسی کوڈ کی تصدیق کریں۔","nl":"Gebruik de interface die je al kent en controleer hetzelfde issueradres en dezelfde valutacode."},"pages":[4205]},{"source":"Posts are supplied by X and may not load here. Open the profile directly or review your cookie choices.","translations":{"zh-Hans":"帖子由 X 提供,可能无法在此加载。您可以直接打开主页,或查看您的 Cookie 选项。","hi":"पोस्ट X से मिलती हैं और हो सकता है कि यहाँ लोड न हों। प्रोफ़ाइल सीधे खोलें या अपनी कुकी संबंधी पसंद की समीक्षा करें।","es":"X proporciona las publicaciones y es posible que no se carguen aquí. Abre el perfil directamente o revisa tus preferencias de cookies.","ar":"يوفّر X المنشورات، وقد لا يتم تحميلها هنا. افتح الملف الشخصي مباشرة أو راجع خيارات ملفات تعريف الارتباط.","fr":"Les publications sont fournies par X et peuvent ne pas se charger ici. Ouvrez directement le profil ou consultez vos choix de cookies.","bn":"পোস্টগুলো X থেকে আসে এবং এখানে লোড নাও হতে পারে। সরাসরি প্রোফাইল খুলুন অথবা আপনার কুকি পছন্দগুলো দেখুন।","pt":"As publicações são fornecidas pelo X e podem não carregar aqui. Abra o perfil diretamente ou reveja as suas preferências de cookies.","id":"Postingan disediakan oleh X dan mungkin tidak dimuat di sini. Buka profil secara langsung atau tinjau pilihan cookie Anda.","ur":"پوسٹس X فراہم کرتا ہے اور ممکن ہے یہاں لوڈ نہ ہوں۔ براہ راست پروفائل کھولیں یا کوکیز کے بارے میں اپنے انتخاب کا جائزہ لیں۔","nl":"Berichten worden door X geleverd en laden hier mogelijk niet. Open het profiel rechtstreeks of bekijk je cookievoorkeuren."},"pages":[1207]},{"source":"Open @CalorieToken on X","translations":{"zh-Hans":"在 X 上打开 @CalorieToken","hi":"X पर @CalorieToken खोलें","es":"Abrir @CalorieToken en X","ar":"افتح ⁦@CalorieToken⁩ على X","fr":"Ouvrir @CalorieToken sur X","bn":"X-এ @CalorieToken খুলুন","pt":"Abrir @CalorieToken no X","id":"Buka @CalorieToken di X","ur":"X پر ⁦@CalorieToken⁩ کھولیں","nl":"Open @CalorieToken op X"},"pages":[1207]},{"source":"Cookie settings","translations":{"zh-Hans":"Cookie 设置","hi":"कुकी सेटिंग","es":"Ajustes de cookies","ar":"إعدادات ملفات الارتباط","fr":"Réglages des cookies","bn":"কুকি সেটিংস","pt":"Definições de cookies","id":"Pengaturan kuki","ur":"کوکی ترتیبات","nl":"Cookie-instellingen"},"pages":[1205,1207,4205,7880]},{"source":"CAL, crypto and your wallet: choose an option below.","translations":{"nl":"CAL, crypto en je wallet: kies hieronder een mogelijkheid.","zh-Hans":"CAL、加密资产与钱包:请选择以下方式。","hi":"CAL, क्रिप्टो और आपका वॉलेट: नीचे एक विकल्प चुनें।","es":"CAL, cripto y tu cartera: elige una opción.","ar":"CAL والعملات المشفرة ومحفظتك: اختر أحد الخيارات أدناه.","fr":"CAL, crypto et portefeuille : choisissez une option.","bn":"CAL, ক্রিপ্টো ও আপনার ওয়ালেট: নিচে একটি উপায় বেছে নিন।","pt":"CAL, cripto e a sua carteira: escolha uma opção.","id":"CAL, kripto, dan dompet Anda: pilih opsi di bawah.","ur":"CAL، کرپٹو اور آپ کا والیٹ: نیچے ایک طریقہ منتخب کریں۔"},"pages":[1205,4205,7880]},{"source":"1 · XRPL DEX","translations":{"nl":"1 · XRPL DEX","zh-Hans":"1 · XRPL DEX","hi":"1 · XRPL DEX","es":"1 · XRPL DEX","ar":"1 · XRPL DEX","fr":"1 · XRPL DEX","bn":"1 · XRPL DEX","pt":"1 · XRPL DEX","id":"1 · XRPL DEX","ur":"1 · XRPL DEX"},"pages":[1205,4205,7880]},{"source":"Own trading interface not enabled","translations":{"nl":"Eigen handelsinterface nog niet actief","zh-Hans":"自有交易界面尚未启用","hi":"अपना ट्रेडिंग इंटरफ़ेस अभी सक्रिय नहीं","es":"Interfaz de negociación propia aún inactiva","ar":"واجهة التداول الخاصة غير مفعلة بعد","fr":"Interface de trading propre pas encore activée","bn":"নিজস্ব ট্রেডিং ইন্টারফেস এখনও সক্রিয় নয়","pt":"Interface de negociação própria ainda inativa","id":"Antarmuka perdagangan sendiri belum aktif","ur":"اپنا ٹریڈنگ انٹرفیس ابھی فعال نہیں"},"pages":[1205,4205,7880]},{"source":"Open the CAL/XRP order book or swap on XPMarket. Check the asset, issuer, fees and final wallet request. Trading takes place with the external service; this website does not place orders.","translations":{"nl":"Open het CAL/XRP-orderboek of de swap op XPMarket. Controleer de munt, uitgever, kosten en het uiteindelijke verzoek in je wallet. Je handelt bij de externe dienst; deze website plaatst geen orders.","zh-Hans":"在 XPMarket 打开 CAL/XRP 订单簿或兑换。核对资产、发行方、费用和最终钱包请求。交易通过外部服务进行;本网站不会下单。","hi":"XPMarket पर CAL/XRP ऑर्डर बुक या स्वैप खोलें। एसेट, जारीकर्ता, शुल्क और वॉलेट में अंतिम अनुरोध जाँचें। ट्रेड बाहरी सेवा पर होता है; यह वेबसाइट ऑर्डर नहीं देती।","es":"Abre el libro de órdenes o el intercambio CAL/XRP en XPMarket. Revisa el activo, emisor, comisiones y la solicitud final en tu cartera. Operas con el servicio externo; esta web no envía órdenes.","ar":"افتح دفتر أوامر CAL/XRP أو المبادلة على XPMarket. تحقق من الأصل والجهة المصدرة والرسوم والطلب النهائي في محفظتك. يتم التداول عبر الخدمة الخارجية؛ لا يرسل هذا الموقع أوامر تداول.","fr":"Ouvrez le carnet d’ordres ou l’échange CAL/XRP sur XPMarket. Vérifiez l’actif, l’émetteur, les frais et la demande finale dans votre portefeuille. La transaction passe par le service externe ; ce site ne transmet pas d’ordres.","bn":"XPMarket-এ CAL/XRP অর্ডার বুক বা সোয়াপ খুলুন। সম্পদ, ইস্যুকারী, ফি ও ওয়ালেটের চূড়ান্ত অনুরোধ যাচাই করুন। লেনদেন বাইরের পরিষেবায় হয়; এই ওয়েবসাইট অর্ডার দেয় না।","pt":"Abra o livro de ordens ou a troca CAL/XRP no XPMarket. Verifique o ativo, emissor, taxas e o pedido final na carteira. A negociação ocorre no serviço externo; este site não envia ordens.","id":"Buka buku pesanan atau swap CAL/XRP di XPMarket. Periksa aset, penerbit, biaya, dan permintaan akhir di dompet. Perdagangan berlangsung di layanan eksternal; situs ini tidak mengirim pesanan.","ur":"XPMarket پر CAL/XRP آرڈر بک یا سواپ کھولیں۔ اثاثہ، جاری کنندہ، فیس اور والٹ میں آخری درخواست چیک کریں۔ تجارت بیرونی سروس پر ہوتی ہے؛ یہ ویب سائٹ آرڈر نہیں بھیجتی۔"},"pages":[1205,4205,7880]},{"source":"3 · AllChainBridge / SWFT","translations":{"nl":"3 · AllChainBridge / SWFT","zh-Hans":"3 · AllChainBridge / SWFT","hi":"3 · AllChainBridge / SWFT","es":"3 · AllChainBridge / SWFT","ar":"3 · AllChainBridge / SWFT","fr":"3 · AllChainBridge / SWFT","bn":"3 · AllChainBridge / SWFT","pt":"3 · AllChainBridge / SWFT","id":"3 · AllChainBridge / SWFT","ur":"3 · AllChainBridge / SWFT"},"pages":[1205,4205,7880]},{"source":"CAL is temporarily delisted at SWFT","translations":{"nl":"CAL is tijdelijk gedelist bij SWFT","zh-Hans":"CAL 暂时从 SWFT 下架","hi":"SWFT पर CAL अस्थायी रूप से डीलिस्ट है","es":"CAL está temporalmente retirado de SWFT","ar":"CAL مشطوب مؤقتًا من SWFT","fr":"CAL est temporairement retiré de SWFT","bn":"SWFT থেকে CAL সাময়িকভাবে তালিকাচ্যুত","pt":"CAL temporariamente removido da SWFT","id":"CAL sementara dihapus dari daftar SWFT","ur":"SWFT سے CAL عارضی طور پر ڈی لسٹ ہے"},"pages":[1205,4205,7880]},{"source":"SWFT still supports exchanging other supported coins. CAL is temporarily unavailable here. Availability, terms and fees depend on the provider and your region.","translations":{"nl":"Met SWFT kun je nog steeds andere ondersteunde munten wisselen. CAL is hier tijdelijk niet beschikbaar. Beschikbaarheid, voorwaarden en kosten hangen af van de aanbieder en je regio.","zh-Hans":"SWFT 仍支持兑换其他受支持的币种。CAL 暂不可用。可用性、条款及费用取决于提供方和您所在地区。","hi":"SWFT से अन्य समर्थित सिक्के अब भी बदले जा सकते हैं। CAL यहाँ अस्थायी रूप से अनुपलब्ध है। उपलब्धता, शर्तें और शुल्क प्रदाता और आपके क्षेत्र पर निर्भर हैं।","es":"SWFT sigue permitiendo cambiar otras monedas compatibles. CAL no está disponible aquí temporalmente. Disponibilidad, condiciones y comisiones dependen del proveedor y tu región.","ar":"ما زال SWFT يتيح تبديل العملات الأخرى المدعومة. CAL غير متاح هنا مؤقتًا. يعتمد التوفر والشروط والرسوم على المزود ومنطقتك.","fr":"SWFT permet toujours d’échanger d’autres monnaies prises en charge. CAL est temporairement indisponible ici. Disponibilité, conditions et frais dépendent du fournisseur et de votre région.","bn":"SWFT দিয়ে অন্য সমর্থিত কয়েন এখনও বদলানো যায়। CAL এখানে সাময়িকভাবে অনুপলব্ধ। প্রাপ্যতা, শর্ত ও ফি প্রদানকারী এবং আপনার অঞ্চলের ওপর নির্ভর করে।","pt":"A SWFT continua permitindo trocar outras moedas compatíveis. CAL está temporariamente indisponível aqui. Disponibilidade, condições e taxas dependem do provedor e da sua região.","id":"SWFT masih mendukung pertukaran koin lain yang didukung. CAL sementara tidak tersedia di sini. Ketersediaan, ketentuan dan biaya bergantung pada penyedia dan wilayah Anda.","ur":"SWFT میں دوسرے معاون سکے اب بھی بدلے جا سکتے ہیں۔ CAL یہاں عارضی طور پر دستیاب نہیں۔ دستیابی، شرائط اور فیس فراہم کنندہ اور آپ کے علاقے پر منحصر ہیں۔"},"pages":[1205,4205,7880]},{"source":"Loading the exchange connects your browser to SWFT, which receives connection information and may use cookies. Your CalorieApp login is not passed to the frame.","translations":{"nl":"Bij het laden maakt je browser verbinding met SWFT. SWFT ontvangt verbindingsgegevens en kan cookies gebruiken. Je CalorieApp-login wordt niet aan het frame doorgegeven.","zh-Hans":"加载后,浏览器会连接 SWFT。SWFT 会接收连接信息,并可能使用 Cookie。您的 CalorieApp 登录信息不会传递给此框架。","hi":"एक्सचेंज लोड करने पर ब्राउज़र SWFT से जुड़ता है। उसे कनेक्शन की जानकारी मिलती है और वह कुकीज़ इस्तेमाल कर सकता है। आपका CalorieApp लॉगिन फ़्रेम को नहीं दिया जाता।","es":"Al cargarlo, tu navegador se conecta a SWFT, que recibe datos de conexión y puede usar cookies. Tu sesión de CalorieApp no se transmite al marco.","ar":"يصل متصفحك إلى SWFT عند التحميل، ويتلقى المزود معلومات الاتصال وقد يستخدم ملفات تعريف الارتباط. لا تُرسل جلسة CalorieApp إلى الإطار.","fr":"Le chargement connecte votre navigateur à SWFT, qui reçoit des données de connexion et peut utiliser des cookies. Votre session CalorieApp n’est pas transmise au cadre.","bn":"লোড করলে ব্রাউজার SWFT-এর সঙ্গে যুক্ত হয়। SWFT সংযোগের তথ্য পায় এবং কুকি ব্যবহার করতে পারে। আপনার CalorieApp লগইন ফ্রেমে পাঠানো হয় না।","pt":"Ao carregar, o navegador liga-se à SWFT, que recebe dados de ligação e pode usar cookies. A sua sessão CalorieApp não é enviada ao quadro.","id":"Memuat bursa menghubungkan peramban ke SWFT, yang menerima informasi koneksi dan mungkin menggunakan kuki. Login CalorieApp Anda tidak diteruskan ke bingkai.","ur":"لوڈ کرنے سے براؤزر SWFT سے جڑتا ہے، جو کنکشن کی معلومات وصول کرتا ہے اور کوکیز استعمال کر سکتا ہے۔ آپ کا CalorieApp لاگ اِن فریم کو نہیں بھیجا جاتا۔"},"pages":[1205,4205,7880]},{"source":"Load external exchange","translations":{"nl":"Externe exchange laden","zh-Hans":"加载外部兑换界面","hi":"बाहरी एक्सचेंज लोड करें","es":"Cargar exchange externo","ar":"تحميل منصة التبادل الخارجية","fr":"Charger l’exchange externe","bn":"বাইরের এক্সচেঞ্জ লোড করুন","pt":"Carregar exchange externa","id":"Muat bursa eksternal","ur":"بیرونی ایکسچینج لوڈ کریں"},"pages":[1205,4205,7880]},{"source":"Close exchange","translations":{"nl":"Exchange sluiten","zh-Hans":"关闭兑换界面","hi":"एक्सचेंज बंद करें","es":"Cerrar exchange","ar":"إغلاق المنصة","fr":"Fermer l’exchange","bn":"এক্সচেঞ্জ বন্ধ করুন","pt":"Fechar exchange","id":"Tutup bursa","ur":"ایکسچینج بند کریں"},"pages":[1205,4205,7880]},{"source":"External SWFT content needs permission in the site’s cookie settings. You can also visit the provider directly.","translations":{"nl":"Voor externe SWFT-inhoud is toestemming nodig in de cookie-instellingen van de site. Je kunt ook rechtstreeks naar de aanbieder gaan.","zh-Hans":"外部 SWFT 内容需要在本网站的 Cookie 设置中获得许可。您也可以直接访问提供方。","hi":"बाहरी SWFT सामग्री के लिए साइट की कुकी सेटिंग में अनुमति चाहिए। आप प्रदाता की साइट सीधे भी खोल सकते हैं।","es":"El contenido externo de SWFT requiere permiso en los ajustes de cookies del sitio. También puedes visitar al proveedor directamente.","ar":"يحتاج محتوى SWFT الخارجي إلى إذن في إعدادات ملفات الارتباط بالموقع. يمكنك أيضًا زيارة المزود مباشرة.","fr":"Le contenu externe SWFT nécessite une autorisation dans les réglages des cookies du site. Vous pouvez aussi visiter directement le prestataire.","bn":"বাইরের SWFT কনটেন্টের জন্য সাইটের কুকি সেটিংসে অনুমতি দরকার। প্রদানকারীর সাইট সরাসরিও খুলতে পারেন।","pt":"O conteúdo externo da SWFT precisa de autorização nas definições de cookies do site. Também pode visitar diretamente o fornecedor.","id":"Konten SWFT eksternal memerlukan izin dalam pengaturan kuki situs. Anda juga dapat membuka situs penyedia langsung.","ur":"بیرونی SWFT مواد کے لیے سائٹ کی کوکی ترتیبات میں اجازت درکار ہے۔ آپ فراہم کنندہ کی سائٹ براہِ راست بھی کھول سکتے ہیں۔"},"pages":[1205,4205,7880]},{"source":"Open at SWFT","translations":{"nl":"Openen bij SWFT","zh-Hans":"在 SWFT 打开","hi":"SWFT पर खोलें","es":"Abrir en SWFT","ar":"فتح لدى SWFT","fr":"Ouvrir chez SWFT","bn":"SWFT-এ খুলুন","pt":"Abrir na SWFT","id":"Buka di SWFT","ur":"SWFT پر کھولیں"},"pages":[1205,4205,7880]},{"source":"If the frame is blank or your wallet does not connect, open SWFT directly. Available pairs and execution are controlled by the provider.","translations":{"nl":"Is het frame leeg of verbindt je wallet niet? Open SWFT rechtstreeks. De aanbieder bepaalt de beschikbare paren en uitvoering.","zh-Hans":"如果框架空白或钱包无法连接,请直接打开 SWFT。可用交易对和执行由提供方控制。","hi":"फ़्रेम खाली हो या वॉलेट न जुड़े तो SWFT सीधे खोलें। उपलब्ध जोड़े और निष्पादन प्रदाता के नियंत्रण में हैं।","es":"Si el marco está vacío o la cartera no conecta, abre SWFT directamente. El proveedor controla los pares disponibles y la ejecución.","ar":"إذا كان الإطار فارغًا أو لم تتصل المحفظة، افتح SWFT مباشرة. يتحكم المزود في الأزواج المتاحة والتنفيذ.","fr":"Si le cadre reste vide ou le portefeuille ne se connecte pas, ouvrez SWFT directement. Le prestataire contrôle les paires disponibles et l’exécution.","bn":"ফ্রেম ফাঁকা থাকলে বা ওয়ালেট যুক্ত না হলে SWFT সরাসরি খুলুন। উপলব্ধ জোড়া ও লেনদেন সম্পাদন প্রদানকারীর নিয়ন্ত্রণে।","pt":"Se o quadro ficar vazio ou a carteira não ligar, abra a SWFT diretamente. O fornecedor controla os pares disponíveis e a execução.","id":"Jika bingkai kosong atau dompet tidak terhubung, buka SWFT langsung. Pasangan yang tersedia dan pelaksanaan transaksi dikendalikan penyedia.","ur":"فریم خالی ہو یا والیٹ نہ جڑے تو SWFT براہِ راست کھولیں۔ دستیاب جوڑے اور لین دین کا اجرا فراہم کنندہ کے اختیار میں ہیں۔"},"pages":[1205,4205,7880]},{"source":"2 · Ways to buy and sell CAL","translations":{"nl":"2 · CAL kopen en verkopen","zh-Hans":"2 · 买入和卖出 CAL 的方式","hi":"2 · CAL खरीदने और बेचने के तरीके","es":"2 · Formas de comprar y vender CAL","ar":"2 · طرق شراء CAL وبيعه","fr":"2 · Acheter et vendre CAL","bn":"2 · CAL কেনাবেচার উপায়","pt":"2 · Formas de comprar e vender CAL","id":"2 · Cara membeli dan menjual CAL","ur":"2 · CAL خریدنے اور بیچنے کے طریقے"},"pages":[1205,4205,7880]},{"source":"Set a CAL trustline","translations":{"nl":"CAL-trustline instellen","zh-Hans":"设置 CAL 信任线","hi":"CAL ट्रस्टलाइन सेट करें","es":"Configurar una línea de confianza CAL","ar":"إعداد خط ثقة CAL","fr":"Configurer une ligne de confiance CAL","bn":"CAL ট্রাস্টলাইন তৈরি করুন","pt":"Configurar uma linha de confiança CAL","id":"Atur trustline CAL","ur":"CAL ٹرسٹ لائن بنائیں"},"pages":[1205,4205,6855,7880]},{"source":"Mainnet CAL · Check the issuer, currency, limit and network in Xaman before signing. A trustline does not buy CAL.","translations":{"nl":"Mainnet CAL · Controleer uitgever, valutacode, limiet en netwerk in Xaman voordat je tekent. Een trustline koopt geen CAL.","zh-Hans":"Mainnet CAL · 签名前请在 Xaman 中核对发行方、货币代码、限额和网络。设置信任线不会购买 CAL。","hi":"Mainnet CAL · हस्ताक्षर से पहले Xaman में जारीकर्ता, मुद्रा कोड, सीमा और नेटवर्क जाँचें। ट्रस्टलाइन से CAL नहीं खरीदा जाता।","es":"Mainnet CAL · Antes de firmar, comprueba emisor, código de moneda, límite y red en Xaman. Una línea de confianza no compra CAL.","ar":"Mainnet CAL · تحقق من المُصدر ورمز العملة والحد والشبكة في Xaman قبل التوقيع. إنشاء خط ثقة لا يشتري CAL.","fr":"Mainnet CAL · Vérifiez l’émetteur, le code de devise, la limite et le réseau dans Xaman avant de signer. Une ligne de confiance n’achète pas de CAL.","bn":"Mainnet CAL · সই করার আগে Xaman-এ ইস্যুকারী, মুদ্রা কোড, সীমা ও নেটওয়ার্ক যাচাই করুন। ট্রাস্টলাইন CAL কেনে না।","pt":"Mainnet CAL · Verifique emissor, código da moeda, limite e rede no Xaman antes de assinar. Uma linha de confiança não compra CAL.","id":"Mainnet CAL · Periksa penerbit, kode mata uang, batas, dan jaringan di Xaman sebelum menandatangani. Trustline tidak membeli CAL.","ur":"Mainnet CAL · دستخط سے پہلے Xaman میں جاری کنندہ، کرنسی کوڈ، حد اور نیٹ ورک دیکھیں۔ ٹرسٹ لائن CAL نہیں خریدتی۔"},"pages":[1205,4205,6855,7880]},{"source":"Set trustline in Xaman","translations":{"nl":"Trustline instellen in Xaman","zh-Hans":"在 Xaman 中设置信任线","hi":"Xaman में ट्रस्टलाइन सेट करें","es":"Configurar línea de confianza en Xaman","ar":"إعداد خط الثقة في Xaman","fr":"Configurer la ligne dans Xaman","bn":"Xaman-এ ট্রাস্টলাইন তৈরি করুন","pt":"Configurar a linha no Xaman","id":"Atur trustline di Xaman","ur":"Xaman میں ٹرسٹ لائن بنائیں"},"pages":[1205,4205,7880]},{"source":"Open trustline guide","translations":{"nl":"Trustline-uitleg openen","zh-Hans":"打开信任线指南","hi":"ट्रस्टलाइन गाइड खोलें","es":"Abrir guía de confianza","ar":"فتح دليل خط الثقة","fr":"Ouvrir le guide de confiance","bn":"ট্রাস্টলাইন নির্দেশিকা খুলুন","pt":"Abrir guia de confiança","id":"Buka panduan trustline","ur":"ٹرسٹ لائن رہنما کھولیں"},"pages":[1205,4205,7880]},{"source":"The direct button is available when the installed Xaman service is configured for this exact CAL asset.","translations":{"nl":"De rechtstreekse knop verschijnt wanneer de geïnstalleerde Xaman-dienst voor precies deze CAL-munt is ingesteld.","zh-Hans":"安装的 Xaman 服务配置为此确切 CAL 资产后,将显示直接操作按钮。","hi":"सीधा बटन तभी दिखता है जब स्थापित Xaman सेवा इसी सटीक CAL परिसंपत्ति के लिए कॉन्फ़िगर हो।","es":"El botón directo aparece cuando el servicio Xaman instalado está configurado para este activo CAL exacto.","ar":"يظهر الزر المباشر عندما تكون خدمة Xaman المثبتة مُعدّة لأصل CAL المحدد هذا.","fr":"Le bouton direct apparaît lorsque le service Xaman installé est configuré pour cet actif CAL exact.","bn":"ইনস্টল করা Xaman পরিষেবা ঠিক এই CAL সম্পদের জন্য কনফিগার করা থাকলে সরাসরি বোতাম দেখা যাবে।","pt":"O botão direto aparece quando o serviço Xaman instalado está configurado para este ativo CAL exato.","id":"Tombol langsung muncul jika layanan Xaman yang terpasang dikonfigurasi untuk aset CAL yang persis ini.","ur":"براہِ راست بٹن تب ظاہر ہوتا ہے جب نصب شدہ Xaman سروس عین اسی CAL اثاثے کے لیے ترتیب دی گئی ہو۔"},"pages":[1205,4205,7880]},{"source":"Find food, explore nutrition and keep your food diary.","translations":{"nl":"Zoek voeding, bekijk voedingswaarden en houd je eetdagboek bij.","zh-Hans":"查找食物、了解营养并记录饮食。","hi":"खाद्य पदार्थ खोजें, पोषण देखें और अपनी भोजन डायरी रखें।","es":"Busca alimentos, consulta nutrientes y lleva tu diario alimentario.","ar":"ابحث عن الطعام واستكشف التغذية واحتفظ بيومياتك الغذائية.","fr":"Recherchez des aliments, consultez leur nutrition et tenez votre journal alimentaire.","bn":"খাবার খুঁজুন, পুষ্টির তথ্য দেখুন ও খাবারের ডায়েরি রাখুন।","pt":"Procure alimentos, explore a nutrição e mantenha o seu diário alimentar.","id":"Cari makanan, jelajahi nutrisi, dan catat buku harian makanan Anda.","ur":"کھانے تلاش کریں، غذائیت دیکھیں اور اپنی خوراک کی ڈائری رکھیں۔"},"pages":[1205,4205,7880]},{"source":"Try with test funds","translations":{"nl":"Proberen met testtegoed","zh-Hans":"使用测试资金体验","hi":"टेस्ट निधि से आज़माएँ","es":"Probar con fondos de prueba","ar":"التجربة بأموال اختبار","fr":"Essayer avec des fonds de test","bn":"টেস্ট তহবিল দিয়ে চেষ্টা করুন","pt":"Experimentar com fundos de teste","id":"Coba dengan dana uji","ur":"ٹیسٹ رقم کے ساتھ آزمائیں"},"pages":[1205,4205,6855,7880]},{"source":"Create a free XRPL Testnet account below. It comes with test XRP, which has no real monetary value. Then add it to Xaman to try CalorieApp.","translations":{"nl":"Maak hieronder gratis een XRPL Testnet-account aan. Je ontvangt test-XRP zonder echte geldwaarde. Voeg het account daarna toe aan Xaman om CalorieApp te proberen.","zh-Hans":"在下方免费创建 XRPL Testnet 账户,获得无真实货币价值的测试 XRP。然后将账户添加到 Xaman,体验 CalorieApp。","hi":"नीचे मुफ्त XRPL Testnet खाता बनाएँ। उसमें मिलने वाले टेस्ट XRP का वास्तविक मौद्रिक मूल्य नहीं है। फिर उसे Xaman में जोड़कर CalorieApp आज़माएँ।","es":"Crea gratis una cuenta XRPL Testnet a continuación. Recibirás XRP de prueba sin valor monetario real. Añade la cuenta a Xaman para probar CalorieApp.","ar":"أنشئ حساب XRPL Testnet مجانيًا أدناه. ستحصل على XRP تجريبي بلا قيمة مالية حقيقية. أضف الحساب إلى Xaman لتجربة CalorieApp.","fr":"Créez gratuitement un compte XRPL Testnet ci-dessous. Il reçoit des XRP de test sans valeur monétaire réelle. Ajoutez-le ensuite à Xaman pour essayer CalorieApp.","bn":"নিচে বিনামূল্যে XRPL Testnet অ্যাকাউন্ট তৈরি করুন। এতে বাস্তব আর্থিক মূল্যহীন টেস্ট XRP পাওয়া যাবে। তারপর CalorieApp চেষ্টা করতে অ্যাকাউন্টটি Xaman-এ যোগ করুন।","pt":"Crie uma conta XRPL Testnet gratuita abaixo. Ela recebe XRP de teste sem valor monetário real. Depois adicione-a à Xaman para experimentar o CalorieApp.","id":"Buat akun XRPL Testnet gratis di bawah. Akun menerima XRP uji tanpa nilai uang nyata. Tambahkan ke Xaman untuk mencoba CalorieApp.","ur":"نیچے مفت XRPL Testnet اکاؤنٹ بنائیں۔ اس میں حقیقی مالی قدر کے بغیر ٹیسٹ XRP ملتے ہیں۔ پھر CalorieApp آزمانے کے لیے اسے Xaman میں شامل کریں۔"},"pages":[1205,4205,6855,7880]},{"source":"Use a separate test account. Test networks can be reset. A fresh test-wallet login still needs to be checked on the live CalorieApp; do not fund Mainnet if it fails.","translations":{"nl":"Gebruik een apart testaccount. Testnetwerken kunnen worden gereset. Aanmelden met een nieuwe testwallet moet nog op de live CalorieApp worden gecontroleerd; stort geen geld op Mainnet als dat mislukt.","zh-Hans":"请使用独立测试账户。测试网络可能重置。新测试钱包登录仍需在 CalorieApp 实测;若失败,请勿向 Mainnet 充值。","hi":"अलग टेस्ट खाता इस्तेमाल करें। टेस्ट नेटवर्क रीसेट हो सकते हैं। नए टेस्ट वॉलेट से CalorieApp लॉगिन की लाइव जाँच बाकी है; विफल होने पर Mainnet में पैसे न डालें।","es":"Usa una cuenta de prueba separada. Las redes de prueba pueden reiniciarse. Falta comprobar el acceso con una cartera nueva en CalorieApp; no ingreses fondos en Mainnet si falla.","ar":"استخدم حساب اختبار منفصلًا. قد تُعاد تهيئة شبكات الاختبار. لا يزال تسجيل الدخول بمحفظة اختبار جديدة يحتاج إلى تجربة فعلية في CalorieApp؛ لا تموّل Mainnet إذا فشل.","fr":"Utilisez un compte de test séparé. Les réseaux de test peuvent être réinitialisés. La connexion avec un nouveau portefeuille de test reste à vérifier dans CalorieApp ; ne financez pas Mainnet si elle échoue.","bn":"আলাদা টেস্ট অ্যাকাউন্ট ব্যবহার করুন। টেস্ট নেটওয়ার্ক রিসেট হতে পারে। নতুন টেস্ট ওয়ালেট দিয়ে CalorieApp-এ লগইন এখনও সরাসরি যাচাই করতে হবে; ব্যর্থ হলে Mainnet-এ অর্থ জমা দেবেন না।","pt":"Use uma conta de teste separada. As redes de teste podem ser reiniciadas. O acesso com uma nova carteira de teste ainda precisa ser verificado no CalorieApp; não deposite na Mainnet se falhar.","id":"Gunakan akun uji terpisah. Jaringan uji dapat direset. Login dengan dompet uji baru masih perlu diperiksa di CalorieApp langsung; jangan mendanai Mainnet jika gagal.","ur":"الگ ٹیسٹ اکاؤنٹ استعمال کریں۔ ٹیسٹ نیٹ ورک ری سیٹ ہو سکتے ہیں۔ نئے ٹیسٹ والیٹ سے CalorieApp لاگ اِن کی عملی جانچ باقی ہے؛ ناکامی پر Mainnet میں رقم نہ ڈالیں۔"},"pages":[1205,4205,7880]},{"source":"Show the simple steps","translations":{"nl":"Toon de eenvoudige stappen","zh-Hans":"显示简明步骤","hi":"सरल चरण दिखाएँ","es":"Mostrar pasos sencillos","ar":"عرض الخطوات البسيطة","fr":"Afficher les étapes simples","bn":"সহজ ধাপগুলো দেখুন","pt":"Mostrar passos simples","id":"Tampilkan langkah sederhana","ur":"آسان مراحل دکھائیں"},"pages":[1205,4205,7880]},{"source":"Open Xaman. Create a separate account there and keep its recovery information safe. Never enter recovery words or keys on this website.","translations":{"nl":"Open Xaman. Maak daar een apart account en bewaar de herstelgegevens veilig. Vul herstelwoorden of sleutels nooit op deze website in.","zh-Hans":"打开 Xaman,在其中创建独立账户并妥善保管恢复信息。切勿在本网站输入恢复词或密钥。","hi":"Xaman खोलें। उसमें अलग खाता बनाएँ और रिकवरी जानकारी सुरक्षित रखें। इस वेबसाइट पर रिकवरी शब्द या कुंजियाँ कभी न भरें।","es":"Abre Xaman. Crea allí una cuenta separada y guarda sus datos de recuperación. Nunca introduzcas palabras de recuperación ni claves en este sitio.","ar":"افتح Xaman وأنشئ حسابًا منفصلًا فيه واحفظ معلومات الاسترداد بأمان. لا تدخل كلمات الاسترداد أو المفاتيح في هذا الموقع أبدًا.","fr":"Ouvrez Xaman. Créez-y un compte séparé et conservez ses informations de récupération en sécurité. Ne saisissez jamais vos mots de récupération ou clés sur ce site.","bn":"Xaman খুলুন। সেখানে আলাদা অ্যাকাউন্ট তৈরি করে পুনরুদ্ধারের তথ্য নিরাপদে রাখুন। এই সাইটে কখনো পুনরুদ্ধারের শব্দ বা কী লিখবেন না।","pt":"Abra o Xaman. Crie lá uma conta separada e guarde os dados de recuperação em segurança. Nunca introduza palavras de recuperação ou chaves neste site.","id":"Buka Xaman. Buat akun terpisah di sana dan simpan informasi pemulihannya dengan aman. Jangan pernah memasukkan kata pemulihan atau kunci di situs ini.","ur":"Xaman کھولیں۔ وہیں الگ اکاؤنٹ بنائیں اور بازیابی کی معلومات محفوظ رکھیں۔ اس ویب سائٹ پر بازیابی کے الفاظ یا چابیاں کبھی درج نہ کریں۔"},"pages":[1205,4205,7880]},{"source":"In Xaman, open Settings → Advanced and enable Developer mode. Return Home, open the network switch and select XRPL Testnet.","translations":{"nl":"Open in Xaman Settings → Advanced en zet Developer mode aan. Ga terug naar Home, open de netwerkwissel en kies XRPL Testnet.","zh-Hans":"在 Xaman 中打开 Settings → Advanced,启用 Developer mode。返回 Home,打开网络切换并选择 XRPL Testnet。","hi":"Xaman में Settings → Advanced खोलकर Developer mode चालू करें। Home पर लौटें, नेटवर्क स्विच खोलें और XRPL Testnet चुनें।","es":"En Xaman, abre Settings → Advanced y activa Developer mode. Vuelve a Home, abre el selector de red y elige XRPL Testnet.","ar":"في Xaman، افتح Settings → Advanced وفعّل Developer mode. عُد إلى Home، وافتح تبديل الشبكة واختر XRPL Testnet.","fr":"Dans Xaman, ouvrez Settings → Advanced et activez Developer mode. Revenez à Home, ouvrez le sélecteur de réseau et choisissez XRPL Testnet.","bn":"Xaman-এ Settings → Advanced খুলে Developer mode চালু করুন। Home-এ ফিরুন, নেটওয়ার্ক পরিবর্তন খুলে XRPL Testnet বেছে নিন।","pt":"No Xaman, abra Settings → Advanced e ative Developer mode. Volte a Home, abra o seletor de rede e escolha XRPL Testnet.","id":"Di Xaman, buka Settings → Advanced dan aktifkan Developer mode. Kembali ke Home, buka pengalih jaringan dan pilih XRPL Testnet.","ur":"Xaman میں Settings → Advanced کھول کر Developer mode فعال کریں۔ Home پر واپس آئیں، نیٹ ورک سوئچ کھولیں اور XRPL Testnet منتخب کریں۔"},"pages":[1205,4205,7880]},{"source":"Choose Activate your account, then Activate account. This uses test XRP. Do not buy or send real XRP for this test. Close the xApp when ready.","translations":{"nl":"Kies Activate your account en daarna Activate account. Dit gebruikt test-XRP. Koop of verstuur hiervoor geen echte XRP. Sluit daarna de xApp.","zh-Hans":"选择 Activate your account,然后选择 Activate account。此操作使用测试 XRP。请勿购买或发送真实 XRP。完成后关闭 xApp。","hi":"Activate your account, फिर Activate account चुनें। इसमें टेस्ट XRP लगता है। इस टेस्ट के लिए वास्तविक XRP न खरीदें, न भेजें। तैयार होने पर xApp बंद करें।","es":"Elige Activate your account y luego Activate account. Se usa XRP de prueba. No compres ni envíes XRP real para esta prueba. Cierra la xApp al terminar.","ar":"اختر Activate your account ثم Activate account. تُستخدم أموال XRP للاختبار. لا تشترِ أو ترسل XRP حقيقيًا لهذه التجربة. أغلق xApp عند الانتهاء.","fr":"Choisissez Activate your account, puis Activate account. Cela utilise du XRP de test. N’achetez ni n’envoyez de XRP réel pour cet essai. Fermez la xApp ensuite.","bn":"Activate your account, তারপর Activate account বেছে নিন। এতে টেস্ট XRP ব্যবহার হয়। এই পরীক্ষার জন্য আসল XRP কিনবেন বা পাঠাবেন না। শেষে xApp বন্ধ করুন।","pt":"Escolha Activate your account e depois Activate account. Usa XRP de teste. Não compre nem envie XRP real para este teste. Feche a xApp quando terminar.","id":"Pilih Activate your account, lalu Activate account. Ini menggunakan XRP uji. Jangan membeli atau mengirim XRP nyata untuk pengujian ini. Tutup xApp setelah selesai.","ur":"Activate your account، پھر Activate account منتخب کریں۔ یہ ٹیسٹ XRP استعمال کرتا ہے۔ اس ٹیسٹ کے لیے حقیقی XRP نہ خریدیں، نہ بھیجیں۔ مکمل ہونے پر xApp بند کریں۔"},"pages":[1205,4205,7880]},{"source":"Return to the CalorieApp page. Test funds cannot buy real CAL or complete donations. If test sign-in fails, stop here; funding Mainnet is not the solution.","translations":{"nl":"Ga terug naar de CalorieApp-pagina. Met testtegoed koop je geen echte CAL en doe je geen donaties. Mislukt aanmelden met je testwallet? Stop dan; Mainnet financieren is niet de oplossing.","zh-Hans":"返回 CalorieApp 页面。测试资金不能购买真实 CAL 或完成捐款。若测试登录失败,请停止;充值 Mainnet 不能解决此问题。","hi":"CalorieApp पेज पर लौटें। टेस्ट निधि से वास्तविक CAL नहीं खरीदा जा सकता, न दान किया जा सकता है। टेस्ट लॉगिन विफल हो तो रुकें; Mainnet में पैसे डालना समाधान नहीं है।","es":"Vuelve a CalorieApp. Los fondos de prueba no compran CAL real ni completan donaciones. Si falla el acceso de prueba, detente; financiar Mainnet no es la solución.","ar":"عُد إلى صفحة CalorieApp. لا تشتري أموال الاختبار CAL حقيقيًا ولا تُكمل التبرعات. إذا فشل دخول الاختبار، توقف؛ تمويل Mainnet ليس الحل.","fr":"Revenez à CalorieApp. Les fonds de test ne permettent ni achat de CAL réel ni don. Si la connexion de test échoue, arrêtez-vous ; financer Mainnet ne résout pas le problème.","bn":"CalorieApp পেজে ফিরুন। টেস্ট তহবিলে আসল CAL কেনা বা অনুদান দেওয়া যায় না। টেস্ট লগইন ব্যর্থ হলে থামুন; Mainnet-এ অর্থ দেওয়া সমাধান নয়।","pt":"Volte à página CalorieApp. Fundos de teste não compram CAL real nem concluem donativos. Se o acesso de teste falhar, pare; financiar Mainnet não é a solução.","id":"Kembali ke halaman CalorieApp. Dana uji tidak dapat membeli CAL nyata atau menyelesaikan donasi. Jika login uji gagal, berhenti; mendanai Mainnet bukan solusinya.","ur":"CalorieApp صفحے پر واپس آئیں۔ ٹیسٹ رقم سے حقیقی CAL نہیں خریدا جا سکتا، نہ عطیہ دیا جا سکتا ہے۔ ٹیسٹ لاگ اِن ناکام ہو تو رکیں؛ Mainnet میں رقم ڈالنا حل نہیں۔"},"pages":[1205,4205,7880]},{"source":"Open Xaman’s illustrated guide","translations":{"nl":"Xamans uitleg met plaatjes openen","zh-Hans":"打开 Xaman 图文指南","hi":"Xaman की चित्र वाली गाइड खोलें","es":"Abrir guía ilustrada de Xaman","ar":"فتح دليل Xaman المصوّر","fr":"Ouvrir le guide illustré de Xaman","bn":"Xaman-এর ছবিসহ নির্দেশিকা খুলুন","pt":"Abrir o guia ilustrado do Xaman","id":"Buka panduan bergambar Xaman","ur":"Xaman کا تصویری رہنما کھولیں"},"pages":[1205,4205,7880]},{"source":"Get Xaman","translations":{"nl":"Xaman downloaden","zh-Hans":"获取 Xaman","hi":"Xaman प्राप्त करें","es":"Obtener Xaman","ar":"الحصول على Xaman","fr":"Obtenir Xaman","bn":"Xaman নিন","pt":"Obter Xaman","id":"Dapatkan Xaman","ur":"Xaman حاصل کریں"},"pages":[1205,4205,7880]},{"source":"Open CAL/XRP order book","translations":{"nl":"Open CAL/XRP-orderboek","zh-Hans":"打开 CAL/XRP 订单簿","hi":"CAL/XRP ऑर्डर बुक खोलें","es":"Abrir libro de órdenes CAL/XRP","ar":"فتح دفتر أوامر CAL/XRP","fr":"Ouvrir le carnet CAL/XRP","bn":"CAL/XRP অর্ডার বুক খুলুন","pt":"Abrir livro de ordens CAL/XRP","id":"Buka buku pesanan CAL/XRP","ur":"CAL/XRP آرڈر بک کھولیں"},"pages":[1205,4205,7880]},{"source":"Website language","translations":{"nl":"Taal van de website","zh-Hans":"网站语言","hi":"वेबसाइट की भाषा","es":"Idioma del sitio","ar":"لغة الموقع","fr":"Langue du site","bn":"ওয়েবসাইটের ভাষা","pt":"Idioma do site","id":"Bahasa situs","ur":"ویب سائٹ کی زبان"},"pages":[1205,4205,7880]},{"source":"Swap CAL/XRP","translations":{"nl":"Wissel CAL/XRP","zh-Hans":"兑换 CAL/XRP","hi":"CAL/XRP स्वैप करें","es":"Intercambiar CAL/XRP","ar":"مبادلة CAL/XRP","fr":"Échanger CAL/XRP","bn":"CAL/XRP বিনিময় করুন","pt":"Trocar CAL/XRP","id":"Tukar CAL/XRP","ur":"CAL/XRP تبدیل کریں"},"pages":[1205,4205,7880]},{"source":"Directly via this website → Xaman","translations":{"nl":"Rechtstreeks via deze website → Xaman","zh-Hans":"通过本网站直接进入 Xaman","hi":"इस वेबसाइट से सीधे → Xaman","es":"Directamente desde esta web → Xaman","ar":"مباشرة من هذا الموقع ← Xaman","fr":"Directement depuis ce site → Xaman","bn":"এই ওয়েবসাইট থেকে সরাসরি → Xaman","pt":"Diretamente deste site → Xaman","id":"Langsung dari situs ini → Xaman","ur":"اس ویب سائٹ سے براہ راست ← Xaman"},"pages":[1205,4205,7880]},{"source":"Alternative via xrpl.services → Xaman","translations":{"nl":"Alternatief via xrpl.services → Xaman","zh-Hans":"另一方式:通过 xrpl.services 进入 Xaman","hi":"वैकल्पिक मार्ग: xrpl.services → Xaman","es":"Alternativa mediante xrpl.services → Xaman","ar":"بديل عبر xrpl.services ← Xaman","fr":"Autre voie via xrpl.services → Xaman","bn":"বিকল্প: xrpl.services হয়ে → Xaman","pt":"Alternativa via xrpl.services → Xaman","id":"Alternatif melalui xrpl.services → Xaman","ur":"متبادل: xrpl.services کے ذریعے ← Xaman"},"pages":[1205,4205,7880]},{"source":"Manual setup with XRP Toolkit","translations":{"nl":"Handmatig instellen met XRP Toolkit","zh-Hans":"使用 XRP Toolkit 手动设置","hi":"XRP Toolkit से मैन्युअल सेटअप","es":"Configuración manual con XRP Toolkit","ar":"إعداد يدوي باستخدام XRP Toolkit","fr":"Configuration manuelle avec XRP Toolkit","bn":"XRP Toolkit দিয়ে ম্যানুয়াল সেটআপ","pt":"Configuração manual com XRP Toolkit","id":"Pengaturan manual dengan XRP Toolkit","ur":"XRP Toolkit سے دستی ترتیب"},"pages":[1205,4205,7880]},{"source":"Calorie help","translations":{"nl":"Calorie-hulp","zh-Hans":"Calorie 帮助","hi":"Calorie सहायता","es":"Ayuda de Calorie","ar":"مساعدة Calorie","fr":"Aide Calorie","bn":"Calorie সহায়তা","pt":"Ajuda Calorie","id":"Bantuan Calorie","ur":"Calorie مدد"},"pages":[6855]},{"source":"Choose a topic or ask a short question.","translations":{"nl":"Kies een onderwerp of stel een korte vraag.","zh-Hans":"选择主题或提出简短问题。","hi":"विषय चुनें या छोटा सवाल पूछें।","es":"Elige un tema o haz una pregunta breve.","ar":"اختر موضوعًا أو اطرح سؤالًا قصيرًا.","fr":"Choisissez un sujet ou posez une courte question.","bn":"একটি বিষয় বেছে নিন বা ছোট প্রশ্ন করুন।","pt":"Escolha um tema ou faça uma pergunta curta.","id":"Pilih topik atau ajukan pertanyaan singkat.","ur":"موضوع چنیں یا مختصر سوال پوچھیں۔"},"pages":[6855]},{"source":"Your question","translations":{"nl":"Je vraag","zh-Hans":"你的问题","hi":"आपका सवाल","es":"Tu pregunta","ar":"سؤالك","fr":"Votre question","bn":"আপনার প্রশ্ন","pt":"A sua pergunta","id":"Pertanyaan Anda","ur":"آپ کا سوال"},"pages":[6855]},{"source":"For example: test account or trustline","translations":{"nl":"Bijvoorbeeld: testaccount of trustline","zh-Hans":"例如:测试账户或信任线","hi":"जैसे: टेस्ट खाता या ट्रस्टलाइन","es":"Por ejemplo: cuenta de prueba o trustline","ar":"مثلًا: حساب اختبار أو خط ثقة","fr":"Par exemple : compte de test ou trustline","bn":"যেমন: পরীক্ষার অ্যাকাউন্ট বা ট্রাস্টলাইন","pt":"Por exemplo: conta de teste ou trustline","id":"Misalnya: akun uji atau trustline","ur":"مثلاً: ٹیسٹ اکاؤنٹ یا ٹرسٹ لائن"},"pages":[6855]},{"source":"Find an answer","translations":{"nl":"Zoek een antwoord","zh-Hans":"查找答案","hi":"उत्तर खोजें","es":"Buscar una respuesta","ar":"ابحث عن إجابة","fr":"Trouver une réponse","bn":"উত্তর খুঁজুন","pt":"Encontrar resposta","id":"Cari jawaban","ur":"جواب تلاش کریں"},"pages":[6855]},{"source":"Choose a topic below or read the FAQ and project documents.","translations":{"nl":"Kies hieronder een onderwerp of lees de FAQ en projectdocumenten.","zh-Hans":"请在下方选择主题,或阅读常见问题和项目文档。","hi":"नीचे विषय चुनें या FAQ और परियोजना दस्तावेज़ पढ़ें।","es":"Elige un tema o lee las preguntas frecuentes y los documentos.","ar":"اختر موضوعًا أدناه أو اقرأ الأسئلة الشائعة ووثائق المشروع.","fr":"Choisissez un sujet ou consultez la FAQ et les documents du projet.","bn":"নিচে বিষয় বেছে নিন বা FAQ ও প্রকল্পের নথি পড়ুন।","pt":"Escolha um tema ou consulte as perguntas frequentes e os documentos.","id":"Pilih topik di bawah atau baca FAQ dan dokumen proyek.","ur":"نیچے موضوع چنیں یا FAQ اور منصوبے کی دستاویزات پڑھیں۔"},"pages":[6855]},{"source":"This help does not save questions or create tickets.","translations":{"nl":"Deze hulp slaat vragen niet op en maakt geen tickets aan.","zh-Hans":"此帮助不保存问题,也不创建工单。","hi":"यह सहायता सवाल सहेजती या टिकट नहीं बनाती।","es":"Esta ayuda no guarda preguntas ni crea tickets.","ar":"هذه المساعدة لا تحفظ الأسئلة ولا تنشئ تذاكر دعم.","fr":"Cette aide ne conserve pas les questions et ne crée pas de tickets.","bn":"এই সহায়তা প্রশ্ন জমা রাখে না বা টিকিট তৈরি করে না।","pt":"Esta ajuda não guarda perguntas nem cria pedidos de suporte.","id":"Bantuan ini tidak menyimpan pertanyaan atau membuat tiket.","ur":"یہ مدد سوال محفوظ یا ٹکٹ نہیں بناتی۔"},"pages":[6855]},{"source":"Privacy, terms & licences","translations":{"nl":"Privacy, voorwaarden & licenties","zh-Hans":"隐私、条款和许可","hi":"गोपनीयता, शर्तें और लाइसेंस","es":"Privacidad, condiciones y licencias","ar":"الخصوصية والشروط والتراخيص","fr":"Confidentialité, conditions et licences","bn":"গোপনীয়তা, শর্ত ও লাইসেন্স","pt":"Privacidade, termos e licenças","id":"Privasi, ketentuan & lisensi","ur":"رازداری، شرائط اور لائسنس"},"pages":[6855]},{"source":"Read the privacy policy, terms and source/licence notes for details. This helper does not decide individual legal or financial questions.","translations":{"nl":"Lees het privacybeleid, de voorwaarden en bron-/licentie-informatie voor details. Deze hulp beoordeelt geen persoonlijke juridische of financiële vragen.","zh-Hans":"详情请阅读隐私政策、条款及来源和许可说明。此帮助不判断个人法律或财务问题。","hi":"विवरण के लिए गोपनीयता नीति, शर्तें और स्रोत/लाइसेंस नोट पढ़ें। यह सहायता व्यक्तिगत कानूनी या वित्तीय सवालों का निर्णय नहीं करती।","es":"Lee la política de privacidad, las condiciones y las notas de fuentes y licencias. Esta ayuda no decide cuestiones jurídicas o financieras personales.","ar":"اقرأ سياسة الخصوصية والشروط وملاحظات المصادر والتراخيص للتفاصيل. هذه المساعدة لا تفصل في أسئلة قانونية أو مالية شخصية.","fr":"Lisez la politique de confidentialité, les conditions et les notes de sources et licences. Cette aide ne tranche pas les questions juridiques ou financières personnelles.","bn":"বিস্তারিত জানতে গোপনীয়তা নীতি, শর্ত ও উৎস/লাইসেন্সের নোট পড়ুন। এই সহায়তা ব্যক্তিগত আইনি বা আর্থিক প্রশ্নের সিদ্ধান্ত দেয় না।","pt":"Leia a política de privacidade, os termos e as notas de fontes e licenças. Esta ajuda não decide questões jurídicas ou financeiras pessoais.","id":"Baca kebijakan privasi, ketentuan, serta catatan sumber dan lisensi. Bantuan ini tidak memutuskan persoalan hukum atau keuangan pribadi.","ur":"تفصیل کے لیے رازداری کی پالیسی، شرائط اور ماخذ/لائسنس کی معلومات پڑھیں۔ یہ مدد ذاتی قانونی یا مالی سوالوں کا فیصلہ نہیں کرتی۔"},"pages":[6855]},{"source":"Project documents","translations":{"nl":"Projectdocumenten","zh-Hans":"项目文档","hi":"परियोजना दस्तावेज़","es":"Documentos del proyecto","ar":"وثائق المشروع","fr":"Documents du projet","bn":"প্রকল্পের নথি","pt":"Documentos do projeto","id":"Dokumen proyek","ur":"منصوبے کی دستاویزات"},"pages":[6855]},{"source":"The publications repository contains the whitepaper and detailed roadmap. Consult the relevant document for current details and disclosures.","translations":{"nl":"De publicatierepository bevat de whitepaper en de uitgebreide roadmap. Raadpleeg het betreffende document voor actuele details en toelichtingen.","zh-Hans":"发布库包含白皮书和详细路线图。最新详情及披露请参阅相应文档。","hi":"प्रकाशन रिपॉज़िटरी में व्हाइटपेपर और विस्तृत रोडमैप है। वर्तमान विवरण और खुलासे संबंधित दस्तावेज़ में देखें।","es":"El repositorio contiene el whitepaper y la hoja de ruta detallada. Consulta el documento correspondiente para detalles y declaraciones actuales.","ar":"يضم مستودع المنشورات الورقة البيضاء وخارطة الطريق المفصلة. راجع الوثيقة المعنية للتفاصيل والإفصاحات الحالية.","fr":"Le dépôt contient le livre blanc et la feuille de route détaillée. Consultez le document concerné pour les détails et déclarations actuels.","bn":"প্রকাশনা রিপোজিটরিতে হোয়াইটপেপার ও বিস্তারিত রোডম্যাপ আছে। বর্তমান তথ্য ও প্রকাশের জন্য সংশ্লিষ্ট নথি পড়ুন।","pt":"O repositório contém o whitepaper e o roteiro detalhado. Consulte o documento relevante para detalhes e divulgações atuais.","id":"Repositori publikasi berisi whitepaper dan peta jalan terperinci. Lihat dokumen terkait untuk rincian dan pengungkapan terkini.","ur":"اشاعتی ریپوزٹری میں وائٹ پیپر اور تفصیلی روڈمیپ موجود ہے۔ تازہ تفصیلات اور انکشافات متعلقہ دستاویز میں دیکھیں۔"},"pages":[6855]},{"source":"Contact options","translations":{"nl":"Contactmogelijkheden","zh-Hans":"联系方式","hi":"संपर्क विकल्प","es":"Opciones de contacto","ar":"خيارات الاتصال","fr":"Options de contact","bn":"যোগাযোগের উপায়","pt":"Opções de contacto","id":"Pilihan kontak","ur":"رابطے کے طریقے"},"pages":[6855]},{"source":"You can find the project’s contact options on the Contact page. This help does not send a message or open a ticket.","translations":{"nl":"De contactmogelijkheden staan op de Contact-pagina. Deze hulp stuurt geen bericht en opent geen ticket.","zh-Hans":"项目联系方式位于 Contact 页面。此帮助不会发送消息或创建工单。","hi":"परियोजना के संपर्क विकल्प Contact पृष्ठ पर हैं। यह सहायता संदेश या टिकट नहीं भेजती।","es":"Las opciones de contacto están en la página Contact. Esta ayuda no envía mensajes ni abre tickets.","ar":"تتوفر خيارات اتصال المشروع في صفحة Contact. هذه المساعدة لا ترسل رسائل ولا تفتح تذاكر.","fr":"Les coordonnées du projet figurent sur la page Contact. Cette aide n’envoie aucun message et n’ouvre aucun ticket.","bn":"প্রকল্পের যোগাযোগের উপায় Contact পৃষ্ঠায় আছে। এই সহায়তা বার্তা পাঠায় না বা টিকিট খোলে না।","pt":"As opções de contacto estão na página Contact. Esta ajuda não envia mensagens nem abre pedidos de suporte.","id":"Pilihan kontak proyek ada di halaman Contact. Bantuan ini tidak mengirim pesan atau membuka tiket.","ur":"منصوبے کے رابطے Contact صفحے پر ہیں۔ یہ مدد پیغام نہیں بھیجتی اور ٹکٹ نہیں کھولتی۔"},"pages":[6855]},{"source":"FAQ & quick answers","translations":{"nl":"FAQ & snelle antwoorden","zh-Hans":"常见问题和快速答案","hi":"FAQ और छोटे उत्तर","es":"Preguntas frecuentes y respuestas rápidas","ar":"أسئلة شائعة وإجابات سريعة","fr":"FAQ et réponses rapides","bn":"FAQ ও দ্রুত উত্তর","pt":"Perguntas frequentes e respostas rápidas","id":"FAQ & jawaban singkat","ur":"FAQ اور مختصر جوابات"},"pages":[6855]},{"source":"CalorieApp","translations":{"nl":"CalorieApp","zh-Hans":"CalorieApp","hi":"CalorieApp","es":"CalorieApp","ar":"CalorieApp","fr":"CalorieApp","bn":"CalorieApp","pt":"CalorieApp","id":"CalorieApp","ur":"CalorieApp"},"pages":[6855]},{"source":"Find food, explore nutrition and keep your food diary. CalorieApp food search and food logging do not require buying CAL.","translations":{"nl":"Zoek voeding, bekijk voedingswaarden en houd je eetdagboek bij. Voor voedsel zoeken en je voedingslogboek in CalorieApp hoef je geen CAL te kopen.","zh-Hans":"查找食物、了解营养并记录饮食。 使用 CalorieApp 搜索食物和记录饮食无需购买 CAL。","hi":"खाद्य पदार्थ खोजें, पोषण देखें और अपनी भोजन डायरी रखें। CalorieApp में भोजन खोजने और दर्ज करने के लिए CAL खरीदना आवश्यक नहीं है।","es":"Busca alimentos, consulta nutrientes y lleva tu diario alimentario. Buscar alimentos y registrarlos en CalorieApp no requiere comprar CAL.","ar":"ابحث عن الطعام واستكشف التغذية واحتفظ بيومياتك الغذائية. لا يتطلب البحث عن الطعام وتسجيله في CalorieApp شراء CAL.","fr":"Recherchez des aliments, consultez leur nutrition et tenez votre journal alimentaire. La recherche et le journal alimentaires de CalorieApp ne nécessitent pas l’achat de CAL.","bn":"খাবার খুঁজুন, পুষ্টির তথ্য দেখুন ও খাবারের ডায়েরি রাখুন। CalorieApp-এ খাবার খুঁজতে বা লিখে রাখতে CAL কেনার প্রয়োজন নেই।","pt":"Procure alimentos, explore a nutrição e mantenha o seu diário alimentar. A pesquisa e o registo de alimentos na CalorieApp não exigem a compra de CAL.","id":"Cari makanan, jelajahi nutrisi, dan catat buku harian makanan Anda. Pencarian dan pencatatan makanan di CalorieApp tidak memerlukan pembelian CAL.","ur":"کھانے تلاش کریں، غذائیت دیکھیں اور اپنی خوراک کی ڈائری رکھیں۔ CalorieApp میں خوراک تلاش کرنے اور درج کرنے کے لیے CAL خریدنا ضروری نہیں۔"},"pages":[6855]},{"source":"Open the CAL/XRP order book or swap on XPMarket. Check the asset, issuer, fees and final wallet request. Trading takes place with the external service; this website does not place orders. CAL is temporarily unavailable at SWFT.","translations":{"nl":"Open het CAL/XRP-orderboek of de swap op XPMarket. Controleer de munt, uitgever, kosten en het uiteindelijke verzoek in je wallet. Je handelt bij de externe dienst; deze website plaatst geen orders. CAL is tijdelijk niet beschikbaar bij SWFT.","zh-Hans":"在 XPMarket 打开 CAL/XRP 订单簿或兑换。核对资产、发行方、费用和最终钱包请求。交易通过外部服务进行;本网站不会下单。 SWFT 暂不支持 CAL。","hi":"XPMarket पर CAL/XRP ऑर्डर बुक या स्वैप खोलें। एसेट, जारीकर्ता, शुल्क और वॉलेट में अंतिम अनुरोध जाँचें। ट्रेड बाहरी सेवा पर होता है; यह वेबसाइट ऑर्डर नहीं देती। SWFT पर CAL अस्थायी रूप से उपलब्ध नहीं है।","es":"Abre el libro de órdenes o el intercambio CAL/XRP en XPMarket. Revisa el activo, emisor, comisiones y la solicitud final en tu cartera. Operas con el servicio externo; esta web no envía órdenes. CAL no está disponible temporalmente en SWFT.","ar":"افتح دفتر أوامر CAL/XRP أو المبادلة على XPMarket. تحقق من الأصل والجهة المصدرة والرسوم والطلب النهائي في محفظتك. يتم التداول عبر الخدمة الخارجية؛ لا يرسل هذا الموقع أوامر تداول. CAL غير متاح مؤقتًا لدى SWFT.","fr":"Ouvrez le carnet d’ordres ou l’échange CAL/XRP sur XPMarket. Vérifiez l’actif, l’émetteur, les frais et la demande finale dans votre portefeuille. La transaction passe par le service externe ; ce site ne transmet pas d’ordres. CAL est temporairement indisponible sur SWFT.","bn":"XPMarket-এ CAL/XRP অর্ডার বুক বা সোয়াপ খুলুন। সম্পদ, ইস্যুকারী, ফি ও ওয়ালেটের চূড়ান্ত অনুরোধ যাচাই করুন। লেনদেন বাইরের পরিষেবায় হয়; এই ওয়েবসাইট অর্ডার দেয় না। SWFT-এ CAL সাময়িকভাবে অনুপলব্ধ।","pt":"Abra o livro de ordens ou a troca CAL/XRP no XPMarket. Verifique o ativo, emissor, taxas e o pedido final na carteira. A negociação ocorre no serviço externo; este site não envia ordens. CAL está temporariamente indisponível na SWFT.","id":"Buka buku pesanan atau swap CAL/XRP di XPMarket. Periksa aset, penerbit, biaya, dan permintaan akhir di dompet. Perdagangan berlangsung di layanan eksternal; situs ini tidak mengirim pesanan. CAL sementara tidak tersedia di SWFT.","ur":"XPMarket پر CAL/XRP آرڈر بک یا سواپ کھولیں۔ اثاثہ، جاری کنندہ، فیس اور والٹ میں آخری درخواست چیک کریں۔ تجارت بیرونی سروس پر ہوتی ہے؛ یہ ویب سائٹ آرڈر نہیں بھیجتی۔ SWFT پر CAL عارضی طور پر دستیاب نہیں ہے۔"},"pages":[6855]},{"source":"Create my free test account","translations":{"nl":"Maak mijn gratis testaccount","zh-Hans":"创建免费测试账户","hi":"मेरा मुफ्त टेस्ट खाता बनाएँ","es":"Crear mi cuenta de prueba gratis","ar":"إنشاء حساب الاختبار المجاني","fr":"Créer mon compte de test gratuit","bn":"আমার বিনামূল্যের টেস্ট অ্যাকাউন্ট তৈরি করুন","pt":"Criar minha conta de teste grátis","id":"Buat akun uji gratis saya","ur":"میرا مفت ٹیسٹ اکاؤنٹ بنائیں"},"pages":[7880]},{"source":"This button contacts the official XRPL Testnet faucet. It creates your test account and returns its recovery code to this browser. This page does not save the code; copy it before leaving.","translations":{"nl":"Deze knop maakt verbinding met de officiële XRPL Testnet-faucet. Die maakt je testaccount en stuurt de herstelcode terug naar deze browser. Deze pagina slaat de code niet op; kopieer hem voordat je weggaat.","zh-Hans":"此按钮连接官方 XRPL Testnet 水龙头,由其创建测试账户并将恢复代码返回此浏览器。本页面不保存代码;离开前请复制。","hi":"यह बटन आधिकारिक XRPL Testnet फ़ॉसेट से जुड़ता है। वह खाता बनाकर रिकवरी कोड इस ब्राउज़र को लौटाता है। यह पेज कोड सहेजता नहीं; जाने से पहले कॉपी करें।","es":"Este botón contacta con el faucet oficial de XRPL Testnet. Crea tu cuenta y devuelve el código de recuperación a este navegador. Esta página no guarda el código; cópialo antes de salir.","ar":"يتصل هذا الزر بصنبور XRPL Testnet الرسمي، الذي ينشئ حسابك ويعيد رمز الاسترداد إلى هذا المتصفح. لا تحفظ الصفحة الرمز؛ انسخه قبل المغادرة.","fr":"Ce bouton contacte le faucet officiel XRPL Testnet. Il crée votre compte et renvoie le code de récupération à ce navigateur. Cette page ne le conserve pas ; copiez-le avant de partir.","bn":"এই বোতাম অফিসিয়াল XRPL Testnet ফসেটে সংযোগ করে। সেটি অ্যাকাউন্ট তৈরি করে এই ব্রাউজারে রিকভারি কোড ফেরত দেয়। এই পেজ কোড সংরক্ষণ করে না; যাওয়ার আগে কপি করুন।","pt":"Este botão conecta ao faucet oficial da XRPL Testnet. Ele cria sua conta e devolve o código de recuperação a este navegador. Esta página não guarda o código; copie antes de sair.","id":"Tombol ini menghubungi faucet resmi XRPL Testnet. Faucet membuat akun dan mengembalikan kode pemulihan ke browser ini. Halaman tidak menyimpan kode; salin sebelum keluar.","ur":"یہ بٹن سرکاری XRPL Testnet فاسٹ سے جڑتا ہے۔ وہ اکاؤنٹ بناتا ہے اور بازیابی کوڈ اس براؤزر کو واپس بھیجتا ہے۔ یہ صفحہ کوڈ محفوظ نہیں کرتا؛ جانے سے پہلے کاپی کریں۔"},"pages":[7880]},{"source":"Ready. No real XRP or payment is needed.","translations":{"nl":"Klaar om te beginnen. Je hebt geen echte XRP nodig en hoeft niet te betalen.","zh-Hans":"可以开始。无需真实 XRP 或付款。","hi":"तैयार। वास्तविक XRP या भुगतान की ज़रूरत नहीं।","es":"Todo listo. No necesitas XRP real ni pagar.","ar":"جاهز. لا حاجة إلى XRP حقيقي أو دفع.","fr":"Prêt. Aucun XRP réel ni paiement nécessaire.","bn":"প্রস্তুত। বাস্তব XRP বা পেমেন্ট লাগবে না।","pt":"Pronto. Não é preciso XRP real nem pagamento.","id":"Siap. Tidak perlu XRP nyata atau pembayaran.","ur":"تیار۔ حقیقی XRP یا ادائیگی کی ضرورت نہیں۔"},"pages":[7880]},{"source":"Creating your test account… please wait.","translations":{"nl":"Je testaccount wordt aangemaakt… even geduld.","zh-Hans":"正在创建测试账户,请稍候…","hi":"टेस्ट खाता बन रहा है… कृपया प्रतीक्षा करें।","es":"Creando tu cuenta de prueba… espera un momento.","ar":"جارٍ إنشاء حساب الاختبار… يرجى الانتظار.","fr":"Création de votre compte de test… veuillez patienter.","bn":"টেস্ট অ্যাকাউন্ট তৈরি হচ্ছে… একটু অপেক্ষা করুন।","pt":"Criando sua conta de teste… aguarde.","id":"Membuat akun uji… harap tunggu.","ur":"ٹیسٹ اکاؤنٹ بن رہا ہے… انتظار کریں۔"},"pages":[7880]},{"source":"Test account created.","translations":{"nl":"Testaccount aangemaakt.","zh-Hans":"测试账户已创建。","hi":"टेस्ट खाता बन गया।","es":"Cuenta de prueba creada.","ar":"تم إنشاء حساب الاختبار.","fr":"Compte de test créé.","bn":"টেস্ট অ্যাকাউন্ট তৈরি হয়েছে।","pt":"Conta de teste criada.","id":"Akun uji dibuat.","ur":"ٹیسٹ اکاؤنٹ بن گیا۔"},"pages":[7880]},{"source":"Checking test XRP…","translations":{"nl":"Test-XRP controleren…","zh-Hans":"正在检查测试 XRP…","hi":"टेस्ट XRP की जाँच हो रही है…","es":"Comprobando XRP de prueba…","ar":"جارٍ التحقق من XRP التجريبي…","fr":"Vérification des XRP de test…","bn":"টেস্ট XRP যাচাই হচ্ছে…","pt":"Verificando XRP de teste…","id":"Memeriksa XRP uji…","ur":"ٹیسٹ XRP کی جانچ ہو رہی ہے…"},"pages":[7880]},{"source":"Your account has test XRP. Now add it to Xaman below.","translations":{"nl":"Je account heeft test-XRP. Voeg het nu hieronder toe aan Xaman.","zh-Hans":"账户已有测试 XRP。请按下方步骤添加到 Xaman。","hi":"खाते में टेस्ट XRP हैं। अब नीचे दिए चरणों से Xaman में जोड़ें।","es":"Tu cuenta tiene XRP de prueba. Añádela a Xaman siguiendo los pasos de abajo.","ar":"يحتوي حسابك على XRP تجريبي. أضفه الآن إلى Xaman بالخطوات أدناه.","fr":"Votre compte dispose de XRP de test. Ajoutez-le maintenant à Xaman ci-dessous.","bn":"আপনার অ্যাকাউন্টে টেস্ট XRP আছে। নিচের ধাপ অনুসরণ করে Xaman-এ যোগ করুন।","pt":"Sua conta tem XRP de teste. Agora adicione-a à Xaman abaixo.","id":"Akun Anda memiliki XRP uji. Tambahkan ke Xaman dengan langkah di bawah.","ur":"آپ کے اکاؤنٹ میں ٹیسٹ XRP ہیں۔ نیچے کے مراحل سے اسے Xaman میں شامل کریں۔"},"pages":[7880]},{"source":"Account created; funding could not yet be confirmed. Keep this recovery code and use Check test XRP again.","translations":{"nl":"Account aangemaakt; het testtegoed is nog niet bevestigd. Bewaar deze herstelcode en gebruik Test-XRP controleren opnieuw.","zh-Hans":"账户已创建,但尚未确认测试资金。请保留恢复代码并再次检查测试 XRP。","hi":"खाता बन गया; निधि की पुष्टि अभी नहीं हुई। रिकवरी कोड रखें और टेस्ट XRP फिर जाँचें।","es":"Cuenta creada; aún no se pudo confirmar el saldo. Conserva el código y vuelve a comprobar los XRP de prueba.","ar":"تم إنشاء الحساب؛ لم يتأكد الرصيد بعد. احتفظ برمز الاسترداد وأعد التحقق من XRP التجريبي.","fr":"Compte créé ; les fonds ne sont pas encore confirmés. Gardez ce code et vérifiez à nouveau les XRP de test.","bn":"অ্যাকাউন্ট তৈরি হয়েছে; তহবিল এখনও নিশ্চিত হয়নি। রিকভারি কোড রাখুন এবং আবার টেস্ট XRP যাচাই করুন।","pt":"Conta criada; os fundos ainda não foram confirmados. Guarde o código e verifique o XRP de teste novamente.","id":"Akun dibuat; dana belum terkonfirmasi. Simpan kode pemulihan dan periksa XRP uji lagi.","ur":"اکاؤنٹ بن گیا؛ رقم کی تصدیق ابھی نہیں ہوئی۔ بازیابی کوڈ رکھیں اور ٹیسٹ XRP دوبارہ چیک کریں۔"},"pages":[7880]},{"source":"The faucet could not complete this request. Wait a moment and try again, or open the official faucet below.","translations":{"nl":"De faucet kon dit verzoek niet afronden. Wacht even en probeer opnieuw, of open hieronder de officiële faucet.","zh-Hans":"水龙头未能完成请求。请稍后重试,或打开下方官方水龙头。","hi":"फ़ॉसेट अनुरोध पूरा नहीं कर पाया। थोड़ी देर बाद फिर कोशिश करें या नीचे आधिकारिक फ़ॉसेट खोलें।","es":"El faucet no pudo completar la solicitud. Espera y reinténtalo o abre el faucet oficial de abajo.","ar":"تعذر إكمال الطلب. انتظر قليلًا وحاول مجددًا أو افتح الصنبور الرسمي أدناه.","fr":"Le faucet n’a pas pu terminer la demande. Réessayez dans un moment ou ouvrez le faucet officiel ci-dessous.","bn":"ফসেট অনুরোধ সম্পূর্ণ করতে পারেনি। কিছুক্ষণ পর চেষ্টা করুন বা নিচে অফিসিয়াল ফসেট খুলুন।","pt":"O faucet não concluiu a solicitação. Espere um momento e tente novamente ou abra o faucet oficial abaixo.","id":"Faucet tidak dapat menyelesaikan permintaan. Tunggu lalu coba lagi, atau buka faucet resmi di bawah.","ur":"فاسٹ درخواست مکمل نہیں کر سکا۔ کچھ دیر بعد کوشش کریں یا نیچے سرکاری فاسٹ کھولیں۔"},"pages":[7880]},{"source":"The faucet is busy or has reached its limit. Please try later; no payment is required.","translations":{"nl":"De faucet is druk of heeft zijn limiet bereikt. Probeer het later; je hoeft niet te betalen.","zh-Hans":"水龙头繁忙或已达限额。请稍后再试,无需付款。","hi":"फ़ॉसेट व्यस्त है या उसकी सीमा पूरी हो गई है। बाद में फिर कोशिश करें; भुगतान नहीं चाहिए।","es":"El faucet está ocupado o alcanzó su límite. Inténtalo más tarde; no necesitas pagar.","ar":"الصنبور مشغول أو بلغ الحد. حاول لاحقًا؛ لا حاجة إلى الدفع.","fr":"Le faucet est occupé ou a atteint sa limite. Réessayez plus tard ; aucun paiement nécessaire.","bn":"ফসেট ব্যস্ত বা সীমা পূর্ণ হয়েছে। পরে চেষ্টা করুন; পেমেন্ট লাগবে না।","pt":"O faucet está ocupado ou atingiu o limite. Tente mais tarde; não é preciso pagar.","id":"Faucet sibuk atau mencapai batas. Coba nanti; tidak perlu membayar.","ur":"فاسٹ مصروف ہے یا حد پوری ہو گئی ہے۔ بعد میں کوشش کریں؛ ادائیگی کی ضرورت نہیں۔"},"pages":[7880]},{"source":"This browser cannot create and check the account here. Use the official faucet below.","translations":{"nl":"Deze browser kan het account hier niet aanmaken en controleren. Gebruik hieronder de officiële faucet.","zh-Hans":"此浏览器无法在这里创建并检查账户。请使用下方官方水龙头。","hi":"यह ब्राउज़र यहाँ खाता बनाकर जाँच नहीं सकता। नीचे आधिकारिक फ़ॉसेट उपयोग करें।","es":"Este navegador no puede crear y comprobar la cuenta aquí. Usa el faucet oficial de abajo.","ar":"لا يستطيع هذا المتصفح إنشاء الحساب والتحقق منه هنا. استخدم الصنبور الرسمي أدناه.","fr":"Ce navigateur ne peut pas créer et vérifier le compte ici. Utilisez le faucet officiel ci-dessous.","bn":"এই ব্রাউজার এখানে অ্যাকাউন্ট তৈরি ও যাচাই করতে পারে না। নিচের অফিসিয়াল ফসেট ব্যবহার করুন।","pt":"Este navegador não consegue criar e verificar a conta aqui. Use o faucet oficial abaixo.","id":"Browser ini tidak dapat membuat dan memeriksa akun di sini. Gunakan faucet resmi di bawah.","ur":"یہ براؤزر یہاں اکاؤنٹ بنا کر چیک نہیں کر سکتا۔ نیچے سرکاری فاسٹ استعمال کریں۔"},"pages":[7880]},{"source":"Your test account","translations":{"nl":"Jouw testaccount","zh-Hans":"您的测试账户","hi":"आपका टेस्ट खाता","es":"Tu cuenta de prueba","ar":"حساب الاختبار الخاص بك","fr":"Votre compte de test","bn":"আপনার টেস্ট অ্যাকাউন্ট","pt":"Sua conta de teste","id":"Akun uji Anda","ur":"آپ کا ٹیسٹ اکاؤنٹ"},"pages":[7880]},{"source":"Recovery code — Testnet only","translations":{"nl":"Herstelcode — alleen Testnet","zh-Hans":"恢复代码 — 仅限 Testnet","hi":"रिकवरी कोड — केवल Testnet","es":"Código de recuperación — solo Testnet","ar":"رمز الاسترداد — Testnet فقط","fr":"Code de récupération — Testnet uniquement","bn":"রিকভারি কোড — শুধু Testnet","pt":"Código de recuperação — somente Testnet","id":"Kode pemulihan — hanya Testnet","ur":"بازیابی کوڈ — صرف Testnet"},"pages":[7880]},{"source":"Show / hide recovery code","translations":{"nl":"Herstelcode tonen / verbergen","zh-Hans":"显示 / 隐藏恢复代码","hi":"रिकवरी कोड दिखाएँ / छिपाएँ","es":"Mostrar / ocultar código","ar":"إظهار / إخفاء رمز الاسترداد","fr":"Afficher / masquer le code","bn":"রিকভারি কোড দেখান / লুকান","pt":"Mostrar / ocultar código","id":"Tampilkan / sembunyikan kode","ur":"بازیابی کوڈ دکھائیں / چھپائیں"},"pages":[7880]},{"source":"Copy recovery code","translations":{"nl":"Herstelcode kopiëren","zh-Hans":"复制恢复代码","hi":"रिकवरी कोड कॉपी करें","es":"Copiar código de recuperación","ar":"نسخ رمز الاسترداد","fr":"Copier le code de récupération","bn":"রিকভারি কোড কপি করুন","pt":"Copiar código de recuperação","id":"Salin kode pemulihan","ur":"بازیابی کوڈ کاپی کریں"},"pages":[7880]},{"source":"Copy account address","translations":{"nl":"Accountadres kopiëren","zh-Hans":"复制账户地址","hi":"खाते का पता कॉपी करें","es":"Copiar dirección de cuenta","ar":"نسخ عنوان الحساب","fr":"Copier l’adresse du compte","bn":"অ্যাকাউন্টের ঠিকানা কপি করুন","pt":"Copiar endereço da conta","id":"Salin alamat akun","ur":"اکاؤنٹ کا پتہ کاپی کریں"},"pages":[7880]},{"source":"Copied. Paste into Xaman.","translations":{"nl":"Gekopieerd. Plak dit in Xaman.","zh-Hans":"已复制。请粘贴到 Xaman。","hi":"कॉपी हो गया। Xaman में पेस्ट करें।","es":"Copiado. Pégalo en Xaman.","ar":"تم النسخ. الصقه في Xaman.","fr":"Copié. Collez dans Xaman.","bn":"কপি হয়েছে। Xaman-এ পেস্ট করুন।","pt":"Copiado. Cole na Xaman.","id":"Disalin. Tempel ke Xaman.","ur":"کاپی ہو گیا۔ Xaman میں پیسٹ کریں۔"},"pages":[7880]},{"source":"Copy is unavailable. Show the code, then press and hold the text to copy it manually.","translations":{"nl":"Kopiëren is niet beschikbaar. Toon de code en houd de tekst ingedrukt om hem handmatig te kopiëren.","zh-Hans":"无法自动复制。显示代码后,长按文字手动复制。","hi":"कॉपी उपलब्ध नहीं। कोड दिखाकर टेक्स्ट दबाए रखें और मैन्युअल कॉपी करें।","es":"No se puede copiar automáticamente. Muestra el código y mantén pulsado el texto para copiarlo.","ar":"النسخ غير متاح. أظهر الرمز ثم اضغط مطولًا على النص لنسخه يدويًا.","fr":"Copie indisponible. Affichez le code puis maintenez le texte appuyé pour le copier manuellement.","bn":"কপি উপলব্ধ নয়। কোড দেখিয়ে লেখাটি চেপে ধরে হাতে কপি করুন।","pt":"Cópia indisponível. Mostre o código e pressione o texto para copiar manualmente.","id":"Penyalinan tidak tersedia. Tampilkan kode, lalu tekan dan tahan teks untuk menyalin manual.","ur":"کاپی دستیاب نہیں۔ کوڈ دکھائیں اور متن دبا کر دستی طور پر کاپی کریں۔"},"pages":[7880]},{"source":"Check test XRP","translations":{"nl":"Test-XRP controleren","zh-Hans":"检查测试 XRP","hi":"टेस्ट XRP जाँचें","es":"Comprobar XRP de prueba","ar":"التحقق من XRP التجريبي","fr":"Vérifier les XRP de test","bn":"টেস্ট XRP যাচাই করুন","pt":"Verificar XRP de teste","id":"Periksa XRP uji","ur":"ٹیسٹ XRP چیک کریں"},"pages":[7880]},{"source":"Use this code only for this test account. Never reuse it for real funds. After leaving this page, we cannot recover it for you.","translations":{"nl":"Gebruik deze code alleen voor dit testaccount, nooit voor echt geld. Als je deze pagina verlaat, kunnen wij de code niet voor je terughalen.","zh-Hans":"此代码仅用于此测试账户,切勿用于真实资金。离开页面后,我们无法为您找回。","hi":"इस कोड का उपयोग केवल इस टेस्ट खाते के लिए करें, वास्तविक धन के लिए कभी नहीं। पेज छोड़ने के बाद हम इसे वापस नहीं ला सकते।","es":"Usa este código solo para esta cuenta de prueba, nunca para fondos reales. Al salir de esta página no podremos recuperarlo por ti.","ar":"استخدم هذا الرمز لحساب الاختبار فقط، ولا تستخدمه لأموال حقيقية. لا يمكننا استعادته لك بعد مغادرة الصفحة.","fr":"Utilisez ce code uniquement pour ce compte de test, jamais pour des fonds réels. Après votre départ de cette page, nous ne pourrons pas le récupérer pour vous.","bn":"এই কোড শুধু এই টেস্ট অ্যাকাউন্টের জন্য, বাস্তব অর্থের জন্য কখনও নয়। পেজ ছাড়ার পর আমরা এটি ফিরিয়ে দিতে পারব না।","pt":"Use este código apenas nesta conta de teste, nunca para fundos reais. Depois de sair desta página, não podemos recuperá-lo para você.","id":"Gunakan kode hanya untuk akun uji ini, jangan untuk dana nyata. Setelah Anda meninggalkan halaman, kami tidak dapat memulihkannya.","ur":"کوڈ صرف اس ٹیسٹ اکاؤنٹ کے لیے استعمال کریں، حقیقی رقم کے لیے کبھی نہیں۔ صفحہ چھوڑنے کے بعد ہم اسے واپس نہیں لا سکتے۔"},"pages":[7880]},{"source":"In Xaman: Settings → Advanced → Developer mode. Return Home and select XRPL Testnet using the network switch.","translations":{"nl":"In Xaman: Settings → Advanced → Developer mode. Ga terug naar Home en kies XRPL Testnet met de netwerkwissel.","zh-Hans":"在 Xaman 中:Settings → Advanced → Developer mode。返回 Home,通过网络切换选择 XRPL Testnet。","hi":"Xaman में: Settings → Advanced → Developer mode। Home पर लौटकर नेटवर्क स्विच से XRPL Testnet चुनें।","es":"En Xaman: Settings → Advanced → Developer mode. Vuelve a Home y selecciona XRPL Testnet con el selector de red.","ar":"في Xaman: Settings → Advanced → Developer mode. ارجع إلى Home واختر XRPL Testnet من مفتاح الشبكة.","fr":"Dans Xaman : Settings → Advanced → Developer mode. Revenez à Home et sélectionnez XRPL Testnet avec le sélecteur de réseau.","bn":"Xaman-এ: Settings → Advanced → Developer mode। Home-এ ফিরে নেটওয়ার্ক সুইচ দিয়ে XRPL Testnet বেছে নিন।","pt":"Na Xaman: Settings → Advanced → Developer mode. Volte a Home e escolha XRPL Testnet no seletor de rede.","id":"Di Xaman: Settings → Advanced → Developer mode. Kembali ke Home dan pilih XRPL Testnet dengan pengalih jaringan.","ur":"Xaman میں: Settings → Advanced → Developer mode۔ Home پر واپس آ کر نیٹ ورک سوئچ سے XRPL Testnet منتخب کریں۔"},"pages":[7880]},{"source":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Paste the recovery code copied above.","translations":{"nl":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Plak de hierboven gekopieerde herstelcode.","zh-Hans":"Settings → Accounts → + → Import existing account → Full access → Family Seed。粘贴上方复制的恢复代码。","hi":"Settings → Accounts → + → Import existing account → Full access → Family Seed। ऊपर कॉपी किया रिकवरी कोड पेस्ट करें।","es":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Pega el código copiado arriba.","ar":"Settings → Accounts → + → Import existing account → Full access → Family Seed. الصق رمز الاسترداد المنسوخ أعلاه.","fr":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Collez le code copié ci-dessus.","bn":"Settings → Accounts → + → Import existing account → Full access → Family Seed। উপরে কপি করা রিকভারি কোড পেস্ট করুন।","pt":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Cole o código copiado acima.","id":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Tempel kode yang disalin di atas.","ur":"Settings → Accounts → + → Import existing account → Full access → Family Seed۔ اوپر کاپی کیا ہوا بازیابی کوڈ پیسٹ کریں۔"},"pages":[7880]},{"source":"Check that Xaman shows the same account address. Name it “CalorieApp Test”. Then return here and sign in with that test account.","translations":{"nl":"Controleer dat Xaman hetzelfde accountadres toont. Noem het “CalorieApp Test”. Ga daarna terug naar deze pagina en meld je aan met dat testaccount.","zh-Hans":"确认 Xaman 显示的账户地址一致。命名为“CalorieApp Test”,然后返回本页并使用此测试账户登录。","hi":"देखें कि Xaman में वही खाता पता है। नाम “CalorieApp Test” रखें। फिर यहाँ लौटकर उसी टेस्ट खाते से लॉगिन करें।","es":"Comprueba que Xaman muestra la misma dirección. Ponle “CalorieApp Test”. Vuelve aquí e inicia sesión con esa cuenta de prueba.","ar":"تحقق من تطابق عنوان الحساب في Xaman. سمّه “CalorieApp Test”. ثم ارجع هنا وسجل الدخول بحساب الاختبار.","fr":"Vérifiez que Xaman affiche la même adresse. Nommez le compte « CalorieApp Test ». Revenez ici et connectez-vous avec ce compte de test.","bn":"Xaman-এ একই অ্যাকাউন্টের ঠিকানা আছে কি না দেখুন। নাম দিন “CalorieApp Test”। এখানে ফিরে ওই টেস্ট অ্যাকাউন্ট দিয়ে লগইন করুন।","pt":"Confira se a Xaman mostra o mesmo endereço. Dê o nome “CalorieApp Test”. Volte aqui e entre com essa conta de teste.","id":"Pastikan Xaman menampilkan alamat akun yang sama. Beri nama “CalorieApp Test”. Kembali ke sini dan login dengan akun uji tersebut.","ur":"دیکھیں کہ Xaman میں وہی اکاؤنٹ پتہ ہے۔ نام “CalorieApp Test” رکھیں۔ پھر یہاں واپس آ کر اس ٹیسٹ اکاؤنٹ سے لاگ اِن کریں۔"},"pages":[7880]},{"source":"Import guide with pictures","translations":{"nl":"Importuitleg met plaatjes","zh-Hans":"查看图文导入指南","hi":"चित्रों वाली इम्पोर्ट गाइड","es":"Guía de importación con imágenes","ar":"دليل الاستيراد بالصور","fr":"Guide d’importation illustré","bn":"ছবিসহ ইম্পোর্ট নির্দেশিকা","pt":"Guia de importação com imagens","id":"Panduan impor bergambar","ur":"تصویروں کے ساتھ امپورٹ رہنما"},"pages":[7880]},{"source":"Back to CalorieApp sign-in","translations":{"nl":"Terug naar aanmelden bij CalorieApp","zh-Hans":"返回 CalorieApp 登录","hi":"CalorieApp लॉगिन पर लौटें","es":"Volver al acceso de CalorieApp","ar":"العودة إلى تسجيل دخول CalorieApp","fr":"Retour à la connexion CalorieApp","bn":"CalorieApp লগইনে ফিরুন","pt":"Voltar ao acesso do CalorieApp","id":"Kembali ke login CalorieApp","ur":"CalorieApp لاگ اِن پر واپس جائیں"},"pages":[7880]},{"source":"Open official Testnet faucet","translations":{"nl":"Officiële Testnet-faucet openen","zh-Hans":"打开官方 Testnet 水龙头","hi":"आधिकारिक Testnet फ़ॉसेट खोलें","es":"Abrir faucet oficial de Testnet","ar":"فتح صنبور Testnet الرسمي","fr":"Ouvrir le faucet Testnet officiel","bn":"অফিসিয়াল Testnet ফসেট খুলুন","pt":"Abrir faucet oficial da Testnet","id":"Buka faucet Testnet resmi","ur":"سرکاری Testnet فاسٹ کھولیں"},"pages":[7880]},{"source":"Create a free test account","translations":{"nl":"Maak een gratis testaccount","zh-Hans":"创建免费测试账户","hi":"मुफ़्त टेस्ट खाता बनाएँ","es":"Crea una cuenta de prueba gratuita","ar":"أنشئ حساب اختبار مجانيًا","fr":"Créez un compte de test gratuit","bn":"বিনামূল্যে পরীক্ষার অ্যাকাউন্ট তৈরি করুন","pt":"Crie uma conta de teste gratuita","id":"Buat akun uji gratis","ur":"مفت ٹیسٹ اکاؤنٹ بنائیں"},"pages":[7880]},{"source":"Choose Testnet in Xaman","translations":{"nl":"Kies Testnet in Xaman","zh-Hans":"在 Xaman 中选择 Testnet","hi":"Xaman में Testnet चुनें","es":"Elige Testnet en Xaman","ar":"اختر Testnet في Xaman","fr":"Choisissez Testnet dans Xaman","bn":"Xaman-এ Testnet বেছে নিন","pt":"Escolha Testnet no Xaman","id":"Pilih Testnet di Xaman","ur":"Xaman میں Testnet منتخب کریں"},"pages":[7880]},{"source":"Add your account to Xaman","translations":{"nl":"Voeg je account toe aan Xaman","zh-Hans":"将账户添加到 Xaman","hi":"अपना खाता Xaman में जोड़ें","es":"Añade tu cuenta a Xaman","ar":"أضف حسابك إلى Xaman","fr":"Ajoutez votre compte à Xaman","bn":"Xaman-এ আপনার অ্যাকাউন্ট যোগ করুন","pt":"Adicione a sua conta ao Xaman","id":"Tambahkan akun Anda ke Xaman","ur":"اپنا اکاؤنٹ Xaman میں شامل کریں"},"pages":[7880]},{"source":"Return to CalorieApp","translations":{"nl":"Ga terug naar CalorieApp","zh-Hans":"返回 CalorieApp","hi":"CalorieApp पर लौटें","es":"Vuelve a CalorieApp","ar":"عُد إلى CalorieApp","fr":"Revenez à CalorieApp","bn":"CalorieApp-এ ফিরুন","pt":"Volte ao CalorieApp","id":"Kembali ke CalorieApp","ur":"CalorieApp پر واپس آئیں"},"pages":[7880]},{"source":"Back one step","translations":{"nl":"Een stap terug","zh-Hans":"上一步","hi":"पिछला चरण","es":"Paso anterior","ar":"الخطوة السابقة","fr":"Étape précédente","bn":"আগের ধাপ","pt":"Passo anterior","id":"Langkah sebelumnya","ur":"پچھلا مرحلہ"},"pages":[7880]},{"source":"Next step","translations":{"nl":"Volgende stap","zh-Hans":"下一步","hi":"अगला चरण","es":"Siguiente paso","ar":"الخطوة التالية","fr":"Étape suivante","bn":"পরের ধাপ","pt":"Próximo passo","id":"Langkah berikutnya","ur":"اگلا مرحلہ"},"pages":[7880]},{"source":"Show the Testnet instructions","translations":{"nl":"Toon de Testnet-uitleg","zh-Hans":"查看 Testnet 说明","hi":"Testnet के निर्देश दिखाएँ","es":"Ver las instrucciones de Testnet","ar":"عرض تعليمات Testnet","fr":"Voir les instructions Testnet","bn":"Testnet-এর নির্দেশ দেখুন","pt":"Ver as instruções de Testnet","id":"Lihat petunjuk Testnet","ur":"Testnet کی ہدایات دیکھیں"},"pages":[7880]},{"source":"How is this account created?","translations":{"nl":"Hoe wordt dit account gemaakt?","zh-Hans":"这个账户如何创建?","hi":"यह खाता कैसे बनता है?","es":"¿Cómo se crea esta cuenta?","ar":"كيف يُنشأ هذا الحساب؟","fr":"Comment ce compte est-il créé ?","bn":"এই অ্যাকাউন্ট কীভাবে তৈরি হয়?","pt":"Como é criada esta conta?","id":"Bagaimana akun ini dibuat?","ur":"یہ اکاؤنٹ کیسے بنتا ہے؟"},"pages":[7880]},{"source":"This is a test account, not pilot registration or retailer access.","translations":{"nl":"Dit is een testaccount, geen pilotinschrijving of winkelierstoegang.","zh-Hans":"这是测试账户,不会注册试点计划或授予商家权限。","hi":"यह टेस्ट खाता है, पायलट पंजीकरण या विक्रेता की पहुँच नहीं।","es":"Es una cuenta de prueba, no una inscripción a un piloto ni acceso de comercio.","ar":"هذا حساب اختبار، وليس تسجيلًا في برنامج تجريبي أو صلاحية تاجر.","fr":"Il s’agit d’un compte de test, pas d’une inscription à un pilote ni d’un accès commerçant.","bn":"এটি পরীক্ষার অ্যাকাউন্ট, পাইলটে নিবন্ধন বা বিক্রেতার প্রবেশাধিকার নয়।","pt":"É uma conta de teste, não uma inscrição num piloto nem acesso de comerciante.","id":"Ini akun uji, bukan pendaftaran pilot atau akses pedagang.","ur":"یہ ٹیسٹ اکاؤنٹ ہے، پائلٹ میں اندراج یا دکاندار کی رسائی نہیں۔"},"pages":[7880]},{"source":"{project} roadmap","translations":{"zh-Hans":"{project} 路线图","hi":"{project} की कार्ययोजना","es":"Hoja de ruta de {project}","ar":"خارطة طريق {project}","fr":"Feuille de route de {project}","bn":"{project}-এর রোডম্যাপ","pt":"Roteiro de {project}","id":"Peta jalan {project}","ur":"{project} کا روڈ میپ","nl":"Roadmap van {project}"},"pages":[5580,7699]},{"source":"Updated: {updated}","translations":{"zh-Hans":"更新日期:{updated}","hi":"अद्यतन: {updated}","es":"Actualización: {updated}","ar":"آخر تحديث: {updated}","fr":"Mise à jour : {updated}","bn":"হালনাগাদ: {updated}","pt":"Atualização: {updated}","id":"Diperbarui: {updated}","ur":"تازہ کاری: {updated}","nl":"Bijgewerkt: {updated}"},"pages":[5580,7699]},{"source":"8 September 2026","translations":{"zh-Hans":"2026年9月8日","hi":"8 सितंबर 2026","es":"8 de septiembre de 2026","ar":"8 سبتمبر 2026","fr":"8 septembre 2026","bn":"৮ সেপ্টেম্বর ২০২৬","pt":"8 de setembro de 2026","id":"8 September 2026","ur":"8 ستمبر 2026","nl":"8 september 2026"},"pages":[5580,7699]},{"source":"These eight milestones describe the project’s history, the current preview and future work. Future work has no guaranteed delivery date.","translations":{"zh-Hans":"这八个里程碑介绍了项目历史、当前预览版和未来工作。未来工作的交付日期不作保证。","hi":"ये आठ पड़ाव परियोजना के इतिहास, मौजूदा पूर्वावलोकन और भविष्य के काम का वर्णन करते हैं। भविष्य के काम के पूरा होने की कोई निश्चित तारीख नहीं दी गई है।","es":"Estos ocho hitos describen la historia del proyecto, la versión preliminar actual y el trabajo futuro. No se garantiza una fecha de entrega para el trabajo futuro.","ar":"تصف هذه المحطات الثماني تاريخ المشروع والنسخة التجريبية الحالية والعمل المستقبلي. لا يوجد موعد تسليم مضمون للعمل المستقبلي.","fr":"Ces huit étapes décrivent l’histoire du projet, la version de prévisualisation actuelle et les travaux futurs. Aucune date de livraison n’est garantie pour les travaux futurs.","bn":"এই আটটি মাইলফলক প্রকল্পের ইতিহাস, বর্তমান প্রিভিউ এবং ভবিষ্যৎ কাজের বর্ণনা দেয়। ভবিষ্যৎ কাজ শেষ করার কোনো নির্দিষ্ট তারিখের নিশ্চয়তা দেওয়া হয়নি।","pt":"Estes oito marcos descrevem a história do projeto, a pré-visualização atual e os trabalhos futuros. Não há garantia de uma data de entrega para os trabalhos futuros.","id":"Delapan tonggak ini menjelaskan sejarah proyek, pratinjau saat ini, dan pekerjaan mendatang. Tidak ada jaminan tanggal penyelesaian untuk pekerjaan mendatang.","ur":"یہ آٹھ سنگ میل منصوبے کی تاریخ، موجودہ پیش نظارہ اور آئندہ کام بیان کرتے ہیں۔ آئندہ کام کی تکمیل کی کوئی تاریخ ضمانت کے ساتھ نہیں دی گئی۔","nl":"Deze acht mijlpalen beschrijven de geschiedenis van het project, de huidige preview en toekomstig werk. Voor toekomstig werk is geen opleverdatum gegarandeerd."},"pages":[5580,7699]},{"source":"Milestone","translations":{"zh-Hans":"里程碑","hi":"पड़ाव","es":"Hito","ar":"المحطة","fr":"Étape","bn":"মাইলফলক","pt":"Marco","id":"Tonggak","ur":"سنگ میل","nl":"Mijlpaal"},"pages":[5580,7699]},{"source":"Status","translations":{"zh-Hans":"状态","hi":"स्थिति","es":"Estado","ar":"الحالة","fr":"Statut","bn":"অবস্থা","pt":"Estado","id":"Status","ur":"حالت","nl":"Status"},"pages":[5580,7699]},{"source":"Meaning","translations":{"zh-Hans":"说明","hi":"विवरण","es":"Descripción","ar":"الوصف","fr":"Description","bn":"বিবরণ","pt":"Descrição","id":"Penjelasan","ur":"وضاحت","nl":"Toelichting"},"pages":[5580,7699]},{"source":"Origins","translations":{"zh-Hans":"项目起源","hi":"शुरुआत","es":"Orígenes","ar":"البدايات","fr":"Origines","bn":"সূচনা","pt":"Origens","id":"Awal mula","ur":"آغاز","nl":"Ontstaan"},"pages":[5580,7699]},{"source":"Historical","translations":{"zh-Hans":"历史阶段","hi":"ऐतिहासिक","es":"Histórico","ar":"تاريخية","fr":"Historique","bn":"ঐতিহাসিক","pt":"Histórico","id":"Historis","ur":"تاریخی","nl":"Historisch"},"pages":[5580,7699]},{"source":"The food-focused project and community began in 2021.","translations":{"zh-Hans":"以食品为核心的项目及其社区始于2021年。","hi":"भोजन पर केंद्रित परियोजना और समुदाय की शुरुआत 2021 में हुई।","es":"El proyecto centrado en la alimentación y su comunidad comenzaron en 2021.","ar":"بدأ المشروع الذي يركز على الغذاء ومجتمعه في عام 2021.","fr":"Le projet consacré à l’alimentation et sa communauté ont débuté en 2021.","bn":"খাদ্যকে কেন্দ্র করে প্রকল্প ও কমিউনিটির যাত্রা শুরু হয় ২০২১ সালে।","pt":"O projeto voltado à alimentação e a sua comunidade começaram em 2021.","id":"Proyek yang berfokus pada pangan beserta komunitasnya dimulai pada 2021.","ur":"خوراک پر مرکوز منصوبے اور اس کی کمیونٹی کا آغاز 2021 میں ہوا۔","nl":"Het project rond voeding en de community begonnen in 2021."},"pages":[5580,7699]},{"source":"Early prototypes","translations":{"zh-Hans":"早期原型","hi":"शुरुआती प्रोटोटाइप","es":"Primeros prototipos","ar":"النماذج الأولية المبكرة","fr":"Premiers prototypes","bn":"প্রাথমিক প্রোটোটাইপ","pt":"Primeiros protótipos","id":"Prototipe awal","ur":"ابتدائی نمونے","nl":"Eerste prototypes"},"pages":[5580,7699]},{"source":"Earlier Testnet and mobile experiments informed the project.","translations":{"zh-Hans":"早期的 Testnet 和移动端实验为项目提供了经验。","hi":"पहले के Testnet और मोबाइल प्रयोगों से परियोजना को सीख मिली।","es":"Los primeros experimentos con Testnet y dispositivos móviles aportaron conocimientos al proyecto.","ar":"أسهمت التجارب السابقة على Testnet والأجهزة المحمولة في تطوير فهم المشروع.","fr":"Les premières expérimentations sur Testnet et sur mobile ont apporté des enseignements au projet.","bn":"আগের Testnet ও মোবাইল পরীক্ষাগুলো থেকে প্রকল্পটি অভিজ্ঞতা অর্জন করেছে।","pt":"As experiências anteriores com Testnet e dispositivos móveis contribuíram para o projeto.","id":"Eksperimen Testnet dan perangkat seluler terdahulu memberikan pembelajaran bagi proyek.","ur":"پہلے کے Testnet اور موبائل تجربات سے منصوبے کو رہنمائی ملی۔","nl":"Eerdere experimenten met Testnet en mobiele apps leverden inzichten op voor het project."},"pages":[5580,7699]},{"source":"Food webapp","translations":{"zh-Hans":"食品网页应用","hi":"भोजन संबंधी वेब ऐप","es":"Aplicación web de alimentación","ar":"تطبيق الويب الخاص بالغذاء","fr":"Application web alimentaire","bn":"খাদ্যবিষয়ক ওয়েব অ্যাপ","pt":"Aplicação web de alimentação","id":"Aplikasi web pangan","ur":"خوراک کی ویب ایپ","nl":"Voedingswebapp"},"pages":[5580,7699]},{"source":"Available preview","translations":{"zh-Hans":"预览版已开放","hi":"पूर्वावलोकन उपलब्ध","es":"Versión preliminar disponible","ar":"نسخة تجريبية متاحة","fr":"Prévisualisation disponible","bn":"প্রিভিউ উপলব্ধ","pt":"Pré-visualização disponível","id":"Pratinjau tersedia","ur":"پیش نظارہ دستیاب","nl":"Preview beschikbaar"},"pages":[5580,7699]},{"source":"Food search, nutrition, portions and personal food logs.","translations":{"zh-Hans":"食品搜索、营养信息、份量选择和个人饮食记录。","hi":"खाद्य खोज, पोषण, मात्रा और व्यक्तिगत भोजन रिकॉर्ड।","es":"Búsqueda de alimentos, nutrición, porciones y registros personales de alimentación.","ar":"البحث عن الأغذية، ومعلومات التغذية، والحصص، وسجلات الطعام الشخصية.","fr":"Recherche d’aliments, nutrition, portions et journaux alimentaires personnels.","bn":"খাবার খোঁজা, পুষ্টির তথ্য, পরিমাণ এবং ব্যক্তিগত খাদ্য লগ।","pt":"Pesquisa de alimentos, nutrição, porções e registos alimentares pessoais.","id":"Pencarian makanan, nutrisi, porsi, dan catatan makanan pribadi.","ur":"کھانے کی تلاش، غذائیت، حصوں کی مقدار اور ذاتی غذائی ریکارڈ۔","nl":"Voedingsmiddelen zoeken, voedingsinformatie, porties en een persoonlijk voedingsdagboek."},"pages":[5580,7699]},{"source":"Connected sign-in","translations":{"zh-Hans":"网站与应用联合登录","hi":"संयुक्त साइन-इन","es":"Inicio de sesión conjunto","ar":"تسجيل الدخول المشترك","fr":"Connexion commune","bn":"সমন্বিত সাইন-ইন","pt":"Início de sessão conjunto","id":"Login terpadu","ur":"مشترکہ سائن اِن","nl":"Gezamenlijk inloggen"},"pages":[5580,7699]},{"source":"Available","translations":{"zh-Hans":"已提供","hi":"उपलब्ध","es":"Disponible","ar":"متاح","fr":"Disponible","bn":"উপলব্ধ","pt":"Disponível","id":"Tersedia","ur":"دستیاب","nl":"Beschikbaar"},"pages":[5580,7699]},{"source":"Website and CalorieApp sign-in and logout in the same browser.","translations":{"zh-Hans":"在同一浏览器中登录和退出网站及 CalorieApp。","hi":"एक ही ब्राउज़र में वेबसाइट और CalorieApp में साइन-इन और साइन-आउट।","es":"Inicio y cierre de sesión del sitio web y de CalorieApp en el mismo navegador.","ar":"تسجيل الدخول إلى الموقع وCalorieApp والخروج منهما في المتصفح نفسه.","fr":"Connexion et déconnexion du site et de CalorieApp dans le même navigateur.","bn":"একই ব্রাউজারে ওয়েবসাইট ও CalorieApp-এ সাইন-ইন এবং সাইন-আউট।","pt":"Início e fim de sessão do site e do CalorieApp no mesmo navegador.","id":"Login dan logout situs web serta CalorieApp dalam peramban yang sama.","ur":"ایک ہی براؤزر میں ویب سائٹ اور CalorieApp میں سائن اِن اور سائن آؤٹ۔","nl":"In- en uitloggen op de website en in CalorieApp in dezelfde browser."},"pages":[5580,7699]},{"source":"Website refresh","translations":{"zh-Hans":"网站完善","hi":"वेबसाइट में सुधार","es":"Mejora del sitio web","ar":"تحسين الموقع","fr":"Amélioration du site","bn":"ওয়েবসাইটের উন্নয়ন","pt":"Melhoria do site","id":"Penyempurnaan situs web","ur":"ویب سائٹ کی بہتری","nl":"Website verfijnen"},"pages":[5580,7699]},{"source":"In progress","translations":{"zh-Hans":"进行中","hi":"जारी","es":"En curso","ar":"جارٍ العمل","fr":"En cours","bn":"চলমান","pt":"Em curso","id":"Sedang dikerjakan","ur":"جاری","nl":"In uitvoering"},"pages":[5580,7699]},{"source":"Consistent content and presentation, with publications and old links being consolidated.","translations":{"zh-Hans":"统一内容与呈现方式,并整理出版物和旧链接。","hi":"सामग्री और प्रस्तुति में एकरूपता, साथ ही प्रकाशनों और पुराने लिंक को व्यवस्थित करना।","es":"Contenido y presentación coherentes, con la consolidación de publicaciones y enlaces antiguos.","ar":"توحيد المحتوى والعرض مع تنظيم المنشورات والروابط القديمة.","fr":"Harmonisation du contenu et de la présentation, avec consolidation des publications et des anciens liens.","bn":"প্রকাশনা ও পুরোনো লিংকগুলো গুছিয়ে বিষয়বস্তু ও উপস্থাপনায় সামঞ্জস্য আনা।","pt":"Conteúdo e apresentação consistentes, com a consolidação de publicações e ligações antigas.","id":"Konten dan tampilan yang konsisten, sambil merapikan publikasi dan tautan lama.","ur":"مواد اور پیشکش میں یکسانیت، ساتھ ہی اشاعتوں اور پرانے روابط کی ترتیب۔","nl":"Consistente inhoud en vormgeving, met het samenbrengen van publicaties en oude verwijzingen."},"pages":[5580,7699]},{"source":"Community preview","translations":{"zh-Hans":"社区预览","hi":"समुदाय के लिए पूर्वावलोकन","es":"Presentación a la comunidad","ar":"عرض تجريبي للمجتمع","fr":"Présentation à la communauté","bn":"কমিউনিটির জন্য প্রিভিউ","pt":"Pré-visualização para a comunidade","id":"Pratinjau komunitas","ur":"کمیونٹی کے لیے پیش نظارہ","nl":"Preview voor de community"},"pages":[5580,7699]},{"source":"Next","translations":{"zh-Hans":"下一页","hi":"अगला","es":"Siguiente","ar":"التالي","fr":"Suivant","bn":"পরের","pt":"Seguinte","id":"Berikutnya","ur":"اگلا","nl":"Volgende"},"pages":[5580,6897,6914,6926,6937,7699]},{"source":"Show the current experience and collect feedback.","translations":{"zh-Hans":"展示当前使用体验并收集反馈。","hi":"मौजूदा अनुभव दिखाना और प्रतिक्रिया लेना।","es":"Mostrar la experiencia actual y recoger comentarios.","ar":"عرض التجربة الحالية وجمع الملاحظات.","fr":"Présenter l’expérience actuelle et recueillir des retours.","bn":"বর্তমান অভিজ্ঞতা দেখানো এবং মতামত সংগ্রহ করা।","pt":"Mostrar a experiência atual e recolher comentários.","id":"Menampilkan pengalaman saat ini dan mengumpulkan masukan.","ur":"موجودہ تجربہ دکھانا اور رائے جمع کرنا۔","nl":"De huidige ervaring tonen en feedback verzamelen."},"pages":[5580,7699]},{"source":"Product refinement","translations":{"zh-Hans":"产品完善","hi":"उत्पाद में सुधार","es":"Mejora del producto","ar":"تحسين المنتج","fr":"Amélioration du produit","bn":"পণ্যের উন্নয়ন","pt":"Melhoria do produto","id":"Penyempurnaan produk","ur":"مصنوعے میں بہتری","nl":"Product verbeteren"},"pages":[5580,7699]},{"source":"Planned","translations":{"zh-Hans":"已规划","hi":"नियोजित","es":"Planificado","ar":"مخطط له","fr":"Prévu","bn":"পরিকল্পিত","pt":"Planeado","id":"Direncanakan","ur":"منصوبہ بند","nl":"Gepland"},"pages":[5580,7699]},{"source":"Prioritise improvements from observed use.","translations":{"zh-Hans":"根据实际使用情况确定改进的优先顺序。","hi":"वास्तविक उपयोग से मिली जानकारी के आधार पर सुधारों को प्राथमिकता देना।","es":"Priorizar las mejoras a partir del uso observado.","ar":"تحديد أولويات التحسينات بناءً على الاستخدام الفعلي المرصود.","fr":"Donner la priorité aux améliorations issues de l’usage observé.","bn":"বাস্তব ব্যবহার পর্যবেক্ষণ করে উন্নয়নের অগ্রাধিকার নির্ধারণ করা।","pt":"Dar prioridade às melhorias com base na utilização observada.","id":"Memprioritaskan perbaikan berdasarkan penggunaan yang diamati.","ur":"مشاہدہ شدہ استعمال کی بنیاد پر بہتریوں کو ترجیح دینا۔","nl":"Verbeteringen prioriteren op basis van waargenomen gebruik."},"pages":[5580,7699]},{"source":"Future research","translations":{"zh-Hans":"未来研究","hi":"भविष्य का अनुसंधान","es":"Investigación futura","ar":"البحث المستقبلي","fr":"Recherches futures","bn":"ভবিষ্যৎ গবেষণা","pt":"Investigação futura","id":"Riset mendatang","ur":"آئندہ تحقیق","nl":"Toekomstig onderzoek"},"pages":[5580,7699]},{"source":"Research","translations":{"zh-Hans":"研究阶段","hi":"अनुसंधान","es":"Investigación","ar":"قيد البحث","fr":"Recherche","bn":"গবেষণা","pt":"Investigação","id":"Riset","ur":"تحقیق","nl":"Onderzoek"},"pages":[5580,7699]},{"source":"Investigate food-data and ecosystem ideas separately.","translations":{"zh-Hans":"单独研究食品数据和生态系统相关构想。","hi":"खाद्य डेटा और इकोसिस्टम से जुड़े विचारों की अलग से जाँच करना।","es":"Investigar por separado las ideas sobre datos alimentarios y el ecosistema.","ar":"بحث أفكار بيانات الغذاء والنظام البيئي بصورة منفصلة.","fr":"Étudier séparément les idées relatives aux données alimentaires et à l’écosystème.","bn":"খাদ্যসংক্রান্ত তথ্য ও ইকোসিস্টেমের ধারণাগুলো আলাদাভাবে অনুসন্ধান করা।","pt":"Investigar separadamente ideias sobre dados alimentares e o ecossistema.","id":"Mengkaji gagasan data pangan dan ekosistem secara terpisah.","ur":"خوراک کے ڈیٹا اور ایکو سسٹم کے خیالات کا الگ سے جائزہ لینا۔","nl":"Ideeën rond voedingsdata en het ecosysteem afzonderlijk onderzoeken."},"pages":[5580,7699]},{"source":"Detailed roadmap on GitHub","translations":{"zh-Hans":"查看 GitHub 上的详细路线图","hi":"GitHub पर विस्तृत कार्ययोजना","es":"Hoja de ruta detallada en GitHub","ar":"خارطة الطريق المفصلة على GitHub","fr":"Feuille de route détaillée sur GitHub","bn":"GitHub-এ বিস্তারিত রোডম্যাপ","pt":"Roteiro detalhado no GitHub","id":"Peta jalan terperinci di GitHub","ur":"GitHub پر تفصیلی روڈ میپ","nl":"Uitgebreide roadmap op GitHub"},"pages":[5580,7699]},{"source":"Historical roadmap 2023–2024","translations":{"zh-Hans":"历史路线图:2023–2024年","hi":"ऐतिहासिक कार्ययोजना 2023–2024","es":"Hoja de ruta histórica 2023–2024","ar":"خارطة الطريق التاريخية 2023–2024","fr":"Feuille de route historique 2023–2024","bn":"ঐতিহাসিক রোডম্যাপ ২০২৩–২০২৪","pt":"Roteiro histórico 2023–2024","id":"Peta jalan historis 2023–2024","ur":"تاریخی روڈ میپ 2023–2024","nl":"Historische roadmap 2023–2024"},"pages":[5580,7699]},{"source":"Historical material describes proposals and experiments from that period. It is not a statement of current services or delivery commitments.","translations":{"zh-Hans":"历史资料介绍的是当时的提案和实验,并不代表当前服务或交付承诺。","hi":"ऐतिहासिक सामग्री उस समय के प्रस्तावों और प्रयोगों का वर्णन करती है। यह मौजूदा सेवाओं या काम पूरा करने की प्रतिबद्धताओं का विवरण नहीं है।","es":"El material histórico describe propuestas y experimentos de aquella época. No constituye una descripción de los servicios actuales ni un compromiso de entrega.","ar":"تصف المواد التاريخية مقترحات وتجارب تلك الفترة. وهي لا تمثل بيانًا بالخدمات الحالية أو التزامات بتسليم أعمال مستقبلية.","fr":"Les documents historiques décrivent les propositions et expérimentations de cette période. Ils ne constituent ni une description des services actuels ni un engagement de livraison.","bn":"ঐতিহাসিক উপকরণে সেই সময়ের প্রস্তাব ও পরীক্ষার বিবরণ রয়েছে। এগুলো বর্তমান পরিষেবা বা ভবিষ্যৎ কাজ সরবরাহের অঙ্গীকার নয়।","pt":"O material histórico descreve propostas e experiências desse período. Não representa uma declaração de serviços atuais nem compromissos de entrega.","id":"Materi historis menjelaskan usulan dan eksperimen pada masa itu. Materi tersebut bukan pernyataan tentang layanan saat ini atau komitmen penyelesaian pekerjaan.","ur":"تاریخی مواد اُس دور کی تجاویز اور تجربات بیان کرتا ہے۔ یہ موجودہ خدمات یا کام کی فراہمی کے وعدوں کا بیان نہیں ہے۔","nl":"Historisch materiaal beschrijft voorstellen en experimenten uit die periode. Het beschrijft geen huidige diensten en bevat geen actuele oplevertoezeggingen."},"pages":[5580,7699]},{"source":"Historical Vote: {project} Reward Pool Vote","translations":{"zh-Hans":"历史投票:{project} 奖励池投票","hi":"ऐतिहासिक मतदान: {project} रिवॉर्ड पूल मतदान","es":"Votación histórica: fondo de recompensas de {project}","ar":"تصويت تاريخي: صندوق مكافآت {project}","fr":"Vote historique : fonds de récompenses de {project}","bn":"ঐতিহাসিক ভোট: {project} পুরস্কার তহবিলের ভোট","pt":"Votação histórica: fundo de recompensas de {project}","id":"Pemungutan suara historis: dana imbalan {project}","ur":"تاریخی ووٹ: {project} کے انعامی فنڈ کی ووٹنگ","nl":"Historische stemming: beloningspool van {project}"},"pages":[5580,7699]},{"source":"Description of the historical Voting","translations":{"zh-Hans":"历史投票说明","hi":"ऐतिहासिक मतदान का विवरण","es":"Descripción de la votación histórica","ar":"وصف التصويت التاريخي","fr":"Description du vote historique","bn":"ঐতিহাসিক ভোটের বিবরণ","pt":"Descrição da votação histórica","id":"Penjelasan pemungutan suara historis","ur":"تاریخی ووٹنگ کی وضاحت","nl":"Toelichting op de historische stemming"},"pages":[5580,7699]},{"source":"Voting ran from 1 November 2024 to 8 November 2024.","translations":{"zh-Hans":"投票时间为2024年11月1日至2024年11月8日。","hi":"मतदान 1 नवंबर 2024 से 8 नवंबर 2024 तक चला था।","es":"La votación tuvo lugar del 1 al 8 de noviembre de 2024.","ar":"جرى التصويت من 1 نوفمبر 2024 إلى 8 نوفمبر 2024.","fr":"Le vote s’est déroulé du 1er au 8 novembre 2024.","bn":"ভোট চলেছিল ১ নভেম্বর ২০২৪ থেকে ৮ নভেম্বর ২০২৪ পর্যন্ত।","pt":"A votação decorreu de 1 a 8 de novembro de 2024.","id":"Pemungutan suara berlangsung dari 1 November 2024 hingga 8 November 2024.","ur":"ووٹنگ 1 نومبر 2024 سے 8 نومبر 2024 تک ہوئی تھی۔","nl":"De stemming liep van 1 november 2024 tot 8 november 2024."},"pages":[5580,7699]},{"source":"This page preserves the November 2024 reward-pool proposal and its displayed results. The voting period shown below has ended. Historical figures are not a current giveaway budget or confirmation of funds available.","translations":{"zh-Hans":"本页保留了2024年11月的奖励池提案及页面显示的投票结果。下方所示的投票期已经结束。历史数字不代表当前赠送活动的预算,也不确认目前有多少资金可用。","hi":"यह पृष्ठ नवंबर 2024 के रिवॉर्ड पूल प्रस्ताव और यहाँ दिखाए गए परिणामों को सुरक्षित रखता है। नीचे दी गई मतदान अवधि समाप्त हो चुकी है। ऐतिहासिक आँकड़े मौजूदा गिवअवे बजट या उपलब्ध धन की पुष्टि नहीं हैं।","es":"Esta página conserva la propuesta del fondo de recompensas de noviembre de 2024 y los resultados que se muestran. El periodo de votación indicado a continuación ha finalizado. Las cifras históricas no son un presupuesto actual para obsequios ni confirman la disponibilidad de fondos.","ar":"تحتفظ هذه الصفحة بمقترح صندوق المكافآت من نوفمبر 2024 والنتائج المعروضة له. انتهت فترة التصويت الموضحة أدناه. الأرقام التاريخية ليست ميزانية حالية للهدايا ولا تأكيدًا لتوفر الأموال.","fr":"Cette page conserve la proposition de fonds de récompenses de novembre 2024 et les résultats affichés. La période de vote indiquée ci-dessous est terminée. Les chiffres historiques ne constituent pas un budget actuel de distributions gratuites ni une confirmation des fonds disponibles.","bn":"এই পাতায় নভেম্বর ২০২৪-এর পুরস্কার তহবিলের প্রস্তাব ও প্রদর্শিত ফলাফল সংরক্ষিত আছে। নিচে দেখানো ভোটের সময়সীমা শেষ হয়েছে। ঐতিহাসিক অঙ্কগুলো বর্তমান উপহার বিতরণের বাজেট বা উপলব্ধ অর্থের নিশ্চয়তা নয়।","pt":"Esta página preserva a proposta do fundo de recompensas de novembro de 2024 e os resultados apresentados. O período de votação indicado abaixo terminou. Os valores históricos não são um orçamento atual para ofertas nem uma confirmação de fundos disponíveis.","id":"Halaman ini menyimpan usulan dana imbalan November 2024 beserta hasil yang ditampilkan. Masa pemungutan suara di bawah ini telah berakhir. Angka historis bukan anggaran giveaway saat ini atau konfirmasi dana yang tersedia.","ur":"یہ صفحہ نومبر 2024 کے انعامی فنڈ کی تجویز اور دکھائے گئے نتائج محفوظ رکھتا ہے۔ نیچے درج ووٹنگ کی مدت ختم ہو چکی ہے۔ تاریخی اعداد مفت تقسیم کے موجودہ بجٹ یا دستیاب فنڈز کی تصدیق نہیں ہیں۔","nl":"Deze pagina bewaart het voorstel voor de beloningspool van november 2024 en de getoonde uitslagen. De hieronder vermelde stemperiode is afgelopen. Historische cijfers zijn geen actueel giveawaybudget en bevestigen niet hoeveel middelen beschikbaar zijn."},"pages":[5580,7699]},{"source":"Donation steps","translations":{"zh-Hans":"捐赠步骤","hi":"दान के चरण","es":"Pasos de la donación","ar":"خطوات التبرع","fr":"Étapes du don","bn":"অনুদানের ধাপ","pt":"Passos do donativo","id":"Langkah donasi","ur":"عطیے کے مراحل","nl":"Donatiestappen"},"pages":["*"]},{"source":"Donation","translations":{"zh-Hans":"捐赠","hi":"दान","es":"Donación","ar":"التبرع","fr":"Don","bn":"অনুদান","pt":"Donativo","id":"Donasi","ur":"عطیہ","nl":"Donatie"},"pages":["*"]},{"source":"Amount","translations":{"zh-Hans":"金额","hi":"राशि","es":"Importe","ar":"المبلغ","fr":"Montant","bn":"পরিমাণ","pt":"Montante","id":"Jumlah","ur":"رقم","nl":"Bedrag"},"pages":["*"]},{"source":"Cart","translations":{"zh-Hans":"购物车","hi":"कार्ट","es":"Carrito","ar":"السلة","fr":"Panier","bn":"কার্ট","pt":"Carrinho","id":"Keranjang","ur":"کارٹ","nl":"Winkelwagen"},"pages":["*"]},{"source":"Checkout","translations":{"zh-Hans":"结账","hi":"चेकआउट","es":"Finalizar compra","ar":"إتمام الدفع","fr":"Paiement","bn":"চেকআউট","pt":"Finalizar compra","id":"Pembayaran","ur":"ادائیگی","nl":"Afrekenen"},"pages":["*"]},{"source":"What is the mission of CalorieToken?","translations":{"nl":"Wat is de missie van CalorieToken?","es":"¿Cuál es la misión de CalorieToken?","fr":"Quelle est la mission de CalorieToken ?","pt":"Qual é a missão do CalorieToken?","id":"Apa misi CalorieToken?","zh-Hans":"CalorieToken 的使命是什么?","hi":"CalorieToken का मिशन क्या है?","ar":"ما مهمة CalorieToken؟","bn":"CalorieToken-এর লক্ষ্য কী?","ur":"CalorieToken کا مقصد کیا ہے؟"},"pages":[6855]},{"source":"CalorieToken explores digital tools for the food and beverage sector. The current CalorieApp preview focuses on food search, nutrition information and a personal food log. Payment applications and business integrations belong to the wider project vision.","translations":{"nl":"CalorieToken verkent digitale hulpmiddelen voor de voedings- en drankensector. De huidige CalorieApp-preview richt zich op voedingsmiddelen zoeken, voedingsinformatie en een persoonlijk voedingsdagboek. Betaaltoepassingen en zakelijke integraties horen bij de bredere projectvisie.","es":"CalorieToken explora herramientas digitales para el sector de alimentos y bebidas. La vista previa actual de CalorieApp se centra en buscar alimentos, consultar información nutricional y llevar un diario personal. Las aplicaciones de pago y las integraciones empresariales forman parte de la visión más amplia del proyecto.","fr":"CalorieToken explore des outils numériques pour le secteur de l’alimentation et des boissons. L’aperçu actuel de CalorieApp propose la recherche d’aliments, des informations nutritionnelles et un journal alimentaire personnel. Les paiements et les intégrations professionnelles font partie de la vision élargie du projet.","pt":"O CalorieToken explora ferramentas digitais para o setor de alimentos e bebidas. A prévia atual do CalorieApp concentra-se na pesquisa de alimentos, informação nutricional e diário alimentar pessoal. Aplicações de pagamento e integrações empresariais fazem parte da visão mais ampla do projeto.","id":"CalorieToken menjajaki alat digital untuk sektor makanan dan minuman. Pratinjau CalorieApp saat ini berfokus pada pencarian makanan, informasi gizi, dan catatan makanan pribadi. Aplikasi pembayaran dan integrasi bisnis termasuk dalam visi proyek yang lebih luas.","zh-Hans":"CalorieToken 探索面向食品饮料行业的数字工具。目前的 CalorieApp 预览版专注于食品搜索、营养信息和个人饮食记录。支付应用及企业集成属于项目更广泛的愿景。","hi":"CalorieToken खाद्य और पेय क्षेत्र के लिए डिजिटल साधनों की संभावनाएँ तलाशता है। CalorieApp का वर्तमान प्रीव्यू भोजन खोज, पोषण संबंधी जानकारी और व्यक्तिगत भोजन डायरी पर केंद्रित है। भुगतान ऐप और व्यावसायिक एकीकरण परियोजना की व्यापक दृष्टि का हिस्सा हैं।","ar":"يستكشف CalorieToken أدوات رقمية لقطاع الأغذية والمشروبات. تركز المعاينة الحالية لـ CalorieApp على البحث عن الطعام والمعلومات الغذائية وسجل غذائي شخصي. تنتمي تطبيقات الدفع والتكاملات التجارية إلى رؤية المشروع الأوسع.","bn":"CalorieToken খাদ্য ও পানীয় খাতের জন্য ডিজিটাল সরঞ্জাম নিয়ে কাজের সম্ভাবনা যাচাই করে। CalorieApp-এর বর্তমান প্রিভিউ খাবার খোঁজা, পুষ্টির তথ্য এবং ব্যক্তিগত খাদ্য ডায়েরির ওপর জোর দেয়। পেমেন্ট অ্যাপ ও ব্যবসায়িক সমন্বয় প্রকল্পের বৃহত্তর পরিকল্পনার অংশ।","ur":"CalorieToken خوراک اور مشروبات کے شعبے کے لیے ڈیجیٹل سہولتوں کے امکانات تلاش کرتا ہے۔ CalorieApp کی موجودہ پیشگی جھلک خوراک کی تلاش، غذائی معلومات اور ذاتی خوراک ڈائری پر مرکوز ہے۔ ادائیگی کی ایپس اور کاروباری انضمام منصوبے کے وسیع تر تصور کا حصہ ہیں۔"},"pages":[6855]},{"source":"What is Calorie and what does it aim to achieve?","translations":{"nl":"Wat is Calorie en wat wil het bereiken?","es":"¿Qué es Calorie y qué pretende conseguir?","fr":"Qu’est-ce que Calorie et quel est son objectif ?","pt":"O que é Calorie e o que pretende alcançar?","id":"Apa itu Calorie dan apa tujuannya?","zh-Hans":"Calorie 是什么?它希望实现什么目标?","hi":"Calorie क्या है और उसका लक्ष्य क्या है?","ar":"ما Calorie وما هدفه؟","bn":"Calorie কী এবং এর উদ্দেশ্য কী?","ur":"Calorie کیا ہے اور اس کا ہدف کیا ہے؟"},"pages":[6855]},{"source":"Calorie (CAL) is the project's token on the XRP Ledger. Food and beverage applications are its long-term focus. Buying or holding CAL is not required to search for food or keep a food log in CalorieApp.","translations":{"nl":"Calorie (CAL) is de token van het project op de XRP Ledger. Toepassingen voor voeding en dranken zijn het doel op langere termijn. Je hoeft geen CAL te kopen of te bezitten om voeding te zoeken of een voedingsdagboek bij te houden in CalorieApp.","es":"Calorie (CAL) es el token del proyecto en XRP Ledger. Su enfoque a largo plazo son las aplicaciones de alimentos y bebidas. No es necesario comprar ni tener CAL para buscar alimentos o llevar un diario en CalorieApp.","fr":"Calorie (CAL) est le jeton du projet sur XRP Ledger. Les applications alimentaires constituent son objectif à long terme. Il n’est pas nécessaire d’acheter ou de détenir des CAL pour rechercher des aliments ou tenir un journal dans CalorieApp.","pt":"Calorie (CAL) é o token do projeto no XRP Ledger. Aplicações para alimentos e bebidas são o seu foco a longo prazo. Não é necessário comprar ou possuir CAL para pesquisar alimentos ou manter um diário no CalorieApp.","id":"Calorie (CAL) adalah token proyek di XRP Ledger. Aplikasi makanan dan minuman menjadi fokus jangka panjangnya. Anda tidak perlu membeli atau memiliki CAL untuk mencari makanan atau membuat catatan makanan di CalorieApp.","zh-Hans":"Calorie(CAL)是项目在 XRP Ledger 上的代币,长期关注食品饮料应用。使用 CalorieApp 搜索食品或记录饮食,无须购买或持有 CAL。","hi":"Calorie (CAL), XRP Ledger पर परियोजना का टोकन है। खाद्य और पेय संबंधी उपयोग इसका दीर्घकालिक लक्ष्य हैं। CalorieApp में भोजन खोजने या डायरी रखने के लिए CAL खरीदना या रखना आवश्यक नहीं है।","ar":"Calorie ‏(CAL) هو رمز المشروع على XRP Ledger. وتُعد تطبيقات الأغذية والمشروبات هدفه طويل الأجل. لا يلزم شراء CAL أو امتلاكه للبحث عن الطعام أو الاحتفاظ بسجل غذائي في CalorieApp.","bn":"Calorie (CAL) হলো XRP Ledger-এ প্রকল্পের টোকেন। খাদ্য ও পানীয়সংক্রান্ত ব্যবহার এর দীর্ঘমেয়াদি লক্ষ্য। CalorieApp-এ খাবার খুঁজতে বা খাদ্য ডায়েরি রাখতে CAL কেনা বা রাখা প্রয়োজন নেই।","ur":"Calorie (CAL) منصوبے کا XRP Ledger پر ٹوکن ہے۔ خوراک اور مشروبات سے متعلق استعمال اس کا طویل مدتی ہدف ہے۔ CalorieApp میں خوراک تلاش کرنے یا ڈائری رکھنے کے لیے CAL خریدنا یا رکھنا ضروری نہیں۔"},"pages":[6855]},{"source":"What advantages does Calorie offer over traditional fiat money?","translations":{"nl":"Welke voordelen biedt Calorie ten opzichte van traditioneel geld?","es":"¿Qué ventajas ofrece Calorie frente al dinero tradicional?","fr":"Quels avantages Calorie offre-t-il par rapport à la monnaie traditionnelle ?","pt":"Quais são as vantagens do Calorie em relação ao dinheiro tradicional?","id":"Apa keunggulan Calorie dibandingkan uang konvensional?","zh-Hans":"与传统法定货币相比,Calorie 有哪些优势?","hi":"परंपरागत मुद्रा की तुलना में Calorie के क्या लाभ हैं?","ar":"ما مزايا Calorie مقارنة بالنقود التقليدية؟","bn":"প্রচলিত অর্থের তুলনায় Calorie কী সুবিধা দেয়?","ur":"روایتی رقم کے مقابلے میں Calorie کے کیا فائدے ہیں؟"},"pages":[6855]},{"source":"CAL uses the XRP Ledger for token transactions. Actual transaction costs and trading conditions depend on the transaction and service. A token does not guarantee savings, merchant acceptance or a stable value. Linking food data to payment transactions remains a research direction. The current CalorieApp preview provides food information and personal food logging.","translations":{"nl":"CAL gebruikt de XRP Ledger voor tokentransacties. De werkelijke transactiekosten en handelsvoorwaarden hangen af van de transactie en de dienst. Een token garandeert geen besparing, acceptatie door winkels of stabiele waarde. Voedingsgegevens koppelen aan betaaltransacties blijft een onderzoeksrichting. De huidige CalorieApp-preview biedt voedingsinformatie en een persoonlijk voedingsdagboek.","es":"CAL utiliza XRP Ledger para las transacciones de tokens. Los costes reales y las condiciones de negociación dependen de la transacción y del servicio. Un token no garantiza ahorro, aceptación comercial ni un valor estable. Vincular datos alimentarios con pagos sigue siendo una línea de investigación. La vista previa actual de CalorieApp ofrece información alimentaria y un diario personal.","fr":"CAL utilise XRP Ledger pour les transactions de jetons. Les coûts réels et les conditions de négociation dépendent de la transaction et du service. Un jeton ne garantit ni économies, ni acceptation commerciale, ni valeur stable. Relier les données alimentaires aux paiements reste un axe de recherche. L’aperçu actuel de CalorieApp fournit des informations alimentaires et un journal personnel.","pt":"CAL utiliza o XRP Ledger para transações de tokens. Os custos reais e as condições de negociação dependem da transação e do serviço. Um token não garante poupança, aceitação comercial ou valor estável. Associar dados alimentares a pagamentos continua sendo uma linha de investigação. A prévia atual do CalorieApp oferece informação alimentar e um diário pessoal.","id":"CAL memakai XRP Ledger untuk transaksi token. Biaya dan ketentuan perdagangan sebenarnya bergantung pada transaksi dan layanan. Token tidak menjamin penghematan, penerimaan pedagang, atau nilai yang stabil. Menghubungkan data makanan dengan pembayaran masih merupakan arah penelitian. Pratinjau CalorieApp saat ini menyediakan informasi makanan dan catatan makanan pribadi.","zh-Hans":"CAL 使用 XRP Ledger 进行代币交易。实际交易费用和交易条件取决于具体交易及服务。代币不保证节省费用、商家接受或价值稳定。将食品数据与支付交易关联仍是研究方向。目前的 CalorieApp 预览版提供食品信息和个人饮食记录。","hi":"CAL टोकन लेनदेन के लिए XRP Ledger का उपयोग करता है। वास्तविक शुल्क और व्यापार की शर्तें लेनदेन तथा सेवा पर निर्भर करती हैं। टोकन बचत, दुकानदारों द्वारा स्वीकार किए जाने या स्थिर मूल्य की गारंटी नहीं देता। खाद्य डेटा को भुगतान से जोड़ना अभी शोध की दिशा है। CalorieApp का वर्तमान प्रीव्यू भोजन की जानकारी और व्यक्तिगत भोजन डायरी देता है।","ar":"يستخدم CAL شبكة XRP Ledger لمعاملات الرموز. تعتمد التكاليف الفعلية وشروط التداول على المعاملة والخدمة. لا يضمن الرمز توفير المال أو قبول التجار أو استقرار القيمة. لا يزال ربط البيانات الغذائية بمعاملات الدفع اتجاهًا بحثيًا. تقدم معاينة CalorieApp الحالية معلومات غذائية وسجلًا غذائيًا شخصيًا.","bn":"CAL টোকেন লেনদেনের জন্য XRP Ledger ব্যবহার করে। প্রকৃত খরচ ও বাণিজ্যের শর্ত লেনদেন এবং পরিষেবার ওপর নির্ভর করে। টোকেন সাশ্রয়, ব্যবসায়ীর গ্রহণযোগ্যতা বা স্থিতিশীল মূল্যের নিশ্চয়তা দেয় না। খাদ্যতথ্যের সঙ্গে পেমেন্ট যুক্ত করা এখনো গবেষণার বিষয়। CalorieApp-এর বর্তমান প্রিভিউ খাদ্যতথ্য ও ব্যক্তিগত খাদ্য ডায়েরি দেয়।","ur":"CAL ٹوکن لین دین کے لیے XRP Ledger استعمال کرتا ہے۔ اصل فیس اور تجارتی شرائط لین دین اور سروس پر منحصر ہیں۔ ٹوکن بچت، تاجروں کی قبولیت یا مستحکم قدر کی ضمانت نہیں دیتا۔ غذائی ڈیٹا کو ادائیگیوں سے جوڑنا اب بھی تحقیق کی سمت ہے۔ موجودہ CalorieApp پیشگی جھلک غذائی معلومات اور ذاتی خوراک ڈائری فراہم کرتی ہے۔"},"pages":[6855]},{"source":"How does Calorie plan to address the needs of the food and beverage industry?","translations":{"nl":"Hoe wil Calorie inspelen op de behoeften van de voedings- en drankensector?","es":"¿Cómo pretende Calorie atender las necesidades del sector alimentario?","fr":"Comment Calorie prévoit-il de répondre aux besoins du secteur alimentaire ?","pt":"Como o Calorie pretende responder às necessidades do setor alimentar?","id":"Bagaimana Calorie berencana memenuhi kebutuhan industri makanan dan minuman?","zh-Hans":"Calorie 计划如何满足食品饮料行业的需求?","hi":"Calorie खाद्य और पेय उद्योग की जरूरतें कैसे पूरी करना चाहता है?","ar":"كيف يخطط Calorie لتلبية احتياجات قطاع الأغذية والمشروبات؟","bn":"Calorie খাদ্য ও পানীয় শিল্পের প্রয়োজন কীভাবে মেটাতে চায়?","ur":"Calorie خوراک اور مشروبات کی صنعت کی ضروریات کیسے پوری کرنا چاہتا ہے؟"},"pages":[6855]},{"source":"The use-case illustrations explore delivery, shops, hospitality and supply chains. They describe concepts. Ordering, merchant payments and token-based food assistance would require further development and participating organisations.","translations":{"nl":"De gebruiksvoorbeelden verkennen bezorging, winkels, horeca en toeleveringsketens. Het zijn concepten. Bestellen, winkelbetalingen en voedselhulp met tokens vragen verdere ontwikkeling en deelnemende organisaties.","es":"Las ilustraciones de casos de uso exploran entregas, tiendas, hostelería y cadenas de suministro. Describen conceptos. Los pedidos, pagos comerciales y ayudas alimentarias con tokens requieren más desarrollo y organizaciones participantes.","fr":"Les illustrations explorent la livraison, les commerces, la restauration et les chaînes d’approvisionnement. Elles décrivent des concepts. Les commandes, les paiements marchands et l’aide alimentaire en jetons nécessiteraient davantage de développement et des organismes participants.","pt":"As ilustrações exploram entregas, lojas, restauração e cadeias de abastecimento. Descrevem conceitos. Encomendas, pagamentos comerciais e apoio alimentar com tokens exigiriam mais desenvolvimento e organizações participantes.","id":"Ilustrasi penggunaan menjajaki pengiriman, toko, usaha kuliner, dan rantai pasok. Semuanya merupakan konsep. Pemesanan, pembayaran pedagang, dan bantuan pangan berbasis token memerlukan pengembangan lanjutan serta organisasi yang berpartisipasi.","zh-Hans":"使用场景插图探索配送、商店、餐饮及供应链。这些是概念。订购、商家支付和基于代币的食品援助,需要进一步开发和相关组织参与。","hi":"उपयोग के चित्र डिलीवरी, दुकानों, आतिथ्य और आपूर्ति श्रृंखलाओं की संभावनाएँ दिखाते हैं। ये अवधारणाएँ हैं। ऑर्डर, व्यापारी भुगतान और टोकन आधारित खाद्य सहायता के लिए और विकास तथा सहभागी संस्थाओं की जरूरत होगी।","ar":"تستكشف رسوم حالات الاستخدام التوصيل والمتاجر والضيافة وسلاسل الإمداد. وهي تصف مفاهيم. تتطلب الطلبات ومدفوعات التجار والمساعدات الغذائية بالرموز مزيدًا من التطوير ومشاركة مؤسسات.","bn":"ব্যবহারের ছবিগুলো ডেলিভারি, দোকান, আতিথেয়তা ও সরবরাহ ব্যবস্থার সম্ভাবনা দেখায়। এগুলো ধারণা। অর্ডার, ব্যবসায়িক পেমেন্ট ও টোকেনভিত্তিক খাদ্যসহায়তার জন্য আরও উন্নয়ন এবং অংশগ্রহণকারী প্রতিষ্ঠান প্রয়োজন।","ur":"استعمال کی تصاویر ترسیل، دکانوں، مہمان نوازی اور سپلائی چین کے امکانات دکھاتی ہیں۔ یہ تصورات ہیں۔ آرڈر، تاجروں کو ادائیگی اور ٹوکن پر مبنی غذائی مدد کے لیے مزید ترقی اور شریک ادارے درکار ہوں گے۔"},"pages":[6855]},{"source":"How does Calorie plan to integrate payment applications for companies and consumers?","translations":{"nl":"Hoe wil Calorie betaaltoepassingen voor bedrijven en consumenten integreren?","es":"¿Cómo pretende Calorie integrar pagos para empresas y consumidores?","fr":"Comment Calorie prévoit-il d’intégrer les paiements pour les entreprises et les particuliers ?","pt":"Como o Calorie pretende integrar pagamentos para empresas e consumidores?","id":"Bagaimana Calorie berencana mengintegrasikan pembayaran untuk bisnis dan konsumen?","zh-Hans":"Calorie 计划如何为企业和消费者集成支付应用?","hi":"Calorie व्यवसायों और उपभोक्ताओं के लिए भुगतान कैसे एकीकृत करना चाहता है?","ar":"كيف يخطط Calorie لدمج تطبيقات الدفع للشركات والمستهلكين؟","bn":"Calorie ব্যবসা ও ভোক্তাদের পেমেন্ট কীভাবে সমন্বয় করতে চায়?","ur":"Calorie کاروباروں اور صارفین کے لیے ادائیگیوں کو کیسے یکجا کرنا چاہتا ہے؟"},"pages":[6855]},{"source":"Point-of-sale and business payment integrations remain possible future developments. They are not available as merchant services in the current CalorieApp preview.","translations":{"nl":"Kassa-integraties en zakelijke betaalintegraties zijn mogelijke toekomstige ontwikkelingen. Ze zijn niet beschikbaar als diensten voor ondernemers in de huidige CalorieApp-preview.","es":"Las integraciones de puntos de venta y pagos empresariales siguen siendo posibles desarrollos futuros. No están disponibles como servicios comerciales en la vista previa actual de CalorieApp.","fr":"Les intégrations aux points de vente et aux paiements professionnels restent des développements futurs possibles. Elles ne sont pas proposées comme services marchands dans l’aperçu actuel de CalorieApp.","pt":"As integrações com pontos de venda e pagamentos empresariais continuam sendo possíveis desenvolvimentos futuros. Não estão disponíveis como serviços comerciais na prévia atual do CalorieApp.","id":"Integrasi kasir dan pembayaran bisnis masih merupakan kemungkinan pengembangan masa depan. Layanan pedagang tersebut belum tersedia dalam pratinjau CalorieApp saat ini.","zh-Hans":"销售点及企业支付集成仍是未来可能的开发方向。目前的 CalorieApp 预览版不提供这些商家服务。","hi":"बिक्री केंद्र और व्यावसायिक भुगतान एकीकरण भविष्य के संभावित विकास हैं। वर्तमान CalorieApp प्रीव्यू में ये व्यापारी सेवाओं के रूप में उपलब्ध नहीं हैं।","ar":"تظل تكاملات نقاط البيع ومدفوعات الشركات تطورات مستقبلية محتملة. ولا تتوفر كخدمات للتجار في معاينة CalorieApp الحالية.","bn":"বিক্রয়কেন্দ্র ও ব্যবসায়িক পেমেন্টের সমন্বয় ভবিষ্যতের সম্ভাব্য উন্নয়ন। CalorieApp-এর বর্তমান প্রিভিউতে এগুলো ব্যবসায়ীদের পরিষেবা হিসেবে উপলব্ধ নয়।","ur":"فروخت کے مراکز اور کاروباری ادائیگیوں کا انضمام مستقبل کی ممکنہ پیش رفت ہے۔ موجودہ CalorieApp پیشگی جھلک میں یہ تاجروں کی خدمات کے طور پر دستیاب نہیں۔"},"pages":[6855]},{"source":"What is Calorie's vision for the future?","translations":{"nl":"Wat is de toekomstvisie van Calorie?","es":"¿Cuál es la visión de futuro de Calorie?","fr":"Quelle est la vision de Calorie pour l’avenir ?","pt":"Qual é a visão do Calorie para o futuro?","id":"Apa visi Calorie untuk masa depan?","zh-Hans":"Calorie 对未来的愿景是什么?","hi":"भविष्य के लिए Calorie की दृष्टि क्या है?","ar":"ما رؤية Calorie للمستقبل؟","bn":"ভবিষ্যৎ নিয়ে Calorie-এর পরিকল্পনা কী?","ur":"مستقبل کے لیے Calorie کا تصور کیا ہے؟"},"pages":[6855]},{"source":"The wider vision includes useful food data tools and possible payment applications for consumers and businesses. The roadmap separates current work from longer-term ideas; future adoption is not guaranteed.","translations":{"nl":"De bredere visie omvat nuttige hulpmiddelen voor voedingsgegevens en mogelijke betaaltoepassingen voor consumenten en bedrijven. De roadmap onderscheidt het huidige werk van ideeën voor de langere termijn; toekomstig gebruik is niet gegarandeerd.","es":"La visión más amplia incluye herramientas útiles de datos alimentarios y posibles aplicaciones de pago para consumidores y empresas. La hoja de ruta separa el trabajo actual de las ideas a largo plazo; no se garantiza su adopción futura.","fr":"La vision élargie comprend des outils utiles de données alimentaires et de possibles applications de paiement pour les particuliers et les entreprises. La feuille de route distingue les travaux actuels des idées à long terme ; leur adoption future n’est pas garantie.","pt":"A visão mais ampla inclui ferramentas úteis de dados alimentares e possíveis aplicações de pagamento para consumidores e empresas. O roteiro separa o trabalho atual de ideias de longo prazo; a adoção futura não é garantida.","id":"Visi yang lebih luas mencakup alat data makanan yang bermanfaat dan kemungkinan aplikasi pembayaran bagi konsumen dan bisnis. Peta jalan memisahkan pekerjaan saat ini dari gagasan jangka panjang; adopsi di masa depan tidak dijamin.","zh-Hans":"更广泛的愿景包括实用的食品数据工具,以及面向消费者和企业的潜在支付应用。路线图区分了当前工作和长期构想;未来的采用情况没有保证。","hi":"व्यापक दृष्टि में उपयोगी खाद्य डेटा साधन तथा उपभोक्ताओं और व्यवसायों के लिए संभावित भुगतान अनुप्रयोग शामिल हैं। रोडमैप वर्तमान काम और दीर्घकालिक विचारों में अंतर करता है; भविष्य में अपनाए जाने की गारंटी नहीं है।","ar":"تشمل الرؤية الأوسع أدوات مفيدة للبيانات الغذائية وتطبيقات دفع محتملة للمستهلكين والشركات. تفصل خارطة الطريق العمل الحالي عن الأفكار طويلة الأجل؛ ولا يوجد ضمان للتبني المستقبلي.","bn":"বৃহত্তর পরিকল্পনায় উপকারী খাদ্যতথ্য সরঞ্জাম এবং ভোক্তা ও ব্যবসার জন্য সম্ভাব্য পেমেন্ট অ্যাপ রয়েছে। রোডম্যাপ বর্তমান কাজ ও দীর্ঘমেয়াদি ধারণা আলাদা করে; ভবিষ্যৎ গ্রহণযোগ্যতা নিশ্চিত নয়।","ur":"وسیع تر تصور میں مفید غذائی ڈیٹا کی سہولتیں اور صارفین و کاروباروں کے لیے ممکنہ ادائیگی ایپس شامل ہیں۔ روڈمیپ موجودہ کام اور طویل مدتی خیالات میں فرق کرتا ہے؛ مستقبل میں قبولیت کی ضمانت نہیں۔"},"pages":[6855]},{"source":"How can Calorie contribute to a healthier financial lifestyle?","translations":{"nl":"Hoe kan Calorie bijdragen aan een gezondere financiële leefstijl?","es":"¿Cómo puede Calorie contribuir a un estilo de vida financiero más saludable?","fr":"Comment Calorie peut-il contribuer à un mode de vie financier plus sain ?","pt":"Como o Calorie pode contribuir para uma vida financeira mais saudável?","id":"Bagaimana Calorie dapat membantu gaya hidup keuangan yang lebih sehat?","zh-Hans":"Calorie 如何帮助实现更健康的财务生活方式?","hi":"Calorie स्वस्थ वित्तीय जीवनशैली में कैसे मदद कर सकता है?","ar":"كيف يمكن أن يسهم Calorie في نمط حياة مالي أكثر صحة؟","bn":"Calorie কীভাবে স্বাস্থ্যকর আর্থিক জীবনযাপনে সাহায্য করতে পারে?","ur":"Calorie صحت مند مالی طرزِ زندگی میں کیسے مدد دے سکتا ہے؟"},"pages":[6855]},{"source":"CalorieApp helps users explore nutrition information and keep a personal food log. Combined payment and expense tracking belongs to the earlier broader vision and is not a feature of the current preview.","translations":{"nl":"CalorieApp helpt gebruikers voedingsinformatie te verkennen en een persoonlijk voedingsdagboek bij te houden. Gecombineerde betaal- en uitgavenregistratie hoort bij de eerdere, bredere visie en is geen functie van de huidige preview.","es":"CalorieApp ayuda a consultar información nutricional y llevar un diario alimentario personal. El seguimiento conjunto de pagos y gastos pertenece a la visión anterior más amplia y no es una función de la vista previa actual.","fr":"CalorieApp aide à explorer les informations nutritionnelles et à tenir un journal alimentaire personnel. Le suivi combiné des paiements et des dépenses relève de la vision élargie antérieure et ne fait pas partie de l’aperçu actuel.","pt":"O CalorieApp ajuda a explorar informações nutricionais e manter um diário alimentar pessoal. O acompanhamento conjunto de pagamentos e despesas pertence à visão anterior mais ampla e não é uma funcionalidade da prévia atual.","id":"CalorieApp membantu pengguna menjelajahi informasi gizi dan membuat catatan makanan pribadi. Pencatatan gabungan pembayaran dan pengeluaran termasuk visi terdahulu yang lebih luas, bukan fitur pratinjau saat ini.","zh-Hans":"CalorieApp 帮助用户了解营养信息并记录个人饮食。将支付与支出记录结合属于早期更广泛的愿景,并非当前预览版的功能。","hi":"CalorieApp पोषण की जानकारी समझने और व्यक्तिगत भोजन डायरी रखने में मदद करता है। भुगतान और खर्च का संयुक्त रिकॉर्ड पहले की व्यापक दृष्टि का हिस्सा है, वर्तमान प्रीव्यू का फीचर नहीं।","ar":"يساعد CalorieApp المستخدمين على استكشاف المعلومات الغذائية والاحتفاظ بسجل غذائي شخصي. ينتمي تتبع المدفوعات والنفقات معًا إلى الرؤية السابقة الأوسع، وليس ميزة في المعاينة الحالية.","bn":"CalorieApp পুষ্টির তথ্য বুঝতে ও ব্যক্তিগত খাদ্য ডায়েরি রাখতে সাহায্য করে। পেমেন্ট ও ব্যয়ের সম্মিলিত হিসাব আগের বৃহত্তর পরিকল্পনার অংশ; বর্তমান প্রিভিউয়ের বৈশিষ্ট্য নয়।","ur":"CalorieApp غذائی معلومات سمجھنے اور ذاتی خوراک ڈائری رکھنے میں مدد دیتا ہے۔ ادائیگی اور اخراجات کا مشترکہ ریکارڈ پہلے کے وسیع تصور کا حصہ ہے، موجودہ پیشگی جھلک کا فیچر نہیں۔"},"pages":[6855]},{"source":"How does Calorie contribute to corporate social responsibility?","translations":{"nl":"Hoe draagt Calorie bij aan maatschappelijk verantwoord ondernemen?","es":"¿Cómo contribuye Calorie a la responsabilidad social empresarial?","fr":"Comment Calorie contribue-t-il à la responsabilité sociale des entreprises ?","pt":"Como o Calorie contribui para a responsabilidade social empresarial?","id":"Bagaimana Calorie berkontribusi pada tanggung jawab sosial perusahaan?","zh-Hans":"Calorie 如何履行企业社会责任?","hi":"Calorie सामाजिक उत्तरदायित्व में कैसे योगदान करता है?","ar":"كيف يسهم Calorie في المسؤولية الاجتماعية للشركات؟","bn":"Calorie সামাজিক দায়বদ্ধতায় কীভাবে অবদান রাখে?","ur":"Calorie سماجی ذمہ داری میں کیسے حصہ ڈالتا ہے؟"},"pages":[6855]},{"source":"Responsible food information and practical tools are project goals. Using a ledger alone does not establish a measurable social or environmental benefit; those outcomes need evidence from an implemented use case.","translations":{"nl":"Verantwoorde voedingsinformatie en praktische hulpmiddelen zijn projectdoelen. Alleen een ledger gebruiken bewijst geen meetbare maatschappelijke of milieuwinst; daarvoor is bewijs uit een uitgevoerde toepassing nodig.","es":"La información alimentaria responsable y las herramientas prácticas son objetivos del proyecto. Utilizar un registro por sí solo no demuestra beneficios sociales o ambientales medibles; hacen falta pruebas de un caso de uso implementado.","fr":"Une information alimentaire responsable et des outils pratiques sont des objectifs du projet. L’utilisation d’un registre ne démontre pas à elle seule un bénéfice social ou environnemental mesurable ; des preuves issues d’un cas d’usage réalisé sont nécessaires.","pt":"Informação alimentar responsável e ferramentas práticas são objetivos do projeto. Usar um registo, por si só, não demonstra benefícios sociais ou ambientais mensuráveis; são necessárias evidências de um caso de uso implementado.","id":"Informasi pangan yang bertanggung jawab dan alat praktis adalah tujuan proyek. Penggunaan ledger saja tidak membuktikan manfaat sosial atau lingkungan yang terukur; hasil tersebut memerlukan bukti dari penerapan nyata.","zh-Hans":"负责任的食品信息和实用工具是项目目标。仅仅使用账本不能证明可量化的社会或环境效益;这些结果需要来自已实施应用的证据。","hi":"जिम्मेदार खाद्य जानकारी और व्यावहारिक साधन परियोजना के लक्ष्य हैं। केवल लेजर का उपयोग मापने योग्य सामाजिक या पर्यावरणीय लाभ सिद्ध नहीं करता; ऐसे परिणामों के लिए लागू उपयोग से प्रमाण चाहिए।","ar":"المعلومات الغذائية المسؤولة والأدوات العملية من أهداف المشروع. لا يثبت استخدام سجل وحده فائدة اجتماعية أو بيئية قابلة للقياس؛ بل تحتاج هذه النتائج إلى أدلة من حالة استخدام مطبقة.","bn":"দায়িত্বশীল খাদ্যতথ্য ও ব্যবহারিক সরঞ্জাম প্রকল্পের লক্ষ্য। শুধু লেজার ব্যবহার করলেই পরিমাপযোগ্য সামাজিক বা পরিবেশগত উপকার প্রমাণিত হয় না; বাস্তবায়িত ব্যবহার থেকে তার প্রমাণ দরকার।","ur":"ذمہ دارانہ غذائی معلومات اور عملی سہولتیں منصوبے کے اہداف ہیں۔ صرف لیجر استعمال کرنا قابلِ پیمائش سماجی یا ماحولیاتی فائدہ ثابت نہیں کرتا؛ اس کے لیے نافذ شدہ استعمال سے ثبوت چاہیے۔"},"pages":[6855]},{"source":"How can governments use Calorie to assist citizens' basic needs?","translations":{"nl":"Hoe kunnen overheden Calorie gebruiken om burgers te helpen met hun basisbehoeften?","es":"¿Cómo pueden los gobiernos utilizar Calorie para cubrir necesidades básicas?","fr":"Comment les pouvoirs publics peuvent-ils utiliser Calorie pour répondre aux besoins essentiels ?","pt":"Como os governos podem usar o Calorie para apoiar necessidades básicas?","id":"Bagaimana pemerintah dapat menggunakan Calorie untuk membantu kebutuhan dasar warga?","zh-Hans":"政府如何利用 Calorie 帮助满足居民的基本需求?","hi":"सरकारें नागरिकों की बुनियादी जरूरतों के लिए Calorie का उपयोग कैसे कर सकती हैं?","ar":"كيف يمكن للحكومات استخدام Calorie لدعم الاحتياجات الأساسية للمواطنين؟","bn":"নাগরিকদের মৌলিক প্রয়োজনে সরকার কীভাবে Calorie ব্যবহার করতে পারে?","ur":"حکومتیں شہریوں کی بنیادی ضروریات کے لیے Calorie کیسے استعمال کر سکتی ہیں؟"},"pages":[6855]},{"source":"Token-based food assistance is a proposed use case. This page does not announce an operating government programme or an accepted food-benefit payment service.","translations":{"nl":"Voedselhulp met tokens is een voorgestelde toepassing. Deze pagina kondigt geen werkend overheidsprogramma of geaccepteerde betaaldienst voor voedselhulp aan.","es":"La ayuda alimentaria mediante tokens es un caso de uso propuesto. Esta página no anuncia un programa gubernamental operativo ni un servicio de pago aceptado para prestaciones alimentarias.","fr":"L’aide alimentaire en jetons est un cas d’usage proposé. Cette page n’annonce aucun programme public opérationnel ni service de paiement accepté pour l’aide alimentaire.","pt":"O apoio alimentar baseado em tokens é um caso de uso proposto. Esta página não anuncia um programa governamental em funcionamento nem um serviço de pagamento aceite para benefícios alimentares.","id":"Bantuan pangan berbasis token merupakan usulan penggunaan. Halaman ini tidak mengumumkan program pemerintah yang beroperasi atau layanan pembayaran bantuan pangan yang diterima.","zh-Hans":"基于代币的食品援助是一项提议。本页没有宣布正在运行的政府计划,也没有宣布已被接受的食品福利支付服务。","hi":"टोकन आधारित खाद्य सहायता एक प्रस्तावित उपयोग है। यह पृष्ठ किसी चालू सरकारी कार्यक्रम या स्वीकृत खाद्य सहायता भुगतान सेवा की घोषणा नहीं करता।","ar":"المساعدة الغذائية القائمة على الرموز حالة استخدام مقترحة. لا تعلن هذه الصفحة عن برنامج حكومي يعمل بالفعل أو خدمة دفع مقبولة للمزايا الغذائية.","bn":"টোকেনভিত্তিক খাদ্যসহায়তা একটি প্রস্তাবিত ব্যবহার। এই পৃষ্ঠা কোনো চালু সরকারি কর্মসূচি বা গৃহীত খাদ্যসহায়তা পেমেন্ট পরিষেবা ঘোষণা করে না।","ur":"ٹوکن پر مبنی غذائی مدد ایک مجوزہ استعمال ہے۔ یہ صفحہ کسی فعال سرکاری پروگرام یا قبول شدہ غذائی امداد کی ادائیگی سروس کا اعلان نہیں کرتا۔"},"pages":[6855]},{"source":"What is CalorieApp, and how will it enhance the F&B experience?","translations":{"nl":"Wat is CalorieApp en hoe verbetert het de ervaring rond voeding en dranken?","es":"¿Qué es CalorieApp y cómo mejorará la experiencia alimentaria?","fr":"Qu’est-ce que CalorieApp et comment améliorera-t-il l’expérience alimentaire ?","pt":"O que é o CalorieApp e como irá melhorar a experiência alimentar?","id":"Apa itu CalorieApp dan bagaimana aplikasi ini meningkatkan pengalaman makanan dan minuman?","zh-Hans":"CalorieApp 是什么?它将如何改善食品饮料体验?","hi":"CalorieApp क्या है और यह खाद्य अनुभव को कैसे बेहतर बनाएगा?","ar":"ما CalorieApp وكيف سيحسن تجربة الأغذية والمشروبات؟","bn":"CalorieApp কী এবং এটি খাদ্য ও পানীয়ের অভিজ্ঞতা কীভাবে উন্নত করবে?","ur":"CalorieApp کیا ہے اور یہ خوراک و مشروبات کا تجربہ کیسے بہتر بنائے گا؟"},"pages":[6855]},{"source":"CalorieApp is the project's browser-based preview for food search, nutrition information and a personal food log, using Open Food Facts. Website sign-in is connected through Xaman. Retail payments, an integrated exchange and business expense management are outside the current preview.","translations":{"nl":"CalorieApp is de browserpreview van het project voor voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek, met Open Food Facts. Inloggen op de website is gekoppeld via Xaman. Winkelbetalingen, een geïntegreerde exchange en zakelijk uitgavenbeheer vallen buiten de huidige preview.","es":"CalorieApp es la vista previa del proyecto en el navegador para buscar alimentos, consultar información nutricional y llevar un diario personal con Open Food Facts. El acceso al sitio está conectado mediante Xaman. Los pagos minoristas, una plataforma de intercambio integrada y la gestión de gastos empresariales quedan fuera de esta versión.","fr":"CalorieApp est l’aperçu du projet dans le navigateur pour rechercher des aliments, consulter leur valeur nutritionnelle et tenir un journal personnel avec Open Food Facts. La connexion au site est reliée par Xaman. Les paiements en magasin, un service d’échange intégré et la gestion des dépenses professionnelles ne font pas partie de cet aperçu.","pt":"O CalorieApp é a prévia do projeto no navegador para pesquisar alimentos, consultar informação nutricional e manter um diário pessoal com Open Food Facts. O acesso ao site está ligado através do Xaman. Pagamentos no comércio, uma plataforma de troca integrada e gestão de despesas empresariais estão fora desta prévia.","id":"CalorieApp adalah pratinjau proyek berbasis peramban untuk pencarian makanan, informasi gizi, dan catatan makanan pribadi menggunakan Open Food Facts. Login situs terhubung melalui Xaman. Pembayaran ritel, layanan pertukaran terintegrasi, dan pengelolaan biaya bisnis berada di luar pratinjau saat ini.","zh-Hans":"CalorieApp 是项目的浏览器预览版,使用 Open Food Facts 提供食品搜索、营养信息和个人饮食记录。网站登录通过 Xaman 关联。零售支付、内置兑换服务和企业支出管理不属于当前预览版。","hi":"CalorieApp परियोजना का ब्राउज़र प्रीव्यू है, जिसमें Open Food Facts की मदद से भोजन खोज, पोषण जानकारी और व्यक्तिगत भोजन डायरी मिलती है। वेबसाइट का लॉगिन Xaman से जुड़ा है। खुदरा भुगतान, एकीकृत एक्सचेंज और व्यावसायिक खर्च प्रबंधन वर्तमान प्रीव्यू में शामिल नहीं हैं।","ar":"CalorieApp هو معاينة المشروع عبر المتصفح للبحث عن الطعام والمعلومات الغذائية والسجل الغذائي الشخصي باستخدام Open Food Facts. يرتبط تسجيل دخول الموقع عبر Xaman. مدفوعات المتاجر والصرافة المدمجة وإدارة نفقات الشركات ليست ضمن المعاينة الحالية.","bn":"CalorieApp হলো প্রকল্পের ব্রাউজারভিত্তিক প্রিভিউ, যা Open Food Facts দিয়ে খাবার খোঁজা, পুষ্টির তথ্য ও ব্যক্তিগত খাদ্য ডায়েরি দেয়। ওয়েবসাইটে লগইন Xaman-এর মাধ্যমে সংযুক্ত। দোকানে পেমেন্ট, সমন্বিত এক্সচেঞ্জ ও ব্যবসায়িক ব্যয় পরিচালনা বর্তমান প্রিভিউয়ের বাইরে।","ur":"CalorieApp منصوبے کی براؤزر میں پیشگی جھلک ہے، جو Open Food Facts کے ذریعے خوراک کی تلاش، غذائی معلومات اور ذاتی خوراک ڈائری دیتی ہے۔ ویب سائٹ کا لاگ ان Xaman سے منسلک ہے۔ دکانوں کی ادائیگیاں، مربوط ایکسچینج اور کاروباری اخراجات کا انتظام موجودہ پیشگی جھلک میں شامل نہیں۔"},"pages":[6855]},{"source":"Can you explain the phased buildup of CalorieApp's release?","translations":{"nl":"Hoe verloopt de gefaseerde introductie van CalorieApp?","es":"¿Cómo se desarrolla el lanzamiento por fases de CalorieApp?","fr":"Comment se déroule le lancement progressif de CalorieApp ?","pt":"Como funciona o lançamento faseado do CalorieApp?","id":"Bagaimana tahapan peluncuran CalorieApp?","zh-Hans":"CalorieApp 的分阶段发布如何进行?","hi":"CalorieApp चरणों में कैसे जारी किया जा रहा है?","ar":"كيف يتم إطلاق CalorieApp على مراحل؟","bn":"CalorieApp কীভাবে ধাপে ধাপে প্রকাশ করা হচ্ছে?","ur":"CalorieApp کی مرحلہ وار ریلیز کیسے ہوتی ہے؟"},"pages":[6855]},{"source":"The browser preview can already be explored from the CalorieApp page. The current roadmap records eight milestones and their status. Earlier Android and Testnet prototypes remain development history; additional services will need their own implementation and release announcement.","translations":{"nl":"De browserpreview is al te bekijken via de CalorieApp-pagina. De huidige roadmap bevat acht mijlpalen en hun status. Eerdere Android- en Testnet-prototypes blijven onderdeel van de ontwikkelgeschiedenis; extra diensten hebben hun eigen implementatie en aankondiging nodig.","es":"La vista previa en el navegador ya puede explorarse desde la página CalorieApp. La hoja de ruta actual registra ocho hitos y su estado. Los prototipos anteriores de Android y Testnet son parte de la historia del desarrollo; los servicios adicionales requieren su propia implementación y anuncio de lanzamiento.","fr":"L’aperçu dans le navigateur est déjà accessible depuis la page CalorieApp. La feuille de route actuelle présente huit étapes et leur état. Les anciens prototypes Android et Testnet restent des éléments historiques du développement ; les services supplémentaires nécessiteront leur propre réalisation et annonce de lancement.","pt":"A prévia no navegador já pode ser explorada na página CalorieApp. O roteiro atual regista oito marcos e o seu estado. Os protótipos anteriores de Android e Testnet fazem parte da história do desenvolvimento; serviços adicionais precisarão da sua própria implementação e anúncio de lançamento.","id":"Pratinjau peramban sudah dapat dijelajahi melalui halaman CalorieApp. Peta jalan saat ini mencatat delapan tonggak beserta statusnya. Prototipe Android dan Testnet terdahulu tetap menjadi sejarah pengembangan; layanan tambahan memerlukan implementasi dan pengumuman rilis tersendiri.","zh-Hans":"目前可通过 CalorieApp 页面体验浏览器预览版。当前路线图列出八个里程碑及其状态。早期 Android 和 Testnet 原型属于开发历史;新增服务需要各自的实施与发布公告。","hi":"ब्राउज़र प्रीव्यू को अभी CalorieApp पृष्ठ से देखा जा सकता है। वर्तमान रोडमैप में आठ पड़ाव और उनकी स्थिति दर्ज हैं। पुराने Android और Testnet प्रोटोटाइप विकास के इतिहास का हिस्सा हैं; अतिरिक्त सेवाओं का अलग कार्यान्वयन और रिलीज़ घोषणा जरूरी होगी।","ar":"يمكن بالفعل استكشاف معاينة المتصفح من صفحة CalorieApp. تسجل خارطة الطريق الحالية ثماني مراحل وحالتها. تظل نماذج Android وTestnet السابقة جزءًا من تاريخ التطوير؛ وستحتاج الخدمات الإضافية إلى تنفيذها وإعلان إطلاقها الخاص.","bn":"CalorieApp পৃষ্ঠা থেকে এখনই ব্রাউজার প্রিভিউ দেখা যায়। বর্তমান রোডম্যাপে আটটি মাইলফলক ও তাদের অবস্থা আছে। আগের Android ও Testnet প্রোটোটাইপ উন্নয়নের ইতিহাসের অংশ; নতুন পরিষেবার নিজস্ব বাস্তবায়ন ও প্রকাশের ঘোষণা প্রয়োজন হবে।","ur":"براؤزر کی پیشگی جھلک CalorieApp صفحے سے ابھی دیکھی جا سکتی ہے۔ موجودہ روڈمیپ میں آٹھ سنگِ میل اور ان کی حالت درج ہے۔ پرانے Android اور Testnet نمونے ترقی کی تاریخ کا حصہ ہیں؛ اضافی خدمات کی اپنی تکمیل اور ریلیز کا اعلان ضروری ہوگا۔"},"pages":[6855]},{"source":"Donations","translations":{"nl":"Donaties","es":"Donaciones","fr":"Dons","pt":"Donativos","id":"Donasi","zh-Hans":"捐赠","hi":"दान","ar":"التبرعات","bn":"অনুদান","ur":"عطیات"},"pages":[6897,6914,6926,6937]},{"source":"Donation In","translations":{"nl":"Donatie in","es":"Donación en","fr":"Don en","pt":"Donativo em","id":"Donasi dalam","zh-Hans":"捐赠币种","hi":"दान की मुद्रा","ar":"التبرع بعملة","bn":"অনুদানের মুদ্রা","ur":"عطیے کی کرنسی"},"pages":[6897,6914,6926,6937]},{"source":"Free choice of the donation amount in XRP","translations":{"nl":"Kies zelf je donatiebedrag in XRP","es":"Elige tu importe de donación en XRP","fr":"Choisissez le montant de votre don en XRP","pt":"Escolha o valor do seu donativo em XRP","id":"Pilih sendiri jumlah donasi dalam XRP","zh-Hans":"自行选择 XRP 捐赠金额","hi":"XRP में अपना दान मूल्य चुनें","ar":"اختر مبلغ تبرعك بعملة XRP","bn":"XRP-এ অনুদানের পরিমাণ নিজে বেছে নিন","ur":"XRP میں عطیے کی رقم خود منتخب کریں"},"pages":[6897,6914,6926,6937]},{"source":"Free choice of the donation amount in XRP quantity","translations":{"nl":"Aantal voor je donatiebedrag in XRP","es":"Cantidad de la donación en XRP","fr":"Quantité du don en XRP","pt":"Quantidade do donativo em XRP","id":"Kuantitas donasi dalam XRP","zh-Hans":"XRP 捐赠数量","hi":"XRP दान की संख्या","ar":"كمية التبرع بعملة XRP","bn":"XRP অনুদানের সংখ্যা","ur":"XRP عطیے کی تعداد"},"pages":[6897,6914,6926,6937]},{"source":"Donate to CalorieToken","translations":{"nl":"Doneer aan CalorieToken","es":"Donar a CalorieToken","fr":"Faire un don à CalorieToken","pt":"Doar ao CalorieToken","id":"Donasi ke CalorieToken","zh-Hans":"向 CalorieToken 捐赠","hi":"CalorieToken को दान दें","ar":"تبرع لـ CalorieToken","bn":"CalorieToken-কে অনুদান দিন","ur":"CalorieToken کو عطیہ دیں"},"pages":[6897,6914,6926,6937]},{"source":"Add Your Donation","translations":{"nl":"Voeg je donatie toe","es":"Añadir tu donación","fr":"Ajouter votre don","pt":"Adicionar o seu donativo","id":"Tambahkan donasi Anda","zh-Hans":"添加捐赠","hi":"अपना दान जोड़ें","ar":"أضف تبرعك","bn":"আপনার অনুদান যোগ করুন","ur":"اپنا عطیہ شامل کریں"},"pages":[6897,6914,6926,6937]},{"source":"No products in the cart.","translations":{"nl":"Geen producten in de winkelwagen.","es":"No hay productos en el carrito.","fr":"Aucun produit dans le panier.","pt":"Não há produtos no carrinho.","id":"Tidak ada produk dalam keranjang.","zh-Hans":"购物车中没有商品。","hi":"कार्ट में कोई उत्पाद नहीं है।","ar":"لا توجد منتجات في السلة.","bn":"কার্টে কোনো পণ্য নেই।","ur":"کارٹ میں کوئی چیز نہیں۔"},"pages":[6897,6914,6926,6937]},{"source":"No Donations Yet","translations":{"nl":"Nog geen donaties","es":"Todavía no hay donaciones","fr":"Aucun don pour le moment","pt":"Ainda não há donativos","id":"Belum ada donasi","zh-Hans":"暂无捐赠","hi":"अभी कोई दान नहीं","ar":"لا توجد تبرعات بعد","bn":"এখনো কোনো অনুদান নেই","ur":"ابھی کوئی عطیہ نہیں"},"pages":[6897,6914,6926,6937]},{"source":"Choose your donation amount in XRP. You can review the amount and quantity in your cart before continuing to payment.","translations":{"nl":"Kies je donatiebedrag in XRP. Je kunt het bedrag en het aantal in je winkelwagen controleren voordat je doorgaat naar de betaling.","es":"Elige el importe de tu donación en XRP. Puedes revisar el importe y la cantidad en el carrito antes de continuar al pago.","fr":"Choisissez le montant de votre don en XRP. Vous pourrez vérifier le montant et la quantité dans le panier avant de passer au paiement.","pt":"Escolha o valor do donativo em XRP. Pode verificar o valor e a quantidade no carrinho antes de continuar para o pagamento.","id":"Pilih jumlah donasi dalam XRP. Anda dapat memeriksa jumlah dan kuantitas di keranjang sebelum melanjutkan pembayaran.","zh-Hans":"选择您的 XRP 捐赠金额。继续付款前,可在购物车中核对金额和数量。","hi":"XRP में दान की राशि चुनें। भुगतान करने से पहले आप कार्ट में राशि और संख्या जाँच सकते हैं।","ar":"اختر مبلغ تبرعك بعملة XRP. يمكنك مراجعة المبلغ والكمية في السلة قبل المتابعة إلى الدفع.","bn":"XRP-এ অনুদানের পরিমাণ বেছে নিন। পেমেন্টের আগে কার্টে পরিমাণ ও সংখ্যা যাচাই করতে পারবেন।","ur":"XRP میں عطیے کی رقم منتخب کریں۔ ادائیگی سے پہلے آپ کارٹ میں رقم اور تعداد چیک کر سکتے ہیں۔"},"pages":[6897,6914,6926,6937]},{"source":"Voluntary contributions support work on CalorieApp and the wider CalorieToken project, including improvements to the food and nutrition experience.","translations":{"nl":"Vrijwillige bijdragen ondersteunen het werk aan CalorieApp en het bredere CalorieToken-project, waaronder verbeteringen aan de ervaring rond voeding en voedingsinformatie.","es":"Las contribuciones voluntarias apoyan el trabajo en CalorieApp y el proyecto CalorieToken, incluidas mejoras en la experiencia alimentaria y nutricional.","fr":"Les contributions volontaires soutiennent CalorieApp et le projet CalorieToken, notamment l’amélioration de l’expérience alimentaire et nutritionnelle.","pt":"As contribuições voluntárias apoiam o trabalho no CalorieApp e no projeto CalorieToken, incluindo melhorias na experiência alimentar e nutricional.","id":"Kontribusi sukarela mendukung pengembangan CalorieApp dan proyek CalorieToken, termasuk perbaikan pengalaman terkait makanan dan gizi.","zh-Hans":"自愿捐赠支持 CalorieApp 及更广泛的 CalorieToken 项目,包括改善食品与营养体验。","hi":"स्वैच्छिक योगदान CalorieApp और व्यापक CalorieToken परियोजना का समर्थन करते हैं, जिसमें भोजन और पोषण के अनुभव को बेहतर बनाना शामिल है।","ar":"تدعم المساهمات الطوعية العمل على CalorieApp ومشروع CalorieToken الأوسع، بما في ذلك تحسين تجربة الطعام والمعلومات الغذائية.","bn":"স্বেচ্ছা অনুদান CalorieApp ও বৃহত্তর CalorieToken প্রকল্পের কাজকে সাহায্য করে, যার মধ্যে খাদ্য ও পুষ্টির অভিজ্ঞতা উন্নত করাও রয়েছে।","ur":"رضاکارانہ تعاون CalorieApp اور وسیع تر CalorieToken منصوبے کے کام کی مدد کرتا ہے، جس میں خوراک اور غذائیت کے تجربے میں بہتری شامل ہے۔"},"pages":[6897,6914,6926,6937]},{"source":"Disclaimer: While your generous support drives our efforts, we cannot guarantee specific outcomes or features as a result of donations. Your contribution helps advance our mission, but the nature and timeline of developments remain subject to various factors and unforeseen circumstances.","translations":{"nl":"Toelichting: je steun helpt ons werk, maar een donatie garandeert geen specifieke resultaten of functies. Je bijdrage ondersteunt onze missie; de aard en planning van ontwikkelingen blijven afhankelijk van uiteenlopende factoren en onvoorziene omstandigheden.","es":"Aviso: tu apoyo impulsa nuestro trabajo, pero las donaciones no garantizan resultados ni funciones concretas. Tu contribución ayuda a nuestra misión; la naturaleza y los plazos de los desarrollos dependen de diversos factores y circunstancias imprevistas.","fr":"Précision : votre soutien aide notre travail, mais les dons ne garantissent aucun résultat ni aucune fonction précise. Votre contribution soutient notre mission ; la nature et le calendrier des développements dépendent de différents facteurs et de circonstances imprévues.","pt":"Aviso: o seu apoio ajuda o nosso trabalho, mas os donativos não garantem resultados ou funcionalidades específicas. A sua contribuição apoia a nossa missão; a natureza e os prazos dos desenvolvimentos dependem de vários fatores e circunstâncias imprevistas.","id":"Keterangan: dukungan Anda membantu pekerjaan kami, tetapi donasi tidak menjamin hasil atau fitur tertentu. Kontribusi Anda mendukung misi kami; sifat dan jadwal pengembangan bergantung pada berbagai faktor serta keadaan tak terduga.","zh-Hans":"说明:您的支持有助于我们的工作,但捐赠不保证特定成果或功能。您的捐赠支持我们的使命;开发内容和时间安排仍取决于各种因素及不可预见的情况。","hi":"स्पष्टीकरण: आपका सहयोग हमारे काम में मदद करता है, लेकिन दान से किसी खास परिणाम या फीचर की गारंटी नहीं मिलती। आपका योगदान हमारे मिशन को आगे बढ़ाता है; विकास का स्वरूप और समय विभिन्न कारकों तथा अप्रत्याशित परिस्थितियों पर निर्भर रहता है।","ar":"توضيح: يساعد دعمك عملنا، لكن التبرعات لا تضمن نتائج أو ميزات محددة. تسهم مساهمتك في مهمتنا؛ وتبقى طبيعة التطوير ومواعيده مرتبطة بعوامل مختلفة وظروف غير متوقعة.","bn":"ব্যাখ্যা: আপনার সহযোগিতা আমাদের কাজে সাহায্য করে, কিন্তু অনুদান নির্দিষ্ট ফলাফল বা বৈশিষ্ট্যের নিশ্চয়তা দেয় না। আপনার অবদান আমাদের লক্ষ্যকে এগিয়ে নেয়; উন্নয়নের ধরন ও সময় বিভিন্ন বিষয় এবং অপ্রত্যাশিত পরিস্থিতির ওপর নির্ভর করে।","ur":"وضاحت: آپ کا تعاون ہمارے کام میں مدد دیتا ہے، لیکن عطیات کسی خاص نتیجے یا فیچر کی ضمانت نہیں دیتے۔ آپ کی شراکت ہمارے مقصد کی مدد کرتی ہے؛ ترقی کی نوعیت اور وقت مختلف عوامل اور غیر متوقع حالات پر منحصر رہتے ہیں۔"},"pages":[6897,6914,6926,6937]},{"source":"Discover CalorieApp","translations":{"nl":"Ontdek CalorieApp","es":"Descubre CalorieApp","fr":"Découvrez CalorieApp","pt":"Descubra o CalorieApp","id":"Jelajahi CalorieApp","zh-Hans":"探索 CalorieApp","hi":"CalorieApp देखें","ar":"اكتشف CalorieApp","bn":"CalorieApp দেখুন","ur":"CalorieApp دیکھیں"},"pages":[5580,6897,6914,6926,6937]},{"source":"Search food, explore nutrition and keep your own food diary. Try the current webapp.","translations":{"nl":"Zoek voeding, bekijk voedingsinformatie en houd je eigen voedingsdagboek bij. Probeer de huidige webapp.","es":"Busca alimentos, consulta su información nutricional y lleva tu diario alimentario. Prueba la aplicación web actual.","fr":"Recherchez des aliments, consultez leur valeur nutritionnelle et tenez votre journal alimentaire. Essayez l’application web actuelle.","pt":"Pesquise alimentos, consulte informação nutricional e mantenha o seu diário alimentar. Experimente a aplicação web atual.","id":"Cari makanan, lihat informasi gizi, dan buat catatan makanan pribadi. Coba aplikasi web saat ini.","zh-Hans":"搜索食品、了解营养并记录个人饮食。体验当前的网页应用。","hi":"भोजन खोजें, पोषण जानें और अपनी भोजन डायरी रखें। वर्तमान वेब ऐप आज़माएँ।","ar":"ابحث عن الطعام واستكشف التغذية واحتفظ بسجلك الغذائي. جرّب تطبيق الويب الحالي.","bn":"খাবার খুঁজুন, পুষ্টি সম্পর্কে জানুন এবং নিজের খাদ্য ডায়েরি রাখুন। বর্তমান ওয়েব অ্যাপটি ব্যবহার করুন।","ur":"خوراک تلاش کریں، غذائی معلومات جانیں اور اپنی خوراک ڈائری رکھیں۔ موجودہ ویب ایپ آزمائیں۔"},"pages":[5580,6897,6914,6926,6937]},{"source":"Historical video · watch on YouTube","translations":{"nl":"Historische video · bekijk op YouTube","es":"Vídeo histórico · ver en YouTube","fr":"Vidéo historique · regarder sur YouTube","pt":"Vídeo histórico · ver no YouTube","id":"Video lama · tonton di YouTube","zh-Hans":"历史视频 · 在 YouTube 观看","hi":"पुराना वीडियो · YouTube पर देखें","ar":"فيديو تاريخي · شاهد على YouTube","bn":"পুরোনো ভিডিও · YouTube-এ দেখুন","ur":"پرانا ویڈیو · YouTube پر دیکھیں"},"pages":[5580,6897,6914,6926,6937]},{"source":"Instructions for XRPToolkit:","translations":{"nl":"Instructies voor XRP Toolkit:","es":"Instrucciones para XRP Toolkit:","fr":"Instructions pour XRP Toolkit :","pt":"Instruções para XRP Toolkit:","id":"Petunjuk XRP Toolkit:","zh-Hans":"XRP Toolkit 使用说明:","hi":"XRP Toolkit के निर्देश:","ar":"تعليمات XRP Toolkit:","bn":"XRP Toolkit-এর নির্দেশনা:","ur":"XRP Toolkit کی ہدایات:"},"pages":[1205,6897,6914,6926,6937]},{"source":"Issuer Account:","translations":{"nl":"Uitgeversaccount:","es":"Cuenta del emisor:","fr":"Compte de l’émetteur :","pt":"Conta do emissor:","id":"Akun penerbit:","zh-Hans":"发行方账户:","hi":"जारीकर्ता का खाता:","ar":"حساب الجهة المصدرة:","bn":"ইস্যুকারীর অ্যাকাউন্ট:","ur":"جاری کنندہ کا اکاؤنٹ:"},"pages":[1205,6897,6914,6926,6937]},{"source":"Currency (HEX-code):","translations":{"nl":"Valuta (HEX-code):","es":"Moneda (código HEX):","fr":"Devise (code HEX) :","pt":"Moeda (código HEX):","id":"Mata uang (kode HEX):","zh-Hans":"货币(HEX 编码):","hi":"मुद्रा (HEX कोड):","ar":"العملة (رمز HEX):","bn":"মুদ্রা (HEX কোড):","ur":"کرنسی (HEX کوڈ):"},"pages":[1205,6897,6914,6926,6937]},{"source":"Limit:","translations":{"nl":"Limiet:","es":"Límite:","fr":"Limite :","pt":"Limite:","id":"Batas:","zh-Hans":"限额:","hi":"सीमा:","ar":"الحد:","bn":"সীমা:","ur":"حد:"},"pages":[1205,6897,6914,6926,6937]},{"source":"Set your trustline with Xaman:","translations":{"nl":"Stel je trustline in met Xaman:","es":"Configura tu trustline con Xaman:","fr":"Configurez votre trustline avec Xaman :","pt":"Configure a sua trustline com Xaman:","id":"Atur trustline Anda dengan Xaman:","zh-Hans":"使用 Xaman 设置信任线:","hi":"Xaman से अपनी ट्रस्टलाइन सेट करें:","ar":"أنشئ خط الثقة باستخدام Xaman:","bn":"Xaman দিয়ে ট্রাস্টলাইন তৈরি করুন:","ur":"Xaman سے اپنی ٹرسٹ لائن بنائیں:"},"pages":[1205,6897,6914,6926,6937]},{"source":"FAQ CalorieToken","translations":{"nl":"Veelgestelde vragen over CalorieToken","es":"Preguntas frecuentes sobre CalorieToken","fr":"Questions fréquentes sur CalorieToken","pt":"Perguntas frequentes sobre CalorieToken","id":"Pertanyaan umum CalorieToken","zh-Hans":"CalorieToken 常见问题","hi":"CalorieToken के सामान्य प्रश्न","ar":"أسئلة CalorieToken الشائعة","bn":"CalorieToken-এর সাধারণ প্রশ্ন","ur":"CalorieToken کے عام سوالات"},"pages":[6855,6897,6914,6926,6937]},{"source":"Team updates","translations":{"nl":"Teamnieuws","es":"Novedades del equipo","fr":"Actualités de l’équipe","pt":"Novidades da equipa","id":"Kabar tim","zh-Hans":"团队动态","hi":"टीम की खबरें","ar":"أخبار الفريق","bn":"দলের খবর","ur":"ٹیم کی خبریں"},"pages":[1207,6897,6914,6926,6937]},{"source":"Billing details","translations":{"nl":"Factuurgegevens","es":"Datos de facturación","fr":"Coordonnées de facturation","pt":"Dados de faturação","id":"Rincian tagihan","zh-Hans":"账单信息","hi":"बिलिंग विवरण","ar":"تفاصيل الفاتورة","bn":"বিলের বিবরণ","ur":"بل کی تفصیلات"},"pages":[6897,6914,6926,6937]},{"source":"First name","translations":{"nl":"Voornaam","es":"Nombre","fr":"Prénom","pt":"Nome","id":"Nama depan","zh-Hans":"名字","hi":"पहला नाम","ar":"الاسم الأول","bn":"নামের প্রথম অংশ","ur":"پہلا نام"},"pages":[6897,6914,6926,6937]},{"source":"Last name","translations":{"nl":"Achternaam","es":"Apellidos","fr":"Nom","pt":"Apelido","id":"Nama belakang","zh-Hans":"姓氏","hi":"अंतिम नाम","ar":"اسم العائلة","bn":"পদবি","ur":"آخری نام"},"pages":[6897,6914,6926,6937]},{"source":"Company name","translations":{"nl":"Bedrijfsnaam","es":"Empresa","fr":"Entreprise","pt":"Empresa","id":"Nama perusahaan","zh-Hans":"公司名称","hi":"कंपनी का नाम","ar":"اسم الشركة","bn":"প্রতিষ্ঠানের নাম","ur":"کمپنی کا نام"},"pages":[6897,6914,6926,6937]},{"source":"Country / Region","translations":{"nl":"Land / regio","es":"País / Región","fr":"Pays / Région","pt":"País / Região","id":"Negara / Wilayah","zh-Hans":"国家/地区","hi":"देश / क्षेत्र","ar":"الدولة / المنطقة","bn":"দেশ / অঞ্চল","ur":"ملک / علاقہ"},"pages":[6897,6914,6926,6937]},{"source":"Street address","translations":{"nl":"Straat en huisnummer","es":"Dirección","fr":"Adresse","pt":"Morada","id":"Alamat jalan","zh-Hans":"街道地址","hi":"सड़क का पता","ar":"عنوان الشارع","bn":"রাস্তার ঠিকানা","ur":"گلی کا پتہ"},"pages":[6897,6914,6926,6937]},{"source":"Town / City","translations":{"nl":"Plaats","es":"Localidad","fr":"Ville","pt":"Localidade","id":"Kota","zh-Hans":"城镇/城市","hi":"शहर","ar":"المدينة","bn":"শহর","ur":"شہر"},"pages":[6897,6914,6926,6937]},{"source":"State / County","translations":{"nl":"Provincie","es":"Provincia","fr":"Région","pt":"Distrito","id":"Provinsi","zh-Hans":"州/省","hi":"राज्य / प्रांत","ar":"المحافظة","bn":"রাজ্য / জেলা","ur":"صوبہ / ضلع"},"pages":[6897,6914,6926,6937]},{"source":"Postcode / ZIP","translations":{"nl":"Postcode","es":"Código postal","fr":"Code postal","pt":"Código postal","id":"Kode pos","zh-Hans":"邮政编码","hi":"पिन कोड","ar":"الرمز البريدي","bn":"পোস্ট কোড","ur":"پوسٹ کوڈ"},"pages":[6897,6914,6926,6937]},{"source":"Phone","translations":{"nl":"Telefoon","es":"Teléfono","fr":"Téléphone","pt":"Telefone","id":"Telepon","zh-Hans":"电话","hi":"फ़ोन","ar":"الهاتف","bn":"ফোন","ur":"فون"},"pages":[6897,6914,6926,6937]},{"source":"Email address","translations":{"nl":"E-mailadres","es":"Correo electrónico","fr":"Adresse e-mail","pt":"E-mail","id":"Alamat email","zh-Hans":"电子邮箱","hi":"ईमेल पता","ar":"البريد الإلكتروني","bn":"ইমেইল ঠিকানা","ur":"ای میل پتہ"},"pages":[6897,6914,6926,6937]},{"source":"Additional information","translations":{"nl":"Aanvullende informatie","es":"Información adicional","fr":"Informations complémentaires","pt":"Informações adicionais","id":"Informasi tambahan","zh-Hans":"其他信息","hi":"अतिरिक्त जानकारी","ar":"معلومات إضافية","bn":"অতিরিক্ত তথ্য","ur":"مزید معلومات"},"pages":[6897,6914,6926,6937]},{"source":"Order notes","translations":{"nl":"Opmerkingen bij je bestelling","es":"Notas del pedido","fr":"Notes de commande","pt":"Notas da encomenda","id":"Catatan pesanan","zh-Hans":"订单备注","hi":"ऑर्डर संबंधी नोट","ar":"ملاحظات الطلب","bn":"অর্ডারের মন্তব্য","ur":"آرڈر کے نوٹس"},"pages":[6897,6914,6926,6937]},{"source":"Your order","translations":{"nl":"Je bestelling","es":"Tu pedido","fr":"Votre commande","pt":"A sua encomenda","id":"Pesanan Anda","zh-Hans":"您的订单","hi":"आपका ऑर्डर","ar":"طلبك","bn":"আপনার অর্ডার","ur":"آپ کا آرڈر"},"pages":[6897,6914,6926,6937]},{"source":"Product","translations":{"nl":"Product","es":"Producto","fr":"Produit","pt":"Produto","id":"Produk","zh-Hans":"商品","hi":"उत्पाद","ar":"المنتج","bn":"পণ্য","ur":"مصنوعہ"},"pages":[6897,6914,6926,6937]},{"source":"Subtotal","translations":{"nl":"Subtotaal","es":"Subtotal","fr":"Sous-total","pt":"Subtotal","id":"Subtotal","zh-Hans":"小计","hi":"उप-योग","ar":"المجموع الفرعي","bn":"উপমোট","ur":"ذیلی مجموعہ"},"pages":[6897,6914,6926,6937]},{"source":"Total","translations":{"nl":"Totaal","es":"Total","fr":"Total","pt":"Total","id":"Total","zh-Hans":"合计","hi":"कुल","ar":"الإجمالي","bn":"মোট","ur":"کل"},"pages":[6897,6914,6926,6937]},{"source":"Quantity","translations":{"nl":"Aantal","es":"Cantidad","fr":"Quantité","pt":"Quantidade","id":"Kuantitas","zh-Hans":"数量","hi":"संख्या","ar":"الكمية","bn":"সংখ্যা","ur":"تعداد"},"pages":[6897,6914,6926,6937]},{"source":"Update cart","translations":{"nl":"Winkelwagen bijwerken","es":"Actualizar carrito","fr":"Mettre à jour le panier","pt":"Atualizar carrinho","id":"Perbarui keranjang","zh-Hans":"更新购物车","hi":"कार्ट अपडेट करें","ar":"تحديث السلة","bn":"কার্ট হালনাগাদ করুন","ur":"کارٹ تازہ کریں"},"pages":[6897,6914,6926,6937]},{"source":"Coupon code","translations":{"nl":"Kortingscode","es":"Código de cupón","fr":"Code promo","pt":"Código de cupão","id":"Kode kupon","zh-Hans":"优惠码","hi":"कूपन कोड","ar":"رمز القسيمة","bn":"কুপন কোড","ur":"کوپن کوڈ"},"pages":[6897,6914,6926,6937]},{"source":"Apply coupon","translations":{"nl":"Kortingscode toepassen","es":"Aplicar cupón","fr":"Appliquer le code","pt":"Aplicar cupão","id":"Gunakan kupon","zh-Hans":"使用优惠码","hi":"कूपन लगाएँ","ar":"تطبيق القسيمة","bn":"কুপন প্রয়োগ করুন","ur":"کوپن لگائیں"},"pages":[6897,6914,6926,6937]},{"source":"Cart totals","translations":{"nl":"Winkelwagentotaal","es":"Totales del carrito","fr":"Total du panier","pt":"Totais do carrinho","id":"Total keranjang","zh-Hans":"购物车合计","hi":"कार्ट का कुल","ar":"إجمالي السلة","bn":"কার্টের মোট","ur":"کارٹ کا کل"},"pages":[6897,6914,6926,6937]},{"source":"Proceed to checkout","translations":{"nl":"Doorgaan naar afrekenen","es":"Continuar al pago","fr":"Passer au paiement","pt":"Continuar para pagamento","id":"Lanjut ke pembayaran","zh-Hans":"继续结账","hi":"भुगतान की ओर बढ़ें","ar":"المتابعة إلى الدفع","bn":"পেমেন্টে এগিয়ে যান","ur":"ادائیگی کی طرف جائیں"},"pages":[6897,6914,6926,6937]},{"source":"Place order","translations":{"nl":"Bestelling plaatsen","es":"Realizar pedido","fr":"Commander","pt":"Fazer encomenda","id":"Buat pesanan","zh-Hans":"提交订单","hi":"ऑर्डर दें","ar":"إرسال الطلب","bn":"অর্ডার করুন","ur":"آرڈر دیں"},"pages":[6897,6914,6926,6937]},{"source":"Return to shop","translations":{"nl":"Terug naar de winkel","es":"Volver a la tienda","fr":"Retour à la boutique","pt":"Voltar à loja","id":"Kembali ke toko","zh-Hans":"返回商店","hi":"दुकान पर लौटें","ar":"العودة إلى المتجر","bn":"দোকানে ফিরুন","ur":"دکان پر واپس جائیں"},"pages":[6897,6914,6926,6937]},{"source":"Your cart is currently empty.","translations":{"nl":"Je winkelwagen is momenteel leeg.","es":"Tu carrito está vacío.","fr":"Votre panier est vide.","pt":"O seu carrinho está vazio.","id":"Keranjang Anda kosong.","zh-Hans":"您的购物车目前为空。","hi":"आपका कार्ट अभी खाली है।","ar":"سلتك فارغة حاليًا.","bn":"আপনার কার্ট এখন খালি।","ur":"آپ کا کارٹ ابھی خالی ہے۔"},"pages":[6897,6914,6926,6937]},{"source":"Read more","translations":{"nl":"Lees meer","es":"Leer más","fr":"Lire la suite","pt":"Ler mais","id":"Baca lebih lanjut","zh-Hans":"阅读更多","hi":"और पढ़ें","ar":"اقرأ المزيد","bn":"আরও পড়ুন","ur":"مزید پڑھیں"},"pages":["*"]},{"source":"READ MORE","translations":{"nl":"LEES MEER","es":"LEER MÁS","fr":"LIRE LA SUITE","pt":"LER MAIS","id":"BACA LEBIH LANJUT","zh-Hans":"阅读更多","hi":"और पढ़ें","ar":"اقرأ المزيد","bn":"আরও পড়ুন","ur":"مزید پڑھیں"},"pages":["*"]},{"source":"Previous","translations":{"nl":"Vorige","es":"Anterior","fr":"Précédent","pt":"Anterior","id":"Sebelumnya","zh-Hans":"上一页","hi":"पिछला","ar":"السابق","bn":"আগের","ur":"پچھلا"},"pages":["*"]},{"source":"Recent posts","translations":{"nl":"Recente berichten","es":"Publicaciones recientes","fr":"Articles récents","pt":"Publicações recentes","id":"Kiriman terbaru","zh-Hans":"最新文章","hi":"हाल की पोस्ट","ar":"أحدث المنشورات","bn":"সাম্প্রতিক পোস্ট","ur":"حالیہ پوسٹس"},"pages":["*"]},{"source":"Category","translations":{"nl":"Categorie","es":"Categoría","fr":"Catégorie","pt":"Categoria","id":"Kategori","zh-Hans":"分类","hi":"श्रेणी","ar":"التصنيف","bn":"বিভাগ","ur":"زمرہ"},"pages":["*"]},{"source":"Categories","translations":{"nl":"Categorieën","es":"Categorías","fr":"Catégories","pt":"Categorias","id":"Kategori","zh-Hans":"分类","hi":"श्रेणियाँ","ar":"التصنيفات","bn":"বিভাগসমূহ","ur":"زمرے"},"pages":["*"]},{"source":"Search","translations":{"nl":"Zoeken","es":"Buscar","fr":"Rechercher","pt":"Pesquisar","id":"Cari","zh-Hans":"搜索","hi":"खोजें","ar":"بحث","bn":"খুঁজুন","ur":"تلاش کریں"},"pages":["*"]},{"source":"Required","translations":{"nl":"Verplicht","es":"Obligatorio","fr":"Obligatoire","pt":"Obrigatório","id":"Wajib","zh-Hans":"必填","hi":"आवश्यक","ar":"مطلوب","bn":"আবশ্যক","ur":"لازمی"},"pages":[6897,6914,6926,6937]},{"source":"optional","translations":{"nl":"optioneel","es":"opcional","fr":"facultatif","pt":"opcional","id":"opsional","zh-Hans":"选填","hi":"वैकल्पिक","ar":"اختياري","bn":"ঐচ্ছিক","ur":"اختیاری"},"pages":[6897,6914,6926,6937]},{"source":"Tackling Frontrunning on the XRPL: Our Journey to a Practical Solution","translations":{"nl":"Frontrunning op de XRPL aanpakken: onze weg naar een praktische oplossing"},"pages":[7545]},{"source":"Introduction","translations":{"nl":"Inleiding"},"pages":[7545,7608]},{"source":"As an early XRP holder and now a developer, I've seen the XRPL ecosystem evolve. Alongside two friends, I founded CalorieToken to create a real use case for tokens on the XRPL. Our journey led us to confront a significant issue: frontrunning on the XRPL order books. This blog post outlines our solution to this problem.","translations":{"nl":"Als vroege XRP-bezitter en inmiddels ontwikkelaar heb ik het XRPL-ecosysteem zien groeien. Samen met twee vrienden richtte ik CalorieToken op om een echte toepassing voor tokens op de XRPL te maken. Onderweg kwamen we een belangrijk probleem tegen: frontrunning in de XRPL-orderboeken. In dit blog beschrijven we onze oplossing voor dit probleem."},"pages":[7545]},{"source":"The Problem: Unfunded Orders and Negative Spread","translations":{"nl":"Het probleem: ongedekte orders en een negatieve spread"},"pages":[7545]},{"source":"During our development phase, we noticed an issue when one of my friends heavily traded EQ for XRP. Despite his efforts to accumulate more XRP, he ended up with less XRP and more EQ. He then sent his EQ to Bitrue for staking, aiming to earn some APY. However, his EQ sell orders remained open on the XRPL, even though his EQ was no longer in his XRPL wallet. This anomaly, known as negative spread, raised concerns about the integrity of the XRPL trading environment.","translations":{"nl":"Tijdens de ontwikkeling merkten we een probleem op toen een vriend veel EQ tegen XRP verhandelde. Ondanks zijn pogingen meer XRP te verzamelen, eindigde hij met minder XRP en meer EQ. Hij stuurde zijn EQ vervolgens naar Bitrue om te staken en daarmee rendement te verdienen. Zijn EQ-verkooporders bleven echter openstaan op de XRPL, hoewel zijn EQ niet meer in zijn XRPL-wallet stond. Deze afwijking, bekend als een negatieve spread, riep vragen op over de integriteit van de XRPL-handelsomgeving."},"pages":[7545]},{"source":"Understanding Frontrunning and Unfunded Offers","translations":{"nl":"Frontrunning en ongedekte orders begrijpen"},"pages":[7545]},{"source":"Frontrunning involves exploiting the time gap between when a trade is initiated and when it is executed. A frontrunner can place their orders ahead of others to profit from price movements. On the XRPL, unfunded offers exacerbate this issue:","translations":{"nl":"Bij frontrunning wordt het tijdsverschil tussen het starten en uitvoeren van een transactie uitgebuit. Een frontrunner kan orders vóór anderen plaatsen om van prijsbewegingen te profiteren. Op de XRPL versterken ongedekte orders dit probleem:"},"pages":[7545]},{"source":"Placing Unfunded Offers: Frontrunners place large buy or sell orders without the actual funds to back them up. These offers appear in the order book, influencing the market price and misleading genuine traders.","translations":{"nl":"Ongedekte orders plaatsen: frontrunners plaatsen grote koop- of verkooporders zonder de middelen om ze te dekken. Deze orders verschijnen in het orderboek, beïnvloeden de marktprijs en misleiden gewone handelaren."},"pages":[7545]},{"source":"Manipulating Market Perception: By placing unfunded offers, frontrunners create a false sense of supply or demand, causing prices to move in their favor.","translations":{"nl":"Het marktbeeld manipuleren: door ongedekte orders te plaatsen, wekken frontrunners een onjuist beeld van vraag of aanbod, waardoor prijzen in hun voordeel bewegen."},"pages":[7545]},{"source":"Profiting from the Spread: Once the price has moved as intended, the frontrunner cancels their unfunded offers and places real orders to exploit the price difference.","translations":{"nl":"Profiteren van de spread: zodra de prijs naar wens is veranderd, annuleert de frontrunner de ongedekte orders en plaatst echte orders om het prijsverschil te benutten."},"pages":[7545]},{"source":"This practice undermines the fairness and transparency of the trading environment, making it crucial to address unfunded offers on the XRPL.","translations":{"nl":"Deze werkwijze ondermijnt de eerlijkheid en transparantie van de handelsomgeving. Daarom is het belangrijk ongedekte orders op de XRPL aan te pakken."},"pages":[7545]},{"source":"Apex 2023: A Keynote that Sparked an Idea","translations":{"nl":"Apex 2023: een presentatie die een idee opriep"},"pages":[7545]},{"source":"At the Apex 2023 conference in Amsterdam, I attended Vito Tumas' keynote on frontrunning on the XRPL. After the session, I proposed a solution: preventing orders from remaining open without the necessary funds. Vito's interest in this idea encouraged me to pursue it further.","translations":{"nl":"Tijdens de Apex-conferentie van 2023 in Amsterdam bezocht ik de presentatie van Vito Tumas over frontrunning op de XRPL. Na afloop stelde ik voor te voorkomen dat orders zonder voldoende dekking open blijven staan. Vito’s belangstelling moedigde mij aan het idee verder uit te werken."},"pages":[7545]},{"source":"Discussing with David Schwartz","translations":{"nl":"In gesprek met David Schwartz"},"pages":[7545]},{"source":"I also shared my thoughts with David Schwartz, suggesting the need to cancel unfunded offers. David highlighted the XRPL's resource constraints, explaining that continuously checking for unfunded offers would be too resource-intensive. His feedback, while sobering, pushed me to think outside the box for a more efficient solution.","translations":{"nl":"Ik deelde mijn gedachten ook met David Schwartz en stelde voor ongedekte orders te annuleren. David wees op de beperkte verwerkingscapaciteit van de XRPL: voortdurend controleren op ongedekte orders zou te veel capaciteit vragen. Zijn nuchtere reactie zette mij aan om verder te denken over een efficiëntere oplossing."},"pages":[7545]},{"source":"Crafting a Resource-Efficient Solution","translations":{"nl":"Een oplossing met beperkt middelengebruik ontwerpen"},"pages":[7545]},{"source":"Determined to resolve the issue without compromising the XRPL's performance, I collaborated with other developers and devised a novel approach:","translations":{"nl":"Vastbesloten het probleem op te lossen zonder de prestaties van de XRPL te schaden, werkte ik met andere ontwikkelaars aan een nieuwe aanpak:"},"pages":[7545]},{"source":"Real-Time Oracle: We proposed running an oracle parallel to the XRPL, capable of monitoring offers in real-time.","translations":{"nl":"Realtime oracle: we stelden een oracle naast de XRPL voor die orders in realtime kan volgen."},"pages":[7545]},{"source":"Indexing Unfunded Offers: This oracle would specifically index unfunded offers, minimizing the resource load.","translations":{"nl":"Ongedekte orders indexeren: deze oracle zou specifiek ongedekte orders indexeren, om de belasting zo klein mogelijk te houden."},"pages":[7545]},{"source":"Cancellation Service: A separate service would then cancel these indexed offers on the XRPL, ensuring the process remains efficient and minimally intrusive.","translations":{"nl":"Annuleringsdienst: een aparte dienst zou deze geïndexeerde orders vervolgens op de XRPL annuleren, zodat het proces efficiënt blijft en zo min mogelijk ingrijpt."},"pages":[7545]},{"source":"Why This Solution Works","translations":{"nl":"Waarom deze oplossing werkt"},"pages":[7545]},{"source":"Resource Efficiency: By offloading the real-time monitoring to an oracle, we avoid overburdening the XRPL.","translations":{"nl":"Efficiënt middelengebruik: door realtime monitoring aan een oracle over te laten, voorkomen we extra belasting van de XRPL."},"pages":[7545]},{"source":"Focused Action: Only the unfunded offers are targeted for cancellation, streamlining the process.","translations":{"nl":"Gerichte actie: alleen ongedekte orders komen in aanmerking voor annulering, wat het proces vereenvoudigt."},"pages":[7545]},{"source":"Preserving Speed: The XRPL's renowned speed remains unaffected, maintaining its core advantage.","translations":{"nl":"Behoud van snelheid: de bekende snelheid van de XRPL blijft behouden, net als dit belangrijke voordeel."},"pages":[7545]},{"source":"A Step Towards a Cleaner XRPL","translations":{"nl":"Een stap naar een schonere XRPL"},"pages":[7545]},{"source":"Implementing this solution underscores our commitment to maintaining a transparent and reliable environment for CalorieToken and the broader XRPL community. While David Schwartz suggested relying on third-party software, our oracle-based approach offers a direct and efficient method to tackle unfunded offers, enhancing the overall integrity of the XRPL.","translations":{"nl":"Het uitvoeren van deze oplossing onderstreept ons streven naar een transparante en betrouwbare omgeving voor CalorieToken en de bredere XRPL-gemeenschap. David Schwartz stelde voor software van derden te gebruiken; onze aanpak met een oracle biedt een directe en efficiënte manier om ongedekte orders aan te pakken en zo de integriteit van de XRPL te verbeteren."},"pages":[7545]},{"source":"Conclusion","translations":{"nl":"Conclusie"},"pages":[7545,7564,7575,7587,7608]},{"source":"Our journey from identifying a problem to proposing a viable solution reflects our dedication to the XRPL ecosystem. By addressing the issue of unfunded offers, we aim to contribute to a more secure and efficient trading environment. Stay tuned as we continue to develop and refine our solutions for the betterment of the XRPL community.","translations":{"nl":"Onze weg van het herkennen van een probleem naar het voorstellen van een haalbare oplossing toont onze inzet voor het XRPL-ecosysteem. Door ongedekte orders aan te pakken, willen we bijdragen aan een veiligere en efficiëntere handelsomgeving. Volg onze verdere ontwikkeling en verfijning van oplossingen voor de XRPL-gemeenschap."},"pages":[7545]},{"source":"CalorieToken: An Update on Our Journey and Future Plans","translations":{"nl":"CalorieToken: een update over onze voortgang en toekomstplannen"},"pages":[7564]},{"source":"Commitment to Transparency and Honesty","translations":{"nl":"Onze inzet voor transparantie en eerlijkheid"},"pages":[7564]},{"source":"At CalorieToken, we have always prioritized transparency and honesty. Our commitment to openness extends to our team and project progress, and we believe it’s important to keep our community informed about our journey. Here’s a detailed update on where we stand and what’s ahead for us.","translations":{"nl":"Bij CalorieToken staan transparantie en eerlijkheid altijd voorop. Onze openheid gaat ook over het team en de voortgang van het project. We vinden het belangrijk onze gemeenschap op de hoogte te houden. Hier volgt een uitgebreide update over waar we staan en wat er voor ons ligt."},"pages":[7564]},{"source":"Our Team","translations":{"nl":"Ons team"},"pages":[7564]},{"source":"We are a small but dedicated team based in the Netherlands. The core members include:","translations":{"nl":"We zijn een klein, toegewijd team in Nederland. De kernleden zijn:"},"pages":[7564]},{"source":"Pieter: Our developer, who is instrumental in bringing our vision to life.","translations":{"nl":"Pieter: onze ontwikkelaar, die een centrale rol speelt bij het realiseren van onze visie."},"pages":[7564]},{"source":"Leroy: Our creative designer, who has been contributing his artistic talents to the project.","translations":{"nl":"Leroy: onze creatieve ontwerper, die zijn artistieke talenten inzet voor het project."},"pages":[7564]},{"source":"Unfortunately, due to personal circumstances, Sam is no longer part of the team, and Leroy’s contributions are temporarily limited. Despite these challenges, we remain committed to advancing CalorieToken and its associated projects.","translations":{"nl":"Door persoonlijke omstandigheden maakt Sam helaas geen deel meer uit van het team en is Leroy’s bijdrage tijdelijk beperkt. Ondanks deze uitdagingen blijven we werken aan CalorieToken en de bijbehorende projecten."},"pages":[7564]},{"source":"Transparency in Token Handling","translations":{"nl":"Transparantie over het gebruik van tokens"},"pages":[7564]},{"source":"We want to clarify that we have never sold any $CAL tokens. This commitment to transparency can be verified on blockchain explorers such as","translations":{"nl":"We willen duidelijk maken dat we nooit $CAL-tokens hebben verkocht. Deze transparantie is te controleren via blockchainverkenners zoals"},"pages":[7564]},{"source":"and","translations":{"nl":"en"},"pages":[7564]},{"source":". Our personal funds were used to list CalorieToken on the","translations":{"nl":". Onze eigen middelen zijn gebruikt om CalorieToken te laten noteren op de"},"pages":[7564]},{"source":"exchange, and we take pride in these accomplishments, no matter how small they may seem.","translations":{"nl":"-exchange. We zijn trots op deze resultaten, hoe klein ze misschien ook lijken."},"pages":[7564]},{"source":"Future Listing Aspirations","translations":{"nl":"Ambities voor toekomstige noteringen"},"pages":[7564]},{"source":"While we aspire to be listed on major platforms like Binance and Bitrue in the future, our current focus is on building the CalorieApp and expanding our brand. We believe that laying a solid foundation is crucial for long-term success.","translations":{"nl":"Hoewel we hopen in de toekomst op grote platforms zoals Binance en Bitrue te komen, ligt onze huidige aandacht bij het bouwen van CalorieApp en het uitbreiden van ons merk. Volgens ons is een stevige basis essentieel voor succes op langere termijn."},"pages":[7564]},{"source":"Navigating Regulatory Landscapes","translations":{"nl":"Omgaan met regelgeving"},"pages":[7564]},{"source":"Regulatory clarity remains a significant factor in our operations. Although there has been some progress in Europe, uncertainties persist. We continue to navigate this landscape with caution and adaptability to ensure compliance and sustainability.","translations":{"nl":"Duidelijkheid over regelgeving blijft belangrijk voor ons werk. Hoewel Europa vooruitgang heeft geboekt, blijven onzekerheden bestaan. We bewegen voorzichtig en flexibel door deze omgeving om naleving en continuïteit te ondersteunen."},"pages":[7564]},{"source":"Development Journey","translations":{"nl":"Ons ontwikkelingstraject"},"pages":[7564]},{"source":"Since September 2021, we have been diligently working on our unique idea for CalorieToken. For more details on our vision and plans, you can check out our","translations":{"nl":"Sinds september 2021 werken we aan ons eigen idee voor CalorieToken. Meer over onze visie en plannen lees je in ons"},"pages":[7564]},{"source":"CalorieApp Development","translations":{"nl":"Ontwikkeling van CalorieApp"},"pages":[7564]},{"source":"The development of the CalorieApp is ongoing. You can explore our code and follow our progress on our","translations":{"nl":"De ontwikkeling van CalorieApp gaat door. Je kunt onze code bekijken en de voortgang volgen op onze"},"pages":[7564]},{"source":"GitHub page","translations":{"nl":"GitHub-pagina"},"pages":[7564]},{"source":". Pieter, our main developer, runs his own IT business five days a week but remains dedicated to building our app. The CalorieApp leverages advanced technologies, including:","translations":{"nl":". Pieter, onze hoofdontwikkelaar, runt vijf dagen per week zijn eigen IT-bedrijf en blijft daarnaast aan de app werken. CalorieApp gebruikt geavanceerde technologieën, waaronder:"},"pages":[7564]},{"source":"IPFS for decentralized storage","translations":{"nl":"IPFS voor gedecentraliseerde opslag"},"pages":[7564]},{"source":"BigchainDB as a decentralized database","translations":{"nl":"BigchainDB als gedecentraliseerde database"},"pages":[7564]},{"source":"FoodRepo as the initial data source","translations":{"nl":"FoodRepo als eerste gegevensbron"},"pages":[7564]},{"source":"Challenges and Progress","translations":{"nl":"Uitdagingen en voortgang"},"pages":[7564]},{"source":"The progress of the CalorieApp is steady but not as fast as we would like due to time and resource constraints. Our priority is to deliver a functional and secure product that meets the needs of our users.","translations":{"nl":"De ontwikkeling van CalorieApp vordert gestaag, maar door beperkte tijd en middelen minder snel dan we zouden willen. Onze prioriteit is een werkend en veilig product dat aansluit bij de behoeften van gebruikers."},"pages":[7564]},{"source":"Community Engagement","translations":{"nl":"Betrokkenheid van de gemeenschap"},"pages":[7564]},{"source":"We encourage the Calorie community to use and build applications on our platform. Our goal is for CalorieToken to evolve and thrive long after us, leaving a lasting legacy. Your support is crucial, and we owe our current position to the unwavering support of the Calorie community.","translations":{"nl":"We moedigen de Calorie-gemeenschap aan ons platform te gebruiken en er toepassingen voor te bouwen. Ons doel is dat CalorieToken ook na ons blijft groeien en voortbestaan. Jullie steun is essentieel; onze huidige positie danken we aan de blijvende steun van de Calorie-gemeenschap."},"pages":[7564]},{"source":"Despite recent changes in our team, we remain steadfast in our mission. Pieter, our main developer, is handling most of the work during this transition period, and we appreciate your understanding and support.","translations":{"nl":"Ondanks recente veranderingen in ons team blijven we vasthouden aan onze missie. Pieter, onze hoofdontwikkelaar, doet tijdens deze overgang het grootste deel van het werk. We waarderen jullie begrip en steun."},"pages":[7564]},{"source":"Thank you for being a part of our journey. Together, we will continue to work towards conquering the Food","translations":{"nl":"Bedankt dat je deel uitmaakt van onze reis. Samen blijven we werken aan het veroveren van de voedings-"},"pages":[7564]},{"source":"Beverage industry with CalorieToken and the CalorieApp!","translations":{"nl":"drankensector met CalorieToken en CalorieApp!"},"pages":[7564]},{"source":"Exciting News: First Release of the CalorieApp Test APK on Android!","translations":{"nl":"Mooi nieuws: de eerste CalorieApp-test-APK voor Android!"},"pages":[7569]},{"source":"We are thrilled to announce a significant milestone in our journey: the first release of the test APK for our CalorieApp on Android! 📱 This achievement is a testament to the dedication and hard work of our solo developer, who has been juggling this project alongside his main job. We invite you to dive into the app on your Android phone and be part of this exciting milestone! 🎉","translations":{"nl":"Met plezier kondigen we een belangrijke mijlpaal aan: de eerste test-APK van CalorieApp voor Android! 📱 Dit resultaat is te danken aan de inzet van onze enige ontwikkelaar, die dit project naast zijn hoofdberoep uitvoert. We nodigen je uit om de app op je Android-telefoon te proberen en deze mijlpaal mee te beleven! 🎉"},"pages":[7569]},{"source":"Download the Test APK Here:","translations":{"nl":"Download de test-APK hier:"},"pages":[7569]},{"source":"Download the Test APK","translations":{"nl":"Download de test-APK"},"pages":[7569]},{"source":"Disclaimer:","translations":{"nl":"Toelichting:"},"pages":[7569,7583]},{"source":"This application is currently in the testing phase and is not yet ready for the Play Store. Install at your own risk.","translations":{"nl":"Deze toepassing bevindt zich in de testfase en is nog niet klaar voor de Play Store. Installeren gebeurt op eigen risico."},"pages":[7569]},{"source":"Test XRPL Wallet:","translations":{"nl":"XRPL-testwallet:"},"pages":[7569]},{"source":"This version features a simple XRPL wallet running on the testnet. Upon generating a test wallet within the app, you will receive 1000 test XRP for experimenting with transactions and exploring the features. This allows you to familiarize yourself with the app's functionality in a risk-free environment.","translations":{"nl":"Deze versie bevat een eenvoudige XRPL-wallet op Testnet. Wanneer je in de app een testwallet maakt, ontvang je 1000 test-XRP om transacties en functies uit te proberen. Zo kun je de werking van de app leren kennen in een omgeving zonder financieel risico."},"pages":[7569]},{"source":"Important:","translations":{"nl":"Belangrijk:"},"pages":[7569]},{"source":"This application is intended for testing and showcase purposes only. It is not suitable for production use, and any transactions made with the test XRP are not real and will not impact your actual XRPL account or holdings.","translations":{"nl":"Deze toepassing is uitsluitend bedoeld voor tests en demonstraties. Ze is niet geschikt voor productiegebruik. Transacties met test-XRP zijn niet echt en beïnvloeden je echte XRPL-account of bezit niet."},"pages":[7569]},{"source":"Explore Our OpenSource Code:","translations":{"nl":"Bekijk onze opensourcecode:"},"pages":[7569]},{"source":"For those interested in the technical details and ongoing development, you can check out our open-source code on GitHub:","translations":{"nl":"Wie belangstelling heeft voor de techniek en de verdere ontwikkeling, kan onze opensourcecode op GitHub bekijken:"},"pages":[7569]},{"source":"CalorieApp Testnet on GitHub","translations":{"nl":"CalorieApp Testnet op GitHub"},"pages":[7569]},{"source":"Our Journey Ahead:","translations":{"nl":"Het vervolg:"},"pages":[7569]},{"source":"This marks the first step towards an incredible journey. Following this milestone, our developer is set to enhance and expand the CalorieApp with more features. While we aim to get it ready for the Play Store, we acknowledge that there's still a lot of work to be done before the official launch.","translations":{"nl":"Dit is de eerste stap in een bijzondere ontwikkeling. Na deze mijlpaal gaat onze ontwikkelaar CalorieApp verder verbeteren en uitbreiden. We willen de app geschikt maken voor de Play Store, maar erkennen dat voor de officiële introductie nog veel werk nodig is."},"pages":[7569]},{"source":"Our Ultimate Goal:","translations":{"nl":"Ons uiteindelijke doel:"},"pages":[7569]},{"source":"We are committed to building a revolutionary food tracking system based on blockchain technology. This ambitious goal will be pursued gradually as we continue to develop the basic CalorieApp.","translations":{"nl":"We willen een vernieuwend systeem voor het volgen van voeding bouwen op basis van blockchaintechnologie. Dat ambitieuze doel streven we geleidelijk na terwijl we de basis van CalorieApp ontwikkelen."},"pages":[7569]},{"source":"Get Involved:","translations":{"nl":"Doe mee:"},"pages":[7569]},{"source":"If you want to test the app with real test currencies, feel free to reach out for Lipisa and CalorieTest! 🔄 Just drop us a message, and we'll provide you with what you need to explore the app further.","translations":{"nl":"Wil je de app met testmunten uitproberen? Vraag gerust naar Lipisa en CalorieTest! 🔄 Stuur een bericht en we geven je wat je nodig hebt om de app verder te verkennen."},"pages":[7569]},{"source":"Stay Tuned:","translations":{"nl":"Blijf op de hoogte:"},"pages":[7569,7583]},{"source":"This release is just the beginning. We appreciate your support and enthusiasm as we continue to innovate and improve. Stay tuned for more updates and exciting developments! 🍏🔗💡","translations":{"nl":"Deze release is pas het begin. We waarderen jullie steun en enthousiasme terwijl we blijven vernieuwen en verbeteren. Volg ons voor verdere updates en nieuwe ontwikkelingen! 🍏🔗💡"},"pages":[7569]},{"source":"Exciting News: Our Donations Page is LIVE!","translations":{"nl":"Mooi nieuws: onze donatiepagina is live!"},"pages":[7573]},{"source":"We are thrilled to announce that our donations page is now live! This is an incredible opportunity for you to fuel the future of finance and shape the revolutionary CalorieApp ecosystem with your XRP contributions. Your support is invaluable to us as we continue to build and innovate within the blockchain space.","translations":{"nl":"We zijn blij te melden dat onze donatiepagina nu live is! Met je bijdrage in XRP kun je de toekomst van financiën en het vernieuwende CalorieApp-ecosysteem helpen vormgeven. Je steun is van grote waarde terwijl we verder bouwen en vernieuwen binnen de blockchainwereld."},"pages":[7573]},{"source":"Join Us in Building the Future","translations":{"nl":"Bouw met ons aan de toekomst"},"pages":[7573]},{"source":"At CalorieToken, we believe in the power of community and collective effort. Our journey has been shaped by the support and enthusiasm of our amazing community, and we are excited to offer a new way for you to get involved. Whether your donation is small, big, or somewhere in between, every bit counts and goes directly into crafting an amazing app experience.","translations":{"nl":"Bij CalorieToken geloven we in de kracht van de gemeenschap en gezamenlijke inzet. Onze ontwikkeling is gevormd door jullie steun en enthousiasme. We bieden graag een nieuwe manier om mee te doen. Of je donatie klein, groot of daartussenin is: elke bijdrage telt en gaat rechtstreeks naar het maken van een mooie app-ervaring."},"pages":[7573]},{"source":"Your contributions will help us:","translations":{"nl":"Je bijdrage helpt ons om:"},"pages":[7573]},{"source":"Enhance the CalorieApp: Develop new features and improve existing ones to create a seamless and innovative user experience.","translations":{"nl":"CalorieApp te verbeteren: nieuwe functies ontwikkelen en bestaande functies verbeteren voor een soepele en vernieuwende gebruikerservaring."},"pages":[7573]},{"source":"Expand Our Ecosystem: Integrate more functionalities and services that align with our vision of revolutionizing the food and beverage industry using blockchain technology.","translations":{"nl":"Ons ecosysteem uit te breiden: meer functies en diensten integreren die passen bij onze visie om de voedings- en drankensector met blockchaintechnologie te vernieuwen."},"pages":[7573]},{"source":"Support Development Costs: Cover the costs associated with ongoing development, maintenance, and scaling of our infrastructure.","translations":{"nl":"Ontwikkelkosten te dragen: de kosten van doorlopende ontwikkeling, onderhoud en uitbreiding van onze infrastructuur betalen."},"pages":[7573]},{"source":"A Heartfelt Thank You to Our Community","translations":{"nl":"Een hartelijk dankjewel aan onze gemeenschap"},"pages":[7573]},{"source":"We extend our heartfelt gratitude to our incredible community. Your unwavering support and belief in our vision drive us to push the boundaries of what is possible. We are committed to transparency and ensuring that every penny donated is used effectively to bring our ideas to life.","translations":{"nl":"We bedanken onze gemeenschap van harte. Jullie blijvende steun en vertrouwen in onze visie moedigen ons aan de grenzen van het mogelijke te verkennen. We zetten ons in voor transparantie en willen elke gedoneerde cent effectief gebruiken om onze ideeën te realiseren."},"pages":[7573]},{"source":"To show our appreciation, we have some exciting plans in the pipeline:","translations":{"nl":"Als blijk van waardering werken we aan enkele mooie plannen:"},"pages":[7573]},{"source":"Merch & NFTs: We will soon be launching exclusive merchandise and NFTs for our supporters. Stay tuned for updates on how you can get your hands on these unique items!","translations":{"nl":"Merchandise en NFT’s: binnenkort willen we exclusieve merchandise en NFT’s voor onze supporters introduceren. Houd onze updates in de gaten om te ontdekken hoe je deze bijzondere artikelen kunt verkrijgen!"},"pages":[7573]},{"source":"Support Us Today","translations":{"nl":"Steun ons vandaag"},"pages":[7573]},{"source":"Ready to contribute? Visit our","translations":{"nl":"Wil je bijdragen? Bezoek onze"},"pages":[7573]},{"source":"donations page","translations":{"nl":"donatiepagina"},"pages":[7573]},{"source":"and make your XRP donation today. Your support will make a significant impact on our ability to deliver a top-notch app and continue our mission of innovation.","translations":{"nl":"en doneer vandaag XRP. Je steun helpt ons om een goede app te leveren en onze vernieuwingsmissie voort te zetten."},"pages":[7573]},{"source":"Important Note","translations":{"nl":"Belangrijke toelichting"},"pages":[7573]},{"source":"While your generous support drives our efforts, we cannot guarantee specific outcomes or features as a result of donations. However, we assure you that every penny will go towards the development of CalorieApp and Calorie Token. Your trust and contributions are the foundation of our progress, and we are committed to using these resources wisely and transparently.","translations":{"nl":"Je steun helpt ons werk, maar donaties garanderen geen specifieke resultaten of functies. Wel verzekeren we je dat elke cent naar de ontwikkeling van CalorieApp en CalorieToken gaat. Jullie vertrouwen en bijdragen vormen de basis van onze voortgang. We willen deze middelen verstandig en transparant gebruiken."},"pages":[7573]},{"source":"Stay Connected","translations":{"nl":"Blijf verbonden"},"pages":[7573,7591]},{"source":"We are just getting started, and there is so much more to come. Follow us on our journey as we work tirelessly to create a better, more efficient ecosystem. Stay connected with us for the latest updates, new features, and exciting announcements.","translations":{"nl":"We zijn pas begonnen en er komt nog veel meer. Volg ons terwijl we hard werken aan een beter en efficiënter ecosysteem. Blijf op de hoogte van updates, nieuwe functies en aankondigingen."},"pages":[7573]},{"source":"Thank you for being an integral part of our journey. Together, we can make a difference and revolutionize the way we think about finance and technology.","translations":{"nl":"Bedankt voor jouw belangrijke bijdrage aan onze ontwikkeling. Samen kunnen we verschil maken en onze kijk op financiën en technologie vernieuwen."},"pages":[7573]},{"source":"Enhancing Trust in the XRPL Ecosystem: The Importance of Verified Domains","translations":{"nl":"Vertrouwen in het XRPL-ecosysteem vergroten: het belang van geverifieerde domeinen"},"pages":[7575]},{"source":"At CalorieToken, we believe that trust and credibility are the cornerstones of any successful blockchain project. That’s why we are excited to extend our heartfelt thanks to","translations":{"nl":"Bij CalorieToken zien we vertrouwen en geloofwaardigheid als de basis van een succesvol blockchainproject. Daarom bedanken we"},"pages":[7575]},{"source":"for providing the XRPL verified domains list! This initiative is a significant step towards fostering a more secure and trustworthy environment within the XRPL community.","translations":{"nl":"van harte voor de lijst met geverifieerde XRPL-domeinen! Dit initiatief is een belangrijke stap naar een veiligere en betrouwbaardere omgeving binnen de XRPL-gemeenschap."},"pages":[7575]},{"source":"Why Verified Domains Matter","translations":{"nl":"Waarom geverifieerde domeinen belangrijk zijn"},"pages":[7575]},{"source":"Verified domains are crucial for several reasons:","translations":{"nl":"Geverifieerde domeinen zijn om verschillende redenen belangrijk:"},"pages":[7575]},{"source":"Enhanced Credibility: Verification helps distinguish legitimate projects and businesses from fraudulent ones. It assures users that they are interacting with genuine entities.","translations":{"nl":"Meer geloofwaardigheid: verificatie helpt legitieme projecten en bedrijven van frauduleuze partijen te onderscheiden. Ze geeft gebruikers vertrouwen dat ze met echte organisaties te maken hebben."},"pages":[7575]},{"source":"Building Trust: Trust is fundamental in the crypto space. By verifying your domain, you demonstrate transparency and commitment to integrity.","translations":{"nl":"Vertrouwen opbouwen: vertrouwen is essentieel in de cryptowereld. Door je domein te verifiëren, laat je transparantie en inzet voor integriteit zien."},"pages":[7575]},{"source":"Increased Security: Verified domains reduce the risk of phishing and other malicious activities, creating a safer environment for users and developers.","translations":{"nl":"Meer veiligheid: geverifieerde domeinen verkleinen het risico op phishing en andere schadelijke activiteiten, wat gebruikers en ontwikkelaars een veiligere omgeving biedt."},"pages":[7575]},{"source":"Join the Movement: Verify Your Domain","translations":{"nl":"Doe mee: verifieer je domein"},"pages":[7575]},{"source":"We encourage all projects and businesses in the XRPL community to follow this lead. Verifying your domain is a straightforward process that can significantly boost your project's credibility and trustworthiness.","translations":{"nl":"We moedigen alle projecten en bedrijven binnen de XRPL-gemeenschap aan dit voorbeeld te volgen. Je domein verifiëren is een eenvoudig proces dat de geloofwaardigheid en betrouwbaarheid van je project kan vergroten."},"pages":[7575]},{"source":"You can check out the list of verified domains here:","translations":{"nl":"Bekijk hier de lijst met geverifieerde domeinen:"},"pages":[7575]},{"source":"Bithomp Verified Domains","translations":{"nl":"Geverifieerde domeinen bij Bithomp"},"pages":[7575]},{"source":"Taking Verification a Step Further: The TOML File","translations":{"nl":"Een extra verificatiestap: het TOML-bestand"},"pages":[7575]},{"source":"For those looking to enhance their verification, consider hosting a TOML file on your domain. This adds an extra layer of authenticity and provides additional information about your project.","translations":{"nl":"Wil je je verificatie uitbreiden? Overweeg een TOML-bestand op je domein te plaatsen. Dat voegt een extra laag van herkomstinformatie toe en geeft meer informatie over je project."},"pages":[7575]},{"source":"Learn How to Create a TOML File: Follow this guide on the XRPL website to create your own TOML file.","translations":{"nl":"Leer een TOML-bestand maken: volg deze handleiding op de XRPL-website om je eigen TOML-bestand te maken."},"pages":[7575]},{"source":"Validate Your TOML: Once created, ensure your TOML file is correctly formatted and validated by using the TOML checker.","translations":{"nl":"Valideer je TOML: controleer na het maken met de TOML-checker of je bestand juist is opgemaakt en geldig is."},"pages":[7575]},{"source":"Our Example","translations":{"nl":"Ons voorbeeld"},"pages":[7575]},{"source":"To provide a clear example, here is our verified domain TOML file:","translations":{"nl":"Als duidelijk voorbeeld vind je hier het TOML-bestand van ons geverifieerde domein:"},"pages":[7575]},{"source":". By hosting this file, we aim to set a standard and inspire others to take similar steps towards building a more secure XRPL ecosystem.","translations":{"nl":". Door dit bestand te hosten, willen we een voorbeeld geven en anderen inspireren soortgelijke stappen te zetten naar een veiliger XRPL-ecosysteem."},"pages":[7575]},{"source":"Together, we can enhance the credibility, trust, and security of the XRPL community. We encourage every project and business to verify their domains and consider the additional step of hosting a TOML file. These efforts will collectively strengthen the entire ecosystem.","translations":{"nl":"Samen kunnen we de geloofwaardigheid, het vertrouwen en de veiligheid binnen de XRPL-gemeenschap vergroten. We moedigen ieder project en bedrijf aan zijn domein te verifiëren en ook een TOML-bestand te overwegen. Samen versterken die inspanningen het hele ecosysteem."},"pages":[7575]},{"source":"Thank you for being a part of this journey towards a more transparent and trustworthy blockchain environment. Let’s build an even more secure and trusted XRPL ecosystem together! 💼🌐","translations":{"nl":"Bedankt dat je meewerkt aan een transparantere en betrouwbaardere blockchainomgeving. Laten we samen een nog veiliger en betrouwbaarder XRPL-ecosysteem bouwen! 💼🌐"},"pages":[7575]},{"source":"Unlock Seamless Trading with Our Integrated Exchange","translations":{"nl":"Soepel handelen met onze geïntegreerde exchange"},"pages":[7578]},{"source":"At CalorieToken, we're thrilled to unveil one of our most powerful features: our integrated exchange, powered by","translations":{"nl":"Bij CalorieToken presenteren we graag een van onze krachtigste functies: onze geïntegreerde exchange, mogelijk gemaakt door"},"pages":[7578]},{"source":". This cutting-edge tool revolutionizes your trading experience by providing seamless connectivity with 44 popular self-custody wallets and access to hundreds of cryptocurrencies, including a special focus on XRP through the Xaman wallet.","translations":{"nl":". Dit vernieuwende hulpmiddel biedt verbinding met 44 populaire wallets in eigen beheer en toegang tot honderden cryptomunten, met bijzondere aandacht voor XRP via de Xaman-wallet."},"pages":[7578]},{"source":"Why Our Integrated Exchange is a Game-Changer","translations":{"nl":"Waarom onze geïntegreerde exchange een verschil maakt"},"pages":[7578]},{"source":"Our integrated exchange is designed to deliver a streamlined and efficient trading experience. Here’s what sets it apart:","translations":{"nl":"Onze geïntegreerde exchange is ontworpen voor eenvoudig en efficiënt handelen. Dit maakt de dienst bijzonder:"},"pages":[7578]},{"source":"Broad Wallet Compatibility: Connect effortlessly with 44 self-custody wallets, including MetaMask, Coinbase Wallet, Trust Wallet, WalletConnect, and Xaman. Xaman, formerly known as Xumm, is tailored specifically for XRP holders, offering enhanced features for managing XRP transactions and assets.","translations":{"nl":"Brede walletcompatibiliteit: verbind eenvoudig met 44 wallets in eigen beheer, waaronder MetaMask, Coinbase Wallet, Trust Wallet, WalletConnect en Xaman. Xaman, eerder Xumm, is speciaal ontwikkeld voor XRP-bezitters en biedt uitgebreide functies voor XRP-transacties en bezittingen."},"pages":[7578]},{"source":"Extensive Cryptocurrency Support: Access a vast range of cryptocurrencies, with hundreds of options available for trading, giving you ample opportunities to diversify and manage your portfolio.","translations":{"nl":"Ondersteuning voor veel cryptomunten: krijg toegang tot honderden verschillende cryptomunten om te verhandelen, met ruime mogelijkheden om je portefeuille te spreiden en te beheren."},"pages":[7578]},{"source":"User-Friendly Interface: Our platform is designed for ease of use, ensuring a smooth experience whether you’re a newcomer or a seasoned trader.","translations":{"nl":"Gebruiksvriendelijke interface: ons platform is ontworpen voor eenvoudig gebruik, zowel voor beginners als ervaren handelaren."},"pages":[7578]},{"source":"Enhanced Security: Self-custody wallets provide you with full control over your assets, ensuring high levels of security and peace of mind.","translations":{"nl":"Meer veiligheid: wallets in eigen beheer geven je volledige controle over je bezittingen, voor een hoog veiligheidsniveau en meer gemoedsrust."},"pages":[7578]},{"source":"How to Get Started","translations":{"nl":"Zo begin je"},"pages":[7578]},{"source":"Getting started with our integrated exchange is simple. Here’s how you can begin:","translations":{"nl":"Beginnen met onze geïntegreerde exchange is eenvoudig. Dit zijn de stappen:"},"pages":[7578]},{"source":"Connect Your Wallet Choose from 44 supported self-custody wallets to link with our exchange. This includes popular options like MetaMask, Trust Wallet, Coinbase Wallet, and Xaman—ideal for XRP enthusiasts who need specialized features. Follow the easy setup instructions to connect your wallet.","translations":{"nl":"Verbind je wallet. Kies uit 44 ondersteunde wallets in eigen beheer om met onze exchange te verbinden. Daarbij zijn bekende opties zoals MetaMask, Trust Wallet, Coinbase Wallet en Xaman, gericht op XRP-gebruikers die speciale functies nodig hebben. Volg de eenvoudige instructies om je wallet te verbinden."},"pages":[7578]},{"source":"Swap Your Assets Once your wallet is connected, you can seamlessly swap a wide range of cryptocurrencies. Trade assets such as $MATIC, $Ethereum, $XRP, and $Calorie quickly and securely, leveraging our platform’s robust features.","translations":{"nl":"Wissel je bezittingen. Zodra je wallet verbonden is, kun je allerlei cryptomunten omwisselen. Verhandel bijvoorbeeld $MATIC, $Ethereum, $XRP en $Calorie snel en veilig met de functies van ons platform."},"pages":[7578]},{"source":"Explore and Trade With access to hundreds of cryptocurrencies, you have extensive options for managing and diversifying your portfolio. Make informed trading decisions and explore various market opportunities with ease.","translations":{"nl":"Verken en handel. Met toegang tot honderden cryptomunten heb je ruime mogelijkheden om je portefeuille te beheren en te spreiden. Neem geïnformeerde handelsbeslissingen en verken verschillende marktmogelijkheden."},"pages":[7578]},{"source":"Supported Wallets and Cryptocurrencies","translations":{"nl":"Ondersteunde wallets en cryptomunten"},"pages":[7578]},{"source":"Our integrated exchange supports a diverse array of wallets and cryptocurrencies, including:","translations":{"nl":"Onze geïntegreerde exchange ondersteunt allerlei wallets en cryptomunten, waaronder:"},"pages":[7578]},{"source":"Wallets: Connect to 44 self-custody wallets, including MetaMask, Coinbase Wallet, Trust Wallet, WalletConnect, and Xaman (formerly Xumm). Xaman is specially designed for XRP users, offering unique tools and features for managing XRP transactions.","translations":{"nl":"Wallets: verbind met 44 wallets in eigen beheer, waaronder MetaMask, Coinbase Wallet, Trust Wallet, WalletConnect en Xaman (voorheen Xumm). Xaman is speciaal ontworpen voor XRP-gebruikers en biedt bijzondere hulpmiddelen voor het beheren van XRP-transacties."},"pages":[7578]},{"source":"Cryptocurrencies: Trade a broad spectrum of assets, from popular tokens like $MATIC, $Ethereum, and $XRP to many other emerging cryptocurrencies. For a full list of supported cryptocurrencies, visit our exchange page.","translations":{"nl":"Cryptomunten: verhandel een breed scala aan bezittingen, van bekende tokens zoals $MATIC, $Ethereum en $XRP tot andere opkomende cryptomunten. Kijk op onze exchangepagina voor de volledige lijst met ondersteunde munten."},"pages":[7578]},{"source":"Experience the Future of Crypto Trading","translations":{"nl":"Ervaar de toekomst van cryptohandel"},"pages":[7578]},{"source":"Our integrated exchange is crafted to enhance your trading experience with its comprehensive wallet compatibility and extensive cryptocurrency support. Whether you're managing a diverse portfolio or executing specific trades, our platform is designed to meet your needs.","translations":{"nl":"Onze geïntegreerde exchange is bedoeld om je handelservaring te verbeteren met brede walletcompatibiliteit en ondersteuning voor veel cryptomunten. Of je nu een gevarieerde portefeuille beheert of specifieke transacties uitvoert, ons platform is ontworpen voor jouw behoeften."},"pages":[7578]},{"source":"Key Features:","translations":{"nl":"Belangrijkste functies:"},"pages":[7578]},{"source":"Connect to 44 Wallets: Including top choices like MetaMask, Trust Wallet, Coinbase Wallet, and Xaman (optimized for XRP).","translations":{"nl":"Verbind met 44 wallets: waaronder bekende keuzes zoals MetaMask, Trust Wallet, Coinbase Wallet en Xaman (geoptimaliseerd voor XRP)."},"pages":[7578]},{"source":"Trade Hundreds of Cryptocurrencies: From well-known assets to new tokens.","translations":{"nl":"Verhandel honderden cryptomunten: van bekende bezittingen tot nieuwe tokens."},"pages":[7578]},{"source":"Fast and Secure: Enjoy efficient transactions with robust security measures.","translations":{"nl":"Snel en veilig: profiteer van efficiënte transacties met stevige beveiligingsmaatregelen."},"pages":[7578]},{"source":"User-Friendly: Navigate the platform with ease, whether you’re new to crypto or a veteran trader.","translations":{"nl":"Gebruiksvriendelijk: navigeer eenvoudig, of je nu begint met crypto of al ervaren bent."},"pages":[7578]},{"source":"Get Started Today","translations":{"nl":"Begin vandaag"},"pages":[7578]},{"source":"Ready to experience the benefits of our integrated exchange? Visit our platform to connect your preferred wallet and start trading today. Discover the convenience and power of a tool designed to elevate your crypto journey:","translations":{"nl":"Wil je de voordelen van onze geïntegreerde exchange ervaren? Bezoek ons platform, verbind je favoriete wallet en begin vandaag met handelen. Ontdek het gemak en de mogelijkheden van een hulpmiddel dat jouw crypto-ervaring wil verbeteren:"},"pages":[7578]},{"source":"Visit the Integrated Exchange","translations":{"nl":"Bezoek de geïntegreerde exchange"},"pages":[7578]},{"source":"Looking Ahead","translations":{"nl":"Vooruitblik"},"pages":[7578]},{"source":"At CalorieToken, we are committed to delivering innovative solutions that enhance your crypto experience. Our integrated exchange is just one example of how we are pushing the boundaries of trading technology. Stay tuned for more updates and features as we continue to advance the future of crypto.","translations":{"nl":"Bij CalorieToken werken we aan vernieuwende oplossingen die jouw crypto-ervaring verbeteren. Onze geïntegreerde exchange is één voorbeeld van hoe we handelstechnologie verder willen brengen. Volg onze updates en nieuwe functies terwijl we verder werken aan de toekomst van crypto."},"pages":[7578]},{"source":"Thank you for being a vital part of our community. Together, let’s shape the future of crypto trading! 🚀💼","translations":{"nl":"Bedankt voor jouw belangrijke rol in onze gemeenschap. Samen geven we de toekomst van cryptohandel vorm! 🚀💼"},"pages":[7578]},{"source":"CalorieToken Hackathon: Building the Future of Health and Blockchain","translations":{"nl":"CalorieToken Hackathon: bouwen aan de toekomst van gezondheid en blockchain"},"pages":[7583]},{"source":"At CalorieToken, we are dedicated to pushing the boundaries of blockchain technology in the health and nutrition sector. Our recent hackathon was a pivotal step in this journey, aimed at fostering innovation and exploring new use cases. Although we faced challenges in participation, the hackathon underscored the importance of such initiatives and their role in driving our mission forward.","translations":{"nl":"Bij CalorieToken willen we de mogelijkheden van blockchaintechnologie voor gezondheid en voeding verder brengen. Onze recente hackathon was daarin een belangrijke stap, gericht op vernieuwing en het verkennen van nieuwe toepassingen. Hoewel de deelname tegenviel, liet de hackathon zien hoe belangrijk zulke initiatieven zijn om onze missie vooruit te helpen."},"pages":[7583]},{"source":"The Essence of the Hackathon","translations":{"nl":"De kern van de hackathon"},"pages":[7583]},{"source":"Our hackathon, held in 2023, was designed to bring together creative minds and technical experts to explore the potential of the Calorie platform. We focused on several key areas to drive development and innovation:","translations":{"nl":"Onze hackathon in 2023 moest creatieve mensen en technische experts samenbrengen om de mogelijkheden van het Calorie-platform te onderzoeken. We richtten ons op enkele belangrijke ontwikkelgebieden:"},"pages":[7583]},{"source":"Encryption Cracking: Enhance the security of login systems.","translations":{"nl":"Versleuteling kraken: de beveiliging van inlogsystemen verbeteren."},"pages":[7583]},{"source":"Food Track App Integration: Develop a comprehensive food tracking system with advanced features.","translations":{"nl":"Integratie van een voedingsregistratie-app: een compleet systeem voor het volgen van voeding ontwikkelen, met uitgebreide functies."},"pages":[7583]},{"source":"DEX Development: Build a customized decentralized exchange on XRPL.","translations":{"nl":"DEX-ontwikkeling: een eigen gedecentraliseerde exchange op de XRPL bouwen."},"pages":[7583]},{"source":"These challenges were aimed at unlocking new possibilities and refining the Calorie ecosystem. Despite the limited participation, the event was a valuable learning experience, highlighting areas for improvement and future focus.","translations":{"nl":"Deze uitdagingen moesten nieuwe mogelijkheden openen en het Calorie-ecosysteem verfijnen. Ondanks de beperkte deelname was het evenement leerzaam en maakte het duidelijk waar verbetering en verdere aandacht nodig zijn."},"pages":[7583]},{"source":"Preparing for the Hackathon: Resources and Tools","translations":{"nl":"Voorbereiding op de hackathon: middelen en hulpmiddelen"},"pages":[7583]},{"source":"To facilitate engagement and ensure participants were well-prepared, we provided a comprehensive setup guide:","translations":{"nl":"Om deelname makkelijker te maken en deelnemers goed voor te bereiden, boden we een uitgebreide installatiehandleiding:"},"pages":[7583]},{"source":"Setup Video Tutorial: We created a step-by-step video guide for installing essential tools like Python, Visual Studio Code, KivyMD, and xrpl-py. This guide was intended to make the setup process accessible to everyone.📹 Watch the Setup Video","translations":{"nl":"Installatievideo: we maakten een stapsgewijze video over het installeren van hulpmiddelen zoals Python, Visual Studio Code, KivyMD en xrpl-py. De handleiding moest de installatie voor iedereen toegankelijk maken. 📹 Bekijk de installatievideo"},"pages":[7583]},{"source":"Installation Steps: Python: Download Python Visual Studio Code: Install VS Code Project Setup: Create a folder and download files from GitHub Install Dependencies: Follow video instructions for installation. Run and Debug: Explore the app’s features using the guide.","translations":{"nl":"Installatiestappen: Python: download Python. Visual Studio Code: installeer VS Code. Projectinrichting: maak een map en download bestanden van GitHub. Afhankelijkheden installeren: volg de video. Starten en fouten onderzoeken: verken de functies van de app met de handleiding."},"pages":[7583]},{"source":"Python: Download Python","translations":{"nl":"Python: download Python"},"pages":[7583]},{"source":"Visual Studio Code: Install VS Code","translations":{"nl":"Visual Studio Code: installeer VS Code"},"pages":[7583]},{"source":"Project Setup: Create a folder and download files from GitHub","translations":{"nl":"Projectinrichting: maak een map en download bestanden van GitHub"},"pages":[7583]},{"source":"Install Dependencies: Follow video instructions for installation.","translations":{"nl":"Afhankelijkheden installeren: volg de installatie-instructies in de video."},"pages":[7583]},{"source":"Run and Debug: Explore the app’s features using the guide.","translations":{"nl":"Starten en fouten onderzoeken: verken de functies van de app met de handleiding."},"pages":[7583]},{"source":"New Developments: Android Integration","translations":{"nl":"Nieuwe ontwikkelingen: Android-integratie"},"pages":[7583]},{"source":"We are excited to announce that the CalorieAppTestnet is now compatible with Android devices. This advancement is a significant step towards making our platform more accessible and user-friendly.","translations":{"nl":"We kondigen met plezier aan dat CalorieAppTestnet nu geschikt is voor Android-apparaten. Dit is een belangrijke stap naar een toegankelijker en gebruiksvriendelijker platform."},"pages":[7583]},{"source":"Download the Test APK:","translations":{"nl":"Download de test-APK:"},"pages":[7583]},{"source":"This APK is for testing purposes only. It is not available on the Play Store, and transactions with test currencies are not real.","translations":{"nl":"Deze APK is uitsluitend bedoeld om te testen. Ze staat niet in de Play Store en transacties met testmunten zijn niet echt."},"pages":[7583]},{"source":"Looking Ahead: Future Hackathons and Use Cases","translations":{"nl":"Vooruitblik: toekomstige hackathons en toepassingen"},"pages":[7583]},{"source":"The experience from this hackathon has reinforced the value of such events in fostering innovation and driving progress. As we look to the future, we are committed to organizing more hackathons and exploring new use cases for the Calorie platform. These events are crucial for gathering insights, testing new ideas, and pushing the boundaries of blockchain technology in health and nutrition.","translations":{"nl":"De ervaring met deze hackathon heeft de waarde van zulke evenementen voor vernieuwing en vooruitgang bevestigd. We willen meer hackathons organiseren en nieuwe toepassingen voor het Calorie-platform verkennen. Die evenementen helpen inzichten te verzamelen, ideeën te testen en de mogelijkheden van blockchain voor gezondheid en voeding te vergroten."},"pages":[7583]},{"source":"Follow us on social media for updates on future hackathons and developments.","translations":{"nl":"Volg ons op sociale media voor updates over toekomstige hackathons en ontwikkelingen."},"pages":[7583]},{"source":"Explore our GitHub repository for the latest on our projects.","translations":{"nl":"Bekijk onze GitHub-repository voor het laatste nieuws over onze projecten."},"pages":[7583]},{"source":"Join Us in Shaping the Future","translations":{"nl":"Geef samen met ons de toekomst vorm"},"pages":[7583]},{"source":"The journey of innovation and development is ongoing. We invite you to stay engaged and be part of our mission to transform health and nutrition through blockchain technology. Your support and involvement are key to making this vision a reality.","translations":{"nl":"Vernieuwing en ontwikkeling gaan door. We nodigen je uit betrokken te blijven en mee te werken aan onze missie om gezondheid en voeding met blockchaintechnologie te veranderen. Jouw steun en betrokkenheid zijn belangrijk om deze visie waar te maken."},"pages":[7583]},{"source":"Let’s continue to explore, innovate, and build the future together! 🌟💡🚀","translations":{"nl":"Laten we samen blijven verkennen, vernieuwen en bouwen aan de toekomst! 🌟💡🚀"},"pages":[7583]},{"source":"The Importance of Test Currency and Testing in Blockchain Development","translations":{"nl":"Het belang van testmunten en testen bij blockchainontwikkeling"},"pages":[7587]},{"source":"In the world of blockchain technology, testing is crucial for ensuring the stability, security, and functionality of new applications and features. At CalorieToken, we recently introduced the \"CalorieTest\" currency, a dedicated test asset designed to facilitate rigorous testing and experimentation. Here’s why having a test currency and robust testing practices are essential in blockchain development.","translations":{"nl":"Bij blockchaintechnologie zijn tests essentieel om de stabiliteit, veiligheid en werking van nieuwe toepassingen en functies te controleren. Bij CalorieToken introduceerden we onlangs CalorieTest, een speciale testmunt voor grondige tests en experimenten. Hieronder leggen we uit waarom een testmunt en goede testmethoden belangrijk zijn bij blockchainontwikkeling."},"pages":[7587]},{"source":"The Role of Test Currency","translations":{"nl":"De rol van testmunten"},"pages":[1207,7587]},{"source":"The introduction of CalorieTest, our exclusive test currency, played a vital role in our development and testing processes. Here's why test currencies are so valuable:","translations":{"nl":"De introductie van onze eigen testmunt CalorieTest speelde een belangrijke rol bij ontwikkeling en tests. Daarom zijn testmunten zo waardevol:"},"pages":[7587]},{"source":"Safe Experimentation: Test currencies allow developers and users to experiment with new features and functionalities without risking real assets. This safe environment encourages creativity and innovation, enabling developers to test new ideas and features thoroughly before they are deployed on the main network.","translations":{"nl":"Veilig experimenteren: met testmunten kunnen ontwikkelaars en gebruikers nieuwe functies proberen zonder echte bezittingen te riskeren. Deze veilige omgeving moedigt creativiteit en vernieuwing aan. Ontwikkelaars kunnen ideeën en functies grondig testen voordat ze op het hoofdnetwerk worden uitgerold."},"pages":[7587]},{"source":"Realistic Testing: By integrating a test currency into our test environment, we can simulate real-world scenarios and interactions. This realistic approach helps identify potential issues and ensures that the application behaves as expected in various conditions.","translations":{"nl":"Realistische tests: door een testmunt in onze testomgeving op te nemen, kunnen we praktijksituaties en interacties nabootsen. Dat helpt mogelijke problemen te herkennen en te controleren of de toepassing onder verschillende omstandigheden werkt zoals verwacht."},"pages":[7587]},{"source":"Improved Feedback: Test currencies facilitate detailed feedback from developers and users. Participants can explore the application, report bugs, and suggest improvements based on their experience with the test currency, leading to more effective refinements and enhancements.","translations":{"nl":"Betere feedback: testmunten maken gedetailleerde feedback van ontwikkelaars en gebruikers mogelijk. Deelnemers kunnen de toepassing verkennen, fouten melden en verbeteringen voorstellen vanuit hun ervaring met de testmunt. Zo kunnen we gerichter verbeteren."},"pages":[7587]},{"source":"Controlled Development: Using a test currency helps manage potential risks by isolating experimental activities from the main network. This controlled environment allows developers to address issues and make adjustments without impacting real transactions or assets.","translations":{"nl":"Gecontroleerde ontwikkeling: een testmunt helpt risico’s te beheersen door experimenten van het hoofdnetwerk te scheiden. In die gecontroleerde omgeving kunnen ontwikkelaars problemen oplossen en aanpassingen doen zonder echte transacties of bezittingen te beïnvloeden."},"pages":[7587]},{"source":"The Broader Importance of Testing","translations":{"nl":"Het bredere belang van testen"},"pages":[7587]},{"source":"Testing is not limited to just the use of test currencies. It encompasses a range of practices and methodologies that are crucial for the successful development and deployment of blockchain applications:","translations":{"nl":"Testen omvat meer dan testmunten gebruiken. Het gaat om verschillende werkwijzen die belangrijk zijn voor het succesvol ontwikkelen en uitrollen van blockchaintoepassingen:"},"pages":[7587]},{"source":"Ensuring Quality: Comprehensive testing is essential for identifying and resolving issues before they affect end users. This includes testing for functionality, performance, security, and usability to ensure that the final product meets high standards of quality.","translations":{"nl":"Kwaliteit bewaken: uitgebreide tests zijn essentieel om problemen te vinden en op te lossen voordat eindgebruikers er last van krijgen. Daarbij horen tests op werking, prestaties, veiligheid en gebruiksgemak, zodat het eindproduct aan hoge kwaliteitseisen voldoet."},"pages":[7587]},{"source":"Fostering Innovation: Testing encourages innovation by providing a space for developers to experiment with new features and technologies. It allows for the exploration of new ideas and solutions, contributing to the evolution and advancement of blockchain technology.","translations":{"nl":"Vernieuwing stimuleren: tests bieden ruimte om met nieuwe functies en technologieën te experimenteren. Ze helpen nieuwe ideeën en oplossingen te verkennen en dragen zo bij aan de verdere ontwikkeling van blockchaintechnologie."},"pages":[7587]},{"source":"Managing Risks: Testing helps manage risks by identifying potential vulnerabilities and weaknesses in the system. By addressing these issues before deployment, developers can minimize the impact of security threats and operational failures.","translations":{"nl":"Risico’s beheersen: tests helpen kwetsbaarheden en zwakke plekken in het systeem te herkennen. Door ze vóór de uitrol op te lossen, kunnen ontwikkelaars de gevolgen van beveiligingsproblemen en storingen beperken."},"pages":[7587]},{"source":"Improving User Experience: Rigorous testing ensures that applications are user-friendly and function smoothly. It helps identify and address any usability issues, leading to a better overall experience for users.","translations":{"nl":"De gebruikerservaring verbeteren: grondige tests helpen toepassingen gebruiksvriendelijk en soepel te laten werken. Ze brengen gebruiksproblemen aan het licht, zodat die kunnen worden opgelost."},"pages":[7587]},{"source":"Supporting Continuous Improvement: Testing is an ongoing process that supports continuous improvement. Regular updates, bug fixes, and feature enhancements are driven by feedback and insights gained from testing, ensuring that the application remains relevant and effective over time.","translations":{"nl":"Voortdurend verbeteren: testen is een doorlopend proces. Regelmatige updates, foutoplossingen en functieverbeteringen worden gevoed door feedback en inzichten uit tests, zodat de toepassing ook later nuttig en effectief blijft."},"pages":[7587]},{"source":"The introduction of CalorieTest and the emphasis on rigorous testing highlight the importance of these practices in blockchain development. By providing a safe and controlled environment for experimentation, test currencies and comprehensive testing methodologies play a crucial role in ensuring the success and reliability of blockchain applications.","translations":{"nl":"De introductie van CalorieTest en de nadruk op grondig testen laten zien hoe belangrijk deze werkwijzen zijn bij blockchainontwikkeling. Een veilige, gecontroleerde experimenteeromgeving met testmunten en uitgebreide tests draagt bij aan het succes en de betrouwbaarheid van blockchaintoepassingen."},"pages":[7587]},{"source":"As we continue to advance in the field of blockchain technology, we remain committed to maintaining high standards of testing and quality assurance. We believe that by prioritizing these practices, we can drive innovation, enhance user experiences, and build robust and secure applications that meet the needs of our community.","translations":{"nl":"Terwijl we verder werken aan blockchaintechnologie, blijven we hoge eisen stellen aan testen en kwaliteitsbewaking. Door deze werkwijzen voorrang te geven, kunnen we vernieuwen, de gebruikerservaring verbeteren en stevige, veilige toepassingen bouwen die aansluiten bij onze gemeenschap."},"pages":[7587]},{"source":"Stay tuned for more updates and developments as we continue to push the boundaries of blockchain technology and strive for excellence in every aspect of our work.","translations":{"nl":"Volg onze updates en ontwikkelingen terwijl we de mogelijkheden van blockchaintechnologie verder verkennen en in elk onderdeel van ons werk naar kwaliteit streven."},"pages":[7587]},{"source":"Discover the Future of Food with Calorie Token: A Comprehensive Overview","translations":{"nl":"Ontdek de toekomst van voeding met CalorieToken: een uitgebreid overzicht"},"pages":[7591]},{"source":"Welcome to the future of food and finance with Calorie Token! Developed by the visionary team of P.H.J. Hendrikse, L.A. Visscher, and S. Zoetman, Calorie Token is poised to revolutionize the food and beverage (F","translations":{"nl":"Welkom bij de toekomst van voeding en financiën met CalorieToken! Het team van P.H.J. Hendrikse, L.A. Visscher en S. Zoetman ontwikkelde CalorieToken om de voedings-"},"pages":[7591]},{"source":"B) industry by seamlessly integrating advanced cryptocurrency technology into everyday transactions. This blog provides an in-depth look at the principles, goals, and innovative features that underpin our groundbreaking initiative.","translations":{"nl":"drankensector te vernieuwen door cryptotechnologie met alledaagse transacties te verbinden. Dit blog beschrijft de uitgangspunten, doelen en vernieuwende functies achter ons initiatief."},"pages":[7591]},{"source":"Introducing Calorie Token","translations":{"nl":"Kennismaking met CalorieToken"},"pages":[7591]},{"source":"Calorie Token is a sub-token built on the XRP Ledger (XRPL), designed specifically for the food and beverage sector. As a digital currency, Calorie Token is set to transform how payments are processed, provide new opportunities for businesses, and enhance consumer experiences in the F","translations":{"nl":"CalorieToken is een subtoken op de XRP Ledger (XRPL), speciaal ontworpen voor de voedings- en drankensector. Als digitale munt wil CalorieToken betalingen veranderen, nieuwe mogelijkheden voor bedrijven bieden en de ervaring van consumenten in de voedings-"},"pages":[7591]},{"source":"B industry. Leveraging the XRPL’s strengths—speed, scalability, and low cost—Calorie Token aims to become the premier digital currency for the global food and beverage market.","translations":{"nl":"drankensector verbeteren. Met de snelheid, schaalbaarheid en lage kosten van de XRPL wil CalorieToken een toonaangevende digitale munt voor de wereldwijde voedings- en drankenmarkt worden."},"pages":[7591]},{"source":"Why Calorie Token?","translations":{"nl":"Waarom CalorieToken?"},"pages":[7591]},{"source":"The transition from traditional fiat money to cryptocurrencies offers numerous advantages, particularly in terms of transaction efficiency, cost-effectiveness, and sustainability. The XRPL’s technology provides a robust foundation for our token, facilitating rapid and secure transactions while minimizing environmental impact. With these benefits, Calorie Token is uniquely positioned to drive innovation and adoption across the F","translations":{"nl":"De overgang van traditioneel geld naar cryptomunten biedt verschillende voordelen, vooral voor de efficiëntie, kosten en duurzaamheid van transacties. De XRPL-technologie vormt een stevige basis voor onze token en maakt snelle, veilige transacties mogelijk met beperkte milieubelasting. Daarmee wil CalorieToken vernieuwing en gebruik binnen de voedings-"},"pages":[7591]},{"source":"B industry.","translations":{"nl":"drankensector stimuleren."},"pages":[7591]},{"source":"Our Purpose: Why We’re Doing This","translations":{"nl":"Ons doel: waarom we dit doen"},"pages":[7591]},{"source":"Calorie Token addresses several pressing needs within the food and beverage sector:","translations":{"nl":"CalorieToken richt zich op verschillende dringende behoeften binnen de voedings- en drankensector:"},"pages":[7591]},{"source":"Access and Convenience: Digital tokens can simplify transactions at various food distributors, including supermarkets, restaurants, and home delivery services.","translations":{"nl":"Toegang en gemak: digitale tokens kunnen betalingen vereenvoudigen bij onder meer supermarkten, restaurants en bezorgdiensten."},"pages":[7591]},{"source":"Government Support: Digital currency solutions like Calorie Token offer governments a new tool to support citizens in accessing essential food resources efficiently.","translations":{"nl":"Overheidssteun: digitale muntoplossingen zoals CalorieToken bieden overheden een nieuw hulpmiddel om burgers efficiënt toegang tot noodzakelijke voeding te geven."},"pages":[7591]},{"source":"Future-Readiness: As the world moves towards digital solutions, Calorie Token is at the forefront of this shift, providing a practical and scalable solution to meet modern needs.","translations":{"nl":"Klaar voor de toekomst: terwijl de wereld overstapt op digitale oplossingen, wil CalorieToken daarin vooroplopen met een praktische, schaalbare oplossing voor moderne behoeften."},"pages":[7591]},{"source":"Calorie Token’s Mission and Vision","translations":{"nl":"De missie en visie van CalorieToken"},"pages":[7591]},{"source":"Mission:","translations":{"nl":"Missie:"},"pages":[7591]},{"source":"Establish a Diverse Network: Forge partnerships within the F&B industry to enhance payment solutions and expand brand presence.","translations":{"nl":"Een breed netwerk opbouwen: samenwerkingen in de voedings- en drankensector aangaan om betaaloplossingen en merkbekendheid te versterken."},"pages":[7591]},{"source":"Integrate Payment Applications: Develop and implement payment systems for businesses and consumers, contributing to a healthier financial ecosystem.","translations":{"nl":"Betaaltoepassingen integreren: betaalsystemen voor bedrijven en consumenten ontwikkelen en invoeren, als bijdrage aan een gezonder financieel ecosysteem."},"pages":[7591]},{"source":"Promote Corporate Social Responsibility: Leverage the XRPL’s advantages to support sustainable practices and corporate social responsibility.","translations":{"nl":"Maatschappelijk verantwoord ondernemen bevorderen: de voordelen van de XRPL gebruiken om duurzame werkwijzen en maatschappelijke verantwoordelijkheid te ondersteunen."},"pages":[7591]},{"source":"Encourage Healthier Lifestyles: Offer tools to help users track their calorie intake and manage their food expenses more effectively.","translations":{"nl":"Een gezondere leefstijl stimuleren: hulpmiddelen bieden om calorie-inname bij te houden en voedingsuitgaven beter te beheren."},"pages":[7591]},{"source":"Vision:","translations":{"nl":"Visie:"},"pages":[7591]},{"source":"Global Integration: Achieve widespread adoption of Calorie Token as a standard currency within the global F&B sector.","translations":{"nl":"Wereldwijde integratie: breed gebruik van CalorieToken als standaardmunt binnen de wereldwijde voedings- en drankensector bereiken."},"pages":[7591]},{"source":"Innovative Solutions: Develop advanced payment systems and applications that streamline transactions and enhance user experiences for consumers, businesses, and investors.","translations":{"nl":"Vernieuwende oplossingen: geavanceerde betaalsystemen en toepassingen ontwikkelen die transacties vereenvoudigen en de ervaring van consumenten, bedrijven en investeerders verbeteren."},"pages":[7591]},{"source":"Currency Details:","translations":{"nl":"Muntgegevens:"},"pages":[7591]},{"source":"Name: Calorie","translations":{"nl":"Naam: Calorie"},"pages":[7591]},{"source":"HEX Code: 43616C6F72696500000000000000000000000000","translations":{"nl":"HEX-code: 43616C6F72696500000000000000000000000000"},"pages":[7591]},{"source":"Issuing Address: rNqGa93B8ewQP9mUwpwqA19SApbf62U7PY","translations":{"nl":"Uitgeversadres: rNqGa93B8ewQP9mUwpwqA19SApbf62U7PY"},"pages":[7591]},{"source":"Initial Supply: 100,000,000,000 Calorie (Fixed Supply)","translations":{"nl":"Oorspronkelijk aanbod: 100.000.000.000 Calorie (vast aanbod)"},"pages":[7591]},{"source":"Distribution Breakdown:","translations":{"nl":"Verdeling:"},"pages":[7591]},{"source":"Community (60%): To foster engagement and build a strong user base through airdrops, giveaways, and community events.","translations":{"nl":"Gemeenschap (60%): om betrokkenheid en een sterke gebruikersbasis op te bouwen via airdrops, weggeefacties en gemeenschapsevenementen."},"pages":[7591]},{"source":"Escrow (20%): Reserved for future needs, including token burns and exchange listings to support long-term growth and liquidity.","translations":{"nl":"Escrow (20%): gereserveerd voor toekomstige behoeften, waaronder het vernietigen van tokens en exchangenoteringen om groei en liquiditeit op langere termijn te ondersteunen."},"pages":[7591]},{"source":"Branding (10%): Dedicated to enhancing brand awareness, developing applications, and securing strategic partnerships.","translations":{"nl":"Merkontwikkeling (10%): voor merkbekendheid, het ontwikkelen van toepassingen en strategische samenwerkingen."},"pages":[7591]},{"source":"Developers (10%): Allocated for future development and innovation to keep the Calorie Token ecosystem at the cutting edge.","translations":{"nl":"Ontwikkelaars (10%): bestemd voor toekomstige ontwikkeling en vernieuwing van het CalorieToken-ecosysteem."},"pages":[7591]},{"source":"CalorieApp: A Comprehensive F&B Solution","translations":{"nl":"CalorieApp: een complete oplossing voor voeding en dranken"},"pages":[7591]},{"source":"CalorieApp is designed to be the ultimate platform for all participants in the F","translations":{"nl":"CalorieApp is bedoeld als breed platform voor alle deelnemers in het voedings-"},"pages":[7591]},{"source":"B ecosystem, offering a range of features tailored to different user needs:","translations":{"nl":"drankenecosysteem, met functies voor verschillende gebruikers:"},"pages":[7591]},{"source":"Consumers:","translations":{"nl":"Consumenten:"},"pages":[7591]},{"source":"Payment System: Easily make and receive payments using Calorie Token, with options to exchange it for other currencies on the XRPL.","translations":{"nl":"Betaalsysteem: eenvoudig betalen en ontvangen met CalorieToken, met mogelijkheden om de token op de XRPL tegen andere munten te wisselen."},"pages":[7591]},{"source":"Food Tracker System (FTS): Track nutritional values and calorie intake, view spending patterns, and manage food expenses effectively.","translations":{"nl":"Voedingsregistratiesysteem (FTS): voedingswaarden en calorie-inname volgen, uitgavenpatronen bekijken en voedingsuitgaven effectief beheren."},"pages":[7591]},{"source":"Custom NFTs: Create and trade NFTs representing unique recipes, preserving and sharing culinary creations.","translations":{"nl":"Eigen NFT’s: NFT’s van bijzondere recepten maken en verhandelen om culinaire creaties te bewaren en te delen."},"pages":[7591]},{"source":"Merchants/Producers:","translations":{"nl":"Winkeliers en producenten:"},"pages":[7591]},{"source":"Integrated Payment System: Manage transactions seamlessly with connections to inventory management, accounting systems, and cash register systems.","translations":{"nl":"Geïntegreerd betaalsysteem: transacties beheren met koppelingen naar voorraadbeheer, boekhouding en kassasystemen."},"pages":[7591]},{"source":"Inventory Management: Real-time updates and notifications for stock levels, with secure data storage and retrieval.","translations":{"nl":"Voorraadbeheer: actuele updates en meldingen over voorraadniveaus, met veilige opslag en teruglezing van gegevens."},"pages":[7591]},{"source":"Custom NFTs: Mint NFTs for menu items, allowing for automated royalty payments and increased revenue through digital assets.","translations":{"nl":"Eigen NFT’s: NFT’s van menugerechten maken, voor automatische royaltybetalingen en extra inkomsten via digitale bezittingen."},"pages":[7591]},{"source":"Investors:","translations":{"nl":"Investeerders:"},"pages":[7591]},{"source":"Payment System: Send and receive payments with comprehensive financial tracking linked to accounting and analysis systems.","translations":{"nl":"Betaalsysteem: betalingen versturen en ontvangen met uitgebreide financiële registratie, gekoppeld aan boekhoud- en analysesystemen."},"pages":[7591]},{"source":"Industry News: Stay updated with the latest developments in the F&B industry, crypto markets, and financial news.","translations":{"nl":"Sectornieuws: op de hoogte blijven van ontwikkelingen in de voedings- en drankensector, cryptomarkten en financieel nieuws."},"pages":[7591]},{"source":"Network Platform: Connect with businesses using Calorie Token, explore investment opportunities, and engage with potential partners.","translations":{"nl":"Netwerkplatform: contact leggen met bedrijven die CalorieToken gebruiken, investeringsmogelijkheden verkennen en mogelijke partners ontmoeten."},"pages":[7591]},{"source":"CalorieApp Network","translations":{"nl":"CalorieApp-netwerk"},"pages":[7591]},{"source":"The CalorieApp Network is designed to enhance online sales and streamline operations for F","translations":{"nl":"Het CalorieApp-netwerk is bedoeld om online verkoop en bedrijfsprocessen voor voedings-"},"pages":[7591]},{"source":"B businesses:","translations":{"nl":"drankenbedrijven te verbeteren:"},"pages":[7591]},{"source":"Online Sales Platform: Enables companies to manage and sell their products online, including takeaway and delivery services.","translations":{"nl":"Online verkoopplatform: bedrijven hun producten online laten beheren en verkopen, waaronder afhaal- en bezorgdiensten."},"pages":[7591]},{"source":"Inventory and Accounting Integration: Supports seamless management of stock and financial records, with tools to improve efficiency and accuracy.","translations":{"nl":"Integratie van voorraad en boekhouding: voorraad en financiële administratie gezamenlijk beheren, met hulpmiddelen voor meer efficiëntie en nauwkeurigheid."},"pages":[7591]},{"source":"Web3 Integration: CalorieDB","translations":{"nl":"Web3-integratie: CalorieDB"},"pages":[7591]},{"source":"At the heart of Calorie Token's infrastructure is CalorieDB, a sophisticated Web3 solution that ensures a seamless and secure user experience:","translations":{"nl":"CalorieDB staat centraal in de infrastructuur van CalorieToken: een uitgebreide Web3-oplossing voor een soepele en veilige gebruikerservaring."},"pages":[7591]},{"source":"On-Chain Data Handling: CalorieDB leverages the XRPL for transaction processing, utilizing private keys to access payment systems, Calorie trustlines, and other XRPL functionalities. Each transaction is linked to a unique hash that connects on-chain activities with off-chain records.","translations":{"nl":"Gegevens op de blockchain: CalorieDB gebruikt de XRPL voor transactieverwerking en privésleutels voor toegang tot betaalsystemen, Calorie-trustlines en andere XRPL-functies. Elke transactie krijgt een unieke hash die activiteiten op de blockchain aan externe gegevens koppelt."},"pages":[7591]},{"source":"Off-Chain Database Layer: CalorieDB utilizes BigchainDB for decentralized data storage, handling extensive transactional data such as purchase details, expenses, and seller information. This off-chain layer integrates with XRPL transactions, ensuring comprehensive data management while preserving privacy and security.","translations":{"nl":"Databaselaag buiten de blockchain: CalorieDB gebruikt BigchainDB voor gedecentraliseerde opslag van uitgebreide transactiegegevens, zoals aankopen, uitgaven en verkoperinformatie. Deze laag is gekoppeld aan XRPL-transacties voor volledig gegevensbeheer met behoud van privacy en veiligheid."},"pages":[7591]},{"source":"Food Product Integration: Data from the Open Food Repo is incorporated into CalorieDB, providing a robust database of nutritional information. This integration allows users to access detailed food product data while maintaining privacy through the use of unique transaction hashes.","translations":{"nl":"Integratie van voedingsproducten: gegevens uit Open Food Repo worden opgenomen in CalorieDB als uitgebreide bron van voedingsinformatie. Daarmee kunnen gebruikers gedetailleerde productgegevens raadplegen, terwijl unieke transactiehashes de privacy ondersteunen."},"pages":[7591]},{"source":"Application Layer: Powered by IPFS services, the CalorieDB application layer ensures decentralization and security. It supports various functions including the Food Tracker System, Inventory Management System, and Analysis System, operating through a Client/Server Model for optimal performance.","translations":{"nl":"Toepassingslaag: de CalorieDB-toepassingslaag gebruikt IPFS-diensten voor decentralisatie en veiligheid. Ze ondersteunt onder meer voedingsregistratie, voorraadbeheer en analyse, via een client-servermodel voor goede prestaties."},"pages":[7591]},{"source":"Security and Privacy","translations":{"nl":"Veiligheid en privacy"},"pages":[7591]},{"source":"At Calorie Token, we prioritize user privacy and data security:","translations":{"nl":"Bij CalorieToken geven we voorrang aan privacy en gegevensbeveiliging:"},"pages":[7591]},{"source":"Private Keys: Access to your Calorie Database is controlled by private keys, ensuring secure and exclusive control over your data.","translations":{"nl":"Privésleutels: toegang tot je Calorie Database wordt door privésleutels geregeld, zodat jij veilig en exclusief controle over je gegevens hebt."},"pages":[7591]},{"source":"Data Management: Users can manage, back up, and delete their data as desired. We adhere to strict legal standards for data protection and privacy.","translations":{"nl":"Gegevensbeheer: gebruikers kunnen hun gegevens naar wens beheren, opslaan als reservekopie en verwijderen. We volgen strenge wettelijke eisen voor gegevensbescherming en privacy."},"pages":[7591]},{"source":"Stay informed and join the conversation as we reshape the future of food with Calorie Token. Follow us on:","translations":{"nl":"Blijf op de hoogte en praat mee terwijl we met CalorieToken de toekomst van voeding vormgeven. Volg ons op:"},"pages":[7591]},{"source":"LinkedIn: Calorie Token","translations":{"nl":"LinkedIn: CalorieToken"},"pages":[7591]},{"source":"Embrace the future of food and finance with Calorie Token, and be part of a revolutionary journey towards a more efficient, sustainable, and healthier F","translations":{"nl":"Omarm de toekomst van voeding en financiën met CalorieToken en werk mee aan een efficiënter, duurzamer en gezonder voedings-"},"pages":[7591]},{"source":"Title: Embracing the Future: The Potential of CalorieToken as a DAO","translations":{"nl":"Titel: de toekomst omarmen: de mogelijkheden van CalorieToken als DAO"},"pages":[7608]},{"source":"We are absolutely thrilled with the recent outcome of the Ripple vs. SEC case, which concluded after a long wait of 3 years and 7 months. This victory marks a significant milestone for the XRPL ecosystem, and we couldn’t be happier. In addition to this exciting development, the Inclusive Financial Technology Foundation (@IncFinTech), formerly known as the XRPL Foundation, has announced their rebranding and is exploring the concept of a Decentralized Autonomous Organization (DAO). This exploration resonates with our own ongoing considerations about transitioning CalorieToken into a DAO.","translations":{"nl":"We zijn erg blij met de recente uitkomst van de rechtszaak tussen Ripple en de SEC, die na drie jaar en zeven maanden is afgerond. Deze overwinning is een belangrijke mijlpaal voor het XRPL-ecosysteem. Daarnaast heeft de Inclusive Financial Technology Foundation (@IncFinTech), voorheen XRPL Foundation, een nieuwe naam aangekondigd en onderzoekt ze het concept van een gedecentraliseerde autonome organisatie (DAO). Dat sluit aan bij onze eigen overwegingen om CalorieToken om te vormen tot een DAO."},"pages":[7608]},{"source":"The Vision for CalorieToken as a DAO","translations":{"nl":"De visie op CalorieToken als DAO"},"pages":[7608]},{"source":"As an officially registered company with the Chamber of Commerce, CalorieToken is committed to exploring the transition to a DAO. We believe that shifting our current responsibilities to a DAO structure could significantly benefit our ecosystem. Here’s why we think a DAO is an important future development:","translations":{"nl":"Als officieel bij de Kamer van Koophandel geregistreerd bedrijf onderzoekt CalorieToken een overgang naar een DAO. We denken dat het overdragen van onze huidige verantwoordelijkheden aan een DAO ons ecosysteem veel kan opleveren. Daarom zien we een DAO als een belangrijke toekomstige ontwikkeling:"},"pages":[7608]},{"source":"Community Involvement Moving to a DAO would enable every participant in the $CAL ecosystem to have a voice in key decisions. This democratic approach aims to involve the community directly in shaping the project's future. By transitioning responsibilities to a DAO, we ensure that decisions reflect the collective input of all stakeholders, not just a central authority.","translations":{"nl":"Betrokkenheid van de gemeenschap: een DAO zou iedere deelnemer aan het $CAL-ecosysteem een stem in belangrijke besluiten geven. Deze democratische aanpak wil de gemeenschap rechtstreeks betrekken bij de toekomst van het project. Door verantwoordelijkheden aan een DAO over te dragen, laten we besluiten aansluiten bij de gezamenlijke inbreng van belanghebbenden en niet alleen bij een centrale partij."},"pages":[7608]},{"source":"Transparency DAOs operate on blockchain technology, allowing all transactions and decisions to be recorded on a public ledger. This transparency could greatly enhance trust within our community. By managing responsibilities through a DAO, we can ensure that all actions are visible and verifiable, promoting openness and accountability.","translations":{"nl":"Transparantie: DAO’s werken met blockchaintechnologie, waardoor transacties en besluiten in een openbaar register kunnen worden vastgelegd. Die transparantie kan het vertrouwen binnen onze gemeenschap versterken. Als een DAO de verantwoordelijkheden beheert, kunnen handelingen zichtbaar en controleerbaar zijn, wat openheid en verantwoording bevordert."},"pages":[7608]},{"source":"Democratic Governance A DAO structure could provide a more democratic governance model, where stakeholders have the power to propose and vote on changes. This could lead to more balanced and well-considered decisions, as opposed to a centralized model. Transitioning our company’s responsibilities to a DAO would involve our community in governance, making the decision-making process more inclusive.","translations":{"nl":"Democratisch bestuur: een DAO kan een democratischer bestuursmodel bieden, waarin belanghebbenden wijzigingen kunnen voorstellen en erover stemmen. Dat kan leiden tot evenwichtigere en beter overwogen besluiten dan bij een centraal model. Het overdragen van bedrijfsverantwoordelijkheden aan een DAO zou onze gemeenschap bij het bestuur betrekken en de besluitvorming toegankelijker maken."},"pages":[7608]},{"source":"Efficient Fund Management Smart contracts within a DAO could automate the allocation and spending of funds, potentially reducing overhead costs and improving efficiency. While not all features might be feasible with current technology, the use of smart contracts could streamline financial management and ensure resources are used effectively for the development of the $CAL ecosystem.","translations":{"nl":"Efficiënt beheer van middelen: slimme contracten binnen een DAO kunnen de verdeling en besteding van geld automatiseren, mogelijk met lagere overheadkosten en meer efficiëntie. Niet alle functies zijn met de huidige technologie haalbaar, maar slimme contracten kunnen financieel beheer vereenvoudigen en middelen effectief inzetten voor het $CAL-ecosysteem."},"pages":[7608]},{"source":"Challenges and Considerations","translations":{"nl":"Uitdagingen en overwegingen"},"pages":[7608]},{"source":"Transitioning to a DAO involves several challenges. Developing robust smart contracts, ensuring secure voting mechanisms, and establishing a comprehensive governance framework are crucial steps. Additionally, educating our community about the DAO model and encouraging active participation will be essential for its success.","translations":{"nl":"Een overgang naar een DAO brengt uitdagingen mee. Betrouwbare slimme contracten, veilige stemmechanismen en een volledig bestuurskader zijn essentiële stappen. Ook moeten we de gemeenschap uitleg geven over het DAO-model en actieve deelname aanmoedigen."},"pages":[7608]},{"source":"The Path Forward","translations":{"nl":"De weg vooruit"},"pages":[7608]},{"source":"As we explore the transition to a DAO, we recognize the complexities involved but also the significant potential benefits. Moving our company’s responsibilities to a DAO could enhance how we manage operations, engage with our community, and ensure transparency and efficiency. We are committed to carefully evaluating this transition and working towards a model that benefits all participants in our ecosystem.","translations":{"nl":"We erkennen zowel de complexiteit als de mogelijke voordelen van een overgang naar een DAO. Het overdragen van bedrijfsverantwoordelijkheden kan onze bedrijfsvoering, betrokkenheid van de gemeenschap, transparantie en efficiëntie verbeteren. We willen die overgang zorgvuldig beoordelen en werken aan een model dat alle deelnemers helpt."},"pages":[7608]},{"source":"The recent Ripple vs. SEC case outcome and the evolving focus of organizations like @IncFinTech (Inclusive Financial Technology Foundation) highlight the dynamic nature of the blockchain space. At CalorieToken, we are excited about the potential of a DAO structure to transform how we manage our responsibilities and involve our community. We will continue to explore this promising direction and keep you updated on our progress.","translations":{"nl":"De recente uitkomst van Ripple tegen de SEC en de veranderende aandacht van organisaties zoals @IncFinTech laten zien hoe dynamisch de blockchainwereld is. Bij CalorieToken zijn we enthousiast over de mogelijkheden van een DAO om onze verantwoordelijkheden anders te beheren en de gemeenschap te betrekken. We blijven deze richting onderzoeken en houden jullie op de hoogte."},"pages":[7608]},{"source":"For more detailed insights and updates, follow our blog and join us on this exciting journey towards decentralization and community empowerment.","translations":{"nl":"Volg ons blog voor meer inzichten en updates en ga met ons mee op weg naar decentralisatie en invloed voor de gemeenschap."},"pages":[7608]},{"source":"Read more on our blog:","translations":{"nl":"Lees meer op ons blog:"},"pages":[7608]},{"source":"This article records a discussion from August 2024 about a possible DAO structure. A full transfer of the project to a DAO remains a future direction, not an announced completed transition. The legal and organisational references below belong to the article’s original period. For the current product scope, see the project FAQ.","translations":{"nl":"Dit artikel beschrijft een discussie uit augustus 2024 over een mogelijke DAO-structuur. Volledige overdracht van het project aan een DAO blijft een toekomstige richting en is niet als voltooid aangekondigd. De juridische en organisatorische verwijzingen horen bij de oorspronkelijke periode van het artikel. Bekijk de project-FAQ voor de huidige mogelijkheden."},"pages":[7608]},{"source":"This article records a proposed approach to unfunded XRPL offers. The XRP Ledger can remove unfunded offers when it encounters them during transaction processing; an OfferCancel transaction cancels an offer owned by its sending account. An external monitoring service does not automatically gain permission to cancel other accounts’ offers. The proposal below should not be read as evidence of a deployed CalorieApp service.","translations":{"nl":"Dit artikel beschrijft een voorgestelde aanpak voor ongedekte XRPL-orders. De XRP Ledger kan ongedekte orders verwijderen wanneer hij die tegenkomt tijdens transactieverwerking. Een OfferCancel-transactie annuleert een order van de rekening die de transactie verstuurt. Een externe controledienst krijgt niet automatisch toestemming om orders van andere accounts te annuleren. Het voorstel hieronder bewijst niet dat zo’n CalorieApp-dienst is uitgerold."},"pages":[7545]},{"source":"This is a historical project update from 2024. Today, CalorieApp provides food search, nutrition information and a personal food log using Open Food Facts. The FoodRepo, BigchainDB and IPFS descriptions below record earlier development plans; they are not a description of the current web app’s deployed architecture. Team circumstances and listing aspirations should also be read in their original context.","translations":{"nl":"Dit is een historische projectupdate uit 2024. CalorieApp biedt nu voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek met Open Food Facts. De beschrijvingen van FoodRepo, BigchainDB en IPFS hieronder gaan over eerdere ontwikkelplannen en niet over de uitgerolde architectuur van de huidige webapp. Ook de teamsituatie en noteringsambities horen bij hun oorspronkelijke context."},"pages":[7564]},{"source":"This article concerns the earlier CalorieAppTestnet Android prototype. The APK link below is retained as development history and is not the current web app or a Play Store release. For the current food search, nutrition information and personal food log, open CalorieApp in your browser.","translations":{"nl":"Dit artikel gaat over het eerdere Android-prototype CalorieAppTestnet. De APK-link blijft bewaard als ontwikkelgeschiedenis en is niet de huidige webapp of een Play Store-release. Open CalorieApp in je browser voor de huidige voedingszoekfunctie, voedingsinformatie en het persoonlijke voedingsdagboek."},"pages":[7569]},{"source":"This article describes the former embedded exchange experience. That integration is outside the current CalorieApp web version, which focuses on food search, nutrition information and a personal food log. The wallet counts, supported assets and trading instructions below are historical descriptions, not a current service list. For today’s product scope, see the project FAQ.","translations":{"nl":"Dit artikel beschrijft de vroegere ingebedde exchange. Die integratie valt buiten de huidige CalorieApp-webversie, die zich richt op voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek. De aantallen wallets, ondersteunde munten en handelsinstructies hieronder zijn historische beschrijvingen en geen actuele dienstenlijst. Bekijk de project-FAQ voor de huidige mogelijkheden."},"pages":[7578]},{"source":"This article announced the donation page in 2023. Voluntary contributions support work on CalorieApp and the wider project. The merchandise and NFT launch language below belongs to that historical announcement and does not confirm current availability. Read the current donation page and its terms before contributing.","translations":{"nl":"Dit artikel kondigde in 2023 de donatiepagina aan. Vrijwillige bijdragen ondersteunen het werk aan CalorieApp en het bredere project. De tekst over de introductie van merchandise en NFT’s hoort bij die historische aankondiging en bevestigt geen huidige beschikbaarheid. Lees de actuele donatiepagina en voorwaarden voordat je bijdraagt."},"pages":[7573]},{"source":"This article records the 2023 CalorieToken hackathon and early development experiments. It is not an open invitation to a current competition, and the APK links refer to the earlier testnet prototype. New events will have their own announcements. The current browser app is available from the CalorieApp page.","translations":{"nl":"Dit artikel beschrijft de CalorieToken-hackathon van 2023 en vroege ontwikkelproeven. Het is geen uitnodiging voor een huidige wedstrijd. De APK-links verwijzen naar het eerdere Testnet-prototype. Nieuwe evenementen krijgen een eigen aankondiging. De huidige browserapp is bereikbaar via de CalorieApp-pagina."},"pages":[7583]},{"source":"This article records the project’s work on domain and account information. Domain verification establishes a specific link between a domain and an account or validator; it is not an endorsement of every claim, service or token associated with that domain. A TOML entry claiming an account must be checked against that account’s on-ledger Domain field.","translations":{"nl":"Dit artikel beschrijft het werk van het project aan domein- en accountinformatie. Domeinverificatie bevestigt een specifieke koppeling tussen een domein en een account of validator; ze keurt niet iedere claim, dienst of token bij dat domein goed. Een accountvermelding in een TOML-bestand moet worden vergeleken met het Domain-veld van dat account op de ledger."},"pages":[7575]},{"source":"This article explains the role of test assets in the project’s early experiments. CalorieTest and Lipisa belong to that development context. Their mention does not mean that the current CalorieApp browser experience includes a test wallet, token faucet or payment service. Its current focus is food search, nutrition information and a personal food log.","translations":{"nl":"Dit artikel legt de rol van testmiddelen in de vroege experimenten uit. CalorieTest en Lipisa horen bij die ontwikkelcontext. Hun vermelding betekent niet dat de huidige CalorieApp-browserervaring een testwallet, tokenfaucet of betaaldienst bevat. De huidige aandacht ligt bij voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek."},"pages":[7587]},{"source":"This is the project’s original 2023 overview. The allocation percentages describe the initial plan, not current wallet balances. The payment, NFT, inventory, private-key and BigchainDB/IPFS sections describe that early vision. Today, CalorieApp provides food search, nutrition information and a personal food log using Open Food Facts. The project FAQ explains the current scope.","translations":{"nl":"Dit is het oorspronkelijke projectoverzicht uit 2023. De verdelingspercentages beschrijven het eerste plan en niet de huidige wallettegoeden. De onderdelen over betalingen, NFT’s, voorraad, privésleutels en BigchainDB/IPFS beschrijven die vroege visie. CalorieApp biedt nu voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek met Open Food Facts. De project-FAQ licht de huidige mogelijkheden toe."},"pages":[7591]},{"source":"Tackling Frontrunning on the XRPL","translations":{"nl":"Frontrunning op de XRPL aanpakken"},"pages":[1207,7545]},{"source":"An Update on Our Journey and Future Plans","translations":{"nl":"Een update over onze voortgang en toekomstplannen"},"pages":[1207,7564]},{"source":"CalorieApp Test APK on Android!","translations":{"nl":"CalorieApp-test-APK voor Android!"},"pages":[1207,7569]},{"source":"Our Donations Page is LIVE!","translations":{"nl":"Onze donatiepagina is live!"},"pages":[1207,7573]},{"source":"The Importance of Verified Domains","translations":{"nl":"Het belang van geverifieerde domeinen"},"pages":[1207,7575]},{"source":"Our Integrated Exchange","translations":{"nl":"Onze geïntegreerde exchange"},"pages":[1207,7578]},{"source":"CalorieToken Hackathon","translations":{"nl":"CalorieToken-hackathon"},"pages":[1207,7583]},{"source":"A Comprehensive Overview","translations":{"nl":"Een uitgebreid overzicht"},"pages":[1207,7591]},{"source":"The Potential of CalorieToken as a DAO","translations":{"nl":"De mogelijkheden van CalorieToken als DAO"},"pages":[1207,7608]},{"source":"About this article — September 2026","translations":{"nl":"Over dit artikel — september 2026"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]},{"source":"Project FAQ","translations":{"nl":"Project-FAQ"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]},{"source":"Current donation information","translations":{"nl":"Actuele donatie-informatie"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]},{"source":"XRPL offer lifecycle","translations":{"nl":"Levenscyclus van XRPL-orders"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]},{"source":"XRPL TOML specification","translations":{"nl":"XRPL TOML-specificatie"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]}]} +{"schema":1,"entries":[{"source":"Find food products, explore nutrition and keep your own food diary.","translations":{"zh-Hans":"查找食品、了解营养信息并记录自己的饮食日记。","hi":"खाद्य उत्पाद खोजें, पोषण संबंधी जानकारी देखें और अपनी भोजन डायरी रखें।","es":"Busca alimentos, consulta su información nutricional y lleva tu propio diario de alimentación.","ar":"ابحث عن المنتجات الغذائية، واطّلع على معلوماتها الغذائية، واحتفظ بسجل طعامك الخاص.","fr":"Recherchez des aliments, consultez leurs informations nutritionnelles et tenez votre propre journal alimentaire.","bn":"খাদ্যপণ্য খুঁজুন, পুষ্টির তথ্য দেখুন এবং নিজের খাবারের ডায়েরি রাখুন।","pt":"Encontre alimentos, consulte informações nutricionais e mantenha o seu próprio diário alimentar.","id":"Cari produk makanan, lihat informasi gizi, dan buat catatan makanan pribadi Anda.","ur":"غذائی مصنوعات تلاش کریں، غذائیت کی معلومات دیکھیں اور اپنی خوراک کی ڈائری رکھیں۔","nl":"Zoek voedingsproducten, bekijk voedingswaarden en houd je eigen eetdagboek bij."},"pages":["*"]},{"source":"Open CalorieApp","translations":{"zh-Hans":"打开 CalorieApp","hi":"CalorieApp खोलें","es":"Abrir CalorieApp","ar":"فتح CalorieApp","fr":"Ouvrir CalorieApp","bn":"CalorieApp খুলুন","pt":"Abrir CalorieApp","id":"Buka CalorieApp","ur":"CalorieApp کھولیں","nl":"CalorieApp openen"},"pages":[1205,4205,7880]},{"source":"CalorieApp is available above.","translations":{"zh-Hans":"CalorieApp 位于本页上方。","hi":"CalorieApp इस पृष्ठ पर ऊपर उपलब्ध है।","es":"CalorieApp está disponible más arriba en esta página.","ar":"يتوفر CalorieApp في أعلى هذه الصفحة.","fr":"CalorieApp est disponible plus haut sur cette page.","bn":"এই পৃষ্ঠার উপরে CalorieApp রয়েছে।","pt":"O CalorieApp está disponível acima nesta página.","id":"CalorieApp tersedia di bagian atas halaman ini.","ur":"CalorieApp اس صفحے پر اوپر موجود ہے۔","nl":"CalorieApp staat hierboven op deze pagina."},"pages":["*"]},{"source":"Product data from {off} (database licence: {odbl}).","translations":{"zh-Hans":"产品数据来自 {off}(数据库许可协议:{odbl})。","hi":"उत्पाद डेटा का स्रोत: {off} (डेटाबेस लाइसेंस: {odbl})।","es":"Datos de productos de {off} (licencia de la base de datos: {odbl}).","ar":"بيانات المنتجات من {off} (ترخيص قاعدة البيانات: {odbl}).","fr":"Données produits issues de {off} (licence de la base de données : {odbl}).","bn":"পণ্যের ডেটার উৎস: {off} (ডেটাবেসের লাইসেন্স: {odbl})।","pt":"Dados de produtos de {off} (licença da base de dados: {odbl}).","id":"Data produk dari {off} (lisensi basis data: {odbl}).","ur":"مصنوعات کے ڈیٹا کا ذریعہ: {off} (ڈیٹابیس کا لائسنس: {odbl})۔","nl":"Productgegevens van {off} (databaselicentie: {odbl})."},"pages":["*"]},{"source":"Reference foods: {usda} · {cc0}.","translations":{"zh-Hans":"参考食品数据:{usda} · {cc0}。","hi":"संदर्भ के लिए खाद्य डेटा: {usda} · {cc0}।","es":"Alimentos de referencia: {usda} · {cc0}.","ar":"أغذية مرجعية: {usda} · {cc0}.","fr":"Aliments de référence : {usda} · {cc0}.","bn":"তথ্যসূত্র হিসেবে দেওয়া খাদ্য: {usda} · {cc0}।","pt":"Alimentos de referência: {usda} · {cc0}.","id":"Makanan referensi: {usda} · {cc0}.","ur":"حوالے کے لیے غذاؤں کا ڈیٹا: {usda} · {cc0}۔","nl":"Voedingsmiddelen ter referentie: {usda} · {cc0}."},"pages":["*"]},{"source":"Home","translations":{"nl":"Home","zh-Hans":"首页","hi":"होम","es":"Inicio","ar":"الرئيسية","fr":"Accueil","bn":"হোম","pt":"Início","id":"Beranda","ur":"ہوم"},"pages":["*"]},{"source":"Whitepaper","translations":{"nl":"Whitepaper","zh-Hans":"白皮书","hi":"श्वेतपत्र","es":"Libro blanco","ar":"الورقة البيضاء","fr":"Livre blanc","bn":"শ্বেতপত্র","pt":"Livro branco","id":"Whitepaper","ur":"وائٹ پیپر"},"pages":["*"]},{"source":"CAL & Crypto","translations":{"nl":"CAL & Crypto","zh-Hans":"CAL & Crypto","hi":"CAL & Crypto","es":"CAL & Crypto","ar":"CAL & Crypto","fr":"CAL & Crypto","bn":"CAL & Crypto","pt":"CAL & Crypto","id":"CAL & Crypto","ur":"CAL & Crypto"},"pages":[1205,4205,6855,7880]},{"source":"Trustline","translations":{"nl":"Trustline","zh-Hans":"信任线","hi":"ट्रस्टलाइन","es":"Línea de confianza","ar":"خط الثقة","fr":"Ligne de confiance","bn":"ট্রাস্টলাইন","pt":"Linha de confiança","id":"Trustline","ur":"ٹرسٹ لائن"},"pages":["*"]},{"source":"Tokenomics update","translations":{"nl":"Tokenomics-update","zh-Hans":"代币经济更新","hi":"टोकन अर्थव्यवस्था अपडेट","es":"Actualización de tokenómica","ar":"تحديث اقتصاد الرمز","fr":"Mise à jour de la tokenomique","bn":"টোকেন অর্থনীতি আপডেট","pt":"Atualização da tokenomia","id":"Pembaruan tokenomik","ur":"ٹوکن معاشیات کی تازہ کاری"},"pages":["*"]},{"source":"Roadmap","translations":{"nl":"Routekaart","zh-Hans":"路线图","hi":"रोडमैप","es":"Hoja de ruta","ar":"خارطة الطريق","fr":"Feuille de route","bn":"রোডম্যাপ","pt":"Roteiro","id":"Peta jalan","ur":"روڈمیپ"},"pages":["*"]},{"source":"Richlist","translations":{"nl":"Houderslijst","zh-Hans":"持有者排名","hi":"धारकों की सूची","es":"Lista de titulares","ar":"قائمة الحائزين","fr":"Liste des détenteurs","bn":"ধারকদের তালিকা","pt":"Lista de detentores","id":"Daftar pemegang","ur":"ہولڈرز کی فہرست"},"pages":["*"]},{"source":"Blog","translations":{"nl":"Blog","zh-Hans":"博客","hi":"ब्लॉग","es":"Blog","ar":"المدونة","fr":"Blog","bn":"ব্লগ","pt":"Blog","id":"Blog","ur":"بلاگ"},"pages":["*"]},{"source":"Contact","translations":{"nl":"Contact","zh-Hans":"联系","hi":"संपर्क","es":"Contacto","ar":"اتصل بنا","fr":"Contact","bn":"যোগাযোগ","pt":"Contacto","id":"Kontak","ur":"رابطہ"},"pages":["*"]},{"source":"FAQ","translations":{"nl":"Veelgestelde vragen","zh-Hans":"常见问题","hi":"अक्सर पूछे जाने वाले प्रश्न","es":"Preguntas frecuentes","ar":"الأسئلة الشائعة","fr":"Questions fréquentes","bn":"সাধারণ প্রশ্ন","pt":"Perguntas frequentes","id":"Pertanyaan umum","ur":"اکثر پوچھے گئے سوالات"},"pages":["*"]},{"source":"Donate","translations":{"nl":"Doneren","zh-Hans":"捐赠","hi":"दान करें","es":"Donar","ar":"تبرع","fr":"Faire un don","bn":"অনুদান দিন","pt":"Doar","id":"Donasi","ur":"عطیہ دیں"},"pages":["*"]},{"source":"Merch&NFTs","translations":{"nl":"Merch & NFT’s","zh-Hans":"商品与 NFT","hi":"उत्पाद और NFT","es":"Productos y NFT","ar":"المنتجات وNFT","fr":"Produits et NFT","bn":"পণ্য ও NFT","pt":"Produtos e NFT","id":"Produk & NFT","ur":"مصنوعات اور NFT"},"pages":["*"]},{"source":"Privacy Policy","translations":{"nl":"Privacybeleid","zh-Hans":"隐私政策","hi":"गोपनीयता नीति","es":"Política de privacidad","ar":"سياسة الخصوصية","fr":"Politique de confidentialité","bn":"গোপনীয়তা নীতি","pt":"Política de privacidade","id":"Kebijakan privasi","ur":"رازداری کی پالیسی"},"pages":["*"]},{"source":"Terms & Conditions","translations":{"nl":"Algemene voorwaarden","zh-Hans":"条款与条件","hi":"नियम और शर्तें","es":"Términos y condiciones","ar":"الشروط والأحكام","fr":"Conditions générales","bn":"শর্তাবলি","pt":"Termos e condições","id":"Syarat dan ketentuan","ur":"شرائط و ضوابط"},"pages":["*"]},{"source":"Copy issuer","translations":{"zh-Hans":"复制发行方地址","hi":"जारीकर्ता का पता कॉपी करें","es":"Copiar emisor","ar":"نسخ عنوان المُصدر","fr":"Copier l’émetteur","bn":"ইস্যুকারীর ঠিকানা কপি করুন","pt":"Copiar emissor","id":"Salin penerbit","ur":"جاری کنندہ کا پتہ کاپی کریں","nl":"Issuer kopiëren"},"pages":[1205]},{"source":"Copy currency code","translations":{"zh-Hans":"复制货币代码","hi":"मुद्रा कोड कॉपी करें","es":"Copiar código de moneda","ar":"نسخ رمز العملة","fr":"Copier le code de devise","bn":"মুদ্রার কোড কপি করুন","pt":"Copiar código da moeda","id":"Salin kode mata uang","ur":"کرنسی کوڈ کاپی کریں","nl":"Valutacode kopiëren"},"pages":[1205]},{"source":"Copied issuer.","translations":{"zh-Hans":"已复制发行方地址。","hi":"जारीकर्ता का पता कॉपी हो गया।","es":"Emisor copiado.","ar":"تم نسخ عنوان المُصدر.","fr":"Émetteur copié.","bn":"ইস্যুকারীর ঠিকানা কপি হয়েছে।","pt":"Emissor copiado.","id":"Penerbit disalin.","ur":"جاری کنندہ کا پتہ کاپی ہو گیا۔","nl":"Issuer gekopieerd."},"pages":[1205]},{"source":"Copied currency code.","translations":{"zh-Hans":"已复制货币代码。","hi":"मुद्रा कोड कॉपी हो गया।","es":"Código de moneda copiado.","ar":"تم نسخ رمز العملة.","fr":"Code de devise copié.","bn":"মুদ্রার কোড কপি হয়েছে।","pt":"Código da moeda copiado.","id":"Kode mata uang disalin.","ur":"کرنسی کوڈ کاپی ہو گیا۔","nl":"Valutacode gekopieerd."},"pages":[1205]},{"source":"Select the issuer above to copy it.","translations":{"zh-Hans":"请选择上方的发行方地址进行复制。","hi":"कॉपी करने के लिए ऊपर दिया गया जारीकर्ता का पता चुनें।","es":"Selecciona el emisor de arriba para copiarlo.","ar":"حدّد عنوان المُصدر أعلاه لنسخه.","fr":"Sélectionnez l’émetteur ci-dessus pour le copier.","bn":"কপি করতে উপরের ইস্যুকারীর ঠিকানা নির্বাচন করুন।","pt":"Selecione o emissor acima para o copiar.","id":"Pilih penerbit di atas untuk menyalinnya.","ur":"کاپی کرنے کے لیے اوپر دیا گیا جاری کنندہ کا پتہ منتخب کریں۔","nl":"Selecteer de issuer hierboven om deze te kopiëren."},"pages":[1205]},{"source":"Select the currency code above to copy it.","translations":{"zh-Hans":"请选择上方的货币代码进行复制。","hi":"कॉपी करने के लिए ऊपर दिया गया मुद्रा कोड चुनें।","es":"Selecciona el código de moneda de arriba para copiarlo.","ar":"حدّد رمز العملة أعلاه لنسخه.","fr":"Sélectionnez le code de devise ci-dessus pour le copier.","bn":"কপি করতে উপরের মুদ্রার কোড নির্বাচন করুন।","pt":"Selecione o código da moeda acima para o copiar.","id":"Pilih kode mata uang di atas untuk menyalinnya.","ur":"کاپی کرنے کے لیے اوپر دیا گیا کرنسی کوڈ منتخب کریں۔","nl":"Selecteer de valutacode hierboven om deze te kopiëren."},"pages":[1205]},{"source":"Set CAL trustline in Xaman","translations":{"zh-Hans":"在 Xaman 中设置 CAL 信任线","hi":"Xaman में CAL ट्रस्टलाइन सेट करें","es":"Configurar línea de confianza CAL en Xaman","ar":"إعداد خط ثقة ⁦CAL⁩ في ⁦Xaman⁩","fr":"Configurer la ligne de confiance CAL dans Xaman","bn":"Xaman-এ CAL ট্রাস্টলাইন সেট করুন","pt":"Configurar linha de confiança CAL no Xaman","id":"Atur trustline CAL di Xaman","ur":"⁦Xaman⁩ میں ⁦CAL⁩ ٹرسٹ لائن قائم کریں","nl":"CAL-trustline instellen in Xaman"},"pages":[1205]},{"source":"Create a CAL trustline request and review it in Xaman","translations":{"zh-Hans":"创建 CAL 信任线请求并在 Xaman 中核对","hi":"CAL ट्रस्टलाइन अनुरोध बनाएं और Xaman में उसकी जाँच करें","es":"Crear una solicitud de línea de confianza CAL y revisarla en Xaman","ar":"إنشاء طلب خط ثقة ⁦CAL⁩ ومراجعته في ⁦Xaman⁩","fr":"Créer une demande de ligne de confiance CAL et la vérifier dans Xaman","bn":"CAL ট্রাস্টলাইনের অনুরোধ তৈরি করুন এবং Xaman-এ যাচাই করুন","pt":"Criar um pedido de linha de confiança CAL e verificá-lo no Xaman","id":"Buat permintaan trustline CAL dan periksa di Xaman","ur":"⁦CAL⁩ ٹرسٹ لائن کی درخواست بنائیں اور ⁦Xaman⁩ میں اس کی جانچ کریں","nl":"Maak een CAL-trustlineverzoek aan en controleer het in Xaman"},"pages":[1205]},{"source":"Before you continue","translations":{"zh-Hans":"继续之前","hi":"आगे बढ़ने से पहले","es":"Antes de continuar","ar":"قبل المتابعة","fr":"Avant de continuer","bn":"এগিয়ে যাওয়ার আগে","pt":"Antes de continuar","id":"Sebelum melanjutkan","ur":"آگے بڑھنے سے پہلے","nl":"Voordat je verdergaat"},"pages":[1205]},{"source":"A CAL trustline belongs to your XRPL wallet. Check the issuer and currency code in the request before signing. Food search and nutrition information in CalorieApp do not require a CAL trustline.","translations":{"zh-Hans":"CAL 信任线属于你的 XRPL 钱包。签署前,请核对请求中的发行方地址和货币代码。使用 CalorieApp 搜索食品和查看营养信息不需要 CAL 信任线。","hi":"CAL ट्रस्टलाइन आपके XRPL वॉलेट से जुड़ी होती है। हस्ताक्षर करने से पहले अनुरोध में जारीकर्ता का पता और मुद्रा कोड जाँचें। CalorieApp में खाद्य खोज और पोषण संबंधी जानकारी के लिए CAL ट्रस्टलाइन की आवश्यकता नहीं है।","es":"Una línea de confianza CAL pertenece a tu cartera XRPL. Comprueba el emisor y el código de moneda en la solicitud antes de firmar. La búsqueda de alimentos y la información nutricional de CalorieApp no requieren una línea de confianza CAL.","ar":"يرتبط خط ثقة ⁦CAL⁩ بمحفظتك على ⁦XRPL⁩. تحقّق من عنوان المُصدر ورمز العملة في الطلب قبل التوقيع. لا يتطلب البحث عن الأغذية وعرض المعلومات الغذائية في ⁦CalorieApp⁩ خط ثقة ⁦CAL⁩.","fr":"Une ligne de confiance CAL appartient à votre portefeuille XRPL. Vérifiez l’émetteur et le code de devise dans la demande avant de signer. La recherche d’aliments et les informations nutritionnelles dans CalorieApp ne nécessitent pas de ligne de confiance CAL.","bn":"CAL ট্রাস্টলাইন আপনার XRPL ওয়ালেটের সঙ্গে যুক্ত থাকে। স্বাক্ষর করার আগে অনুরোধের ইস্যুকারীর ঠিকানা ও মুদ্রার কোড যাচাই করুন। CalorieApp-এ খাবার খোঁজা এবং পুষ্টির তথ্য দেখার জন্য CAL ট্রাস্টলাইন প্রয়োজন হয় না।","pt":"Uma linha de confiança CAL pertence à sua carteira XRPL. Verifique o emissor e o código da moeda no pedido antes de assinar. A pesquisa de alimentos e as informações nutricionais na CalorieApp não exigem uma linha de confiança CAL.","id":"Trustline CAL terhubung ke dompet XRPL Anda. Periksa penerbit dan kode mata uang dalam permintaan sebelum menandatangani. Pencarian makanan dan informasi gizi di CalorieApp tidak memerlukan trustline CAL.","ur":"⁦CAL⁩ ٹرسٹ لائن آپ کے ⁦XRPL⁩ والیٹ سے منسلک ہوتی ہے۔ دستخط کرنے سے پہلے درخواست میں جاری کنندہ کا پتہ اور کرنسی کوڈ چیک کریں۔ ⁦CalorieApp⁩ میں کھانے کی تلاش اور غذائی معلومات کے لیے ⁦CAL⁩ ٹرسٹ لائن کی ضرورت نہیں ہے۔","nl":"Een CAL-trustline hoort bij je XRPL-wallet. Controleer de issuer en de valutacode in het verzoek voordat je ondertekent. Voor het zoeken naar voedingsmiddelen en het bekijken van voedingsinformatie in CalorieApp is geen CAL-trustline nodig."},"pages":[1205]},{"source":"Open XRP Toolkit","translations":{"zh-Hans":"打开 XRP Toolkit","hi":"XRP Toolkit खोलें","es":"Abrir XRP Toolkit","ar":"فتح ⁦XRP Toolkit⁩","fr":"Ouvrir XRP Toolkit","bn":"XRP Toolkit খুলুন","pt":"Abrir XRP Toolkit","id":"Buka XRP Toolkit","ur":"⁦XRP Toolkit⁩ کھولیں","nl":"XRP Toolkit openen"},"pages":[1205]},{"source":"CalorieToken holders. Scroll sideways to see the complete table.","translations":{"zh-Hans":"CalorieToken 持有者。左右滚动以查看完整表格。","hi":"CalorieToken धारक। पूरी तालिका देखने के लिए दाएँ या बाएँ स्क्रॉल करें।","es":"Titulares de CalorieToken. Desplázate horizontalmente para ver la tabla completa.","ar":"حاملو CalorieToken. مرّر أفقياً لرؤية الجدول كاملاً.","fr":"Détenteurs de CalorieToken. Faites défiler horizontalement pour voir le tableau complet.","bn":"CalorieToken ধারকরা। পুরো সারণিটি দেখতে পাশে স্ক্রল করুন।","pt":"Detentores de CalorieToken. Desloque horizontalmente para ver a tabela completa.","id":"Pemegang CalorieToken. Gulir ke samping untuk melihat seluruh tabel.","ur":"CalorieToken کے حاملین۔ مکمل جدول دیکھنے کے لیے دائیں یا بائیں اسکرول کریں۔","nl":"CalorieToken-houders. Scrol horizontaal om de hele tabel te bekijken."},"pages":[3243]},{"source":"My position","translations":{"zh-Hans":"我的排名","hi":"मेरा स्थान","es":"Mi posición","ar":"ترتيبي","fr":"Ma position","bn":"আমার অবস্থান","pt":"A minha posição","id":"Posisi saya","ur":"میری پوزیشن","nl":"Mijn positie"},"pages":[3243]},{"source":"Go to my Richlist position","translations":{"zh-Hans":"跳转到我在持有者排名中的位置","hi":"धारकों की सूची में मेरे स्थान पर जाएँ","es":"Ir a mi posición en la lista de titulares","ar":"انتقل إلى ترتيبي في قائمة الحاملين","fr":"Aller à ma position dans la liste des détenteurs","bn":"ধারকদের তালিকায় আমার অবস্থানে যান","pt":"Ir para a minha posição na lista de detentores","id":"Buka posisi saya dalam daftar pemegang","ur":"حاملین کی فہرست میں میری پوزیشن پر جائیں","nl":"Ga naar mijn positie in de Richlist"},"pages":[3243]},{"source":"Airdrops completed","translations":{"zh-Hans":"空投已完成","hi":"एयरड्रॉप पूरे हो चुके हैं","es":"Airdrops completados","ar":"اكتملت عمليات التوزيع المجاني","fr":"Airdrops terminés","bn":"এয়ারড্রপ সম্পন্ন হয়েছে","pt":"Airdrops concluídos","id":"Airdrop telah selesai","ur":"ایئرڈراپس مکمل ہو چکے ہیں","nl":"Airdrops afgerond"},"pages":[1209]},{"source":"All airdrops are completed, as confirmed by the project on 2026-09-09. The published chart is retained as historical context; earlier references to future airdrops no longer apply.","translations":{"zh-Hans":"项目方于 2026-09-09 确认,所有空投均已完成。保留已发布图表作为历史资料;其中关于未来空投的说明已不再适用。","hi":"परियोजना ने 2026-09-09 को पुष्टि की कि सभी एयरड्रॉप पूरे हो चुके हैं। प्रकाशित चार्ट ऐतिहासिक संदर्भ के लिए रखा गया है; भविष्य के एयरड्रॉप के पुराने उल्लेख अब लागू नहीं होते।","es":"El proyecto confirmó el 2026-09-09 que todos los airdrops han finalizado. El gráfico publicado se conserva como referencia histórica; las menciones anteriores a futuros airdrops ya no se aplican.","ar":"أكد المشروع بتاريخ 2026-09-09 اكتمال جميع عمليات التوزيع المجاني (airdrop). يُحتفظ بالمخطط المنشور كسياق تاريخي؛ ولم تعد الإشارات السابقة إلى عمليات توزيع مستقبلية سارية.","fr":"Le projet a confirmé le 2026-09-09 que tous les airdrops sont terminés. Le graphique publié est conservé à titre historique ; les anciennes mentions de futurs airdrops ne sont plus applicables.","bn":"প্রকল্পটি 2026-09-09 তারিখে নিশ্চিত করেছে যে সব এয়ারড্রপ সম্পন্ন হয়েছে। প্রকাশিত চার্টটি ঐতিহাসিক তথ্য হিসেবে রাখা হয়েছে; ভবিষ্যৎ এয়ারড্রপের পুরোনো উল্লেখ আর প্রযোজ্য নয়।","pt":"O projeto confirmou em 2026-09-09 que todos os airdrops foram concluídos. O gráfico publicado é mantido como referência histórica; as menções anteriores a futuros airdrops já não se aplicam.","id":"Proyek mengonfirmasi pada 2026-09-09 bahwa semua airdrop telah selesai. Grafik yang diterbitkan dipertahankan sebagai konteks historis; keterangan lama tentang airdrop mendatang tidak lagi berlaku.","ur":"منصوبے نے 2026-09-09 کو تصدیق کی کہ تمام ایئرڈراپس مکمل ہو چکے ہیں۔ شائع شدہ چارٹ تاریخی حوالے کے لیے برقرار ہے؛ آئندہ ایئرڈراپس کے پرانے حوالے اب لاگو نہیں ہوتے۔","nl":"Alle airdrops zijn afgerond, zoals door het project bevestigd op 2026-09-09. De gepubliceerde grafiek blijft als historische toelichting staan; eerdere verwijzingen naar toekomstige airdrops zijn niet meer van toepassing."},"pages":[1209]},{"source":"A remaining giveaway allocation will be stated after distributions, internal transfers and liquidity positions have been reconciled.","translations":{"zh-Hans":"在核对分发、内部转账和流动性头寸后,将公布剩余赠送额度。","hi":"वितरण, आंतरिक हस्तांतरण और लिक्विडिटी पोज़िशन का मिलान करने के बाद शेष गिवअवे आवंटन बताया जाएगा।","es":"La asignación restante para sorteos y regalos se indicará tras conciliar las distribuciones, las transferencias internas y las posiciones de liquidez.","ar":"سيُذكر المخصص المتبقي للهدايا بعد مطابقة التوزيعات والتحويلات الداخلية ومراكز السيولة.","fr":"Le solde alloué aux cadeaux sera indiqué après rapprochement des distributions, des transferts internes et des positions de liquidité.","bn":"বিতরণ, অভ্যন্তরীণ স্থানান্তর এবং লিকুইডিটি পজিশন মিলিয়ে দেখার পর গিভঅ্যাওয়ের অবশিষ্ট বরাদ্দ জানানো হবে।","pt":"A alocação restante para brindes será indicada após a reconciliação das distribuições, transferências internas e posições de liquidez.","id":"Sisa alokasi giveaway akan dinyatakan setelah distribusi, transfer internal, dan posisi likuiditas direkonsiliasi.","ur":"تقسیم، داخلی منتقلیوں اور لیکویڈیٹی پوزیشنوں کا حساب ملانے کے بعد تحائف کے لیے باقی مختص مقدار بتائی جائے گی۔","nl":"Het resterende giveawaybudget wordt vermeld nadat uitdelingen, interne overboekingen en liquiditeitsposities met elkaar zijn afgestemd."},"pages":[1209]},{"source":"Consolidation wallet","translations":{"zh-Hans":"归集钱包","hi":"समेकन वॉलेट","es":"Billetera de consolidación","ar":"محفظة التجميع","fr":"Portefeuille de consolidation","bn":"একত্রীকরণ ওয়ালেট","pt":"Carteira de consolidação","id":"Dompet konsolidasi","ur":"یکجا فنڈز کا والیٹ","nl":"Consolidatiewallet"},"pages":[1209]},{"source":"Project wallet for consolidated CalorieToken funds. Its balance is not a statement of the remaining giveaway allocation.","translations":{"zh-Hans":"用于归集 CalorieToken 项目资金的钱包。其余额不代表剩余赠送额度。","hi":"CalorieToken परियोजना की समेकित निधियों का वॉलेट। इसका बैलेंस शेष गिवअवे आवंटन नहीं दर्शाता।","es":"Billetera del proyecto para fondos consolidados de CalorieToken. Su saldo no indica la asignación restante para sorteos y regalos.","ar":"محفظة المشروع لتجميع أموال CalorieToken. لا يمثل رصيدها المخصص المتبقي للهدايا.","fr":"Portefeuille du projet destiné aux fonds consolidés de CalorieToken. Son solde ne représente pas le montant restant alloué aux cadeaux.","bn":"CalorieToken প্রকল্পের একত্রিত তহবিলের ওয়ালেট। এর ব্যালেন্স গিভঅ্যাওয়ের অবশিষ্ট বরাদ্দ নির্দেশ করে না।","pt":"Carteira do projeto para fundos consolidados de CalorieToken. O seu saldo não representa a alocação restante para brindes.","id":"Dompet proyek untuk dana CalorieToken yang dikonsolidasikan. Saldonya tidak menyatakan sisa alokasi giveaway.","ur":"CalorieToken منصوبے کے یکجا فنڈز کا والیٹ۔ اس کا بیلنس تحائف کے لیے باقی مختص مقدار کو ظاہر نہیں کرتا۔","nl":"Projectwallet voor samengebrachte CalorieToken-fondsen. Het saldo geeft niet het resterende giveawaybudget aan."},"pages":[1209]},{"source":"At the ledger check on 2026-09-09, this wallet held a liquidity position in the CAL/XRP AMM. The project therefore has a financial interest in this pool.","translations":{"zh-Hans":"在 2026-09-09 的账本核查中,此钱包持有 CAL/XRP AMM 的流动性头寸。因此,项目在该资金池中具有经济利益。","hi":"2026-09-09 की लेजर जाँच के समय इस वॉलेट की CAL/XRP AMM में लिक्विडिटी पोज़िशन थी। इसलिए इस पूल में परियोजना का वित्तीय हित है।","es":"En la comprobación del ledger del 2026-09-09, esta billetera mantenía una posición de liquidez en el AMM CAL/XRP. Por tanto, el proyecto tiene un interés financiero en este pool.","ar":"عند فحص السجل بتاريخ 2026-09-09، كانت لهذه المحفظة حصة سيولة في مجمع CAL/XRP AMM. ولذلك للمشروع مصلحة مالية في هذا المجمع.","fr":"Lors de la vérification du registre le 2026-09-09, ce portefeuille détenait une position de liquidité dans l’AMM CAL/XRP. Le projet a donc un intérêt financier dans ce pool.","bn":"2026-09-09 তারিখে লেজার যাচাইয়ের সময় এই ওয়ালেটের CAL/XRP AMM-এ লিকুইডিটি পজিশন ছিল। তাই এই পুলে প্রকল্পটির আর্থিক স্বার্থ রয়েছে।","pt":"Na verificação do ledger em 2026-09-09, esta carteira mantinha uma posição de liquidez no AMM CAL/XRP. Por isso, o projeto tem um interesse financeiro neste pool.","id":"Pada pemeriksaan ledger tanggal 2026-09-09, dompet ini memiliki posisi likuiditas di AMM CAL/XRP. Karena itu, proyek memiliki kepentingan finansial dalam pool ini.","ur":"2026-09-09 کی لیجر جانچ کے وقت اس والیٹ کی CAL/XRP AMM میں لیکویڈیٹی پوزیشن تھی۔ اس لیے اس پول میں منصوبے کا مالی مفاد ہے۔","nl":"Bij de ledgercontrole op 2026-09-09 had deze wallet een liquiditeitspositie in de CAL/XRP-AMM. Het project heeft daarmee een financieel belang in deze pool."},"pages":[1209]},{"source":"View wallet on XPMarket (new tab)","translations":{"zh-Hans":"在 XPMarket 查看钱包(新标签页)","hi":"XPMarket पर वॉलेट देखें (नया टैब)","es":"Ver billetera en XPMarket (nueva pestaña)","ar":"عرض المحفظة على XPMarket (علامة تبويب جديدة)","fr":"Voir le portefeuille sur XPMarket (nouvel onglet)","bn":"XPMarket-এ ওয়ালেট দেখুন (নতুন ট্যাব)","pt":"Ver carteira no XPMarket (nova aba)","id":"Lihat dompet di XPMarket (tab baru)","ur":"XPMarket پر والیٹ دیکھیں (نیا ٹیب)","nl":"Bekijk wallet op XPMarket (nieuw tabblad)"},"pages":[1209]},{"source":"Page shortcuts","translations":{"zh-Hans":"页面快捷操作","hi":"पेज शॉर्टकट","es":"Accesos de página","ar":"اختصارات الصفحة","fr":"Raccourcis de la page","bn":"পৃষ্ঠার শর্টকাট","pt":"Atalhos da página","id":"Pintasan halaman","ur":"صفحے کے شارٹ کٹس","nl":"Paginasnelkoppelingen"},"pages":["*"]},{"source":"Go to Home","translations":{"zh-Hans":"返回首页","hi":"होम पर जाएँ","es":"Ir al inicio","ar":"إلى الصفحة الرئيسية","fr":"Aller à l’accueil","bn":"হোমে যান","pt":"Ir para o início","id":"Ke beranda","ur":"ہوم پر جائیں","nl":"Naar de homepage"},"pages":["*"]},{"source":"Back to top","translations":{"zh-Hans":"返回顶部","hi":"ऊपर लौटें","es":"Volver arriba","ar":"إلى أعلى الصفحة","fr":"Revenir en haut","bn":"উপরে ফিরুন","pt":"Voltar ao topo","id":"Kembali ke atas","ur":"اوپر واپس جائیں","nl":"Naar boven"},"pages":["*"]},{"source":"Go to bottom","translations":{"zh-Hans":"前往底部","hi":"नीचे जाएँ","es":"Ir abajo","ar":"إلى أسفل الصفحة","fr":"Aller en bas","bn":"নিচে যান","pt":"Ir para o fim","id":"Ke bagian bawah","ur":"صفحے کے نیچے جائیں","nl":"Naar beneden"},"pages":["*"]},{"source":"XRPL buying and selling guide","translations":{"zh-Hans":"XRPL 买卖指南","hi":"XRPL खरीदने और बेचने की मार्गदर्शिका","es":"Guía de compra y venta en XRPL","ar":"دليل الشراء والبيع على XRPL","fr":"Guide d’achat et de vente sur XRPL","bn":"XRPL কেনাবেচার নির্দেশিকা","pt":"Guia de compra e venda na XRPL","id":"Panduan jual beli XRPL","ur":"XRPL خرید و فروخت کی رہنمائی","nl":"Koop- en verkoopgids voor XRPL"},"pages":[4205]},{"source":"Buy or sell CAL in four clear steps","translations":{"zh-Hans":"四步买入或卖出 CAL","hi":"चार स्पष्ट चरणों में CAL खरीदें या बेचें","es":"Compra o vende CAL en cuatro pasos claros","ar":"اشترِ أو بِع CAL في أربع خطوات واضحة","fr":"Acheter ou vendre CAL en quatre étapes claires","bn":"চারটি স্পষ্ট ধাপে CAL কিনুন বা বিক্রি করুন","pt":"Compre ou venda CAL em quatro passos claros","id":"Beli atau jual CAL dalam empat langkah jelas","ur":"چار واضح مراحل میں CAL خریدیں یا بیچیں","nl":"Koop of verkoop CAL in vier duidelijke stappen"},"pages":[4205]},{"source":"CAL trades on the XRP Ledger DEX. Verify the complete issuer and currency code, not just the name or logo.","translations":{"zh-Hans":"CAL 在 XRP Ledger DEX 上交易。请核对完整发行方地址和货币代码,而不只是名称或标志。","hi":"CAL का कारोबार XRP Ledger DEX पर होता है। केवल नाम या लोगो नहीं, पूरा जारीकर्ता पता और मुद्रा कोड जाँचें।","es":"CAL se negocia en el DEX de XRP Ledger. Comprueba el emisor completo y el código de moneda, no solo el nombre o el logotipo.","ar":"يُتداول CAL على منصة XRP Ledger اللامركزية. تحقّق من عنوان المُصدر الكامل ورمز العملة، وليس الاسم أو الشعار فقط.","fr":"CAL se négocie sur le DEX du XRP Ledger. Vérifiez l’adresse complète de l’émetteur et le code de devise, pas seulement le nom ou le logo.","bn":"XRP Ledger DEX-এ CAL লেনদেন হয়। শুধু নাম বা লোগো নয়, সম্পূর্ণ ইস্যুকারীর ঠিকানা ও মুদ্রার কোড যাচাই করুন।","pt":"CAL é negociado na DEX do XRP Ledger. Verifique o endereço completo do emissor e o código da moeda, não apenas o nome ou o logótipo.","id":"CAL diperdagangkan di XRP Ledger DEX. Verifikasi alamat penerbit lengkap dan kode mata uang, bukan hanya nama atau logo.","ur":"CAL کی تجارت XRP Ledger DEX پر ہوتی ہے۔ صرف نام یا لوگو نہیں، جاری کنندہ کا مکمل پتہ اور کرنسی کوڈ جانچیں۔","nl":"CAL wordt verhandeld op de XRP Ledger DEX. Controleer het volledige issueradres en de valutacode, niet alleen de naam of het logo."},"pages":[4205]},{"source":"Asset","translations":{"zh-Hans":"资产","hi":"संपत्ति","es":"Activo","ar":"الأصل","fr":"Actif","bn":"সম্পদ","pt":"Ativo","id":"Aset","ur":"اثاثہ","nl":"Token"},"pages":[4205]},{"source":"Issuer","translations":{"zh-Hans":"发行方","hi":"जारीकर्ता","es":"Emisor","ar":"المُصدر","fr":"Émetteur","bn":"ইস্যুকারী","pt":"Emissor","id":"Penerbit","ur":"جاری کنندہ","nl":"Issuer"},"pages":[4205]},{"source":"Currency code","translations":{"zh-Hans":"货币代码","hi":"मुद्रा कोड","es":"Código de moneda","ar":"رمز العملة","fr":"Code de devise","bn":"মুদ্রার কোড","pt":"Código da moeda","id":"Kode mata uang","ur":"کرنسی کوڈ","nl":"Valutacode"},"pages":[4205]},{"source":"Set up an XRPL wallet","translations":{"zh-Hans":"设置 XRPL 钱包","hi":"XRPL वॉलेट सेट करें","es":"Configura una billetera XRPL","ar":"إعداد محفظة XRPL","fr":"Configurer un portefeuille XRPL","bn":"XRPL ওয়ালেট সেট করুন","pt":"Configure uma carteira XRPL","id":"Siapkan dompet XRPL","ur":"XRPL والیٹ بنائیں","nl":"Maak een XRPL-wallet aan"},"pages":[4205]},{"source":"Install Xaman from its official source. Keep recovery information offline; never enter it on this website or a trading page.","translations":{"zh-Hans":"从官方来源安装 Xaman。离线保存恢复信息,切勿在本网站或交易页面输入。","hi":"आधिकारिक स्रोत से Xaman इंस्टॉल करें। रिकवरी जानकारी ऑफलाइन रखें; इसे इस वेबसाइट या किसी ट्रेडिंग पेज पर कभी न डालें।","es":"Instala Xaman desde su fuente oficial. Guarda los datos de recuperación fuera de línea; nunca los introduzcas en esta web ni en una página de trading.","ar":"ثبّت Xaman من مصدره الرسمي. احفظ معلومات الاسترداد دون اتصال؛ ولا تدخلها أبداً في هذا الموقع أو صفحات التداول.","fr":"Installez Xaman depuis sa source officielle. Conservez les informations de récupération hors ligne ; ne les saisissez jamais sur ce site ou une page de trading.","bn":"অফিশিয়াল উৎস থেকে Xaman ইনস্টল করুন। পুনরুদ্ধারের তথ্য অফলাইনে রাখুন; এই ওয়েবসাইট বা কোনো ট্রেডিং পেজে কখনো লিখবেন না।","pt":"Instale o Xaman a partir da fonte oficial. Guarde os dados de recuperação offline; nunca os introduza neste site ou numa página de negociação.","id":"Instal Xaman dari sumber resminya. Simpan informasi pemulihan secara offline; jangan pernah memasukkannya di situs ini atau halaman perdagangan.","ur":"Xaman اس کے سرکاری ذریعے سے انسٹال کریں۔ بحالی کی معلومات آف لائن رکھیں؛ انہیں اس ویب سائٹ یا تجارتی صفحے پر کبھی درج نہ کریں۔","nl":"Installeer Xaman via de officiële bron. Bewaar herstelgegevens offline; voer ze nooit op deze website of een handelspagina in."},"pages":[4205]},{"source":"Visit Xaman","translations":{"zh-Hans":"访问 Xaman","hi":"Xaman पर जाएँ","es":"Visitar Xaman","ar":"زيارة Xaman","fr":"Visiter Xaman","bn":"Xaman দেখুন","pt":"Visitar Xaman","id":"Kunjungi Xaman","ur":"Xaman دیکھیں","nl":"Bezoek Xaman"},"pages":[4205]},{"source":"Fund your wallet with XRP","translations":{"zh-Hans":"向钱包存入 XRP","hi":"वॉलेट में XRP डालें","es":"Ingresa XRP en tu billetera","ar":"تمويل محفظتك بعملة XRP","fr":"Alimenter votre portefeuille en XRP","bn":"ওয়ালেটে XRP যোগ করুন","pt":"Adicione XRP à sua carteira","id":"Isi dompet Anda dengan XRP","ur":"والیٹ میں XRP شامل کریں","nl":"Zet XRP op je wallet"},"pages":[4205]},{"source":"Xaman lists independent providers by country and currency. Check eligibility, identity checks, rates and fees; availability varies by region. Already hold BTC, ETH or another supported coin? AllChainBridge / SWFT is another possible route to XRP across networks. See section 3 below. Select the source coin and network, choose XRP on XRP Ledger as the destination, and check the available wallets, route, fees and any destination tag with SWFT. CAL is temporarily unavailable there.","translations":{"zh-Hans":"Xaman 按国家和货币列出独立服务商。请核查资格、身份验证、汇率和费用;可用性因地区而异。 已有 BTC、ETH 或其他受支持的币?AllChainBridge / SWFT 也是跨网络兑换 XRP 的一种可能途径,见下方第 3 节。选择来源币和网络,将 XRP Ledger 上的 XRP 设为目标,并在 SWFT 检查可用钱包、路径、费用及所需的目标标签。CAL 暂时不可用。","hi":"Xaman देश और मुद्रा के अनुसार स्वतंत्र प्रदाता दिखाता है। पात्रता, पहचान जाँच, दरें और शुल्क जाँचें; उपलब्धता क्षेत्र के अनुसार बदलती है। क्या आपके पास BTC, ETH या कोई समर्थित कॉइन है? AllChainBridge / SWFT अलग नेटवर्क से XRP पाने का एक और संभावित रास्ता है। नीचे भाग 3 देखें। स्रोत कॉइन और नेटवर्क चुनें, गंतव्य में XRP Ledger का XRP चुनें और SWFT में उपलब्ध वॉलेट, मार्ग, शुल्क तथा आवश्यक डेस्टिनेशन टैग जाँचें। वहाँ CAL अस्थायी रूप से उपलब्ध नहीं है।","es":"Xaman muestra proveedores independientes por país y moneda. Comprueba requisitos, verificación de identidad, tipos de cambio y comisiones; la disponibilidad varía por región. ¿Ya tienes BTC, ETH u otra moneda compatible? AllChainBridge / SWFT es otra posible vía hacia XRP entre redes; consulta la sección 3. Elige moneda y red de origen, XRP en XRP Ledger como destino y comprueba en SWFT carteras disponibles, ruta, costes y posible etiqueta de destino. CAL no está disponible allí temporalmente.","ar":"يعرض Xaman مزودين مستقلين حسب البلد والعملة. تحقّق من الأهلية وفحوص الهوية والأسعار والرسوم؛ فالتوفر يختلف حسب المنطقة. هل لديك BTC أو ETH أو عملة مدعومة أخرى؟ يتيح AllChainBridge / SWFT مسارًا محتملًا آخر إلى XRP بين الشبكات؛ انظر القسم 3 أدناه. اختر عملة المصدر وشبكتها، ثم XRP على XRP Ledger كوجهة، وتحقق لدى SWFT من المحافظ والمسار والرسوم وأي وسم وجهة مطلوب. CAL غير متاح هناك مؤقتًا.","fr":"Xaman présente des prestataires indépendants par pays et devise. Vérifiez l’éligibilité, les contrôles d’identité, les taux et les frais ; la disponibilité varie selon les régions. Vous détenez déjà du BTC, de l’ETH ou une autre monnaie prise en charge ? AllChainBridge / SWFT est une autre voie possible vers XRP entre réseaux ; voir la section 3. Choisissez la monnaie et le réseau source, puis XRP sur XRP Ledger comme destination. Vérifiez chez SWFT les portefeuilles, le parcours, les frais et l’éventuel tag de destination. CAL y est temporairement indisponible.","bn":"Xaman দেশ ও মুদ্রা অনুযায়ী স্বাধীন সেবাদাতা দেখায়। যোগ্যতা, পরিচয় যাচাই, হার ও ফি পরীক্ষা করুন; অঞ্চলভেদে উপলভ্যতা বদলায়। ইতিমধ্যে BTC, ETH বা অন্য সমর্থিত কয়েন আছে? AllChainBridge / SWFT বিভিন্ন নেটওয়ার্ক থেকে XRP পাওয়ার আরেকটি সম্ভাব্য পথ; নিচের ৩ নম্বর অংশ দেখুন। উৎস কয়েন ও নেটওয়ার্ক বেছে নিন, গন্তব্য হিসেবে XRP Ledger-এর XRP দিন এবং SWFT-এ উপলব্ধ ওয়ালেট, পথ, ফি ও প্রয়োজনীয় ডেস্টিনেশন ট্যাগ যাচাই করুন। সেখানে CAL সাময়িকভাবে অনুপলব্ধ।","pt":"O Xaman apresenta prestadores independentes por país e moeda. Verifique elegibilidade, identificação, taxas de câmbio e comissões; a disponibilidade varia por região. Já tem BTC, ETH ou outra moeda compatível? AllChainBridge / SWFT é outra possível via para XRP entre redes; veja a secção 3. Escolha a moeda e rede de origem e XRP na XRP Ledger como destino. Confirme no SWFT as carteiras disponíveis, o percurso, as taxas e uma eventual etiqueta de destino. CAL está temporariamente indisponível ali.","id":"Xaman menampilkan penyedia independen berdasarkan negara dan mata uang. Periksa kelayakan, verifikasi identitas, kurs, dan biaya; ketersediaan berbeda menurut wilayah. Sudah memiliki BTC, ETH, atau koin lain yang didukung? AllChainBridge / SWFT adalah jalur lain yang mungkin tersedia menuju XRP lintas jaringan; lihat bagian 3. Pilih koin dan jaringan asal, XRP di XRP Ledger sebagai tujuan, lalu periksa dompet yang tersedia, rute, biaya, dan tag tujuan yang diperlukan di SWFT. CAL sementara tidak tersedia di sana.","ur":"Xaman ملک اور کرنسی کے مطابق آزاد فراہم کنندگان دکھاتا ہے۔ اہلیت، شناخت کی جانچ، شرح اور فیس دیکھیں؛ دستیابی علاقے کے لحاظ سے بدلتی ہے۔ کیا آپ کے پاس BTC، ETH یا کوئی معاون کوائن ہے؟ AllChainBridge / SWFT مختلف نیٹ ورکس سے XRP حاصل کرنے کا ایک اور ممکنہ راستہ ہے؛ نیچے حصہ 3 دیکھیں۔ اصل کوائن اور نیٹ ورک منتخب کریں، منزل میں XRP Ledger کا XRP چنیں اور SWFT پر دستیاب والیٹس، راستہ، فیس اور مطلوبہ ڈیسٹینیشن ٹیگ دیکھیں۔ وہاں CAL عارضی طور پر دستیاب نہیں۔","nl":"Xaman toont onafhankelijke aanbieders per land en valuta. Controleer toelating, identiteitscontroles, koersen en kosten; beschikbaarheid verschilt per regio. Heb je al BTC, ETH of een andere ondersteunde munt? AllChainBridge / SWFT biedt ook een mogelijke route naar XRP tussen netwerken. Zie sectie 3 hieronder. Kies de bronmunt en het netwerk, kies XRP op XRP Ledger als bestemming en controleer bij SWFT de beschikbare wallets, route, kosten en een eventuele destination tag. CAL is daar tijdelijk niet beschikbaar."},"pages":[4205]},{"source":"Find options in Xaman","translations":{"zh-Hans":"在 Xaman 查看选项","hi":"Xaman में विकल्प देखें","es":"Ver opciones en Xaman","ar":"عرض الخيارات في Xaman","fr":"Voir les options dans Xaman","bn":"Xaman-এ বিকল্প দেখুন","pt":"Ver opções no Xaman","id":"Lihat pilihan di Xaman","ur":"Xaman میں اختیارات دیکھیں","nl":"Bekijk mogelijkheden in Xaman"},"pages":[4205]},{"source":"Keep enough XRP for the reserve and network fees shown by your wallet. Reserve requirements can change.","translations":{"zh-Hans":"保留足够的 XRP,以满足钱包显示的储备金和网络费用要求。储备金要求可能变化。","hi":"वॉलेट में दिखाए गए रिज़र्व और नेटवर्क शुल्क के लिए पर्याप्त XRP रखें। रिज़र्व की आवश्यकताएँ बदल सकती हैं।","es":"Mantén suficiente XRP para la reserva y las comisiones de red que indica tu billetera. Los requisitos de reserva pueden cambiar.","ar":"احتفظ بما يكفي من XRP للاحتياطي ورسوم الشبكة التي تعرضها محفظتك. قد تتغير متطلبات الاحتياطي.","fr":"Gardez assez de XRP pour la réserve et les frais de réseau indiqués par votre portefeuille. Les exigences de réserve peuvent changer.","bn":"ওয়ালেটে দেখানো রিজার্ভ ও নেটওয়ার্ক ফির জন্য পর্যাপ্ত XRP রাখুন। রিজার্ভের শর্ত বদলাতে পারে।","pt":"Mantenha XRP suficiente para a reserva e as taxas de rede indicadas pela carteira. Os requisitos de reserva podem mudar.","id":"Sisakan cukup XRP untuk cadangan dan biaya jaringan yang ditampilkan dompet Anda. Persyaratan cadangan dapat berubah.","ur":"والیٹ میں دکھائے گئے ریزرو اور نیٹ ورک فیس کے لیے کافی XRP رکھیں۔ ریزرو کی ضروریات بدل سکتی ہیں۔","nl":"Houd genoeg XRP aan voor de reserve en netwerkkosten die je wallet toont. Reservevereisten kunnen veranderen."},"pages":[4205]},{"source":"XRPL reserve information","translations":{"zh-Hans":"XRPL 储备金说明","hi":"XRPL रिज़र्व की जानकारी","es":"Información sobre reservas XRPL","ar":"معلومات احتياطي XRPL","fr":"Informations sur les réserves XRPL","bn":"XRPL রিজার্ভের তথ্য","pt":"Informações sobre reservas XRPL","id":"Informasi cadangan XRPL","ur":"XRPL ریزرو کی معلومات","nl":"Uitleg over XRPL-reserves"},"pages":[4205]},{"source":"Add the exact CAL trustline","translations":{"zh-Hans":"添加准确的 CAL 信任线","hi":"सही CAL ट्रस्टलाइन जोड़ें","es":"Añade la línea de confianza CAL exacta","ar":"إضافة خط الثقة الصحيح لعملة CAL","fr":"Ajouter la ligne de confiance CAL exacte","bn":"সঠিক CAL ট্রাস্টলাইন যোগ করুন","pt":"Adicione a linha de confiança CAL exata","id":"Tambahkan trustline CAL yang tepat","ur":"درست CAL ٹرسٹ لائن شامل کریں","nl":"Voeg de juiste CAL-trustline toe"},"pages":[4205]},{"source":"To receive CAL, review its issuer and currency code before approving the trustline in Xaman.","translations":{"zh-Hans":"接收 CAL 前,请核对发行方地址和货币代码,再在 Xaman 中批准信任线。","hi":"CAL प्राप्त करने के लिए Xaman में ट्रस्टलाइन स्वीकृत करने से पहले जारीकर्ता और मुद्रा कोड जाँचें।","es":"Para recibir CAL, revisa su emisor y código de moneda antes de aprobar la línea de confianza en Xaman.","ar":"لاستلام CAL، راجع المُصدر ورمز العملة قبل الموافقة على خط الثقة في Xaman.","fr":"Pour recevoir CAL, vérifiez son émetteur et son code de devise avant d’approuver la ligne de confiance dans Xaman.","bn":"CAL পেতে Xaman-এ ট্রাস্টলাইন অনুমোদনের আগে ইস্যুকারী ও মুদ্রার কোড যাচাই করুন।","pt":"Para receber CAL, verifique o emissor e o código da moeda antes de aprovar a linha de confiança no Xaman.","id":"Untuk menerima CAL, periksa penerbit dan kode mata uangnya sebelum menyetujui trustline di Xaman.","ur":"CAL وصول کرنے کے لیے Xaman میں ٹرسٹ لائن منظور کرنے سے پہلے جاری کنندہ اور کرنسی کوڈ جانچیں۔","nl":"Controleer voor het ontvangen van CAL de issuer en valutacode voordat je de trustline in Xaman goedkeurt."},"pages":[4205]},{"source":"Open CAL trustline guide","translations":{"zh-Hans":"打开 CAL 信任线指南","hi":"CAL ट्रस्टलाइन मार्गदर्शिका खोलें","es":"Abrir guía de línea de confianza CAL","ar":"فتح دليل خط الثقة CAL","fr":"Ouvrir le guide de ligne de confiance CAL","bn":"CAL ট্রাস্টলাইন নির্দেশিকা খুলুন","pt":"Abrir guia da linha de confiança CAL","id":"Buka panduan trustline CAL","ur":"CAL ٹرسٹ لائن کی رہنمائی کھولیں","nl":"Open de CAL-trustlinegids"},"pages":[4205]},{"source":"Choose buy or sell","translations":{"zh-Hans":"选择买入或卖出","hi":"खरीदना या बेचना चुनें","es":"Elige comprar o vender","ar":"اختيار الشراء أو البيع","fr":"Choisir l’achat ou la vente","bn":"কেনা বা বিক্রি বেছে নিন","pt":"Escolha comprar ou vender","id":"Pilih beli atau jual","ur":"خریدنا یا بیچنا منتخب کریں","nl":"Kies kopen of verkopen"},"pages":[4205]},{"source":"On XPMarket, check what you pay and receive, the issuers, route, fees, liquidity and price impact. Review the final request in your wallet.","translations":{"zh-Hans":"在 XPMarket 核对支付和接收的资产、发行方、路径、费用、流动性和价格影响。在钱包中审核最终请求。","hi":"XPMarket पर भुगतान और प्राप्ति की संपत्ति, जारीकर्ता, रूट, शुल्क, लिक्विडिटी और मूल्य प्रभाव जाँचें। अंतिम अनुरोध अपने वॉलेट में जाँचें।","es":"En XPMarket, comprueba lo que pagas y recibes, los emisores, la ruta, las comisiones, la liquidez y el impacto en el precio. Revisa la solicitud final en tu billetera.","ar":"في XPMarket، تحقّق مما تدفعه وتستلمه والمُصدرين والمسار والرسوم والسيولة وتأثير السعر. راجع الطلب النهائي في محفظتك.","fr":"Sur XPMarket, vérifiez ce que vous payez et recevez, les émetteurs, le parcours, les frais, la liquidité et l’impact sur le prix. Vérifiez la demande finale dans votre portefeuille.","bn":"XPMarket-এ কী দিচ্ছেন ও পাচ্ছেন, ইস্যুকারী, রুট, ফি, লিকুইডিটি এবং দামের প্রভাব যাচাই করুন। চূড়ান্ত অনুরোধ ওয়ালেটে যাচাই করুন।","pt":"Na XPMarket, confira o que paga e recebe, os emissores, a rota, as taxas, a liquidez e o impacto no preço. Reveja o pedido final na carteira.","id":"Di XPMarket, periksa apa yang dibayar dan diterima, penerbit, rute, biaya, likuiditas, dan dampak harga. Tinjau permintaan akhir di dompet Anda.","ur":"XPMarket پر ادائیگی اور وصولی کے اثاثے، جاری کنندگان، راستہ، فیس، لیکویڈیٹی اور قیمت پر اثر جانچیں۔ حتمی درخواست والیٹ میں دیکھیں۔","nl":"Controleer op XPMarket wat je betaalt en ontvangt, de issuers, route, kosten, liquiditeit en prijsimpact. Bekijk het definitieve verzoek in je wallet."},"pages":[4205]},{"source":"Open CAL on XPMarket","translations":{"zh-Hans":"在 XPMarket 打开 CAL","hi":"XPMarket पर CAL खोलें","es":"Abrir CAL en XPMarket","ar":"فتح CAL على XPMarket","fr":"Ouvrir CAL sur XPMarket","bn":"XPMarket-এ CAL খুলুন","pt":"Abrir CAL na XPMarket","id":"Buka CAL di XPMarket","ur":"XPMarket پر CAL کھولیں","nl":"Open CAL op XPMarket"},"pages":[4205]},{"source":"To sell, select CAL as the asset you pay and check the asset you receive. A received token may need its own trustline. Converting proceeds to money in a bank account is a separate provider service.","translations":{"zh-Hans":"卖出时,选择 CAL 作为支付资产,并核对接收的资产。接收的代币可能需要单独的信任线。将所得兑换为银行账户中的资金是服务商提供的另一项服务。","hi":"बेचने के लिए भुगतान वाली संपत्ति के रूप में CAL चुनें और मिलने वाली संपत्ति जाँचें। प्राप्त होने वाले टोकन के लिए अलग ट्रस्टलाइन की आवश्यकता हो सकती है। प्राप्त राशि को बैंक खाते में पैसे के रूप में बदलना प्रदाता की अलग सेवा है।","es":"Para vender, selecciona CAL como el activo que pagas y comprueba el activo que recibes. El token recibido puede necesitar su propia línea de confianza. Convertir lo obtenido en dinero en una cuenta bancaria es un servicio independiente del proveedor.","ar":"للبيع، اختر CAL بوصفه الأصل الذي تدفعه وتحقّق من الأصل الذي تستلمه. قد يحتاج الرمز المستلم إلى خط ثقة خاص به. تحويل العائد إلى أموال في حساب مصرفي خدمة منفصلة يقدمها المزود.","fr":"Pour vendre, choisissez CAL comme actif à payer et vérifiez l’actif que vous recevez. Le jeton reçu peut nécessiter sa propre ligne de confiance. Convertir le produit de la vente en argent sur un compte bancaire est un service distinct du prestataire.","bn":"বিক্রি করতে যে সম্পদ দিয়ে পরিশোধ করবেন সেটি হিসেবে CAL বেছে নিন এবং যে সম্পদ পাবেন সেটি যাচাই করুন। প্রাপ্ত টোকেনের নিজস্ব ট্রাস্টলাইন প্রয়োজন হতে পারে। প্রাপ্ত সম্পদ ব্যাংক হিসাবে অর্থে রূপান্তর করা সেবাদাতার আলাদা সেবা।","pt":"Para vender, selecione CAL como o ativo que paga e verifique o ativo que recebe. O token recebido pode precisar de uma linha de confiança própria. Converter o resultado da venda em dinheiro numa conta bancária é um serviço separado do prestador.","id":"Untuk menjual, pilih CAL sebagai aset yang Anda bayarkan dan periksa aset yang diterima. Token yang diterima mungkin memerlukan trustline sendiri. Mengubah hasil penjualan menjadi uang di rekening bank adalah layanan penyedia yang terpisah.","ur":"بیچنے کے لیے ادائیگی کے اثاثے کے طور پر CAL منتخب کریں اور وصول ہونے والا اثاثہ جانچیں۔ وصول ہونے والے ٹوکن کو اپنی ٹرسٹ لائن درکار ہو سکتی ہے۔ حاصل رقم کو بینک اکاؤنٹ میں پیسوں میں تبدیل کرنا فراہم کنندہ کی الگ سروس ہے۔","nl":"Kies bij verkopen CAL als de token waarmee je betaalt en controleer wat je ontvangt. Voor een ontvangen token kan een eigen trustline nodig zijn. De opbrengst omzetten naar geld op een bankrekening is een afzonderlijke dienst van een aanbieder."},"pages":[4205]},{"source":"Trading through external services","translations":{"zh-Hans":"通过外部服务交易","hi":"बाहरी सेवाओं के माध्यम से कारोबार","es":"Operar mediante servicios externos","ar":"التداول عبر خدمات خارجية","fr":"Négocier via des services externes","bn":"বাইরের সেবার মাধ্যমে লেনদেন","pt":"Negociação através de serviços externos","id":"Perdagangan melalui layanan eksternal","ur":"بیرونی خدمات کے ذریعے تجارت","nl":"Handelen via externe diensten"},"pages":[4205]},{"source":"This website provides guidance and external links. Trading takes place with the selected external service and your wallet.","translations":{"zh-Hans":"本网站提供指南和外部链接。交易通过选定的外部服务及您的钱包进行。","hi":"यह वेबसाइट मार्गदर्शन और बाहरी लिंक देती है। कारोबार चुनी गई बाहरी सेवा और आपके वॉलेट के माध्यम से होता है।","es":"Esta web ofrece orientación y enlaces externos. Las operaciones se realizan con el servicio externo elegido y tu billetera.","ar":"يوفّر هذا الموقع إرشادات وروابط خارجية. يتم التداول عبر الخدمة الخارجية المختارة ومحفظتك.","fr":"Ce site fournit des conseils pratiques et des liens externes. Les opérations passent par le service externe choisi et votre portefeuille.","bn":"এই ওয়েবসাইট নির্দেশনা ও বাইরের লিংক দেয়। নির্বাচিত বাইরের সেবা এবং আপনার ওয়ালেটের মাধ্যমে লেনদেন হয়।","pt":"Este site fornece orientação e links externos. A negociação ocorre através do serviço externo escolhido e da sua carteira.","id":"Situs ini menyediakan panduan dan tautan eksternal. Perdagangan berlangsung melalui layanan eksternal pilihan Anda dan dompet Anda.","ur":"یہ ویب سائٹ رہنمائی اور بیرونی روابط دیتی ہے۔ تجارت منتخب بیرونی سروس اور آپ کے والیٹ کے ذریعے ہوتی ہے۔","nl":"Deze website biedt uitleg en externe links. Handelen gebeurt via de gekozen externe dienst en je wallet."},"pages":[4205]},{"source":"Before you trade","translations":{"zh-Hans":"交易前须知","hi":"कारोबार से पहले","es":"Antes de operar","ar":"قبل التداول","fr":"Avant de négocier","bn":"লেনদেনের আগে","pt":"Antes de negociar","id":"Sebelum berdagang","ur":"تجارت سے پہلے","nl":"Voordat je handelt"},"pages":[4205]},{"source":"Trading can cause loss. Project-related wallets may provide liquidity and have a financial interest. Quotes, liquidity and execution are not guaranteed. This guide is not investment advice.","translations":{"zh-Hans":"交易可能造成损失。项目关联钱包可能提供流动性并具有经济利益。报价、流动性和成交均无保证。本指南不构成投资建议。","hi":"कारोबार में नुकसान हो सकता है। परियोजना से जुड़े वॉलेट लिक्विडिटी दे सकते हैं और उनका वित्तीय हित हो सकता है। कोट, लिक्विडिटी और निष्पादन की गारंटी नहीं है। यह निवेश सलाह नहीं है।","es":"Operar puede causar pérdidas. Las billeteras vinculadas al proyecto pueden aportar liquidez y tener un interés financiero. Las cotizaciones, la liquidez y la ejecución no están garantizadas. Esta guía no es asesoramiento de inversión.","ar":"قد يؤدي التداول إلى خسارة. قد توفّر محافظ مرتبطة بالمشروع السيولة وتكون لها مصلحة مالية. الأسعار والسيولة والتنفيذ غير مضمونة. هذا الدليل ليس نصيحة استثمارية.","fr":"Le trading peut entraîner des pertes. Des portefeuilles liés au projet peuvent fournir de la liquidité et avoir un intérêt financier. Les cotations, la liquidité et l’exécution ne sont pas garanties. Ce guide n’est pas un conseil en investissement.","bn":"লেনদেনে ক্ষতি হতে পারে। প্রকল্প-সংশ্লিষ্ট ওয়ালেট লিকুইডিটি দিতে পারে এবং তাদের আর্থিক স্বার্থ থাকতে পারে। দর, লিকুইডিটি ও লেনদেন সম্পাদনের নিশ্চয়তা নেই। এটি বিনিয়োগের পরামর্শ নয়।","pt":"A negociação pode causar perdas. Carteiras ligadas ao projeto podem fornecer liquidez e ter um interesse financeiro. Cotações, liquidez e execução não são garantidas. Este guia não é aconselhamento de investimento.","id":"Perdagangan dapat menyebabkan kerugian. Dompet terkait proyek mungkin menyediakan likuiditas dan memiliki kepentingan finansial. Harga, likuiditas, dan eksekusi tidak dijamin. Panduan ini bukan saran investasi.","ur":"تجارت میں نقصان ہو سکتا ہے۔ منصوبے سے متعلق والیٹ لیکویڈیٹی فراہم کر سکتے ہیں اور ان کا مالی مفاد ہو سکتا ہے۔ قیمت، لیکویڈیٹی اور عمل درآمد کی ضمانت نہیں۔ یہ سرمایہ کاری کا مشورہ نہیں ہے۔","nl":"Handelen kan verlies opleveren. Projectgerelateerde wallets kunnen liquiditeit leveren en een financieel belang hebben. Koersen, liquiditeit en uitvoering zijn niet gegarandeerd. Deze gids is geen beleggingsadvies."},"pages":[4205]},{"source":"CalorieApp food search and food logging do not require buying CAL.","translations":{"zh-Hans":"使用 CalorieApp 搜索食物和记录饮食无需购买 CAL。","hi":"CalorieApp में भोजन खोजने और दर्ज करने के लिए CAL खरीदना आवश्यक नहीं है।","es":"Buscar alimentos y registrarlos en CalorieApp no requiere comprar CAL.","ar":"لا يتطلب البحث عن الطعام وتسجيله في CalorieApp شراء CAL.","fr":"La recherche et le journal alimentaires de CalorieApp ne nécessitent pas l’achat de CAL.","bn":"CalorieApp-এ খাবার খুঁজতে বা লিখে রাখতে CAL কেনার প্রয়োজন নেই।","pt":"A pesquisa e o registo de alimentos na CalorieApp não exigem a compra de CAL.","id":"Pencarian dan pencatatan makanan di CalorieApp tidak memerlukan pembelian CAL.","ur":"CalorieApp میں خوراک تلاش کرنے اور درج کرنے کے لیے CAL خریدنا ضروری نہیں۔","nl":"Voor voedsel zoeken en je voedingslogboek in CalorieApp hoef je geen CAL te kopen."},"pages":[4205]},{"source":"Other trading interfaces","translations":{"zh-Hans":"其他交易界面","hi":"अन्य ट्रेडिंग इंटरफ़ेस","es":"Otras interfaces de negociación","ar":"واجهات تداول أخرى","fr":"Autres interfaces de négociation","bn":"অন্যান্য ট্রেডিং ইন্টারফেস","pt":"Outras interfaces de negociação","id":"Antarmuka perdagangan lainnya","ur":"دیگر تجارتی انٹرفیس","nl":"Andere handelsinterfaces"},"pages":[4205]},{"source":"Use the interface you already know and verify the same issuer and currency code.","translations":{"zh-Hans":"使用您熟悉的交易界面,并核对同一发行方地址和货币代码。","hi":"जिस इंटरफ़ेस से आप परिचित हैं उसका उपयोग करें और वही जारीकर्ता पता तथा मुद्रा कोड सत्यापित करें।","es":"Usa la interfaz que ya conoces y verifica la misma dirección del emisor y el código de moneda.","ar":"استخدم الواجهة التي تعرفها وتحقّق من عنوان المُصدر ورمز العملة نفسيهما.","fr":"Utilisez l’interface que vous connaissez et vérifiez la même adresse d’émetteur et le même code de devise.","bn":"আপনার পরিচিত ইন্টারফেস ব্যবহার করুন এবং একই ইস্যুকারীর ঠিকানা ও মুদ্রার কোড যাচাই করুন।","pt":"Use a interface que já conhece e verifique o mesmo endereço do emissor e o mesmo código da moeda.","id":"Gunakan antarmuka yang sudah Anda kenal dan verifikasi alamat penerbit serta kode mata uang yang sama.","ur":"وہ انٹرفیس استعمال کریں جس سے آپ واقف ہیں اور اسی جاری کنندہ کے پتے اور کرنسی کوڈ کی تصدیق کریں۔","nl":"Gebruik de interface die je al kent en controleer hetzelfde issueradres en dezelfde valutacode."},"pages":[4205]},{"source":"Posts are supplied by X and may not load here. Open the profile directly or review your cookie choices.","translations":{"zh-Hans":"帖子由 X 提供,可能无法在此加载。您可以直接打开主页,或查看您的 Cookie 选项。","hi":"पोस्ट X से मिलती हैं और हो सकता है कि यहाँ लोड न हों। प्रोफ़ाइल सीधे खोलें या अपनी कुकी संबंधी पसंद की समीक्षा करें।","es":"X proporciona las publicaciones y es posible que no se carguen aquí. Abre el perfil directamente o revisa tus preferencias de cookies.","ar":"يوفّر X المنشورات، وقد لا يتم تحميلها هنا. افتح الملف الشخصي مباشرة أو راجع خيارات ملفات تعريف الارتباط.","fr":"Les publications sont fournies par X et peuvent ne pas se charger ici. Ouvrez directement le profil ou consultez vos choix de cookies.","bn":"পোস্টগুলো X থেকে আসে এবং এখানে লোড নাও হতে পারে। সরাসরি প্রোফাইল খুলুন অথবা আপনার কুকি পছন্দগুলো দেখুন।","pt":"As publicações são fornecidas pelo X e podem não carregar aqui. Abra o perfil diretamente ou reveja as suas preferências de cookies.","id":"Postingan disediakan oleh X dan mungkin tidak dimuat di sini. Buka profil secara langsung atau tinjau pilihan cookie Anda.","ur":"پوسٹس X فراہم کرتا ہے اور ممکن ہے یہاں لوڈ نہ ہوں۔ براہ راست پروفائل کھولیں یا کوکیز کے بارے میں اپنے انتخاب کا جائزہ لیں۔","nl":"Berichten worden door X geleverd en laden hier mogelijk niet. Open het profiel rechtstreeks of bekijk je cookievoorkeuren."},"pages":[1207]},{"source":"Open @CalorieToken on X","translations":{"zh-Hans":"在 X 上打开 @CalorieToken","hi":"X पर @CalorieToken खोलें","es":"Abrir @CalorieToken en X","ar":"افتح ⁦@CalorieToken⁩ على X","fr":"Ouvrir @CalorieToken sur X","bn":"X-এ @CalorieToken খুলুন","pt":"Abrir @CalorieToken no X","id":"Buka @CalorieToken di X","ur":"X پر ⁦@CalorieToken⁩ کھولیں","nl":"Open @CalorieToken op X"},"pages":[1207]},{"source":"Cookie settings","translations":{"zh-Hans":"Cookie 设置","hi":"कुकी सेटिंग","es":"Ajustes de cookies","ar":"إعدادات ملفات الارتباط","fr":"Réglages des cookies","bn":"কুকি সেটিংস","pt":"Definições de cookies","id":"Pengaturan kuki","ur":"کوکی ترتیبات","nl":"Cookie-instellingen"},"pages":[1205,1207,4205,7880]},{"source":"CAL, crypto and your wallet: choose an option below.","translations":{"nl":"CAL, crypto en je wallet: kies hieronder een mogelijkheid.","zh-Hans":"CAL、加密资产与钱包:请选择以下方式。","hi":"CAL, क्रिप्टो और आपका वॉलेट: नीचे एक विकल्प चुनें।","es":"CAL, cripto y tu cartera: elige una opción.","ar":"CAL والعملات المشفرة ومحفظتك: اختر أحد الخيارات أدناه.","fr":"CAL, crypto et portefeuille : choisissez une option.","bn":"CAL, ক্রিপ্টো ও আপনার ওয়ালেট: নিচে একটি উপায় বেছে নিন।","pt":"CAL, cripto e a sua carteira: escolha uma opção.","id":"CAL, kripto, dan dompet Anda: pilih opsi di bawah.","ur":"CAL، کرپٹو اور آپ کا والیٹ: نیچے ایک طریقہ منتخب کریں۔"},"pages":[1205,4205,7880]},{"source":"1 · XRPL DEX","translations":{"nl":"1 · XRPL DEX","zh-Hans":"1 · XRPL DEX","hi":"1 · XRPL DEX","es":"1 · XRPL DEX","ar":"1 · XRPL DEX","fr":"1 · XRPL DEX","bn":"1 · XRPL DEX","pt":"1 · XRPL DEX","id":"1 · XRPL DEX","ur":"1 · XRPL DEX"},"pages":[1205,4205,7880]},{"source":"Own trading interface not enabled","translations":{"nl":"Eigen handelsinterface nog niet actief","zh-Hans":"自有交易界面尚未启用","hi":"अपना ट्रेडिंग इंटरफ़ेस अभी सक्रिय नहीं","es":"Interfaz de negociación propia aún inactiva","ar":"واجهة التداول الخاصة غير مفعلة بعد","fr":"Interface de trading propre pas encore activée","bn":"নিজস্ব ট্রেডিং ইন্টারফেস এখনও সক্রিয় নয়","pt":"Interface de negociação própria ainda inativa","id":"Antarmuka perdagangan sendiri belum aktif","ur":"اپنا ٹریڈنگ انٹرفیس ابھی فعال نہیں"},"pages":[1205,4205,7880]},{"source":"Open the CAL/XRP order book or swap on XPMarket. Check the asset, issuer, fees and final wallet request. Trading takes place with the external service; this website does not place orders.","translations":{"nl":"Open het CAL/XRP-orderboek of de swap op XPMarket. Controleer de munt, uitgever, kosten en het uiteindelijke verzoek in je wallet. Je handelt bij de externe dienst; deze website plaatst geen orders.","zh-Hans":"在 XPMarket 打开 CAL/XRP 订单簿或兑换。核对资产、发行方、费用和最终钱包请求。交易通过外部服务进行;本网站不会下单。","hi":"XPMarket पर CAL/XRP ऑर्डर बुक या स्वैप खोलें। एसेट, जारीकर्ता, शुल्क और वॉलेट में अंतिम अनुरोध जाँचें। ट्रेड बाहरी सेवा पर होता है; यह वेबसाइट ऑर्डर नहीं देती।","es":"Abre el libro de órdenes o el intercambio CAL/XRP en XPMarket. Revisa el activo, emisor, comisiones y la solicitud final en tu cartera. Operas con el servicio externo; esta web no envía órdenes.","ar":"افتح دفتر أوامر CAL/XRP أو المبادلة على XPMarket. تحقق من الأصل والجهة المصدرة والرسوم والطلب النهائي في محفظتك. يتم التداول عبر الخدمة الخارجية؛ لا يرسل هذا الموقع أوامر تداول.","fr":"Ouvrez le carnet d’ordres ou l’échange CAL/XRP sur XPMarket. Vérifiez l’actif, l’émetteur, les frais et la demande finale dans votre portefeuille. La transaction passe par le service externe ; ce site ne transmet pas d’ordres.","bn":"XPMarket-এ CAL/XRP অর্ডার বুক বা সোয়াপ খুলুন। সম্পদ, ইস্যুকারী, ফি ও ওয়ালেটের চূড়ান্ত অনুরোধ যাচাই করুন। লেনদেন বাইরের পরিষেবায় হয়; এই ওয়েবসাইট অর্ডার দেয় না।","pt":"Abra o livro de ordens ou a troca CAL/XRP no XPMarket. Verifique o ativo, emissor, taxas e o pedido final na carteira. A negociação ocorre no serviço externo; este site não envia ordens.","id":"Buka buku pesanan atau swap CAL/XRP di XPMarket. Periksa aset, penerbit, biaya, dan permintaan akhir di dompet. Perdagangan berlangsung di layanan eksternal; situs ini tidak mengirim pesanan.","ur":"XPMarket پر CAL/XRP آرڈر بک یا سواپ کھولیں۔ اثاثہ، جاری کنندہ، فیس اور والٹ میں آخری درخواست چیک کریں۔ تجارت بیرونی سروس پر ہوتی ہے؛ یہ ویب سائٹ آرڈر نہیں بھیجتی۔"},"pages":[1205,4205,7880]},{"source":"3 · AllChainBridge / SWFT","translations":{"nl":"3 · AllChainBridge / SWFT","zh-Hans":"3 · AllChainBridge / SWFT","hi":"3 · AllChainBridge / SWFT","es":"3 · AllChainBridge / SWFT","ar":"3 · AllChainBridge / SWFT","fr":"3 · AllChainBridge / SWFT","bn":"3 · AllChainBridge / SWFT","pt":"3 · AllChainBridge / SWFT","id":"3 · AllChainBridge / SWFT","ur":"3 · AllChainBridge / SWFT"},"pages":[1205,4205,7880]},{"source":"CAL is temporarily delisted at SWFT","translations":{"nl":"CAL is tijdelijk gedelist bij SWFT","zh-Hans":"CAL 暂时从 SWFT 下架","hi":"SWFT पर CAL अस्थायी रूप से डीलिस्ट है","es":"CAL está temporalmente retirado de SWFT","ar":"CAL مشطوب مؤقتًا من SWFT","fr":"CAL est temporairement retiré de SWFT","bn":"SWFT থেকে CAL সাময়িকভাবে তালিকাচ্যুত","pt":"CAL temporariamente removido da SWFT","id":"CAL sementara dihapus dari daftar SWFT","ur":"SWFT سے CAL عارضی طور پر ڈی لسٹ ہے"},"pages":[1205,4205,7880]},{"source":"SWFT still supports exchanging other supported coins. CAL is temporarily unavailable here. Availability, terms and fees depend on the provider and your region.","translations":{"nl":"Met SWFT kun je nog steeds andere ondersteunde munten wisselen. CAL is hier tijdelijk niet beschikbaar. Beschikbaarheid, voorwaarden en kosten hangen af van de aanbieder en je regio.","zh-Hans":"SWFT 仍支持兑换其他受支持的币种。CAL 暂不可用。可用性、条款及费用取决于提供方和您所在地区。","hi":"SWFT से अन्य समर्थित सिक्के अब भी बदले जा सकते हैं। CAL यहाँ अस्थायी रूप से अनुपलब्ध है। उपलब्धता, शर्तें और शुल्क प्रदाता और आपके क्षेत्र पर निर्भर हैं।","es":"SWFT sigue permitiendo cambiar otras monedas compatibles. CAL no está disponible aquí temporalmente. Disponibilidad, condiciones y comisiones dependen del proveedor y tu región.","ar":"ما زال SWFT يتيح تبديل العملات الأخرى المدعومة. CAL غير متاح هنا مؤقتًا. يعتمد التوفر والشروط والرسوم على المزود ومنطقتك.","fr":"SWFT permet toujours d’échanger d’autres monnaies prises en charge. CAL est temporairement indisponible ici. Disponibilité, conditions et frais dépendent du fournisseur et de votre région.","bn":"SWFT দিয়ে অন্য সমর্থিত কয়েন এখনও বদলানো যায়। CAL এখানে সাময়িকভাবে অনুপলব্ধ। প্রাপ্যতা, শর্ত ও ফি প্রদানকারী এবং আপনার অঞ্চলের ওপর নির্ভর করে।","pt":"A SWFT continua permitindo trocar outras moedas compatíveis. CAL está temporariamente indisponível aqui. Disponibilidade, condições e taxas dependem do provedor e da sua região.","id":"SWFT masih mendukung pertukaran koin lain yang didukung. CAL sementara tidak tersedia di sini. Ketersediaan, ketentuan dan biaya bergantung pada penyedia dan wilayah Anda.","ur":"SWFT میں دوسرے معاون سکے اب بھی بدلے جا سکتے ہیں۔ CAL یہاں عارضی طور پر دستیاب نہیں۔ دستیابی، شرائط اور فیس فراہم کنندہ اور آپ کے علاقے پر منحصر ہیں۔"},"pages":[1205,4205,7880]},{"source":"Loading the exchange connects your browser to SWFT, which receives connection information and may use cookies. Your CalorieApp login is not passed to the frame.","translations":{"nl":"Bij het laden maakt je browser verbinding met SWFT. SWFT ontvangt verbindingsgegevens en kan cookies gebruiken. Je CalorieApp-login wordt niet aan het frame doorgegeven.","zh-Hans":"加载后,浏览器会连接 SWFT。SWFT 会接收连接信息,并可能使用 Cookie。您的 CalorieApp 登录信息不会传递给此框架。","hi":"एक्सचेंज लोड करने पर ब्राउज़र SWFT से जुड़ता है। उसे कनेक्शन की जानकारी मिलती है और वह कुकीज़ इस्तेमाल कर सकता है। आपका CalorieApp लॉगिन फ़्रेम को नहीं दिया जाता।","es":"Al cargarlo, tu navegador se conecta a SWFT, que recibe datos de conexión y puede usar cookies. Tu sesión de CalorieApp no se transmite al marco.","ar":"يصل متصفحك إلى SWFT عند التحميل، ويتلقى المزود معلومات الاتصال وقد يستخدم ملفات تعريف الارتباط. لا تُرسل جلسة CalorieApp إلى الإطار.","fr":"Le chargement connecte votre navigateur à SWFT, qui reçoit des données de connexion et peut utiliser des cookies. Votre session CalorieApp n’est pas transmise au cadre.","bn":"লোড করলে ব্রাউজার SWFT-এর সঙ্গে যুক্ত হয়। SWFT সংযোগের তথ্য পায় এবং কুকি ব্যবহার করতে পারে। আপনার CalorieApp লগইন ফ্রেমে পাঠানো হয় না।","pt":"Ao carregar, o navegador liga-se à SWFT, que recebe dados de ligação e pode usar cookies. A sua sessão CalorieApp não é enviada ao quadro.","id":"Memuat bursa menghubungkan peramban ke SWFT, yang menerima informasi koneksi dan mungkin menggunakan kuki. Login CalorieApp Anda tidak diteruskan ke bingkai.","ur":"لوڈ کرنے سے براؤزر SWFT سے جڑتا ہے، جو کنکشن کی معلومات وصول کرتا ہے اور کوکیز استعمال کر سکتا ہے۔ آپ کا CalorieApp لاگ اِن فریم کو نہیں بھیجا جاتا۔"},"pages":[1205,4205,7880]},{"source":"Load external exchange","translations":{"nl":"Externe exchange laden","zh-Hans":"加载外部兑换界面","hi":"बाहरी एक्सचेंज लोड करें","es":"Cargar exchange externo","ar":"تحميل منصة التبادل الخارجية","fr":"Charger l’exchange externe","bn":"বাইরের এক্সচেঞ্জ লোড করুন","pt":"Carregar exchange externa","id":"Muat bursa eksternal","ur":"بیرونی ایکسچینج لوڈ کریں"},"pages":[1205,4205,7880]},{"source":"Close exchange","translations":{"nl":"Exchange sluiten","zh-Hans":"关闭兑换界面","hi":"एक्सचेंज बंद करें","es":"Cerrar exchange","ar":"إغلاق المنصة","fr":"Fermer l’exchange","bn":"এক্সচেঞ্জ বন্ধ করুন","pt":"Fechar exchange","id":"Tutup bursa","ur":"ایکسچینج بند کریں"},"pages":[1205,4205,7880]},{"source":"External SWFT content needs permission in the site’s cookie settings. You can also visit the provider directly.","translations":{"nl":"Voor externe SWFT-inhoud is toestemming nodig in de cookie-instellingen van de site. Je kunt ook rechtstreeks naar de aanbieder gaan.","zh-Hans":"外部 SWFT 内容需要在本网站的 Cookie 设置中获得许可。您也可以直接访问提供方。","hi":"बाहरी SWFT सामग्री के लिए साइट की कुकी सेटिंग में अनुमति चाहिए। आप प्रदाता की साइट सीधे भी खोल सकते हैं।","es":"El contenido externo de SWFT requiere permiso en los ajustes de cookies del sitio. También puedes visitar al proveedor directamente.","ar":"يحتاج محتوى SWFT الخارجي إلى إذن في إعدادات ملفات الارتباط بالموقع. يمكنك أيضًا زيارة المزود مباشرة.","fr":"Le contenu externe SWFT nécessite une autorisation dans les réglages des cookies du site. Vous pouvez aussi visiter directement le prestataire.","bn":"বাইরের SWFT কনটেন্টের জন্য সাইটের কুকি সেটিংসে অনুমতি দরকার। প্রদানকারীর সাইট সরাসরিও খুলতে পারেন।","pt":"O conteúdo externo da SWFT precisa de autorização nas definições de cookies do site. Também pode visitar diretamente o fornecedor.","id":"Konten SWFT eksternal memerlukan izin dalam pengaturan kuki situs. Anda juga dapat membuka situs penyedia langsung.","ur":"بیرونی SWFT مواد کے لیے سائٹ کی کوکی ترتیبات میں اجازت درکار ہے۔ آپ فراہم کنندہ کی سائٹ براہِ راست بھی کھول سکتے ہیں۔"},"pages":[1205,4205,7880]},{"source":"Open at SWFT","translations":{"nl":"Openen bij SWFT","zh-Hans":"在 SWFT 打开","hi":"SWFT पर खोलें","es":"Abrir en SWFT","ar":"فتح لدى SWFT","fr":"Ouvrir chez SWFT","bn":"SWFT-এ খুলুন","pt":"Abrir na SWFT","id":"Buka di SWFT","ur":"SWFT پر کھولیں"},"pages":[1205,4205,7880]},{"source":"If the frame is blank or your wallet does not connect, open SWFT directly. Available pairs and execution are controlled by the provider.","translations":{"nl":"Is het frame leeg of verbindt je wallet niet? Open SWFT rechtstreeks. De aanbieder bepaalt de beschikbare paren en uitvoering.","zh-Hans":"如果框架空白或钱包无法连接,请直接打开 SWFT。可用交易对和执行由提供方控制。","hi":"फ़्रेम खाली हो या वॉलेट न जुड़े तो SWFT सीधे खोलें। उपलब्ध जोड़े और निष्पादन प्रदाता के नियंत्रण में हैं।","es":"Si el marco está vacío o la cartera no conecta, abre SWFT directamente. El proveedor controla los pares disponibles y la ejecución.","ar":"إذا كان الإطار فارغًا أو لم تتصل المحفظة، افتح SWFT مباشرة. يتحكم المزود في الأزواج المتاحة والتنفيذ.","fr":"Si le cadre reste vide ou le portefeuille ne se connecte pas, ouvrez SWFT directement. Le prestataire contrôle les paires disponibles et l’exécution.","bn":"ফ্রেম ফাঁকা থাকলে বা ওয়ালেট যুক্ত না হলে SWFT সরাসরি খুলুন। উপলব্ধ জোড়া ও লেনদেন সম্পাদন প্রদানকারীর নিয়ন্ত্রণে।","pt":"Se o quadro ficar vazio ou a carteira não ligar, abra a SWFT diretamente. O fornecedor controla os pares disponíveis e a execução.","id":"Jika bingkai kosong atau dompet tidak terhubung, buka SWFT langsung. Pasangan yang tersedia dan pelaksanaan transaksi dikendalikan penyedia.","ur":"فریم خالی ہو یا والیٹ نہ جڑے تو SWFT براہِ راست کھولیں۔ دستیاب جوڑے اور لین دین کا اجرا فراہم کنندہ کے اختیار میں ہیں۔"},"pages":[1205,4205,7880]},{"source":"2 · Ways to buy and sell CAL","translations":{"nl":"2 · CAL kopen en verkopen","zh-Hans":"2 · 买入和卖出 CAL 的方式","hi":"2 · CAL खरीदने और बेचने के तरीके","es":"2 · Formas de comprar y vender CAL","ar":"2 · طرق شراء CAL وبيعه","fr":"2 · Acheter et vendre CAL","bn":"2 · CAL কেনাবেচার উপায়","pt":"2 · Formas de comprar e vender CAL","id":"2 · Cara membeli dan menjual CAL","ur":"2 · CAL خریدنے اور بیچنے کے طریقے"},"pages":[1205,4205,7880]},{"source":"Set a CAL trustline","translations":{"nl":"CAL-trustline instellen","zh-Hans":"设置 CAL 信任线","hi":"CAL ट्रस्टलाइन सेट करें","es":"Configurar una línea de confianza CAL","ar":"إعداد خط ثقة CAL","fr":"Configurer une ligne de confiance CAL","bn":"CAL ট্রাস্টলাইন তৈরি করুন","pt":"Configurar uma linha de confiança CAL","id":"Atur trustline CAL","ur":"CAL ٹرسٹ لائن بنائیں"},"pages":[1205,4205,6855,7880]},{"source":"Mainnet CAL · Check the issuer, currency, limit and network in Xaman before signing. A trustline does not buy CAL.","translations":{"nl":"Mainnet CAL · Controleer uitgever, valutacode, limiet en netwerk in Xaman voordat je tekent. Een trustline koopt geen CAL.","zh-Hans":"Mainnet CAL · 签名前请在 Xaman 中核对发行方、货币代码、限额和网络。设置信任线不会购买 CAL。","hi":"Mainnet CAL · हस्ताक्षर से पहले Xaman में जारीकर्ता, मुद्रा कोड, सीमा और नेटवर्क जाँचें। ट्रस्टलाइन से CAL नहीं खरीदा जाता।","es":"Mainnet CAL · Antes de firmar, comprueba emisor, código de moneda, límite y red en Xaman. Una línea de confianza no compra CAL.","ar":"Mainnet CAL · تحقق من المُصدر ورمز العملة والحد والشبكة في Xaman قبل التوقيع. إنشاء خط ثقة لا يشتري CAL.","fr":"Mainnet CAL · Vérifiez l’émetteur, le code de devise, la limite et le réseau dans Xaman avant de signer. Une ligne de confiance n’achète pas de CAL.","bn":"Mainnet CAL · সই করার আগে Xaman-এ ইস্যুকারী, মুদ্রা কোড, সীমা ও নেটওয়ার্ক যাচাই করুন। ট্রাস্টলাইন CAL কেনে না।","pt":"Mainnet CAL · Verifique emissor, código da moeda, limite e rede no Xaman antes de assinar. Uma linha de confiança não compra CAL.","id":"Mainnet CAL · Periksa penerbit, kode mata uang, batas, dan jaringan di Xaman sebelum menandatangani. Trustline tidak membeli CAL.","ur":"Mainnet CAL · دستخط سے پہلے Xaman میں جاری کنندہ، کرنسی کوڈ، حد اور نیٹ ورک دیکھیں۔ ٹرسٹ لائن CAL نہیں خریدتی۔"},"pages":[1205,4205,6855,7880]},{"source":"Set trustline in Xaman","translations":{"nl":"Trustline instellen in Xaman","zh-Hans":"在 Xaman 中设置信任线","hi":"Xaman में ट्रस्टलाइन सेट करें","es":"Configurar línea de confianza en Xaman","ar":"إعداد خط الثقة في Xaman","fr":"Configurer la ligne dans Xaman","bn":"Xaman-এ ট্রাস্টলাইন তৈরি করুন","pt":"Configurar a linha no Xaman","id":"Atur trustline di Xaman","ur":"Xaman میں ٹرسٹ لائن بنائیں"},"pages":[1205,4205,7880]},{"source":"Open trustline guide","translations":{"nl":"Trustline-uitleg openen","zh-Hans":"打开信任线指南","hi":"ट्रस्टलाइन गाइड खोलें","es":"Abrir guía de confianza","ar":"فتح دليل خط الثقة","fr":"Ouvrir le guide de confiance","bn":"ট্রাস্টলাইন নির্দেশিকা খুলুন","pt":"Abrir guia de confiança","id":"Buka panduan trustline","ur":"ٹرسٹ لائن رہنما کھولیں"},"pages":[1205,4205,7880]},{"source":"The direct button is available when the installed Xaman service is configured for this exact CAL asset.","translations":{"nl":"De rechtstreekse knop verschijnt wanneer de geïnstalleerde Xaman-dienst voor precies deze CAL-munt is ingesteld.","zh-Hans":"安装的 Xaman 服务配置为此确切 CAL 资产后,将显示直接操作按钮。","hi":"सीधा बटन तभी दिखता है जब स्थापित Xaman सेवा इसी सटीक CAL परिसंपत्ति के लिए कॉन्फ़िगर हो।","es":"El botón directo aparece cuando el servicio Xaman instalado está configurado para este activo CAL exacto.","ar":"يظهر الزر المباشر عندما تكون خدمة Xaman المثبتة مُعدّة لأصل CAL المحدد هذا.","fr":"Le bouton direct apparaît lorsque le service Xaman installé est configuré pour cet actif CAL exact.","bn":"ইনস্টল করা Xaman পরিষেবা ঠিক এই CAL সম্পদের জন্য কনফিগার করা থাকলে সরাসরি বোতাম দেখা যাবে।","pt":"O botão direto aparece quando o serviço Xaman instalado está configurado para este ativo CAL exato.","id":"Tombol langsung muncul jika layanan Xaman yang terpasang dikonfigurasi untuk aset CAL yang persis ini.","ur":"براہِ راست بٹن تب ظاہر ہوتا ہے جب نصب شدہ Xaman سروس عین اسی CAL اثاثے کے لیے ترتیب دی گئی ہو۔"},"pages":[1205,4205,7880]},{"source":"Find food, explore nutrition and keep your food diary.","translations":{"nl":"Zoek voeding, bekijk voedingswaarden en houd je eetdagboek bij.","zh-Hans":"查找食物、了解营养并记录饮食。","hi":"खाद्य पदार्थ खोजें, पोषण देखें और अपनी भोजन डायरी रखें।","es":"Busca alimentos, consulta nutrientes y lleva tu diario alimentario.","ar":"ابحث عن الطعام واستكشف التغذية واحتفظ بيومياتك الغذائية.","fr":"Recherchez des aliments, consultez leur nutrition et tenez votre journal alimentaire.","bn":"খাবার খুঁজুন, পুষ্টির তথ্য দেখুন ও খাবারের ডায়েরি রাখুন।","pt":"Procure alimentos, explore a nutrição e mantenha o seu diário alimentar.","id":"Cari makanan, jelajahi nutrisi, dan catat buku harian makanan Anda.","ur":"کھانے تلاش کریں، غذائیت دیکھیں اور اپنی خوراک کی ڈائری رکھیں۔"},"pages":[1205,4205,7880]},{"source":"Try with test funds","translations":{"nl":"Proberen met testtegoed","zh-Hans":"使用测试资金体验","hi":"टेस्ट निधि से आज़माएँ","es":"Probar con fondos de prueba","ar":"التجربة بأموال اختبار","fr":"Essayer avec des fonds de test","bn":"টেস্ট তহবিল দিয়ে চেষ্টা করুন","pt":"Experimentar com fundos de teste","id":"Coba dengan dana uji","ur":"ٹیسٹ رقم کے ساتھ آزمائیں"},"pages":[1205,4205,6855,7880]},{"source":"Create a free XRPL Testnet account below. It comes with test XRP, which has no real monetary value. Then add it to Xaman to try CalorieApp.","translations":{"nl":"Maak hieronder gratis een XRPL Testnet-account aan. Je ontvangt test-XRP zonder echte geldwaarde. Voeg het account daarna toe aan Xaman om CalorieApp te proberen.","zh-Hans":"在下方免费创建 XRPL Testnet 账户,获得无真实货币价值的测试 XRP。然后将账户添加到 Xaman,体验 CalorieApp。","hi":"नीचे मुफ्त XRPL Testnet खाता बनाएँ। उसमें मिलने वाले टेस्ट XRP का वास्तविक मौद्रिक मूल्य नहीं है। फिर उसे Xaman में जोड़कर CalorieApp आज़माएँ।","es":"Crea gratis una cuenta XRPL Testnet a continuación. Recibirás XRP de prueba sin valor monetario real. Añade la cuenta a Xaman para probar CalorieApp.","ar":"أنشئ حساب XRPL Testnet مجانيًا أدناه. ستحصل على XRP تجريبي بلا قيمة مالية حقيقية. أضف الحساب إلى Xaman لتجربة CalorieApp.","fr":"Créez gratuitement un compte XRPL Testnet ci-dessous. Il reçoit des XRP de test sans valeur monétaire réelle. Ajoutez-le ensuite à Xaman pour essayer CalorieApp.","bn":"নিচে বিনামূল্যে XRPL Testnet অ্যাকাউন্ট তৈরি করুন। এতে বাস্তব আর্থিক মূল্যহীন টেস্ট XRP পাওয়া যাবে। তারপর CalorieApp চেষ্টা করতে অ্যাকাউন্টটি Xaman-এ যোগ করুন।","pt":"Crie uma conta XRPL Testnet gratuita abaixo. Ela recebe XRP de teste sem valor monetário real. Depois adicione-a à Xaman para experimentar o CalorieApp.","id":"Buat akun XRPL Testnet gratis di bawah. Akun menerima XRP uji tanpa nilai uang nyata. Tambahkan ke Xaman untuk mencoba CalorieApp.","ur":"نیچے مفت XRPL Testnet اکاؤنٹ بنائیں۔ اس میں حقیقی مالی قدر کے بغیر ٹیسٹ XRP ملتے ہیں۔ پھر CalorieApp آزمانے کے لیے اسے Xaman میں شامل کریں۔"},"pages":[1205,4205,6855,7880]},{"source":"Use a separate test account. Test networks can be reset. A fresh test-wallet login still needs to be checked on the live CalorieApp; do not fund Mainnet if it fails.","translations":{"nl":"Gebruik een apart testaccount. Testnetwerken kunnen worden gereset. Aanmelden met een nieuwe testwallet moet nog op de live CalorieApp worden gecontroleerd; stort geen geld op Mainnet als dat mislukt.","zh-Hans":"请使用独立测试账户。测试网络可能重置。新测试钱包登录仍需在 CalorieApp 实测;若失败,请勿向 Mainnet 充值。","hi":"अलग टेस्ट खाता इस्तेमाल करें। टेस्ट नेटवर्क रीसेट हो सकते हैं। नए टेस्ट वॉलेट से CalorieApp लॉगिन की लाइव जाँच बाकी है; विफल होने पर Mainnet में पैसे न डालें।","es":"Usa una cuenta de prueba separada. Las redes de prueba pueden reiniciarse. Falta comprobar el acceso con una cartera nueva en CalorieApp; no ingreses fondos en Mainnet si falla.","ar":"استخدم حساب اختبار منفصلًا. قد تُعاد تهيئة شبكات الاختبار. لا يزال تسجيل الدخول بمحفظة اختبار جديدة يحتاج إلى تجربة فعلية في CalorieApp؛ لا تموّل Mainnet إذا فشل.","fr":"Utilisez un compte de test séparé. Les réseaux de test peuvent être réinitialisés. La connexion avec un nouveau portefeuille de test reste à vérifier dans CalorieApp ; ne financez pas Mainnet si elle échoue.","bn":"আলাদা টেস্ট অ্যাকাউন্ট ব্যবহার করুন। টেস্ট নেটওয়ার্ক রিসেট হতে পারে। নতুন টেস্ট ওয়ালেট দিয়ে CalorieApp-এ লগইন এখনও সরাসরি যাচাই করতে হবে; ব্যর্থ হলে Mainnet-এ অর্থ জমা দেবেন না।","pt":"Use uma conta de teste separada. As redes de teste podem ser reiniciadas. O acesso com uma nova carteira de teste ainda precisa ser verificado no CalorieApp; não deposite na Mainnet se falhar.","id":"Gunakan akun uji terpisah. Jaringan uji dapat direset. Login dengan dompet uji baru masih perlu diperiksa di CalorieApp langsung; jangan mendanai Mainnet jika gagal.","ur":"الگ ٹیسٹ اکاؤنٹ استعمال کریں۔ ٹیسٹ نیٹ ورک ری سیٹ ہو سکتے ہیں۔ نئے ٹیسٹ والیٹ سے CalorieApp لاگ اِن کی عملی جانچ باقی ہے؛ ناکامی پر Mainnet میں رقم نہ ڈالیں۔"},"pages":[1205,4205,7880]},{"source":"Show the simple steps","translations":{"nl":"Toon de eenvoudige stappen","zh-Hans":"显示简明步骤","hi":"सरल चरण दिखाएँ","es":"Mostrar pasos sencillos","ar":"عرض الخطوات البسيطة","fr":"Afficher les étapes simples","bn":"সহজ ধাপগুলো দেখুন","pt":"Mostrar passos simples","id":"Tampilkan langkah sederhana","ur":"آسان مراحل دکھائیں"},"pages":[1205,4205,7880]},{"source":"Open Xaman. Create a separate account there and keep its recovery information safe. Never enter recovery words or keys on this website.","translations":{"nl":"Open Xaman. Maak daar een apart account en bewaar de herstelgegevens veilig. Vul herstelwoorden of sleutels nooit op deze website in.","zh-Hans":"打开 Xaman,在其中创建独立账户并妥善保管恢复信息。切勿在本网站输入恢复词或密钥。","hi":"Xaman खोलें। उसमें अलग खाता बनाएँ और रिकवरी जानकारी सुरक्षित रखें। इस वेबसाइट पर रिकवरी शब्द या कुंजियाँ कभी न भरें।","es":"Abre Xaman. Crea allí una cuenta separada y guarda sus datos de recuperación. Nunca introduzcas palabras de recuperación ni claves en este sitio.","ar":"افتح Xaman وأنشئ حسابًا منفصلًا فيه واحفظ معلومات الاسترداد بأمان. لا تدخل كلمات الاسترداد أو المفاتيح في هذا الموقع أبدًا.","fr":"Ouvrez Xaman. Créez-y un compte séparé et conservez ses informations de récupération en sécurité. Ne saisissez jamais vos mots de récupération ou clés sur ce site.","bn":"Xaman খুলুন। সেখানে আলাদা অ্যাকাউন্ট তৈরি করে পুনরুদ্ধারের তথ্য নিরাপদে রাখুন। এই সাইটে কখনো পুনরুদ্ধারের শব্দ বা কী লিখবেন না।","pt":"Abra o Xaman. Crie lá uma conta separada e guarde os dados de recuperação em segurança. Nunca introduza palavras de recuperação ou chaves neste site.","id":"Buka Xaman. Buat akun terpisah di sana dan simpan informasi pemulihannya dengan aman. Jangan pernah memasukkan kata pemulihan atau kunci di situs ini.","ur":"Xaman کھولیں۔ وہیں الگ اکاؤنٹ بنائیں اور بازیابی کی معلومات محفوظ رکھیں۔ اس ویب سائٹ پر بازیابی کے الفاظ یا چابیاں کبھی درج نہ کریں۔"},"pages":[1205,4205,7880]},{"source":"In Xaman, open Settings → Advanced and enable Developer mode. Return Home, open the network switch and select XRPL Testnet.","translations":{"nl":"Open in Xaman Settings → Advanced en zet Developer mode aan. Ga terug naar Home, open de netwerkwissel en kies XRPL Testnet.","zh-Hans":"在 Xaman 中打开 Settings → Advanced,启用 Developer mode。返回 Home,打开网络切换并选择 XRPL Testnet。","hi":"Xaman में Settings → Advanced खोलकर Developer mode चालू करें। Home पर लौटें, नेटवर्क स्विच खोलें और XRPL Testnet चुनें।","es":"En Xaman, abre Settings → Advanced y activa Developer mode. Vuelve a Home, abre el selector de red y elige XRPL Testnet.","ar":"في Xaman، افتح Settings → Advanced وفعّل Developer mode. عُد إلى Home، وافتح تبديل الشبكة واختر XRPL Testnet.","fr":"Dans Xaman, ouvrez Settings → Advanced et activez Developer mode. Revenez à Home, ouvrez le sélecteur de réseau et choisissez XRPL Testnet.","bn":"Xaman-এ Settings → Advanced খুলে Developer mode চালু করুন। Home-এ ফিরুন, নেটওয়ার্ক পরিবর্তন খুলে XRPL Testnet বেছে নিন।","pt":"No Xaman, abra Settings → Advanced e ative Developer mode. Volte a Home, abra o seletor de rede e escolha XRPL Testnet.","id":"Di Xaman, buka Settings → Advanced dan aktifkan Developer mode. Kembali ke Home, buka pengalih jaringan dan pilih XRPL Testnet.","ur":"Xaman میں Settings → Advanced کھول کر Developer mode فعال کریں۔ Home پر واپس آئیں، نیٹ ورک سوئچ کھولیں اور XRPL Testnet منتخب کریں۔"},"pages":[1205,4205,7880]},{"source":"Choose Activate your account, then Activate account. This uses test XRP. Do not buy or send real XRP for this test. Close the xApp when ready.","translations":{"nl":"Kies Activate your account en daarna Activate account. Dit gebruikt test-XRP. Koop of verstuur hiervoor geen echte XRP. Sluit daarna de xApp.","zh-Hans":"选择 Activate your account,然后选择 Activate account。此操作使用测试 XRP。请勿购买或发送真实 XRP。完成后关闭 xApp。","hi":"Activate your account, फिर Activate account चुनें। इसमें टेस्ट XRP लगता है। इस टेस्ट के लिए वास्तविक XRP न खरीदें, न भेजें। तैयार होने पर xApp बंद करें।","es":"Elige Activate your account y luego Activate account. Se usa XRP de prueba. No compres ni envíes XRP real para esta prueba. Cierra la xApp al terminar.","ar":"اختر Activate your account ثم Activate account. تُستخدم أموال XRP للاختبار. لا تشترِ أو ترسل XRP حقيقيًا لهذه التجربة. أغلق xApp عند الانتهاء.","fr":"Choisissez Activate your account, puis Activate account. Cela utilise du XRP de test. N’achetez ni n’envoyez de XRP réel pour cet essai. Fermez la xApp ensuite.","bn":"Activate your account, তারপর Activate account বেছে নিন। এতে টেস্ট XRP ব্যবহার হয়। এই পরীক্ষার জন্য আসল XRP কিনবেন বা পাঠাবেন না। শেষে xApp বন্ধ করুন।","pt":"Escolha Activate your account e depois Activate account. Usa XRP de teste. Não compre nem envie XRP real para este teste. Feche a xApp quando terminar.","id":"Pilih Activate your account, lalu Activate account. Ini menggunakan XRP uji. Jangan membeli atau mengirim XRP nyata untuk pengujian ini. Tutup xApp setelah selesai.","ur":"Activate your account، پھر Activate account منتخب کریں۔ یہ ٹیسٹ XRP استعمال کرتا ہے۔ اس ٹیسٹ کے لیے حقیقی XRP نہ خریدیں، نہ بھیجیں۔ مکمل ہونے پر xApp بند کریں۔"},"pages":[1205,4205,7880]},{"source":"Return to the CalorieApp page. Test funds cannot buy real CAL or complete donations. If test sign-in fails, stop here; funding Mainnet is not the solution.","translations":{"nl":"Ga terug naar de CalorieApp-pagina. Met testtegoed koop je geen echte CAL en doe je geen donaties. Mislukt aanmelden met je testwallet? Stop dan; Mainnet financieren is niet de oplossing.","zh-Hans":"返回 CalorieApp 页面。测试资金不能购买真实 CAL 或完成捐款。若测试登录失败,请停止;充值 Mainnet 不能解决此问题。","hi":"CalorieApp पेज पर लौटें। टेस्ट निधि से वास्तविक CAL नहीं खरीदा जा सकता, न दान किया जा सकता है। टेस्ट लॉगिन विफल हो तो रुकें; Mainnet में पैसे डालना समाधान नहीं है।","es":"Vuelve a CalorieApp. Los fondos de prueba no compran CAL real ni completan donaciones. Si falla el acceso de prueba, detente; financiar Mainnet no es la solución.","ar":"عُد إلى صفحة CalorieApp. لا تشتري أموال الاختبار CAL حقيقيًا ولا تُكمل التبرعات. إذا فشل دخول الاختبار، توقف؛ تمويل Mainnet ليس الحل.","fr":"Revenez à CalorieApp. Les fonds de test ne permettent ni achat de CAL réel ni don. Si la connexion de test échoue, arrêtez-vous ; financer Mainnet ne résout pas le problème.","bn":"CalorieApp পেজে ফিরুন। টেস্ট তহবিলে আসল CAL কেনা বা অনুদান দেওয়া যায় না। টেস্ট লগইন ব্যর্থ হলে থামুন; Mainnet-এ অর্থ দেওয়া সমাধান নয়।","pt":"Volte à página CalorieApp. Fundos de teste não compram CAL real nem concluem donativos. Se o acesso de teste falhar, pare; financiar Mainnet não é a solução.","id":"Kembali ke halaman CalorieApp. Dana uji tidak dapat membeli CAL nyata atau menyelesaikan donasi. Jika login uji gagal, berhenti; mendanai Mainnet bukan solusinya.","ur":"CalorieApp صفحے پر واپس آئیں۔ ٹیسٹ رقم سے حقیقی CAL نہیں خریدا جا سکتا، نہ عطیہ دیا جا سکتا ہے۔ ٹیسٹ لاگ اِن ناکام ہو تو رکیں؛ Mainnet میں رقم ڈالنا حل نہیں۔"},"pages":[1205,4205,7880]},{"source":"Open Xaman’s illustrated guide","translations":{"nl":"Xamans uitleg met plaatjes openen","zh-Hans":"打开 Xaman 图文指南","hi":"Xaman की चित्र वाली गाइड खोलें","es":"Abrir guía ilustrada de Xaman","ar":"فتح دليل Xaman المصوّر","fr":"Ouvrir le guide illustré de Xaman","bn":"Xaman-এর ছবিসহ নির্দেশিকা খুলুন","pt":"Abrir o guia ilustrado do Xaman","id":"Buka panduan bergambar Xaman","ur":"Xaman کا تصویری رہنما کھولیں"},"pages":[1205,4205,7880]},{"source":"Get Xaman","translations":{"nl":"Xaman downloaden","zh-Hans":"获取 Xaman","hi":"Xaman प्राप्त करें","es":"Obtener Xaman","ar":"الحصول على Xaman","fr":"Obtenir Xaman","bn":"Xaman নিন","pt":"Obter Xaman","id":"Dapatkan Xaman","ur":"Xaman حاصل کریں"},"pages":[1205,4205,7880]},{"source":"Open CAL/XRP order book","translations":{"nl":"Open CAL/XRP-orderboek","zh-Hans":"打开 CAL/XRP 订单簿","hi":"CAL/XRP ऑर्डर बुक खोलें","es":"Abrir libro de órdenes CAL/XRP","ar":"فتح دفتر أوامر CAL/XRP","fr":"Ouvrir le carnet CAL/XRP","bn":"CAL/XRP অর্ডার বুক খুলুন","pt":"Abrir livro de ordens CAL/XRP","id":"Buka buku pesanan CAL/XRP","ur":"CAL/XRP آرڈر بک کھولیں"},"pages":[1205,4205,7880]},{"source":"Website language","translations":{"nl":"Taal van de website","zh-Hans":"网站语言","hi":"वेबसाइट की भाषा","es":"Idioma del sitio","ar":"لغة الموقع","fr":"Langue du site","bn":"ওয়েবসাইটের ভাষা","pt":"Idioma do site","id":"Bahasa situs","ur":"ویب سائٹ کی زبان"},"pages":[1205,4205,7880]},{"source":"Swap CAL/XRP","translations":{"nl":"Wissel CAL/XRP","zh-Hans":"兑换 CAL/XRP","hi":"CAL/XRP स्वैप करें","es":"Intercambiar CAL/XRP","ar":"مبادلة CAL/XRP","fr":"Échanger CAL/XRP","bn":"CAL/XRP বিনিময় করুন","pt":"Trocar CAL/XRP","id":"Tukar CAL/XRP","ur":"CAL/XRP تبدیل کریں"},"pages":[1205,4205,7880]},{"source":"Directly via this website → Xaman","translations":{"nl":"Rechtstreeks via deze website → Xaman","zh-Hans":"通过本网站直接进入 Xaman","hi":"इस वेबसाइट से सीधे → Xaman","es":"Directamente desde esta web → Xaman","ar":"مباشرة من هذا الموقع ← Xaman","fr":"Directement depuis ce site → Xaman","bn":"এই ওয়েবসাইট থেকে সরাসরি → Xaman","pt":"Diretamente deste site → Xaman","id":"Langsung dari situs ini → Xaman","ur":"اس ویب سائٹ سے براہ راست ← Xaman"},"pages":[1205,4205,7880]},{"source":"Alternative via xrpl.services → Xaman","translations":{"nl":"Alternatief via xrpl.services → Xaman","zh-Hans":"另一方式:通过 xrpl.services 进入 Xaman","hi":"वैकल्पिक मार्ग: xrpl.services → Xaman","es":"Alternativa mediante xrpl.services → Xaman","ar":"بديل عبر xrpl.services ← Xaman","fr":"Autre voie via xrpl.services → Xaman","bn":"বিকল্প: xrpl.services হয়ে → Xaman","pt":"Alternativa via xrpl.services → Xaman","id":"Alternatif melalui xrpl.services → Xaman","ur":"متبادل: xrpl.services کے ذریعے ← Xaman"},"pages":[1205,4205,7880]},{"source":"Manual setup with XRP Toolkit","translations":{"nl":"Handmatig instellen met XRP Toolkit","zh-Hans":"使用 XRP Toolkit 手动设置","hi":"XRP Toolkit से मैन्युअल सेटअप","es":"Configuración manual con XRP Toolkit","ar":"إعداد يدوي باستخدام XRP Toolkit","fr":"Configuration manuelle avec XRP Toolkit","bn":"XRP Toolkit দিয়ে ম্যানুয়াল সেটআপ","pt":"Configuração manual com XRP Toolkit","id":"Pengaturan manual dengan XRP Toolkit","ur":"XRP Toolkit سے دستی ترتیب"},"pages":[1205,4205,7880]},{"source":"Calorie help","translations":{"nl":"Calorie-hulp","zh-Hans":"Calorie 帮助","hi":"Calorie सहायता","es":"Ayuda de Calorie","ar":"مساعدة Calorie","fr":"Aide Calorie","bn":"Calorie সহায়তা","pt":"Ajuda Calorie","id":"Bantuan Calorie","ur":"Calorie مدد"},"pages":[6855]},{"source":"Choose a topic or ask a short question.","translations":{"nl":"Kies een onderwerp of stel een korte vraag.","zh-Hans":"选择主题或提出简短问题。","hi":"विषय चुनें या छोटा सवाल पूछें।","es":"Elige un tema o haz una pregunta breve.","ar":"اختر موضوعًا أو اطرح سؤالًا قصيرًا.","fr":"Choisissez un sujet ou posez une courte question.","bn":"একটি বিষয় বেছে নিন বা ছোট প্রশ্ন করুন।","pt":"Escolha um tema ou faça uma pergunta curta.","id":"Pilih topik atau ajukan pertanyaan singkat.","ur":"موضوع چنیں یا مختصر سوال پوچھیں۔"},"pages":[6855]},{"source":"Your question","translations":{"nl":"Je vraag","zh-Hans":"你的问题","hi":"आपका सवाल","es":"Tu pregunta","ar":"سؤالك","fr":"Votre question","bn":"আপনার প্রশ্ন","pt":"A sua pergunta","id":"Pertanyaan Anda","ur":"آپ کا سوال"},"pages":[6855]},{"source":"For example: test account or trustline","translations":{"nl":"Bijvoorbeeld: testaccount of trustline","zh-Hans":"例如:测试账户或信任线","hi":"जैसे: टेस्ट खाता या ट्रस्टलाइन","es":"Por ejemplo: cuenta de prueba o trustline","ar":"مثلًا: حساب اختبار أو خط ثقة","fr":"Par exemple : compte de test ou trustline","bn":"যেমন: পরীক্ষার অ্যাকাউন্ট বা ট্রাস্টলাইন","pt":"Por exemplo: conta de teste ou trustline","id":"Misalnya: akun uji atau trustline","ur":"مثلاً: ٹیسٹ اکاؤنٹ یا ٹرسٹ لائن"},"pages":[6855]},{"source":"Find an answer","translations":{"nl":"Zoek een antwoord","zh-Hans":"查找答案","hi":"उत्तर खोजें","es":"Buscar una respuesta","ar":"ابحث عن إجابة","fr":"Trouver une réponse","bn":"উত্তর খুঁজুন","pt":"Encontrar resposta","id":"Cari jawaban","ur":"جواب تلاش کریں"},"pages":[6855]},{"source":"Choose a topic below or read the FAQ and project documents.","translations":{"nl":"Kies hieronder een onderwerp of lees de FAQ en projectdocumenten.","zh-Hans":"请在下方选择主题,或阅读常见问题和项目文档。","hi":"नीचे विषय चुनें या FAQ और परियोजना दस्तावेज़ पढ़ें।","es":"Elige un tema o lee las preguntas frecuentes y los documentos.","ar":"اختر موضوعًا أدناه أو اقرأ الأسئلة الشائعة ووثائق المشروع.","fr":"Choisissez un sujet ou consultez la FAQ et les documents du projet.","bn":"নিচে বিষয় বেছে নিন বা FAQ ও প্রকল্পের নথি পড়ুন।","pt":"Escolha um tema ou consulte as perguntas frequentes e os documentos.","id":"Pilih topik di bawah atau baca FAQ dan dokumen proyek.","ur":"نیچے موضوع چنیں یا FAQ اور منصوبے کی دستاویزات پڑھیں۔"},"pages":[6855]},{"source":"This help does not save questions or create tickets.","translations":{"nl":"Deze hulp slaat vragen niet op en maakt geen tickets aan.","zh-Hans":"此帮助不保存问题,也不创建工单。","hi":"यह सहायता सवाल सहेजती या टिकट नहीं बनाती।","es":"Esta ayuda no guarda preguntas ni crea tickets.","ar":"هذه المساعدة لا تحفظ الأسئلة ولا تنشئ تذاكر دعم.","fr":"Cette aide ne conserve pas les questions et ne crée pas de tickets.","bn":"এই সহায়তা প্রশ্ন জমা রাখে না বা টিকিট তৈরি করে না।","pt":"Esta ajuda não guarda perguntas nem cria pedidos de suporte.","id":"Bantuan ini tidak menyimpan pertanyaan atau membuat tiket.","ur":"یہ مدد سوال محفوظ یا ٹکٹ نہیں بناتی۔"},"pages":[6855]},{"source":"Privacy, terms & licences","translations":{"nl":"Privacy, voorwaarden & licenties","zh-Hans":"隐私、条款和许可","hi":"गोपनीयता, शर्तें और लाइसेंस","es":"Privacidad, condiciones y licencias","ar":"الخصوصية والشروط والتراخيص","fr":"Confidentialité, conditions et licences","bn":"গোপনীয়তা, শর্ত ও লাইসেন্স","pt":"Privacidade, termos e licenças","id":"Privasi, ketentuan & lisensi","ur":"رازداری، شرائط اور لائسنس"},"pages":[6855]},{"source":"Read the privacy policy, terms and source/licence notes for details. This helper does not decide individual legal or financial questions.","translations":{"nl":"Lees het privacybeleid, de voorwaarden en bron-/licentie-informatie voor details. Deze hulp beoordeelt geen persoonlijke juridische of financiële vragen.","zh-Hans":"详情请阅读隐私政策、条款及来源和许可说明。此帮助不判断个人法律或财务问题。","hi":"विवरण के लिए गोपनीयता नीति, शर्तें और स्रोत/लाइसेंस नोट पढ़ें। यह सहायता व्यक्तिगत कानूनी या वित्तीय सवालों का निर्णय नहीं करती।","es":"Lee la política de privacidad, las condiciones y las notas de fuentes y licencias. Esta ayuda no decide cuestiones jurídicas o financieras personales.","ar":"اقرأ سياسة الخصوصية والشروط وملاحظات المصادر والتراخيص للتفاصيل. هذه المساعدة لا تفصل في أسئلة قانونية أو مالية شخصية.","fr":"Lisez la politique de confidentialité, les conditions et les notes de sources et licences. Cette aide ne tranche pas les questions juridiques ou financières personnelles.","bn":"বিস্তারিত জানতে গোপনীয়তা নীতি, শর্ত ও উৎস/লাইসেন্সের নোট পড়ুন। এই সহায়তা ব্যক্তিগত আইনি বা আর্থিক প্রশ্নের সিদ্ধান্ত দেয় না।","pt":"Leia a política de privacidade, os termos e as notas de fontes e licenças. Esta ajuda não decide questões jurídicas ou financeiras pessoais.","id":"Baca kebijakan privasi, ketentuan, serta catatan sumber dan lisensi. Bantuan ini tidak memutuskan persoalan hukum atau keuangan pribadi.","ur":"تفصیل کے لیے رازداری کی پالیسی، شرائط اور ماخذ/لائسنس کی معلومات پڑھیں۔ یہ مدد ذاتی قانونی یا مالی سوالوں کا فیصلہ نہیں کرتی۔"},"pages":[6855]},{"source":"Project documents","translations":{"nl":"Projectdocumenten","zh-Hans":"项目文档","hi":"परियोजना दस्तावेज़","es":"Documentos del proyecto","ar":"وثائق المشروع","fr":"Documents du projet","bn":"প্রকল্পের নথি","pt":"Documentos do projeto","id":"Dokumen proyek","ur":"منصوبے کی دستاویزات"},"pages":[6855]},{"source":"The publications repository contains the whitepaper and detailed roadmap. Consult the relevant document for current details and disclosures.","translations":{"nl":"De publicatierepository bevat de whitepaper en de uitgebreide roadmap. Raadpleeg het betreffende document voor actuele details en toelichtingen.","zh-Hans":"发布库包含白皮书和详细路线图。最新详情及披露请参阅相应文档。","hi":"प्रकाशन रिपॉज़िटरी में व्हाइटपेपर और विस्तृत रोडमैप है। वर्तमान विवरण और खुलासे संबंधित दस्तावेज़ में देखें।","es":"El repositorio contiene el whitepaper y la hoja de ruta detallada. Consulta el documento correspondiente para detalles y declaraciones actuales.","ar":"يضم مستودع المنشورات الورقة البيضاء وخارطة الطريق المفصلة. راجع الوثيقة المعنية للتفاصيل والإفصاحات الحالية.","fr":"Le dépôt contient le livre blanc et la feuille de route détaillée. Consultez le document concerné pour les détails et déclarations actuels.","bn":"প্রকাশনা রিপোজিটরিতে হোয়াইটপেপার ও বিস্তারিত রোডম্যাপ আছে। বর্তমান তথ্য ও প্রকাশের জন্য সংশ্লিষ্ট নথি পড়ুন।","pt":"O repositório contém o whitepaper e o roteiro detalhado. Consulte o documento relevante para detalhes e divulgações atuais.","id":"Repositori publikasi berisi whitepaper dan peta jalan terperinci. Lihat dokumen terkait untuk rincian dan pengungkapan terkini.","ur":"اشاعتی ریپوزٹری میں وائٹ پیپر اور تفصیلی روڈمیپ موجود ہے۔ تازہ تفصیلات اور انکشافات متعلقہ دستاویز میں دیکھیں۔"},"pages":[6855]},{"source":"Contact options","translations":{"nl":"Contactmogelijkheden","zh-Hans":"联系方式","hi":"संपर्क विकल्प","es":"Opciones de contacto","ar":"خيارات الاتصال","fr":"Options de contact","bn":"যোগাযোগের উপায়","pt":"Opções de contacto","id":"Pilihan kontak","ur":"رابطے کے طریقے"},"pages":[6855]},{"source":"You can find the project’s contact options on the Contact page. This help does not send a message or open a ticket.","translations":{"nl":"De contactmogelijkheden staan op de Contact-pagina. Deze hulp stuurt geen bericht en opent geen ticket.","zh-Hans":"项目联系方式位于 Contact 页面。此帮助不会发送消息或创建工单。","hi":"परियोजना के संपर्क विकल्प Contact पृष्ठ पर हैं। यह सहायता संदेश या टिकट नहीं भेजती।","es":"Las opciones de contacto están en la página Contact. Esta ayuda no envía mensajes ni abre tickets.","ar":"تتوفر خيارات اتصال المشروع في صفحة Contact. هذه المساعدة لا ترسل رسائل ولا تفتح تذاكر.","fr":"Les coordonnées du projet figurent sur la page Contact. Cette aide n’envoie aucun message et n’ouvre aucun ticket.","bn":"প্রকল্পের যোগাযোগের উপায় Contact পৃষ্ঠায় আছে। এই সহায়তা বার্তা পাঠায় না বা টিকিট খোলে না।","pt":"As opções de contacto estão na página Contact. Esta ajuda não envia mensagens nem abre pedidos de suporte.","id":"Pilihan kontak proyek ada di halaman Contact. Bantuan ini tidak mengirim pesan atau membuka tiket.","ur":"منصوبے کے رابطے Contact صفحے پر ہیں۔ یہ مدد پیغام نہیں بھیجتی اور ٹکٹ نہیں کھولتی۔"},"pages":[6855]},{"source":"FAQ & quick answers","translations":{"nl":"FAQ & snelle antwoorden","zh-Hans":"常见问题和快速答案","hi":"FAQ और छोटे उत्तर","es":"Preguntas frecuentes y respuestas rápidas","ar":"أسئلة شائعة وإجابات سريعة","fr":"FAQ et réponses rapides","bn":"FAQ ও দ্রুত উত্তর","pt":"Perguntas frequentes e respostas rápidas","id":"FAQ & jawaban singkat","ur":"FAQ اور مختصر جوابات"},"pages":[6855]},{"source":"CalorieApp","translations":{"nl":"CalorieApp","zh-Hans":"CalorieApp","hi":"CalorieApp","es":"CalorieApp","ar":"CalorieApp","fr":"CalorieApp","bn":"CalorieApp","pt":"CalorieApp","id":"CalorieApp","ur":"CalorieApp"},"pages":[6855]},{"source":"Find food, explore nutrition and keep your food diary. CalorieApp food search and food logging do not require buying CAL.","translations":{"nl":"Zoek voeding, bekijk voedingswaarden en houd je eetdagboek bij. Voor voedsel zoeken en je voedingslogboek in CalorieApp hoef je geen CAL te kopen.","zh-Hans":"查找食物、了解营养并记录饮食。 使用 CalorieApp 搜索食物和记录饮食无需购买 CAL。","hi":"खाद्य पदार्थ खोजें, पोषण देखें और अपनी भोजन डायरी रखें। CalorieApp में भोजन खोजने और दर्ज करने के लिए CAL खरीदना आवश्यक नहीं है।","es":"Busca alimentos, consulta nutrientes y lleva tu diario alimentario. Buscar alimentos y registrarlos en CalorieApp no requiere comprar CAL.","ar":"ابحث عن الطعام واستكشف التغذية واحتفظ بيومياتك الغذائية. لا يتطلب البحث عن الطعام وتسجيله في CalorieApp شراء CAL.","fr":"Recherchez des aliments, consultez leur nutrition et tenez votre journal alimentaire. La recherche et le journal alimentaires de CalorieApp ne nécessitent pas l’achat de CAL.","bn":"খাবার খুঁজুন, পুষ্টির তথ্য দেখুন ও খাবারের ডায়েরি রাখুন। CalorieApp-এ খাবার খুঁজতে বা লিখে রাখতে CAL কেনার প্রয়োজন নেই।","pt":"Procure alimentos, explore a nutrição e mantenha o seu diário alimentar. A pesquisa e o registo de alimentos na CalorieApp não exigem a compra de CAL.","id":"Cari makanan, jelajahi nutrisi, dan catat buku harian makanan Anda. Pencarian dan pencatatan makanan di CalorieApp tidak memerlukan pembelian CAL.","ur":"کھانے تلاش کریں، غذائیت دیکھیں اور اپنی خوراک کی ڈائری رکھیں۔ CalorieApp میں خوراک تلاش کرنے اور درج کرنے کے لیے CAL خریدنا ضروری نہیں۔"},"pages":[6855]},{"source":"Open the CAL/XRP order book or swap on XPMarket. Check the asset, issuer, fees and final wallet request. Trading takes place with the external service; this website does not place orders. CAL is temporarily unavailable at SWFT.","translations":{"nl":"Open het CAL/XRP-orderboek of de swap op XPMarket. Controleer de munt, uitgever, kosten en het uiteindelijke verzoek in je wallet. Je handelt bij de externe dienst; deze website plaatst geen orders. CAL is tijdelijk niet beschikbaar bij SWFT.","zh-Hans":"在 XPMarket 打开 CAL/XRP 订单簿或兑换。核对资产、发行方、费用和最终钱包请求。交易通过外部服务进行;本网站不会下单。 SWFT 暂不支持 CAL。","hi":"XPMarket पर CAL/XRP ऑर्डर बुक या स्वैप खोलें। एसेट, जारीकर्ता, शुल्क और वॉलेट में अंतिम अनुरोध जाँचें। ट्रेड बाहरी सेवा पर होता है; यह वेबसाइट ऑर्डर नहीं देती। SWFT पर CAL अस्थायी रूप से उपलब्ध नहीं है।","es":"Abre el libro de órdenes o el intercambio CAL/XRP en XPMarket. Revisa el activo, emisor, comisiones y la solicitud final en tu cartera. Operas con el servicio externo; esta web no envía órdenes. CAL no está disponible temporalmente en SWFT.","ar":"افتح دفتر أوامر CAL/XRP أو المبادلة على XPMarket. تحقق من الأصل والجهة المصدرة والرسوم والطلب النهائي في محفظتك. يتم التداول عبر الخدمة الخارجية؛ لا يرسل هذا الموقع أوامر تداول. CAL غير متاح مؤقتًا لدى SWFT.","fr":"Ouvrez le carnet d’ordres ou l’échange CAL/XRP sur XPMarket. Vérifiez l’actif, l’émetteur, les frais et la demande finale dans votre portefeuille. La transaction passe par le service externe ; ce site ne transmet pas d’ordres. CAL est temporairement indisponible sur SWFT.","bn":"XPMarket-এ CAL/XRP অর্ডার বুক বা সোয়াপ খুলুন। সম্পদ, ইস্যুকারী, ফি ও ওয়ালেটের চূড়ান্ত অনুরোধ যাচাই করুন। লেনদেন বাইরের পরিষেবায় হয়; এই ওয়েবসাইট অর্ডার দেয় না। SWFT-এ CAL সাময়িকভাবে অনুপলব্ধ।","pt":"Abra o livro de ordens ou a troca CAL/XRP no XPMarket. Verifique o ativo, emissor, taxas e o pedido final na carteira. A negociação ocorre no serviço externo; este site não envia ordens. CAL está temporariamente indisponível na SWFT.","id":"Buka buku pesanan atau swap CAL/XRP di XPMarket. Periksa aset, penerbit, biaya, dan permintaan akhir di dompet. Perdagangan berlangsung di layanan eksternal; situs ini tidak mengirim pesanan. CAL sementara tidak tersedia di SWFT.","ur":"XPMarket پر CAL/XRP آرڈر بک یا سواپ کھولیں۔ اثاثہ، جاری کنندہ، فیس اور والٹ میں آخری درخواست چیک کریں۔ تجارت بیرونی سروس پر ہوتی ہے؛ یہ ویب سائٹ آرڈر نہیں بھیجتی۔ SWFT پر CAL عارضی طور پر دستیاب نہیں ہے۔"},"pages":[6855]},{"source":"Create my free test account","translations":{"nl":"Maak mijn gratis testaccount","zh-Hans":"创建免费测试账户","hi":"मेरा मुफ्त टेस्ट खाता बनाएँ","es":"Crear mi cuenta de prueba gratis","ar":"إنشاء حساب الاختبار المجاني","fr":"Créer mon compte de test gratuit","bn":"আমার বিনামূল্যের টেস্ট অ্যাকাউন্ট তৈরি করুন","pt":"Criar minha conta de teste grátis","id":"Buat akun uji gratis saya","ur":"میرا مفت ٹیسٹ اکاؤنٹ بنائیں"},"pages":[7880]},{"source":"This button contacts the official XRPL Testnet faucet. It creates your test account and returns its recovery code to this browser. This page does not save the code; copy it before leaving.","translations":{"nl":"Deze knop maakt verbinding met de officiële XRPL Testnet-faucet. Die maakt je testaccount en stuurt de herstelcode terug naar deze browser. Deze pagina slaat de code niet op; kopieer hem voordat je weggaat.","zh-Hans":"此按钮连接官方 XRPL Testnet 水龙头,由其创建测试账户并将恢复代码返回此浏览器。本页面不保存代码;离开前请复制。","hi":"यह बटन आधिकारिक XRPL Testnet फ़ॉसेट से जुड़ता है। वह खाता बनाकर रिकवरी कोड इस ब्राउज़र को लौटाता है। यह पेज कोड सहेजता नहीं; जाने से पहले कॉपी करें।","es":"Este botón contacta con el faucet oficial de XRPL Testnet. Crea tu cuenta y devuelve el código de recuperación a este navegador. Esta página no guarda el código; cópialo antes de salir.","ar":"يتصل هذا الزر بصنبور XRPL Testnet الرسمي، الذي ينشئ حسابك ويعيد رمز الاسترداد إلى هذا المتصفح. لا تحفظ الصفحة الرمز؛ انسخه قبل المغادرة.","fr":"Ce bouton contacte le faucet officiel XRPL Testnet. Il crée votre compte et renvoie le code de récupération à ce navigateur. Cette page ne le conserve pas ; copiez-le avant de partir.","bn":"এই বোতাম অফিসিয়াল XRPL Testnet ফসেটে সংযোগ করে। সেটি অ্যাকাউন্ট তৈরি করে এই ব্রাউজারে রিকভারি কোড ফেরত দেয়। এই পেজ কোড সংরক্ষণ করে না; যাওয়ার আগে কপি করুন।","pt":"Este botão conecta ao faucet oficial da XRPL Testnet. Ele cria sua conta e devolve o código de recuperação a este navegador. Esta página não guarda o código; copie antes de sair.","id":"Tombol ini menghubungi faucet resmi XRPL Testnet. Faucet membuat akun dan mengembalikan kode pemulihan ke browser ini. Halaman tidak menyimpan kode; salin sebelum keluar.","ur":"یہ بٹن سرکاری XRPL Testnet فاسٹ سے جڑتا ہے۔ وہ اکاؤنٹ بناتا ہے اور بازیابی کوڈ اس براؤزر کو واپس بھیجتا ہے۔ یہ صفحہ کوڈ محفوظ نہیں کرتا؛ جانے سے پہلے کاپی کریں۔"},"pages":[7880]},{"source":"Ready. No real XRP or payment is needed.","translations":{"nl":"Klaar om te beginnen. Je hebt geen echte XRP nodig en hoeft niet te betalen.","zh-Hans":"可以开始。无需真实 XRP 或付款。","hi":"तैयार। वास्तविक XRP या भुगतान की ज़रूरत नहीं।","es":"Todo listo. No necesitas XRP real ni pagar.","ar":"جاهز. لا حاجة إلى XRP حقيقي أو دفع.","fr":"Prêt. Aucun XRP réel ni paiement nécessaire.","bn":"প্রস্তুত। বাস্তব XRP বা পেমেন্ট লাগবে না।","pt":"Pronto. Não é preciso XRP real nem pagamento.","id":"Siap. Tidak perlu XRP nyata atau pembayaran.","ur":"تیار۔ حقیقی XRP یا ادائیگی کی ضرورت نہیں۔"},"pages":[7880]},{"source":"Creating your test account… please wait.","translations":{"nl":"Je testaccount wordt aangemaakt… even geduld.","zh-Hans":"正在创建测试账户,请稍候…","hi":"टेस्ट खाता बन रहा है… कृपया प्रतीक्षा करें।","es":"Creando tu cuenta de prueba… espera un momento.","ar":"جارٍ إنشاء حساب الاختبار… يرجى الانتظار.","fr":"Création de votre compte de test… veuillez patienter.","bn":"টেস্ট অ্যাকাউন্ট তৈরি হচ্ছে… একটু অপেক্ষা করুন।","pt":"Criando sua conta de teste… aguarde.","id":"Membuat akun uji… harap tunggu.","ur":"ٹیسٹ اکاؤنٹ بن رہا ہے… انتظار کریں۔"},"pages":[7880]},{"source":"Test account created.","translations":{"nl":"Testaccount aangemaakt.","zh-Hans":"测试账户已创建。","hi":"टेस्ट खाता बन गया।","es":"Cuenta de prueba creada.","ar":"تم إنشاء حساب الاختبار.","fr":"Compte de test créé.","bn":"টেস্ট অ্যাকাউন্ট তৈরি হয়েছে।","pt":"Conta de teste criada.","id":"Akun uji dibuat.","ur":"ٹیسٹ اکاؤنٹ بن گیا۔"},"pages":[7880]},{"source":"Checking test XRP…","translations":{"nl":"Test-XRP controleren…","zh-Hans":"正在检查测试 XRP…","hi":"टेस्ट XRP की जाँच हो रही है…","es":"Comprobando XRP de prueba…","ar":"جارٍ التحقق من XRP التجريبي…","fr":"Vérification des XRP de test…","bn":"টেস্ট XRP যাচাই হচ্ছে…","pt":"Verificando XRP de teste…","id":"Memeriksa XRP uji…","ur":"ٹیسٹ XRP کی جانچ ہو رہی ہے…"},"pages":[7880]},{"source":"Your account has test XRP. Now add it to Xaman below.","translations":{"nl":"Je account heeft test-XRP. Voeg het nu hieronder toe aan Xaman.","zh-Hans":"账户已有测试 XRP。请按下方步骤添加到 Xaman。","hi":"खाते में टेस्ट XRP हैं। अब नीचे दिए चरणों से Xaman में जोड़ें।","es":"Tu cuenta tiene XRP de prueba. Añádela a Xaman siguiendo los pasos de abajo.","ar":"يحتوي حسابك على XRP تجريبي. أضفه الآن إلى Xaman بالخطوات أدناه.","fr":"Votre compte dispose de XRP de test. Ajoutez-le maintenant à Xaman ci-dessous.","bn":"আপনার অ্যাকাউন্টে টেস্ট XRP আছে। নিচের ধাপ অনুসরণ করে Xaman-এ যোগ করুন।","pt":"Sua conta tem XRP de teste. Agora adicione-a à Xaman abaixo.","id":"Akun Anda memiliki XRP uji. Tambahkan ke Xaman dengan langkah di bawah.","ur":"آپ کے اکاؤنٹ میں ٹیسٹ XRP ہیں۔ نیچے کے مراحل سے اسے Xaman میں شامل کریں۔"},"pages":[7880]},{"source":"Account created; funding could not yet be confirmed. Keep this recovery code and use Check test XRP again.","translations":{"nl":"Account aangemaakt; het testtegoed is nog niet bevestigd. Bewaar deze herstelcode en gebruik Test-XRP controleren opnieuw.","zh-Hans":"账户已创建,但尚未确认测试资金。请保留恢复代码并再次检查测试 XRP。","hi":"खाता बन गया; निधि की पुष्टि अभी नहीं हुई। रिकवरी कोड रखें और टेस्ट XRP फिर जाँचें।","es":"Cuenta creada; aún no se pudo confirmar el saldo. Conserva el código y vuelve a comprobar los XRP de prueba.","ar":"تم إنشاء الحساب؛ لم يتأكد الرصيد بعد. احتفظ برمز الاسترداد وأعد التحقق من XRP التجريبي.","fr":"Compte créé ; les fonds ne sont pas encore confirmés. Gardez ce code et vérifiez à nouveau les XRP de test.","bn":"অ্যাকাউন্ট তৈরি হয়েছে; তহবিল এখনও নিশ্চিত হয়নি। রিকভারি কোড রাখুন এবং আবার টেস্ট XRP যাচাই করুন।","pt":"Conta criada; os fundos ainda não foram confirmados. Guarde o código e verifique o XRP de teste novamente.","id":"Akun dibuat; dana belum terkonfirmasi. Simpan kode pemulihan dan periksa XRP uji lagi.","ur":"اکاؤنٹ بن گیا؛ رقم کی تصدیق ابھی نہیں ہوئی۔ بازیابی کوڈ رکھیں اور ٹیسٹ XRP دوبارہ چیک کریں۔"},"pages":[7880]},{"source":"The faucet could not complete this request. Wait a moment and try again, or open the official faucet below.","translations":{"nl":"De faucet kon dit verzoek niet afronden. Wacht even en probeer opnieuw, of open hieronder de officiële faucet.","zh-Hans":"水龙头未能完成请求。请稍后重试,或打开下方官方水龙头。","hi":"फ़ॉसेट अनुरोध पूरा नहीं कर पाया। थोड़ी देर बाद फिर कोशिश करें या नीचे आधिकारिक फ़ॉसेट खोलें।","es":"El faucet no pudo completar la solicitud. Espera y reinténtalo o abre el faucet oficial de abajo.","ar":"تعذر إكمال الطلب. انتظر قليلًا وحاول مجددًا أو افتح الصنبور الرسمي أدناه.","fr":"Le faucet n’a pas pu terminer la demande. Réessayez dans un moment ou ouvrez le faucet officiel ci-dessous.","bn":"ফসেট অনুরোধ সম্পূর্ণ করতে পারেনি। কিছুক্ষণ পর চেষ্টা করুন বা নিচে অফিসিয়াল ফসেট খুলুন।","pt":"O faucet não concluiu a solicitação. Espere um momento e tente novamente ou abra o faucet oficial abaixo.","id":"Faucet tidak dapat menyelesaikan permintaan. Tunggu lalu coba lagi, atau buka faucet resmi di bawah.","ur":"فاسٹ درخواست مکمل نہیں کر سکا۔ کچھ دیر بعد کوشش کریں یا نیچے سرکاری فاسٹ کھولیں۔"},"pages":[7880]},{"source":"The faucet is busy or has reached its limit. Please try later; no payment is required.","translations":{"nl":"De faucet is druk of heeft zijn limiet bereikt. Probeer het later; je hoeft niet te betalen.","zh-Hans":"水龙头繁忙或已达限额。请稍后再试,无需付款。","hi":"फ़ॉसेट व्यस्त है या उसकी सीमा पूरी हो गई है। बाद में फिर कोशिश करें; भुगतान नहीं चाहिए।","es":"El faucet está ocupado o alcanzó su límite. Inténtalo más tarde; no necesitas pagar.","ar":"الصنبور مشغول أو بلغ الحد. حاول لاحقًا؛ لا حاجة إلى الدفع.","fr":"Le faucet est occupé ou a atteint sa limite. Réessayez plus tard ; aucun paiement nécessaire.","bn":"ফসেট ব্যস্ত বা সীমা পূর্ণ হয়েছে। পরে চেষ্টা করুন; পেমেন্ট লাগবে না।","pt":"O faucet está ocupado ou atingiu o limite. Tente mais tarde; não é preciso pagar.","id":"Faucet sibuk atau mencapai batas. Coba nanti; tidak perlu membayar.","ur":"فاسٹ مصروف ہے یا حد پوری ہو گئی ہے۔ بعد میں کوشش کریں؛ ادائیگی کی ضرورت نہیں۔"},"pages":[7880]},{"source":"This browser cannot create and check the account here. Use the official faucet below.","translations":{"nl":"Deze browser kan het account hier niet aanmaken en controleren. Gebruik hieronder de officiële faucet.","zh-Hans":"此浏览器无法在这里创建并检查账户。请使用下方官方水龙头。","hi":"यह ब्राउज़र यहाँ खाता बनाकर जाँच नहीं सकता। नीचे आधिकारिक फ़ॉसेट उपयोग करें।","es":"Este navegador no puede crear y comprobar la cuenta aquí. Usa el faucet oficial de abajo.","ar":"لا يستطيع هذا المتصفح إنشاء الحساب والتحقق منه هنا. استخدم الصنبور الرسمي أدناه.","fr":"Ce navigateur ne peut pas créer et vérifier le compte ici. Utilisez le faucet officiel ci-dessous.","bn":"এই ব্রাউজার এখানে অ্যাকাউন্ট তৈরি ও যাচাই করতে পারে না। নিচের অফিসিয়াল ফসেট ব্যবহার করুন।","pt":"Este navegador não consegue criar e verificar a conta aqui. Use o faucet oficial abaixo.","id":"Browser ini tidak dapat membuat dan memeriksa akun di sini. Gunakan faucet resmi di bawah.","ur":"یہ براؤزر یہاں اکاؤنٹ بنا کر چیک نہیں کر سکتا۔ نیچے سرکاری فاسٹ استعمال کریں۔"},"pages":[7880]},{"source":"Your test account","translations":{"nl":"Jouw testaccount","zh-Hans":"您的测试账户","hi":"आपका टेस्ट खाता","es":"Tu cuenta de prueba","ar":"حساب الاختبار الخاص بك","fr":"Votre compte de test","bn":"আপনার টেস্ট অ্যাকাউন্ট","pt":"Sua conta de teste","id":"Akun uji Anda","ur":"آپ کا ٹیسٹ اکاؤنٹ"},"pages":[7880]},{"source":"Recovery code — Testnet only","translations":{"nl":"Herstelcode — alleen Testnet","zh-Hans":"恢复代码 — 仅限 Testnet","hi":"रिकवरी कोड — केवल Testnet","es":"Código de recuperación — solo Testnet","ar":"رمز الاسترداد — Testnet فقط","fr":"Code de récupération — Testnet uniquement","bn":"রিকভারি কোড — শুধু Testnet","pt":"Código de recuperação — somente Testnet","id":"Kode pemulihan — hanya Testnet","ur":"بازیابی کوڈ — صرف Testnet"},"pages":[7880]},{"source":"Show / hide recovery code","translations":{"nl":"Herstelcode tonen / verbergen","zh-Hans":"显示 / 隐藏恢复代码","hi":"रिकवरी कोड दिखाएँ / छिपाएँ","es":"Mostrar / ocultar código","ar":"إظهار / إخفاء رمز الاسترداد","fr":"Afficher / masquer le code","bn":"রিকভারি কোড দেখান / লুকান","pt":"Mostrar / ocultar código","id":"Tampilkan / sembunyikan kode","ur":"بازیابی کوڈ دکھائیں / چھپائیں"},"pages":[7880]},{"source":"Copy recovery code","translations":{"nl":"Herstelcode kopiëren","zh-Hans":"复制恢复代码","hi":"रिकवरी कोड कॉपी करें","es":"Copiar código de recuperación","ar":"نسخ رمز الاسترداد","fr":"Copier le code de récupération","bn":"রিকভারি কোড কপি করুন","pt":"Copiar código de recuperação","id":"Salin kode pemulihan","ur":"بازیابی کوڈ کاپی کریں"},"pages":[7880]},{"source":"Copy account address","translations":{"nl":"Accountadres kopiëren","zh-Hans":"复制账户地址","hi":"खाते का पता कॉपी करें","es":"Copiar dirección de cuenta","ar":"نسخ عنوان الحساب","fr":"Copier l’adresse du compte","bn":"অ্যাকাউন্টের ঠিকানা কপি করুন","pt":"Copiar endereço da conta","id":"Salin alamat akun","ur":"اکاؤنٹ کا پتہ کاپی کریں"},"pages":[7880]},{"source":"Copied. Paste into Xaman.","translations":{"nl":"Gekopieerd. Plak dit in Xaman.","zh-Hans":"已复制。请粘贴到 Xaman。","hi":"कॉपी हो गया। Xaman में पेस्ट करें।","es":"Copiado. Pégalo en Xaman.","ar":"تم النسخ. الصقه في Xaman.","fr":"Copié. Collez dans Xaman.","bn":"কপি হয়েছে। Xaman-এ পেস্ট করুন।","pt":"Copiado. Cole na Xaman.","id":"Disalin. Tempel ke Xaman.","ur":"کاپی ہو گیا۔ Xaman میں پیسٹ کریں۔"},"pages":[7880]},{"source":"Copy is unavailable. Show the code, then press and hold the text to copy it manually.","translations":{"nl":"Kopiëren is niet beschikbaar. Toon de code en houd de tekst ingedrukt om hem handmatig te kopiëren.","zh-Hans":"无法自动复制。显示代码后,长按文字手动复制。","hi":"कॉपी उपलब्ध नहीं। कोड दिखाकर टेक्स्ट दबाए रखें और मैन्युअल कॉपी करें।","es":"No se puede copiar automáticamente. Muestra el código y mantén pulsado el texto para copiarlo.","ar":"النسخ غير متاح. أظهر الرمز ثم اضغط مطولًا على النص لنسخه يدويًا.","fr":"Copie indisponible. Affichez le code puis maintenez le texte appuyé pour le copier manuellement.","bn":"কপি উপলব্ধ নয়। কোড দেখিয়ে লেখাটি চেপে ধরে হাতে কপি করুন।","pt":"Cópia indisponível. Mostre o código e pressione o texto para copiar manualmente.","id":"Penyalinan tidak tersedia. Tampilkan kode, lalu tekan dan tahan teks untuk menyalin manual.","ur":"کاپی دستیاب نہیں۔ کوڈ دکھائیں اور متن دبا کر دستی طور پر کاپی کریں۔"},"pages":[7880]},{"source":"Check test XRP","translations":{"nl":"Test-XRP controleren","zh-Hans":"检查测试 XRP","hi":"टेस्ट XRP जाँचें","es":"Comprobar XRP de prueba","ar":"التحقق من XRP التجريبي","fr":"Vérifier les XRP de test","bn":"টেস্ট XRP যাচাই করুন","pt":"Verificar XRP de teste","id":"Periksa XRP uji","ur":"ٹیسٹ XRP چیک کریں"},"pages":[7880]},{"source":"Use this code only for this test account. Never reuse it for real funds. After leaving this page, we cannot recover it for you.","translations":{"nl":"Gebruik deze code alleen voor dit testaccount, nooit voor echt geld. Als je deze pagina verlaat, kunnen wij de code niet voor je terughalen.","zh-Hans":"此代码仅用于此测试账户,切勿用于真实资金。离开页面后,我们无法为您找回。","hi":"इस कोड का उपयोग केवल इस टेस्ट खाते के लिए करें, वास्तविक धन के लिए कभी नहीं। पेज छोड़ने के बाद हम इसे वापस नहीं ला सकते।","es":"Usa este código solo para esta cuenta de prueba, nunca para fondos reales. Al salir de esta página no podremos recuperarlo por ti.","ar":"استخدم هذا الرمز لحساب الاختبار فقط، ولا تستخدمه لأموال حقيقية. لا يمكننا استعادته لك بعد مغادرة الصفحة.","fr":"Utilisez ce code uniquement pour ce compte de test, jamais pour des fonds réels. Après votre départ de cette page, nous ne pourrons pas le récupérer pour vous.","bn":"এই কোড শুধু এই টেস্ট অ্যাকাউন্টের জন্য, বাস্তব অর্থের জন্য কখনও নয়। পেজ ছাড়ার পর আমরা এটি ফিরিয়ে দিতে পারব না।","pt":"Use este código apenas nesta conta de teste, nunca para fundos reais. Depois de sair desta página, não podemos recuperá-lo para você.","id":"Gunakan kode hanya untuk akun uji ini, jangan untuk dana nyata. Setelah Anda meninggalkan halaman, kami tidak dapat memulihkannya.","ur":"کوڈ صرف اس ٹیسٹ اکاؤنٹ کے لیے استعمال کریں، حقیقی رقم کے لیے کبھی نہیں۔ صفحہ چھوڑنے کے بعد ہم اسے واپس نہیں لا سکتے۔"},"pages":[7880]},{"source":"In Xaman: Settings → Advanced → Developer mode. Return Home and select XRPL Testnet using the network switch.","translations":{"nl":"In Xaman: Settings → Advanced → Developer mode. Ga terug naar Home en kies XRPL Testnet met de netwerkwissel.","zh-Hans":"在 Xaman 中:Settings → Advanced → Developer mode。返回 Home,通过网络切换选择 XRPL Testnet。","hi":"Xaman में: Settings → Advanced → Developer mode। Home पर लौटकर नेटवर्क स्विच से XRPL Testnet चुनें।","es":"En Xaman: Settings → Advanced → Developer mode. Vuelve a Home y selecciona XRPL Testnet con el selector de red.","ar":"في Xaman: Settings → Advanced → Developer mode. ارجع إلى Home واختر XRPL Testnet من مفتاح الشبكة.","fr":"Dans Xaman : Settings → Advanced → Developer mode. Revenez à Home et sélectionnez XRPL Testnet avec le sélecteur de réseau.","bn":"Xaman-এ: Settings → Advanced → Developer mode। Home-এ ফিরে নেটওয়ার্ক সুইচ দিয়ে XRPL Testnet বেছে নিন।","pt":"Na Xaman: Settings → Advanced → Developer mode. Volte a Home e escolha XRPL Testnet no seletor de rede.","id":"Di Xaman: Settings → Advanced → Developer mode. Kembali ke Home dan pilih XRPL Testnet dengan pengalih jaringan.","ur":"Xaman میں: Settings → Advanced → Developer mode۔ Home پر واپس آ کر نیٹ ورک سوئچ سے XRPL Testnet منتخب کریں۔"},"pages":[7880]},{"source":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Paste the recovery code copied above.","translations":{"nl":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Plak de hierboven gekopieerde herstelcode.","zh-Hans":"Settings → Accounts → + → Import existing account → Full access → Family Seed。粘贴上方复制的恢复代码。","hi":"Settings → Accounts → + → Import existing account → Full access → Family Seed। ऊपर कॉपी किया रिकवरी कोड पेस्ट करें।","es":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Pega el código copiado arriba.","ar":"Settings → Accounts → + → Import existing account → Full access → Family Seed. الصق رمز الاسترداد المنسوخ أعلاه.","fr":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Collez le code copié ci-dessus.","bn":"Settings → Accounts → + → Import existing account → Full access → Family Seed। উপরে কপি করা রিকভারি কোড পেস্ট করুন।","pt":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Cole o código copiado acima.","id":"Settings → Accounts → + → Import existing account → Full access → Family Seed. Tempel kode yang disalin di atas.","ur":"Settings → Accounts → + → Import existing account → Full access → Family Seed۔ اوپر کاپی کیا ہوا بازیابی کوڈ پیسٹ کریں۔"},"pages":[7880]},{"source":"Check that Xaman shows the same account address. Name it “CalorieApp Test”. Then return here and sign in with that test account.","translations":{"nl":"Controleer dat Xaman hetzelfde accountadres toont. Noem het “CalorieApp Test”. Ga daarna terug naar deze pagina en meld je aan met dat testaccount.","zh-Hans":"确认 Xaman 显示的账户地址一致。命名为“CalorieApp Test”,然后返回本页并使用此测试账户登录。","hi":"देखें कि Xaman में वही खाता पता है। नाम “CalorieApp Test” रखें। फिर यहाँ लौटकर उसी टेस्ट खाते से लॉगिन करें।","es":"Comprueba que Xaman muestra la misma dirección. Ponle “CalorieApp Test”. Vuelve aquí e inicia sesión con esa cuenta de prueba.","ar":"تحقق من تطابق عنوان الحساب في Xaman. سمّه “CalorieApp Test”. ثم ارجع هنا وسجل الدخول بحساب الاختبار.","fr":"Vérifiez que Xaman affiche la même adresse. Nommez le compte « CalorieApp Test ». Revenez ici et connectez-vous avec ce compte de test.","bn":"Xaman-এ একই অ্যাকাউন্টের ঠিকানা আছে কি না দেখুন। নাম দিন “CalorieApp Test”। এখানে ফিরে ওই টেস্ট অ্যাকাউন্ট দিয়ে লগইন করুন।","pt":"Confira se a Xaman mostra o mesmo endereço. Dê o nome “CalorieApp Test”. Volte aqui e entre com essa conta de teste.","id":"Pastikan Xaman menampilkan alamat akun yang sama. Beri nama “CalorieApp Test”. Kembali ke sini dan login dengan akun uji tersebut.","ur":"دیکھیں کہ Xaman میں وہی اکاؤنٹ پتہ ہے۔ نام “CalorieApp Test” رکھیں۔ پھر یہاں واپس آ کر اس ٹیسٹ اکاؤنٹ سے لاگ اِن کریں۔"},"pages":[7880]},{"source":"Import guide with pictures","translations":{"nl":"Importuitleg met plaatjes","zh-Hans":"查看图文导入指南","hi":"चित्रों वाली इम्पोर्ट गाइड","es":"Guía de importación con imágenes","ar":"دليل الاستيراد بالصور","fr":"Guide d’importation illustré","bn":"ছবিসহ ইম্পোর্ট নির্দেশিকা","pt":"Guia de importação com imagens","id":"Panduan impor bergambar","ur":"تصویروں کے ساتھ امپورٹ رہنما"},"pages":[7880]},{"source":"Back to CalorieApp sign-in","translations":{"nl":"Terug naar aanmelden bij CalorieApp","zh-Hans":"返回 CalorieApp 登录","hi":"CalorieApp लॉगिन पर लौटें","es":"Volver al acceso de CalorieApp","ar":"العودة إلى تسجيل دخول CalorieApp","fr":"Retour à la connexion CalorieApp","bn":"CalorieApp লগইনে ফিরুন","pt":"Voltar ao acesso do CalorieApp","id":"Kembali ke login CalorieApp","ur":"CalorieApp لاگ اِن پر واپس جائیں"},"pages":[7880]},{"source":"Open official Testnet faucet","translations":{"nl":"Officiële Testnet-faucet openen","zh-Hans":"打开官方 Testnet 水龙头","hi":"आधिकारिक Testnet फ़ॉसेट खोलें","es":"Abrir faucet oficial de Testnet","ar":"فتح صنبور Testnet الرسمي","fr":"Ouvrir le faucet Testnet officiel","bn":"অফিসিয়াল Testnet ফসেট খুলুন","pt":"Abrir faucet oficial da Testnet","id":"Buka faucet Testnet resmi","ur":"سرکاری Testnet فاسٹ کھولیں"},"pages":[7880]},{"source":"Create a free test account","translations":{"nl":"Maak een gratis testaccount","zh-Hans":"创建免费测试账户","hi":"मुफ़्त टेस्ट खाता बनाएँ","es":"Crea una cuenta de prueba gratuita","ar":"أنشئ حساب اختبار مجانيًا","fr":"Créez un compte de test gratuit","bn":"বিনামূল্যে পরীক্ষার অ্যাকাউন্ট তৈরি করুন","pt":"Crie uma conta de teste gratuita","id":"Buat akun uji gratis","ur":"مفت ٹیسٹ اکاؤنٹ بنائیں"},"pages":[7880]},{"source":"Choose Testnet in Xaman","translations":{"nl":"Kies Testnet in Xaman","zh-Hans":"在 Xaman 中选择 Testnet","hi":"Xaman में Testnet चुनें","es":"Elige Testnet en Xaman","ar":"اختر Testnet في Xaman","fr":"Choisissez Testnet dans Xaman","bn":"Xaman-এ Testnet বেছে নিন","pt":"Escolha Testnet no Xaman","id":"Pilih Testnet di Xaman","ur":"Xaman میں Testnet منتخب کریں"},"pages":[7880]},{"source":"Add your account to Xaman","translations":{"nl":"Voeg je account toe aan Xaman","zh-Hans":"将账户添加到 Xaman","hi":"अपना खाता Xaman में जोड़ें","es":"Añade tu cuenta a Xaman","ar":"أضف حسابك إلى Xaman","fr":"Ajoutez votre compte à Xaman","bn":"Xaman-এ আপনার অ্যাকাউন্ট যোগ করুন","pt":"Adicione a sua conta ao Xaman","id":"Tambahkan akun Anda ke Xaman","ur":"اپنا اکاؤنٹ Xaman میں شامل کریں"},"pages":[7880]},{"source":"Return to CalorieApp","translations":{"nl":"Ga terug naar CalorieApp","zh-Hans":"返回 CalorieApp","hi":"CalorieApp पर लौटें","es":"Vuelve a CalorieApp","ar":"عُد إلى CalorieApp","fr":"Revenez à CalorieApp","bn":"CalorieApp-এ ফিরুন","pt":"Volte ao CalorieApp","id":"Kembali ke CalorieApp","ur":"CalorieApp پر واپس آئیں"},"pages":[7880]},{"source":"Back one step","translations":{"nl":"Een stap terug","zh-Hans":"上一步","hi":"पिछला चरण","es":"Paso anterior","ar":"الخطوة السابقة","fr":"Étape précédente","bn":"আগের ধাপ","pt":"Passo anterior","id":"Langkah sebelumnya","ur":"پچھلا مرحلہ"},"pages":[7880]},{"source":"Next step","translations":{"nl":"Volgende stap","zh-Hans":"下一步","hi":"अगला चरण","es":"Siguiente paso","ar":"الخطوة التالية","fr":"Étape suivante","bn":"পরের ধাপ","pt":"Próximo passo","id":"Langkah berikutnya","ur":"اگلا مرحلہ"},"pages":[7880]},{"source":"Show the Testnet instructions","translations":{"nl":"Toon de Testnet-uitleg","zh-Hans":"查看 Testnet 说明","hi":"Testnet के निर्देश दिखाएँ","es":"Ver las instrucciones de Testnet","ar":"عرض تعليمات Testnet","fr":"Voir les instructions Testnet","bn":"Testnet-এর নির্দেশ দেখুন","pt":"Ver as instruções de Testnet","id":"Lihat petunjuk Testnet","ur":"Testnet کی ہدایات دیکھیں"},"pages":[7880]},{"source":"How is this account created?","translations":{"nl":"Hoe wordt dit account gemaakt?","zh-Hans":"这个账户如何创建?","hi":"यह खाता कैसे बनता है?","es":"¿Cómo se crea esta cuenta?","ar":"كيف يُنشأ هذا الحساب؟","fr":"Comment ce compte est-il créé ?","bn":"এই অ্যাকাউন্ট কীভাবে তৈরি হয়?","pt":"Como é criada esta conta?","id":"Bagaimana akun ini dibuat?","ur":"یہ اکاؤنٹ کیسے بنتا ہے؟"},"pages":[7880]},{"source":"This is a test account, not pilot registration or retailer access.","translations":{"nl":"Dit is een testaccount, geen pilotinschrijving of winkelierstoegang.","zh-Hans":"这是测试账户,不会注册试点计划或授予商家权限。","hi":"यह टेस्ट खाता है, पायलट पंजीकरण या विक्रेता की पहुँच नहीं।","es":"Es una cuenta de prueba, no una inscripción a un piloto ni acceso de comercio.","ar":"هذا حساب اختبار، وليس تسجيلًا في برنامج تجريبي أو صلاحية تاجر.","fr":"Il s’agit d’un compte de test, pas d’une inscription à un pilote ni d’un accès commerçant.","bn":"এটি পরীক্ষার অ্যাকাউন্ট, পাইলটে নিবন্ধন বা বিক্রেতার প্রবেশাধিকার নয়।","pt":"É uma conta de teste, não uma inscrição num piloto nem acesso de comerciante.","id":"Ini akun uji, bukan pendaftaran pilot atau akses pedagang.","ur":"یہ ٹیسٹ اکاؤنٹ ہے، پائلٹ میں اندراج یا دکاندار کی رسائی نہیں۔"},"pages":[7880]},{"source":"{project} roadmap","translations":{"zh-Hans":"{project} 路线图","hi":"{project} की कार्ययोजना","es":"Hoja de ruta de {project}","ar":"خارطة طريق {project}","fr":"Feuille de route de {project}","bn":"{project}-এর রোডম্যাপ","pt":"Roteiro de {project}","id":"Peta jalan {project}","ur":"{project} کا روڈ میپ","nl":"Roadmap van {project}"},"pages":[5580,7699]},{"source":"Updated: {updated}","translations":{"zh-Hans":"更新日期:{updated}","hi":"अद्यतन: {updated}","es":"Actualización: {updated}","ar":"آخر تحديث: {updated}","fr":"Mise à jour : {updated}","bn":"হালনাগাদ: {updated}","pt":"Atualização: {updated}","id":"Diperbarui: {updated}","ur":"تازہ کاری: {updated}","nl":"Bijgewerkt: {updated}"},"pages":[5580,7699]},{"source":"8 September 2026","translations":{"zh-Hans":"2026年9月8日","hi":"8 सितंबर 2026","es":"8 de septiembre de 2026","ar":"8 سبتمبر 2026","fr":"8 septembre 2026","bn":"৮ সেপ্টেম্বর ২০২৬","pt":"8 de setembro de 2026","id":"8 September 2026","ur":"8 ستمبر 2026","nl":"8 september 2026"},"pages":[5580,7699]},{"source":"These eight milestones describe the project’s history, the current preview and future work. Future work has no guaranteed delivery date.","translations":{"zh-Hans":"这八个里程碑介绍了项目历史、当前预览版和未来工作。未来工作的交付日期不作保证。","hi":"ये आठ पड़ाव परियोजना के इतिहास, मौजूदा पूर्वावलोकन और भविष्य के काम का वर्णन करते हैं। भविष्य के काम के पूरा होने की कोई निश्चित तारीख नहीं दी गई है।","es":"Estos ocho hitos describen la historia del proyecto, la versión preliminar actual y el trabajo futuro. No se garantiza una fecha de entrega para el trabajo futuro.","ar":"تصف هذه المحطات الثماني تاريخ المشروع والنسخة التجريبية الحالية والعمل المستقبلي. لا يوجد موعد تسليم مضمون للعمل المستقبلي.","fr":"Ces huit étapes décrivent l’histoire du projet, la version de prévisualisation actuelle et les travaux futurs. Aucune date de livraison n’est garantie pour les travaux futurs.","bn":"এই আটটি মাইলফলক প্রকল্পের ইতিহাস, বর্তমান প্রিভিউ এবং ভবিষ্যৎ কাজের বর্ণনা দেয়। ভবিষ্যৎ কাজ শেষ করার কোনো নির্দিষ্ট তারিখের নিশ্চয়তা দেওয়া হয়নি।","pt":"Estes oito marcos descrevem a história do projeto, a pré-visualização atual e os trabalhos futuros. Não há garantia de uma data de entrega para os trabalhos futuros.","id":"Delapan tonggak ini menjelaskan sejarah proyek, pratinjau saat ini, dan pekerjaan mendatang. Tidak ada jaminan tanggal penyelesaian untuk pekerjaan mendatang.","ur":"یہ آٹھ سنگ میل منصوبے کی تاریخ، موجودہ پیش نظارہ اور آئندہ کام بیان کرتے ہیں۔ آئندہ کام کی تکمیل کی کوئی تاریخ ضمانت کے ساتھ نہیں دی گئی۔","nl":"Deze acht mijlpalen beschrijven de geschiedenis van het project, de huidige preview en toekomstig werk. Voor toekomstig werk is geen opleverdatum gegarandeerd."},"pages":[5580,7699]},{"source":"Milestone","translations":{"zh-Hans":"里程碑","hi":"पड़ाव","es":"Hito","ar":"المحطة","fr":"Étape","bn":"মাইলফলক","pt":"Marco","id":"Tonggak","ur":"سنگ میل","nl":"Mijlpaal"},"pages":[5580,7699]},{"source":"Status","translations":{"zh-Hans":"状态","hi":"स्थिति","es":"Estado","ar":"الحالة","fr":"Statut","bn":"অবস্থা","pt":"Estado","id":"Status","ur":"حالت","nl":"Status"},"pages":[5580,7699]},{"source":"Meaning","translations":{"zh-Hans":"说明","hi":"विवरण","es":"Descripción","ar":"الوصف","fr":"Description","bn":"বিবরণ","pt":"Descrição","id":"Penjelasan","ur":"وضاحت","nl":"Toelichting"},"pages":[5580,7699]},{"source":"Origins","translations":{"zh-Hans":"项目起源","hi":"शुरुआत","es":"Orígenes","ar":"البدايات","fr":"Origines","bn":"সূচনা","pt":"Origens","id":"Awal mula","ur":"آغاز","nl":"Ontstaan"},"pages":[5580,7699]},{"source":"Historical","translations":{"zh-Hans":"历史阶段","hi":"ऐतिहासिक","es":"Histórico","ar":"تاريخية","fr":"Historique","bn":"ঐতিহাসিক","pt":"Histórico","id":"Historis","ur":"تاریخی","nl":"Historisch"},"pages":[5580,7699]},{"source":"The food-focused project and community began in 2021.","translations":{"zh-Hans":"以食品为核心的项目及其社区始于2021年。","hi":"भोजन पर केंद्रित परियोजना और समुदाय की शुरुआत 2021 में हुई।","es":"El proyecto centrado en la alimentación y su comunidad comenzaron en 2021.","ar":"بدأ المشروع الذي يركز على الغذاء ومجتمعه في عام 2021.","fr":"Le projet consacré à l’alimentation et sa communauté ont débuté en 2021.","bn":"খাদ্যকে কেন্দ্র করে প্রকল্প ও কমিউনিটির যাত্রা শুরু হয় ২০২১ সালে।","pt":"O projeto voltado à alimentação e a sua comunidade começaram em 2021.","id":"Proyek yang berfokus pada pangan beserta komunitasnya dimulai pada 2021.","ur":"خوراک پر مرکوز منصوبے اور اس کی کمیونٹی کا آغاز 2021 میں ہوا۔","nl":"Het project rond voeding en de community begonnen in 2021."},"pages":[5580,7699]},{"source":"Early prototypes","translations":{"zh-Hans":"早期原型","hi":"शुरुआती प्रोटोटाइप","es":"Primeros prototipos","ar":"النماذج الأولية المبكرة","fr":"Premiers prototypes","bn":"প্রাথমিক প্রোটোটাইপ","pt":"Primeiros protótipos","id":"Prototipe awal","ur":"ابتدائی نمونے","nl":"Eerste prototypes"},"pages":[5580,7699]},{"source":"Earlier Testnet and mobile experiments informed the project.","translations":{"zh-Hans":"早期的 Testnet 和移动端实验为项目提供了经验。","hi":"पहले के Testnet और मोबाइल प्रयोगों से परियोजना को सीख मिली।","es":"Los primeros experimentos con Testnet y dispositivos móviles aportaron conocimientos al proyecto.","ar":"أسهمت التجارب السابقة على Testnet والأجهزة المحمولة في تطوير فهم المشروع.","fr":"Les premières expérimentations sur Testnet et sur mobile ont apporté des enseignements au projet.","bn":"আগের Testnet ও মোবাইল পরীক্ষাগুলো থেকে প্রকল্পটি অভিজ্ঞতা অর্জন করেছে।","pt":"As experiências anteriores com Testnet e dispositivos móveis contribuíram para o projeto.","id":"Eksperimen Testnet dan perangkat seluler terdahulu memberikan pembelajaran bagi proyek.","ur":"پہلے کے Testnet اور موبائل تجربات سے منصوبے کو رہنمائی ملی۔","nl":"Eerdere experimenten met Testnet en mobiele apps leverden inzichten op voor het project."},"pages":[5580,7699]},{"source":"Food webapp","translations":{"zh-Hans":"食品网页应用","hi":"भोजन संबंधी वेब ऐप","es":"Aplicación web de alimentación","ar":"تطبيق الويب الخاص بالغذاء","fr":"Application web alimentaire","bn":"খাদ্যবিষয়ক ওয়েব অ্যাপ","pt":"Aplicação web de alimentação","id":"Aplikasi web pangan","ur":"خوراک کی ویب ایپ","nl":"Voedingswebapp"},"pages":[5580,7699]},{"source":"Available preview","translations":{"zh-Hans":"预览版已开放","hi":"पूर्वावलोकन उपलब्ध","es":"Versión preliminar disponible","ar":"نسخة تجريبية متاحة","fr":"Prévisualisation disponible","bn":"প্রিভিউ উপলব্ধ","pt":"Pré-visualização disponível","id":"Pratinjau tersedia","ur":"پیش نظارہ دستیاب","nl":"Preview beschikbaar"},"pages":[5580,7699]},{"source":"Food search, nutrition, portions and personal food logs.","translations":{"zh-Hans":"食品搜索、营养信息、份量选择和个人饮食记录。","hi":"खाद्य खोज, पोषण, मात्रा और व्यक्तिगत भोजन रिकॉर्ड।","es":"Búsqueda de alimentos, nutrición, porciones y registros personales de alimentación.","ar":"البحث عن الأغذية، ومعلومات التغذية، والحصص، وسجلات الطعام الشخصية.","fr":"Recherche d’aliments, nutrition, portions et journaux alimentaires personnels.","bn":"খাবার খোঁজা, পুষ্টির তথ্য, পরিমাণ এবং ব্যক্তিগত খাদ্য লগ।","pt":"Pesquisa de alimentos, nutrição, porções e registos alimentares pessoais.","id":"Pencarian makanan, nutrisi, porsi, dan catatan makanan pribadi.","ur":"کھانے کی تلاش، غذائیت، حصوں کی مقدار اور ذاتی غذائی ریکارڈ۔","nl":"Voedingsmiddelen zoeken, voedingsinformatie, porties en een persoonlijk voedingsdagboek."},"pages":[5580,7699]},{"source":"Connected sign-in","translations":{"zh-Hans":"网站与应用联合登录","hi":"संयुक्त साइन-इन","es":"Inicio de sesión conjunto","ar":"تسجيل الدخول المشترك","fr":"Connexion commune","bn":"সমন্বিত সাইন-ইন","pt":"Início de sessão conjunto","id":"Login terpadu","ur":"مشترکہ سائن اِن","nl":"Gezamenlijk inloggen"},"pages":[5580,7699]},{"source":"Available","translations":{"zh-Hans":"已提供","hi":"उपलब्ध","es":"Disponible","ar":"متاح","fr":"Disponible","bn":"উপলব্ধ","pt":"Disponível","id":"Tersedia","ur":"دستیاب","nl":"Beschikbaar"},"pages":[5580,7699]},{"source":"Website and CalorieApp sign-in and logout in the same browser.","translations":{"zh-Hans":"在同一浏览器中登录和退出网站及 CalorieApp。","hi":"एक ही ब्राउज़र में वेबसाइट और CalorieApp में साइन-इन और साइन-आउट।","es":"Inicio y cierre de sesión del sitio web y de CalorieApp en el mismo navegador.","ar":"تسجيل الدخول إلى الموقع وCalorieApp والخروج منهما في المتصفح نفسه.","fr":"Connexion et déconnexion du site et de CalorieApp dans le même navigateur.","bn":"একই ব্রাউজারে ওয়েবসাইট ও CalorieApp-এ সাইন-ইন এবং সাইন-আউট।","pt":"Início e fim de sessão do site e do CalorieApp no mesmo navegador.","id":"Login dan logout situs web serta CalorieApp dalam peramban yang sama.","ur":"ایک ہی براؤزر میں ویب سائٹ اور CalorieApp میں سائن اِن اور سائن آؤٹ۔","nl":"In- en uitloggen op de website en in CalorieApp in dezelfde browser."},"pages":[5580,7699]},{"source":"Website refresh","translations":{"zh-Hans":"网站完善","hi":"वेबसाइट में सुधार","es":"Mejora del sitio web","ar":"تحسين الموقع","fr":"Amélioration du site","bn":"ওয়েবসাইটের উন্নয়ন","pt":"Melhoria do site","id":"Penyempurnaan situs web","ur":"ویب سائٹ کی بہتری","nl":"Website verfijnen"},"pages":[5580,7699]},{"source":"In progress","translations":{"zh-Hans":"进行中","hi":"जारी","es":"En curso","ar":"جارٍ العمل","fr":"En cours","bn":"চলমান","pt":"Em curso","id":"Sedang dikerjakan","ur":"جاری","nl":"In uitvoering"},"pages":[5580,7699]},{"source":"Consistent content and presentation, with publications and old links being consolidated.","translations":{"zh-Hans":"统一内容与呈现方式,并整理出版物和旧链接。","hi":"सामग्री और प्रस्तुति में एकरूपता, साथ ही प्रकाशनों और पुराने लिंक को व्यवस्थित करना।","es":"Contenido y presentación coherentes, con la consolidación de publicaciones y enlaces antiguos.","ar":"توحيد المحتوى والعرض مع تنظيم المنشورات والروابط القديمة.","fr":"Harmonisation du contenu et de la présentation, avec consolidation des publications et des anciens liens.","bn":"প্রকাশনা ও পুরোনো লিংকগুলো গুছিয়ে বিষয়বস্তু ও উপস্থাপনায় সামঞ্জস্য আনা।","pt":"Conteúdo e apresentação consistentes, com a consolidação de publicações e ligações antigas.","id":"Konten dan tampilan yang konsisten, sambil merapikan publikasi dan tautan lama.","ur":"مواد اور پیشکش میں یکسانیت، ساتھ ہی اشاعتوں اور پرانے روابط کی ترتیب۔","nl":"Consistente inhoud en vormgeving, met het samenbrengen van publicaties en oude verwijzingen."},"pages":[5580,7699]},{"source":"Community preview","translations":{"zh-Hans":"社区预览","hi":"समुदाय के लिए पूर्वावलोकन","es":"Presentación a la comunidad","ar":"عرض تجريبي للمجتمع","fr":"Présentation à la communauté","bn":"কমিউনিটির জন্য প্রিভিউ","pt":"Pré-visualização para a comunidade","id":"Pratinjau komunitas","ur":"کمیونٹی کے لیے پیش نظارہ","nl":"Preview voor de community"},"pages":[5580,7699]},{"source":"Next","translations":{"zh-Hans":"下一页","hi":"अगला","es":"Siguiente","ar":"التالي","fr":"Suivant","bn":"পরের","pt":"Seguinte","id":"Berikutnya","ur":"اگلا","nl":"Volgende"},"pages":[5580,6897,6914,6926,6937,7699]},{"source":"Show the current experience and collect feedback.","translations":{"zh-Hans":"展示当前使用体验并收集反馈。","hi":"मौजूदा अनुभव दिखाना और प्रतिक्रिया लेना।","es":"Mostrar la experiencia actual y recoger comentarios.","ar":"عرض التجربة الحالية وجمع الملاحظات.","fr":"Présenter l’expérience actuelle et recueillir des retours.","bn":"বর্তমান অভিজ্ঞতা দেখানো এবং মতামত সংগ্রহ করা।","pt":"Mostrar a experiência atual e recolher comentários.","id":"Menampilkan pengalaman saat ini dan mengumpulkan masukan.","ur":"موجودہ تجربہ دکھانا اور رائے جمع کرنا۔","nl":"De huidige ervaring tonen en feedback verzamelen."},"pages":[5580,7699]},{"source":"Product refinement","translations":{"zh-Hans":"产品完善","hi":"उत्पाद में सुधार","es":"Mejora del producto","ar":"تحسين المنتج","fr":"Amélioration du produit","bn":"পণ্যের উন্নয়ন","pt":"Melhoria do produto","id":"Penyempurnaan produk","ur":"مصنوعے میں بہتری","nl":"Product verbeteren"},"pages":[5580,7699]},{"source":"Planned","translations":{"zh-Hans":"已规划","hi":"नियोजित","es":"Planificado","ar":"مخطط له","fr":"Prévu","bn":"পরিকল্পিত","pt":"Planeado","id":"Direncanakan","ur":"منصوبہ بند","nl":"Gepland"},"pages":[5580,7699]},{"source":"Prioritise improvements from observed use.","translations":{"zh-Hans":"根据实际使用情况确定改进的优先顺序。","hi":"वास्तविक उपयोग से मिली जानकारी के आधार पर सुधारों को प्राथमिकता देना।","es":"Priorizar las mejoras a partir del uso observado.","ar":"تحديد أولويات التحسينات بناءً على الاستخدام الفعلي المرصود.","fr":"Donner la priorité aux améliorations issues de l’usage observé.","bn":"বাস্তব ব্যবহার পর্যবেক্ষণ করে উন্নয়নের অগ্রাধিকার নির্ধারণ করা।","pt":"Dar prioridade às melhorias com base na utilização observada.","id":"Memprioritaskan perbaikan berdasarkan penggunaan yang diamati.","ur":"مشاہدہ شدہ استعمال کی بنیاد پر بہتریوں کو ترجیح دینا۔","nl":"Verbeteringen prioriteren op basis van waargenomen gebruik."},"pages":[5580,7699]},{"source":"Future research","translations":{"zh-Hans":"未来研究","hi":"भविष्य का अनुसंधान","es":"Investigación futura","ar":"البحث المستقبلي","fr":"Recherches futures","bn":"ভবিষ্যৎ গবেষণা","pt":"Investigação futura","id":"Riset mendatang","ur":"آئندہ تحقیق","nl":"Toekomstig onderzoek"},"pages":[5580,7699]},{"source":"Research","translations":{"zh-Hans":"研究阶段","hi":"अनुसंधान","es":"Investigación","ar":"قيد البحث","fr":"Recherche","bn":"গবেষণা","pt":"Investigação","id":"Riset","ur":"تحقیق","nl":"Onderzoek"},"pages":[5580,7699]},{"source":"Investigate food-data and ecosystem ideas separately.","translations":{"zh-Hans":"单独研究食品数据和生态系统相关构想。","hi":"खाद्य डेटा और इकोसिस्टम से जुड़े विचारों की अलग से जाँच करना।","es":"Investigar por separado las ideas sobre datos alimentarios y el ecosistema.","ar":"بحث أفكار بيانات الغذاء والنظام البيئي بصورة منفصلة.","fr":"Étudier séparément les idées relatives aux données alimentaires et à l’écosystème.","bn":"খাদ্যসংক্রান্ত তথ্য ও ইকোসিস্টেমের ধারণাগুলো আলাদাভাবে অনুসন্ধান করা।","pt":"Investigar separadamente ideias sobre dados alimentares e o ecossistema.","id":"Mengkaji gagasan data pangan dan ekosistem secara terpisah.","ur":"خوراک کے ڈیٹا اور ایکو سسٹم کے خیالات کا الگ سے جائزہ لینا۔","nl":"Ideeën rond voedingsdata en het ecosysteem afzonderlijk onderzoeken."},"pages":[5580,7699]},{"source":"Detailed roadmap on GitHub","translations":{"zh-Hans":"查看 GitHub 上的详细路线图","hi":"GitHub पर विस्तृत कार्ययोजना","es":"Hoja de ruta detallada en GitHub","ar":"خارطة الطريق المفصلة على GitHub","fr":"Feuille de route détaillée sur GitHub","bn":"GitHub-এ বিস্তারিত রোডম্যাপ","pt":"Roteiro detalhado no GitHub","id":"Peta jalan terperinci di GitHub","ur":"GitHub پر تفصیلی روڈ میپ","nl":"Uitgebreide roadmap op GitHub"},"pages":[5580,7699]},{"source":"Historical roadmap 2023–2024","translations":{"zh-Hans":"历史路线图:2023–2024年","hi":"ऐतिहासिक कार्ययोजना 2023–2024","es":"Hoja de ruta histórica 2023–2024","ar":"خارطة الطريق التاريخية 2023–2024","fr":"Feuille de route historique 2023–2024","bn":"ঐতিহাসিক রোডম্যাপ ২০২৩–২০২৪","pt":"Roteiro histórico 2023–2024","id":"Peta jalan historis 2023–2024","ur":"تاریخی روڈ میپ 2023–2024","nl":"Historische roadmap 2023–2024"},"pages":[5580,7699]},{"source":"Historical material describes proposals and experiments from that period. It is not a statement of current services or delivery commitments.","translations":{"zh-Hans":"历史资料介绍的是当时的提案和实验,并不代表当前服务或交付承诺。","hi":"ऐतिहासिक सामग्री उस समय के प्रस्तावों और प्रयोगों का वर्णन करती है। यह मौजूदा सेवाओं या काम पूरा करने की प्रतिबद्धताओं का विवरण नहीं है।","es":"El material histórico describe propuestas y experimentos de aquella época. No constituye una descripción de los servicios actuales ni un compromiso de entrega.","ar":"تصف المواد التاريخية مقترحات وتجارب تلك الفترة. وهي لا تمثل بيانًا بالخدمات الحالية أو التزامات بتسليم أعمال مستقبلية.","fr":"Les documents historiques décrivent les propositions et expérimentations de cette période. Ils ne constituent ni une description des services actuels ni un engagement de livraison.","bn":"ঐতিহাসিক উপকরণে সেই সময়ের প্রস্তাব ও পরীক্ষার বিবরণ রয়েছে। এগুলো বর্তমান পরিষেবা বা ভবিষ্যৎ কাজ সরবরাহের অঙ্গীকার নয়।","pt":"O material histórico descreve propostas e experiências desse período. Não representa uma declaração de serviços atuais nem compromissos de entrega.","id":"Materi historis menjelaskan usulan dan eksperimen pada masa itu. Materi tersebut bukan pernyataan tentang layanan saat ini atau komitmen penyelesaian pekerjaan.","ur":"تاریخی مواد اُس دور کی تجاویز اور تجربات بیان کرتا ہے۔ یہ موجودہ خدمات یا کام کی فراہمی کے وعدوں کا بیان نہیں ہے۔","nl":"Historisch materiaal beschrijft voorstellen en experimenten uit die periode. Het beschrijft geen huidige diensten en bevat geen actuele oplevertoezeggingen."},"pages":[5580,7699]},{"source":"Historical Vote: {project} Reward Pool Vote","translations":{"zh-Hans":"历史投票:{project} 奖励池投票","hi":"ऐतिहासिक मतदान: {project} रिवॉर्ड पूल मतदान","es":"Votación histórica: fondo de recompensas de {project}","ar":"تصويت تاريخي: صندوق مكافآت {project}","fr":"Vote historique : fonds de récompenses de {project}","bn":"ঐতিহাসিক ভোট: {project} পুরস্কার তহবিলের ভোট","pt":"Votação histórica: fundo de recompensas de {project}","id":"Pemungutan suara historis: dana imbalan {project}","ur":"تاریخی ووٹ: {project} کے انعامی فنڈ کی ووٹنگ","nl":"Historische stemming: beloningspool van {project}"},"pages":[5580,7699]},{"source":"Description of the historical Voting","translations":{"zh-Hans":"历史投票说明","hi":"ऐतिहासिक मतदान का विवरण","es":"Descripción de la votación histórica","ar":"وصف التصويت التاريخي","fr":"Description du vote historique","bn":"ঐতিহাসিক ভোটের বিবরণ","pt":"Descrição da votação histórica","id":"Penjelasan pemungutan suara historis","ur":"تاریخی ووٹنگ کی وضاحت","nl":"Toelichting op de historische stemming"},"pages":[5580,7699]},{"source":"Voting ran from 1 November 2024 to 8 November 2024.","translations":{"zh-Hans":"投票时间为2024年11月1日至2024年11月8日。","hi":"मतदान 1 नवंबर 2024 से 8 नवंबर 2024 तक चला था।","es":"La votación tuvo lugar del 1 al 8 de noviembre de 2024.","ar":"جرى التصويت من 1 نوفمبر 2024 إلى 8 نوفمبر 2024.","fr":"Le vote s’est déroulé du 1er au 8 novembre 2024.","bn":"ভোট চলেছিল ১ নভেম্বর ২০২৪ থেকে ৮ নভেম্বর ২০২৪ পর্যন্ত।","pt":"A votação decorreu de 1 a 8 de novembro de 2024.","id":"Pemungutan suara berlangsung dari 1 November 2024 hingga 8 November 2024.","ur":"ووٹنگ 1 نومبر 2024 سے 8 نومبر 2024 تک ہوئی تھی۔","nl":"De stemming liep van 1 november 2024 tot 8 november 2024."},"pages":[5580,7699]},{"source":"This page preserves the November 2024 reward-pool proposal and its displayed results. The voting period shown below has ended. Historical figures are not a current giveaway budget or confirmation of funds available.","translations":{"zh-Hans":"本页保留了2024年11月的奖励池提案及页面显示的投票结果。下方所示的投票期已经结束。历史数字不代表当前赠送活动的预算,也不确认目前有多少资金可用。","hi":"यह पृष्ठ नवंबर 2024 के रिवॉर्ड पूल प्रस्ताव और यहाँ दिखाए गए परिणामों को सुरक्षित रखता है। नीचे दी गई मतदान अवधि समाप्त हो चुकी है। ऐतिहासिक आँकड़े मौजूदा गिवअवे बजट या उपलब्ध धन की पुष्टि नहीं हैं।","es":"Esta página conserva la propuesta del fondo de recompensas de noviembre de 2024 y los resultados que se muestran. El periodo de votación indicado a continuación ha finalizado. Las cifras históricas no son un presupuesto actual para obsequios ni confirman la disponibilidad de fondos.","ar":"تحتفظ هذه الصفحة بمقترح صندوق المكافآت من نوفمبر 2024 والنتائج المعروضة له. انتهت فترة التصويت الموضحة أدناه. الأرقام التاريخية ليست ميزانية حالية للهدايا ولا تأكيدًا لتوفر الأموال.","fr":"Cette page conserve la proposition de fonds de récompenses de novembre 2024 et les résultats affichés. La période de vote indiquée ci-dessous est terminée. Les chiffres historiques ne constituent pas un budget actuel de distributions gratuites ni une confirmation des fonds disponibles.","bn":"এই পাতায় নভেম্বর ২০২৪-এর পুরস্কার তহবিলের প্রস্তাব ও প্রদর্শিত ফলাফল সংরক্ষিত আছে। নিচে দেখানো ভোটের সময়সীমা শেষ হয়েছে। ঐতিহাসিক অঙ্কগুলো বর্তমান উপহার বিতরণের বাজেট বা উপলব্ধ অর্থের নিশ্চয়তা নয়।","pt":"Esta página preserva a proposta do fundo de recompensas de novembro de 2024 e os resultados apresentados. O período de votação indicado abaixo terminou. Os valores históricos não são um orçamento atual para ofertas nem uma confirmação de fundos disponíveis.","id":"Halaman ini menyimpan usulan dana imbalan November 2024 beserta hasil yang ditampilkan. Masa pemungutan suara di bawah ini telah berakhir. Angka historis bukan anggaran giveaway saat ini atau konfirmasi dana yang tersedia.","ur":"یہ صفحہ نومبر 2024 کے انعامی فنڈ کی تجویز اور دکھائے گئے نتائج محفوظ رکھتا ہے۔ نیچے درج ووٹنگ کی مدت ختم ہو چکی ہے۔ تاریخی اعداد مفت تقسیم کے موجودہ بجٹ یا دستیاب فنڈز کی تصدیق نہیں ہیں۔","nl":"Deze pagina bewaart het voorstel voor de beloningspool van november 2024 en de getoonde uitslagen. De hieronder vermelde stemperiode is afgelopen. Historische cijfers zijn geen actueel giveawaybudget en bevestigen niet hoeveel middelen beschikbaar zijn."},"pages":[5580,7699]},{"source":"Donation steps","translations":{"zh-Hans":"捐赠步骤","hi":"दान के चरण","es":"Pasos de la donación","ar":"خطوات التبرع","fr":"Étapes du don","bn":"অনুদানের ধাপ","pt":"Passos do donativo","id":"Langkah donasi","ur":"عطیے کے مراحل","nl":"Donatiestappen"},"pages":["*"]},{"source":"Donation","translations":{"zh-Hans":"捐赠","hi":"दान","es":"Donación","ar":"التبرع","fr":"Don","bn":"অনুদান","pt":"Donativo","id":"Donasi","ur":"عطیہ","nl":"Donatie"},"pages":["*"]},{"source":"Amount","translations":{"zh-Hans":"金额","hi":"राशि","es":"Importe","ar":"المبلغ","fr":"Montant","bn":"পরিমাণ","pt":"Montante","id":"Jumlah","ur":"رقم","nl":"Bedrag"},"pages":["*"]},{"source":"Cart","translations":{"zh-Hans":"购物车","hi":"कार्ट","es":"Carrito","ar":"السلة","fr":"Panier","bn":"কার্ট","pt":"Carrinho","id":"Keranjang","ur":"کارٹ","nl":"Winkelwagen"},"pages":["*"]},{"source":"Checkout","translations":{"zh-Hans":"结账","hi":"भुगतान","es":"Finalizar compra","ar":"إتمام الشراء","fr":"Paiement","bn":"চেকআউট","pt":"Finalizar compra","id":"Pembayaran","ur":"چیک آؤٹ","nl":"Afrekenen"},"pages":["*"]},{"source":"What is the mission of CalorieToken?","translations":{"nl":"Wat is de missie van CalorieToken?","es":"¿Cuál es la misión de CalorieToken?","fr":"Quelle est la mission de CalorieToken ?","pt":"Qual é a missão do CalorieToken?","id":"Apa misi CalorieToken?","zh-Hans":"CalorieToken 的使命是什么?","hi":"CalorieToken का मिशन क्या है?","ar":"ما مهمة CalorieToken؟","bn":"CalorieToken-এর লক্ষ্য কী?","ur":"CalorieToken کا مقصد کیا ہے؟"},"pages":[6855]},{"source":"CalorieToken explores digital tools for the food and beverage sector. The current CalorieApp preview focuses on food search, nutrition information and a personal food log. Payment applications and business integrations belong to the wider project vision.","translations":{"nl":"CalorieToken verkent digitale hulpmiddelen voor de voedings- en drankensector. De huidige CalorieApp-preview richt zich op voedingsmiddelen zoeken, voedingsinformatie en een persoonlijk voedingsdagboek. Betaaltoepassingen en zakelijke integraties horen bij de bredere projectvisie.","es":"CalorieToken explora herramientas digitales para el sector de alimentos y bebidas. La vista previa actual de CalorieApp se centra en buscar alimentos, consultar información nutricional y llevar un diario personal. Las aplicaciones de pago y las integraciones empresariales forman parte de la visión más amplia del proyecto.","fr":"CalorieToken explore des outils numériques pour le secteur de l’alimentation et des boissons. L’aperçu actuel de CalorieApp propose la recherche d’aliments, des informations nutritionnelles et un journal alimentaire personnel. Les paiements et les intégrations professionnelles font partie de la vision élargie du projet.","pt":"O CalorieToken explora ferramentas digitais para o setor de alimentos e bebidas. A prévia atual do CalorieApp concentra-se na pesquisa de alimentos, informação nutricional e diário alimentar pessoal. Aplicações de pagamento e integrações empresariais fazem parte da visão mais ampla do projeto.","id":"CalorieToken menjajaki alat digital untuk sektor makanan dan minuman. Pratinjau CalorieApp saat ini berfokus pada pencarian makanan, informasi gizi, dan catatan makanan pribadi. Aplikasi pembayaran dan integrasi bisnis termasuk dalam visi proyek yang lebih luas.","zh-Hans":"CalorieToken 探索面向食品饮料行业的数字工具。目前的 CalorieApp 预览版专注于食品搜索、营养信息和个人饮食记录。支付应用及企业集成属于项目更广泛的愿景。","hi":"CalorieToken खाद्य और पेय क्षेत्र के लिए डिजिटल साधनों की संभावनाएँ तलाशता है। CalorieApp का वर्तमान प्रीव्यू भोजन खोज, पोषण संबंधी जानकारी और व्यक्तिगत भोजन डायरी पर केंद्रित है। भुगतान ऐप और व्यावसायिक एकीकरण परियोजना की व्यापक दृष्टि का हिस्सा हैं।","ar":"يستكشف CalorieToken أدوات رقمية لقطاع الأغذية والمشروبات. تركز المعاينة الحالية لـ CalorieApp على البحث عن الطعام والمعلومات الغذائية وسجل غذائي شخصي. تنتمي تطبيقات الدفع والتكاملات التجارية إلى رؤية المشروع الأوسع.","bn":"CalorieToken খাদ্য ও পানীয় খাতের জন্য ডিজিটাল সরঞ্জাম নিয়ে কাজের সম্ভাবনা যাচাই করে। CalorieApp-এর বর্তমান প্রিভিউ খাবার খোঁজা, পুষ্টির তথ্য এবং ব্যক্তিগত খাদ্য ডায়েরির ওপর জোর দেয়। পেমেন্ট অ্যাপ ও ব্যবসায়িক সমন্বয় প্রকল্পের বৃহত্তর পরিকল্পনার অংশ।","ur":"CalorieToken خوراک اور مشروبات کے شعبے کے لیے ڈیجیٹل سہولتوں کے امکانات تلاش کرتا ہے۔ CalorieApp کی موجودہ پیشگی جھلک خوراک کی تلاش، غذائی معلومات اور ذاتی خوراک ڈائری پر مرکوز ہے۔ ادائیگی کی ایپس اور کاروباری انضمام منصوبے کے وسیع تر تصور کا حصہ ہیں۔"},"pages":[6855]},{"source":"What is Calorie and what does it aim to achieve?","translations":{"nl":"Wat is Calorie en wat wil het bereiken?","es":"¿Qué es Calorie y qué pretende conseguir?","fr":"Qu’est-ce que Calorie et quel est son objectif ?","pt":"O que é Calorie e o que pretende alcançar?","id":"Apa itu Calorie dan apa tujuannya?","zh-Hans":"Calorie 是什么?它希望实现什么目标?","hi":"Calorie क्या है और उसका लक्ष्य क्या है?","ar":"ما Calorie وما هدفه؟","bn":"Calorie কী এবং এর উদ্দেশ্য কী?","ur":"Calorie کیا ہے اور اس کا ہدف کیا ہے؟"},"pages":[6855]},{"source":"Calorie (CAL) is the project's token on the XRP Ledger. Food and beverage applications are its long-term focus. Buying or holding CAL is not required to search for food or keep a food log in CalorieApp.","translations":{"nl":"Calorie (CAL) is de token van het project op de XRP Ledger. Toepassingen voor voeding en dranken zijn het doel op langere termijn. Je hoeft geen CAL te kopen of te bezitten om voeding te zoeken of een voedingsdagboek bij te houden in CalorieApp.","es":"Calorie (CAL) es el token del proyecto en XRP Ledger. Su enfoque a largo plazo son las aplicaciones de alimentos y bebidas. No es necesario comprar ni tener CAL para buscar alimentos o llevar un diario en CalorieApp.","fr":"Calorie (CAL) est le jeton du projet sur XRP Ledger. Les applications alimentaires constituent son objectif à long terme. Il n’est pas nécessaire d’acheter ou de détenir des CAL pour rechercher des aliments ou tenir un journal dans CalorieApp.","pt":"Calorie (CAL) é o token do projeto no XRP Ledger. Aplicações para alimentos e bebidas são o seu foco a longo prazo. Não é necessário comprar ou possuir CAL para pesquisar alimentos ou manter um diário no CalorieApp.","id":"Calorie (CAL) adalah token proyek di XRP Ledger. Aplikasi makanan dan minuman menjadi fokus jangka panjangnya. Anda tidak perlu membeli atau memiliki CAL untuk mencari makanan atau membuat catatan makanan di CalorieApp.","zh-Hans":"Calorie(CAL)是项目在 XRP Ledger 上的代币,长期关注食品饮料应用。使用 CalorieApp 搜索食品或记录饮食,无须购买或持有 CAL。","hi":"Calorie (CAL), XRP Ledger पर परियोजना का टोकन है। खाद्य और पेय संबंधी उपयोग इसका दीर्घकालिक लक्ष्य हैं। CalorieApp में भोजन खोजने या डायरी रखने के लिए CAL खरीदना या रखना आवश्यक नहीं है।","ar":"Calorie ‏(CAL) هو رمز المشروع على XRP Ledger. وتُعد تطبيقات الأغذية والمشروبات هدفه طويل الأجل. لا يلزم شراء CAL أو امتلاكه للبحث عن الطعام أو الاحتفاظ بسجل غذائي في CalorieApp.","bn":"Calorie (CAL) হলো XRP Ledger-এ প্রকল্পের টোকেন। খাদ্য ও পানীয়সংক্রান্ত ব্যবহার এর দীর্ঘমেয়াদি লক্ষ্য। CalorieApp-এ খাবার খুঁজতে বা খাদ্য ডায়েরি রাখতে CAL কেনা বা রাখা প্রয়োজন নেই।","ur":"Calorie (CAL) منصوبے کا XRP Ledger پر ٹوکن ہے۔ خوراک اور مشروبات سے متعلق استعمال اس کا طویل مدتی ہدف ہے۔ CalorieApp میں خوراک تلاش کرنے یا ڈائری رکھنے کے لیے CAL خریدنا یا رکھنا ضروری نہیں۔"},"pages":[6855]},{"source":"What advantages does Calorie offer over traditional fiat money?","translations":{"nl":"Welke voordelen biedt Calorie ten opzichte van traditioneel geld?","es":"¿Qué ventajas ofrece Calorie frente al dinero tradicional?","fr":"Quels avantages Calorie offre-t-il par rapport à la monnaie traditionnelle ?","pt":"Quais são as vantagens do Calorie em relação ao dinheiro tradicional?","id":"Apa keunggulan Calorie dibandingkan uang konvensional?","zh-Hans":"与传统法定货币相比,Calorie 有哪些优势?","hi":"परंपरागत मुद्रा की तुलना में Calorie के क्या लाभ हैं?","ar":"ما مزايا Calorie مقارنة بالنقود التقليدية؟","bn":"প্রচলিত অর্থের তুলনায় Calorie কী সুবিধা দেয়?","ur":"روایتی رقم کے مقابلے میں Calorie کے کیا فائدے ہیں؟"},"pages":[6855]},{"source":"CAL uses the XRP Ledger for token transactions. Actual transaction costs and trading conditions depend on the transaction and service. A token does not guarantee savings, merchant acceptance or a stable value. Linking food data to payment transactions remains a research direction. The current CalorieApp preview provides food information and personal food logging.","translations":{"nl":"CAL gebruikt de XRP Ledger voor tokentransacties. De werkelijke transactiekosten en handelsvoorwaarden hangen af van de transactie en de dienst. Een token garandeert geen besparing, acceptatie door winkels of stabiele waarde. Voedingsgegevens koppelen aan betaaltransacties blijft een onderzoeksrichting. De huidige CalorieApp-preview biedt voedingsinformatie en een persoonlijk voedingsdagboek.","es":"CAL utiliza XRP Ledger para las transacciones de tokens. Los costes reales y las condiciones de negociación dependen de la transacción y del servicio. Un token no garantiza ahorro, aceptación comercial ni un valor estable. Vincular datos alimentarios con pagos sigue siendo una línea de investigación. La vista previa actual de CalorieApp ofrece información alimentaria y un diario personal.","fr":"CAL utilise XRP Ledger pour les transactions de jetons. Les coûts réels et les conditions de négociation dépendent de la transaction et du service. Un jeton ne garantit ni économies, ni acceptation commerciale, ni valeur stable. Relier les données alimentaires aux paiements reste un axe de recherche. L’aperçu actuel de CalorieApp fournit des informations alimentaires et un journal personnel.","pt":"CAL utiliza o XRP Ledger para transações de tokens. Os custos reais e as condições de negociação dependem da transação e do serviço. Um token não garante poupança, aceitação comercial ou valor estável. Associar dados alimentares a pagamentos continua sendo uma linha de investigação. A prévia atual do CalorieApp oferece informação alimentar e um diário pessoal.","id":"CAL memakai XRP Ledger untuk transaksi token. Biaya dan ketentuan perdagangan sebenarnya bergantung pada transaksi dan layanan. Token tidak menjamin penghematan, penerimaan pedagang, atau nilai yang stabil. Menghubungkan data makanan dengan pembayaran masih merupakan arah penelitian. Pratinjau CalorieApp saat ini menyediakan informasi makanan dan catatan makanan pribadi.","zh-Hans":"CAL 使用 XRP Ledger 进行代币交易。实际交易费用和交易条件取决于具体交易及服务。代币不保证节省费用、商家接受或价值稳定。将食品数据与支付交易关联仍是研究方向。目前的 CalorieApp 预览版提供食品信息和个人饮食记录。","hi":"CAL टोकन लेनदेन के लिए XRP Ledger का उपयोग करता है। वास्तविक शुल्क और व्यापार की शर्तें लेनदेन तथा सेवा पर निर्भर करती हैं। टोकन बचत, दुकानदारों द्वारा स्वीकार किए जाने या स्थिर मूल्य की गारंटी नहीं देता। खाद्य डेटा को भुगतान से जोड़ना अभी शोध की दिशा है। CalorieApp का वर्तमान प्रीव्यू भोजन की जानकारी और व्यक्तिगत भोजन डायरी देता है।","ar":"يستخدم CAL شبكة XRP Ledger لمعاملات الرموز. تعتمد التكاليف الفعلية وشروط التداول على المعاملة والخدمة. لا يضمن الرمز توفير المال أو قبول التجار أو استقرار القيمة. لا يزال ربط البيانات الغذائية بمعاملات الدفع اتجاهًا بحثيًا. تقدم معاينة CalorieApp الحالية معلومات غذائية وسجلًا غذائيًا شخصيًا.","bn":"CAL টোকেন লেনদেনের জন্য XRP Ledger ব্যবহার করে। প্রকৃত খরচ ও বাণিজ্যের শর্ত লেনদেন এবং পরিষেবার ওপর নির্ভর করে। টোকেন সাশ্রয়, ব্যবসায়ীর গ্রহণযোগ্যতা বা স্থিতিশীল মূল্যের নিশ্চয়তা দেয় না। খাদ্যতথ্যের সঙ্গে পেমেন্ট যুক্ত করা এখনো গবেষণার বিষয়। CalorieApp-এর বর্তমান প্রিভিউ খাদ্যতথ্য ও ব্যক্তিগত খাদ্য ডায়েরি দেয়।","ur":"CAL ٹوکن لین دین کے لیے XRP Ledger استعمال کرتا ہے۔ اصل فیس اور تجارتی شرائط لین دین اور سروس پر منحصر ہیں۔ ٹوکن بچت، تاجروں کی قبولیت یا مستحکم قدر کی ضمانت نہیں دیتا۔ غذائی ڈیٹا کو ادائیگیوں سے جوڑنا اب بھی تحقیق کی سمت ہے۔ موجودہ CalorieApp پیشگی جھلک غذائی معلومات اور ذاتی خوراک ڈائری فراہم کرتی ہے۔"},"pages":[6855]},{"source":"How does Calorie plan to address the needs of the food and beverage industry?","translations":{"nl":"Hoe wil Calorie inspelen op de behoeften van de voedings- en drankensector?","es":"¿Cómo pretende Calorie atender las necesidades del sector alimentario?","fr":"Comment Calorie prévoit-il de répondre aux besoins du secteur alimentaire ?","pt":"Como o Calorie pretende responder às necessidades do setor alimentar?","id":"Bagaimana Calorie berencana memenuhi kebutuhan industri makanan dan minuman?","zh-Hans":"Calorie 计划如何满足食品饮料行业的需求?","hi":"Calorie खाद्य और पेय उद्योग की जरूरतें कैसे पूरी करना चाहता है?","ar":"كيف يخطط Calorie لتلبية احتياجات قطاع الأغذية والمشروبات؟","bn":"Calorie খাদ্য ও পানীয় শিল্পের প্রয়োজন কীভাবে মেটাতে চায়?","ur":"Calorie خوراک اور مشروبات کی صنعت کی ضروریات کیسے پوری کرنا چاہتا ہے؟"},"pages":[6855]},{"source":"The use-case illustrations explore delivery, shops, hospitality and supply chains. They describe concepts. Ordering, merchant payments and token-based food assistance would require further development and participating organisations.","translations":{"nl":"De gebruiksvoorbeelden verkennen bezorging, winkels, horeca en toeleveringsketens. Het zijn concepten. Bestellen, winkelbetalingen en voedselhulp met tokens vragen verdere ontwikkeling en deelnemende organisaties.","es":"Las ilustraciones de casos de uso exploran entregas, tiendas, hostelería y cadenas de suministro. Describen conceptos. Los pedidos, pagos comerciales y ayudas alimentarias con tokens requieren más desarrollo y organizaciones participantes.","fr":"Les illustrations explorent la livraison, les commerces, la restauration et les chaînes d’approvisionnement. Elles décrivent des concepts. Les commandes, les paiements marchands et l’aide alimentaire en jetons nécessiteraient davantage de développement et des organismes participants.","pt":"As ilustrações exploram entregas, lojas, restauração e cadeias de abastecimento. Descrevem conceitos. Encomendas, pagamentos comerciais e apoio alimentar com tokens exigiriam mais desenvolvimento e organizações participantes.","id":"Ilustrasi penggunaan menjajaki pengiriman, toko, usaha kuliner, dan rantai pasok. Semuanya merupakan konsep. Pemesanan, pembayaran pedagang, dan bantuan pangan berbasis token memerlukan pengembangan lanjutan serta organisasi yang berpartisipasi.","zh-Hans":"使用场景插图探索配送、商店、餐饮及供应链。这些是概念。订购、商家支付和基于代币的食品援助,需要进一步开发和相关组织参与。","hi":"उपयोग के चित्र डिलीवरी, दुकानों, आतिथ्य और आपूर्ति श्रृंखलाओं की संभावनाएँ दिखाते हैं। ये अवधारणाएँ हैं। ऑर्डर, व्यापारी भुगतान और टोकन आधारित खाद्य सहायता के लिए और विकास तथा सहभागी संस्थाओं की जरूरत होगी।","ar":"تستكشف رسوم حالات الاستخدام التوصيل والمتاجر والضيافة وسلاسل الإمداد. وهي تصف مفاهيم. تتطلب الطلبات ومدفوعات التجار والمساعدات الغذائية بالرموز مزيدًا من التطوير ومشاركة مؤسسات.","bn":"ব্যবহারের ছবিগুলো ডেলিভারি, দোকান, আতিথেয়তা ও সরবরাহ ব্যবস্থার সম্ভাবনা দেখায়। এগুলো ধারণা। অর্ডার, ব্যবসায়িক পেমেন্ট ও টোকেনভিত্তিক খাদ্যসহায়তার জন্য আরও উন্নয়ন এবং অংশগ্রহণকারী প্রতিষ্ঠান প্রয়োজন।","ur":"استعمال کی تصاویر ترسیل، دکانوں، مہمان نوازی اور سپلائی چین کے امکانات دکھاتی ہیں۔ یہ تصورات ہیں۔ آرڈر، تاجروں کو ادائیگی اور ٹوکن پر مبنی غذائی مدد کے لیے مزید ترقی اور شریک ادارے درکار ہوں گے۔"},"pages":[6855]},{"source":"How does Calorie plan to integrate payment applications for companies and consumers?","translations":{"nl":"Hoe wil Calorie betaaltoepassingen voor bedrijven en consumenten integreren?","es":"¿Cómo pretende Calorie integrar pagos para empresas y consumidores?","fr":"Comment Calorie prévoit-il d’intégrer les paiements pour les entreprises et les particuliers ?","pt":"Como o Calorie pretende integrar pagamentos para empresas e consumidores?","id":"Bagaimana Calorie berencana mengintegrasikan pembayaran untuk bisnis dan konsumen?","zh-Hans":"Calorie 计划如何为企业和消费者集成支付应用?","hi":"Calorie व्यवसायों और उपभोक्ताओं के लिए भुगतान कैसे एकीकृत करना चाहता है?","ar":"كيف يخطط Calorie لدمج تطبيقات الدفع للشركات والمستهلكين؟","bn":"Calorie ব্যবসা ও ভোক্তাদের পেমেন্ট কীভাবে সমন্বয় করতে চায়?","ur":"Calorie کاروباروں اور صارفین کے لیے ادائیگیوں کو کیسے یکجا کرنا چاہتا ہے؟"},"pages":[6855]},{"source":"Point-of-sale and business payment integrations remain possible future developments. They are not available as merchant services in the current CalorieApp preview.","translations":{"nl":"Kassa-integraties en zakelijke betaalintegraties zijn mogelijke toekomstige ontwikkelingen. Ze zijn niet beschikbaar als diensten voor ondernemers in de huidige CalorieApp-preview.","es":"Las integraciones de puntos de venta y pagos empresariales siguen siendo posibles desarrollos futuros. No están disponibles como servicios comerciales en la vista previa actual de CalorieApp.","fr":"Les intégrations aux points de vente et aux paiements professionnels restent des développements futurs possibles. Elles ne sont pas proposées comme services marchands dans l’aperçu actuel de CalorieApp.","pt":"As integrações com pontos de venda e pagamentos empresariais continuam sendo possíveis desenvolvimentos futuros. Não estão disponíveis como serviços comerciais na prévia atual do CalorieApp.","id":"Integrasi kasir dan pembayaran bisnis masih merupakan kemungkinan pengembangan masa depan. Layanan pedagang tersebut belum tersedia dalam pratinjau CalorieApp saat ini.","zh-Hans":"销售点及企业支付集成仍是未来可能的开发方向。目前的 CalorieApp 预览版不提供这些商家服务。","hi":"बिक्री केंद्र और व्यावसायिक भुगतान एकीकरण भविष्य के संभावित विकास हैं। वर्तमान CalorieApp प्रीव्यू में ये व्यापारी सेवाओं के रूप में उपलब्ध नहीं हैं।","ar":"تظل تكاملات نقاط البيع ومدفوعات الشركات تطورات مستقبلية محتملة. ولا تتوفر كخدمات للتجار في معاينة CalorieApp الحالية.","bn":"বিক্রয়কেন্দ্র ও ব্যবসায়িক পেমেন্টের সমন্বয় ভবিষ্যতের সম্ভাব্য উন্নয়ন। CalorieApp-এর বর্তমান প্রিভিউতে এগুলো ব্যবসায়ীদের পরিষেবা হিসেবে উপলব্ধ নয়।","ur":"فروخت کے مراکز اور کاروباری ادائیگیوں کا انضمام مستقبل کی ممکنہ پیش رفت ہے۔ موجودہ CalorieApp پیشگی جھلک میں یہ تاجروں کی خدمات کے طور پر دستیاب نہیں۔"},"pages":[6855]},{"source":"What is Calorie's vision for the future?","translations":{"nl":"Wat is de toekomstvisie van Calorie?","es":"¿Cuál es la visión de futuro de Calorie?","fr":"Quelle est la vision de Calorie pour l’avenir ?","pt":"Qual é a visão do Calorie para o futuro?","id":"Apa visi Calorie untuk masa depan?","zh-Hans":"Calorie 对未来的愿景是什么?","hi":"भविष्य के लिए Calorie की दृष्टि क्या है?","ar":"ما رؤية Calorie للمستقبل؟","bn":"ভবিষ্যৎ নিয়ে Calorie-এর পরিকল্পনা কী?","ur":"مستقبل کے لیے Calorie کا تصور کیا ہے؟"},"pages":[6855]},{"source":"The wider vision includes useful food data tools and possible payment applications for consumers and businesses. The roadmap separates current work from longer-term ideas; future adoption is not guaranteed.","translations":{"nl":"De bredere visie omvat nuttige hulpmiddelen voor voedingsgegevens en mogelijke betaaltoepassingen voor consumenten en bedrijven. De roadmap onderscheidt het huidige werk van ideeën voor de langere termijn; toekomstig gebruik is niet gegarandeerd.","es":"La visión más amplia incluye herramientas útiles de datos alimentarios y posibles aplicaciones de pago para consumidores y empresas. La hoja de ruta separa el trabajo actual de las ideas a largo plazo; no se garantiza su adopción futura.","fr":"La vision élargie comprend des outils utiles de données alimentaires et de possibles applications de paiement pour les particuliers et les entreprises. La feuille de route distingue les travaux actuels des idées à long terme ; leur adoption future n’est pas garantie.","pt":"A visão mais ampla inclui ferramentas úteis de dados alimentares e possíveis aplicações de pagamento para consumidores e empresas. O roteiro separa o trabalho atual de ideias de longo prazo; a adoção futura não é garantida.","id":"Visi yang lebih luas mencakup alat data makanan yang bermanfaat dan kemungkinan aplikasi pembayaran bagi konsumen dan bisnis. Peta jalan memisahkan pekerjaan saat ini dari gagasan jangka panjang; adopsi di masa depan tidak dijamin.","zh-Hans":"更广泛的愿景包括实用的食品数据工具,以及面向消费者和企业的潜在支付应用。路线图区分了当前工作和长期构想;未来的采用情况没有保证。","hi":"व्यापक दृष्टि में उपयोगी खाद्य डेटा साधन तथा उपभोक्ताओं और व्यवसायों के लिए संभावित भुगतान अनुप्रयोग शामिल हैं। रोडमैप वर्तमान काम और दीर्घकालिक विचारों में अंतर करता है; भविष्य में अपनाए जाने की गारंटी नहीं है।","ar":"تشمل الرؤية الأوسع أدوات مفيدة للبيانات الغذائية وتطبيقات دفع محتملة للمستهلكين والشركات. تفصل خارطة الطريق العمل الحالي عن الأفكار طويلة الأجل؛ ولا يوجد ضمان للتبني المستقبلي.","bn":"বৃহত্তর পরিকল্পনায় উপকারী খাদ্যতথ্য সরঞ্জাম এবং ভোক্তা ও ব্যবসার জন্য সম্ভাব্য পেমেন্ট অ্যাপ রয়েছে। রোডম্যাপ বর্তমান কাজ ও দীর্ঘমেয়াদি ধারণা আলাদা করে; ভবিষ্যৎ গ্রহণযোগ্যতা নিশ্চিত নয়।","ur":"وسیع تر تصور میں مفید غذائی ڈیٹا کی سہولتیں اور صارفین و کاروباروں کے لیے ممکنہ ادائیگی ایپس شامل ہیں۔ روڈمیپ موجودہ کام اور طویل مدتی خیالات میں فرق کرتا ہے؛ مستقبل میں قبولیت کی ضمانت نہیں۔"},"pages":[6855]},{"source":"How can Calorie contribute to a healthier financial lifestyle?","translations":{"nl":"Hoe kan Calorie bijdragen aan een gezondere financiële leefstijl?","es":"¿Cómo puede Calorie contribuir a un estilo de vida financiero más saludable?","fr":"Comment Calorie peut-il contribuer à un mode de vie financier plus sain ?","pt":"Como o Calorie pode contribuir para uma vida financeira mais saudável?","id":"Bagaimana Calorie dapat membantu gaya hidup keuangan yang lebih sehat?","zh-Hans":"Calorie 如何帮助实现更健康的财务生活方式?","hi":"Calorie स्वस्थ वित्तीय जीवनशैली में कैसे मदद कर सकता है?","ar":"كيف يمكن أن يسهم Calorie في نمط حياة مالي أكثر صحة؟","bn":"Calorie কীভাবে স্বাস্থ্যকর আর্থিক জীবনযাপনে সাহায্য করতে পারে?","ur":"Calorie صحت مند مالی طرزِ زندگی میں کیسے مدد دے سکتا ہے؟"},"pages":[6855]},{"source":"CalorieApp helps users explore nutrition information and keep a personal food log. Combined payment and expense tracking belongs to the earlier broader vision and is not a feature of the current preview.","translations":{"nl":"CalorieApp helpt gebruikers voedingsinformatie te verkennen en een persoonlijk voedingsdagboek bij te houden. Gecombineerde betaal- en uitgavenregistratie hoort bij de eerdere, bredere visie en is geen functie van de huidige preview.","es":"CalorieApp ayuda a consultar información nutricional y llevar un diario alimentario personal. El seguimiento conjunto de pagos y gastos pertenece a la visión anterior más amplia y no es una función de la vista previa actual.","fr":"CalorieApp aide à explorer les informations nutritionnelles et à tenir un journal alimentaire personnel. Le suivi combiné des paiements et des dépenses relève de la vision élargie antérieure et ne fait pas partie de l’aperçu actuel.","pt":"O CalorieApp ajuda a explorar informações nutricionais e manter um diário alimentar pessoal. O acompanhamento conjunto de pagamentos e despesas pertence à visão anterior mais ampla e não é uma funcionalidade da prévia atual.","id":"CalorieApp membantu pengguna menjelajahi informasi gizi dan membuat catatan makanan pribadi. Pencatatan gabungan pembayaran dan pengeluaran termasuk visi terdahulu yang lebih luas, bukan fitur pratinjau saat ini.","zh-Hans":"CalorieApp 帮助用户了解营养信息并记录个人饮食。将支付与支出记录结合属于早期更广泛的愿景,并非当前预览版的功能。","hi":"CalorieApp पोषण की जानकारी समझने और व्यक्तिगत भोजन डायरी रखने में मदद करता है। भुगतान और खर्च का संयुक्त रिकॉर्ड पहले की व्यापक दृष्टि का हिस्सा है, वर्तमान प्रीव्यू का फीचर नहीं।","ar":"يساعد CalorieApp المستخدمين على استكشاف المعلومات الغذائية والاحتفاظ بسجل غذائي شخصي. ينتمي تتبع المدفوعات والنفقات معًا إلى الرؤية السابقة الأوسع، وليس ميزة في المعاينة الحالية.","bn":"CalorieApp পুষ্টির তথ্য বুঝতে ও ব্যক্তিগত খাদ্য ডায়েরি রাখতে সাহায্য করে। পেমেন্ট ও ব্যয়ের সম্মিলিত হিসাব আগের বৃহত্তর পরিকল্পনার অংশ; বর্তমান প্রিভিউয়ের বৈশিষ্ট্য নয়।","ur":"CalorieApp غذائی معلومات سمجھنے اور ذاتی خوراک ڈائری رکھنے میں مدد دیتا ہے۔ ادائیگی اور اخراجات کا مشترکہ ریکارڈ پہلے کے وسیع تصور کا حصہ ہے، موجودہ پیشگی جھلک کا فیچر نہیں۔"},"pages":[6855]},{"source":"How does Calorie contribute to corporate social responsibility?","translations":{"nl":"Hoe draagt Calorie bij aan maatschappelijk verantwoord ondernemen?","es":"¿Cómo contribuye Calorie a la responsabilidad social empresarial?","fr":"Comment Calorie contribue-t-il à la responsabilité sociale des entreprises ?","pt":"Como o Calorie contribui para a responsabilidade social empresarial?","id":"Bagaimana Calorie berkontribusi pada tanggung jawab sosial perusahaan?","zh-Hans":"Calorie 如何履行企业社会责任?","hi":"Calorie सामाजिक उत्तरदायित्व में कैसे योगदान करता है?","ar":"كيف يسهم Calorie في المسؤولية الاجتماعية للشركات؟","bn":"Calorie সামাজিক দায়বদ্ধতায় কীভাবে অবদান রাখে?","ur":"Calorie سماجی ذمہ داری میں کیسے حصہ ڈالتا ہے؟"},"pages":[6855]},{"source":"Responsible food information and practical tools are project goals. Using a ledger alone does not establish a measurable social or environmental benefit; those outcomes need evidence from an implemented use case.","translations":{"nl":"Verantwoorde voedingsinformatie en praktische hulpmiddelen zijn projectdoelen. Alleen een ledger gebruiken bewijst geen meetbare maatschappelijke of milieuwinst; daarvoor is bewijs uit een uitgevoerde toepassing nodig.","es":"La información alimentaria responsable y las herramientas prácticas son objetivos del proyecto. Utilizar un registro por sí solo no demuestra beneficios sociales o ambientales medibles; hacen falta pruebas de un caso de uso implementado.","fr":"Une information alimentaire responsable et des outils pratiques sont des objectifs du projet. L’utilisation d’un registre ne démontre pas à elle seule un bénéfice social ou environnemental mesurable ; des preuves issues d’un cas d’usage réalisé sont nécessaires.","pt":"Informação alimentar responsável e ferramentas práticas são objetivos do projeto. Usar um registo, por si só, não demonstra benefícios sociais ou ambientais mensuráveis; são necessárias evidências de um caso de uso implementado.","id":"Informasi pangan yang bertanggung jawab dan alat praktis adalah tujuan proyek. Penggunaan ledger saja tidak membuktikan manfaat sosial atau lingkungan yang terukur; hasil tersebut memerlukan bukti dari penerapan nyata.","zh-Hans":"负责任的食品信息和实用工具是项目目标。仅仅使用账本不能证明可量化的社会或环境效益;这些结果需要来自已实施应用的证据。","hi":"जिम्मेदार खाद्य जानकारी और व्यावहारिक साधन परियोजना के लक्ष्य हैं। केवल लेजर का उपयोग मापने योग्य सामाजिक या पर्यावरणीय लाभ सिद्ध नहीं करता; ऐसे परिणामों के लिए लागू उपयोग से प्रमाण चाहिए।","ar":"المعلومات الغذائية المسؤولة والأدوات العملية من أهداف المشروع. لا يثبت استخدام سجل وحده فائدة اجتماعية أو بيئية قابلة للقياس؛ بل تحتاج هذه النتائج إلى أدلة من حالة استخدام مطبقة.","bn":"দায়িত্বশীল খাদ্যতথ্য ও ব্যবহারিক সরঞ্জাম প্রকল্পের লক্ষ্য। শুধু লেজার ব্যবহার করলেই পরিমাপযোগ্য সামাজিক বা পরিবেশগত উপকার প্রমাণিত হয় না; বাস্তবায়িত ব্যবহার থেকে তার প্রমাণ দরকার।","ur":"ذمہ دارانہ غذائی معلومات اور عملی سہولتیں منصوبے کے اہداف ہیں۔ صرف لیجر استعمال کرنا قابلِ پیمائش سماجی یا ماحولیاتی فائدہ ثابت نہیں کرتا؛ اس کے لیے نافذ شدہ استعمال سے ثبوت چاہیے۔"},"pages":[6855]},{"source":"How can governments use Calorie to assist citizens' basic needs?","translations":{"nl":"Hoe kunnen overheden Calorie gebruiken om burgers te helpen met hun basisbehoeften?","es":"¿Cómo pueden los gobiernos utilizar Calorie para cubrir necesidades básicas?","fr":"Comment les pouvoirs publics peuvent-ils utiliser Calorie pour répondre aux besoins essentiels ?","pt":"Como os governos podem usar o Calorie para apoiar necessidades básicas?","id":"Bagaimana pemerintah dapat menggunakan Calorie untuk membantu kebutuhan dasar warga?","zh-Hans":"政府如何利用 Calorie 帮助满足居民的基本需求?","hi":"सरकारें नागरिकों की बुनियादी जरूरतों के लिए Calorie का उपयोग कैसे कर सकती हैं?","ar":"كيف يمكن للحكومات استخدام Calorie لدعم الاحتياجات الأساسية للمواطنين؟","bn":"নাগরিকদের মৌলিক প্রয়োজনে সরকার কীভাবে Calorie ব্যবহার করতে পারে?","ur":"حکومتیں شہریوں کی بنیادی ضروریات کے لیے Calorie کیسے استعمال کر سکتی ہیں؟"},"pages":[6855]},{"source":"Token-based food assistance is a proposed use case. This page does not announce an operating government programme or an accepted food-benefit payment service.","translations":{"nl":"Voedselhulp met tokens is een voorgestelde toepassing. Deze pagina kondigt geen werkend overheidsprogramma of geaccepteerde betaaldienst voor voedselhulp aan.","es":"La ayuda alimentaria mediante tokens es un caso de uso propuesto. Esta página no anuncia un programa gubernamental operativo ni un servicio de pago aceptado para prestaciones alimentarias.","fr":"L’aide alimentaire en jetons est un cas d’usage proposé. Cette page n’annonce aucun programme public opérationnel ni service de paiement accepté pour l’aide alimentaire.","pt":"O apoio alimentar baseado em tokens é um caso de uso proposto. Esta página não anuncia um programa governamental em funcionamento nem um serviço de pagamento aceite para benefícios alimentares.","id":"Bantuan pangan berbasis token merupakan usulan penggunaan. Halaman ini tidak mengumumkan program pemerintah yang beroperasi atau layanan pembayaran bantuan pangan yang diterima.","zh-Hans":"基于代币的食品援助是一项提议。本页没有宣布正在运行的政府计划,也没有宣布已被接受的食品福利支付服务。","hi":"टोकन आधारित खाद्य सहायता एक प्रस्तावित उपयोग है। यह पृष्ठ किसी चालू सरकारी कार्यक्रम या स्वीकृत खाद्य सहायता भुगतान सेवा की घोषणा नहीं करता।","ar":"المساعدة الغذائية القائمة على الرموز حالة استخدام مقترحة. لا تعلن هذه الصفحة عن برنامج حكومي يعمل بالفعل أو خدمة دفع مقبولة للمزايا الغذائية.","bn":"টোকেনভিত্তিক খাদ্যসহায়তা একটি প্রস্তাবিত ব্যবহার। এই পৃষ্ঠা কোনো চালু সরকারি কর্মসূচি বা গৃহীত খাদ্যসহায়তা পেমেন্ট পরিষেবা ঘোষণা করে না।","ur":"ٹوکن پر مبنی غذائی مدد ایک مجوزہ استعمال ہے۔ یہ صفحہ کسی فعال سرکاری پروگرام یا قبول شدہ غذائی امداد کی ادائیگی سروس کا اعلان نہیں کرتا۔"},"pages":[6855]},{"source":"What is CalorieApp, and how will it enhance the F&B experience?","translations":{"nl":"Wat is CalorieApp en hoe verbetert het de ervaring rond voeding en dranken?","es":"¿Qué es CalorieApp y cómo mejorará la experiencia alimentaria?","fr":"Qu’est-ce que CalorieApp et comment améliorera-t-il l’expérience alimentaire ?","pt":"O que é o CalorieApp e como irá melhorar a experiência alimentar?","id":"Apa itu CalorieApp dan bagaimana aplikasi ini meningkatkan pengalaman makanan dan minuman?","zh-Hans":"CalorieApp 是什么?它将如何改善食品饮料体验?","hi":"CalorieApp क्या है और यह खाद्य अनुभव को कैसे बेहतर बनाएगा?","ar":"ما CalorieApp وكيف سيحسن تجربة الأغذية والمشروبات؟","bn":"CalorieApp কী এবং এটি খাদ্য ও পানীয়ের অভিজ্ঞতা কীভাবে উন্নত করবে?","ur":"CalorieApp کیا ہے اور یہ خوراک و مشروبات کا تجربہ کیسے بہتر بنائے گا؟"},"pages":[6855]},{"source":"CalorieApp is the project's browser-based preview for food search, nutrition information and a personal food log, using Open Food Facts. Website sign-in is connected through Xaman. Retail payments, an integrated exchange and business expense management are outside the current preview.","translations":{"nl":"CalorieApp is de browserpreview van het project voor voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek, met Open Food Facts. Inloggen op de website is gekoppeld via Xaman. Winkelbetalingen, een geïntegreerde exchange en zakelijk uitgavenbeheer vallen buiten de huidige preview.","es":"CalorieApp es la vista previa del proyecto en el navegador para buscar alimentos, consultar información nutricional y llevar un diario personal con Open Food Facts. El acceso al sitio está conectado mediante Xaman. Los pagos minoristas, una plataforma de intercambio integrada y la gestión de gastos empresariales quedan fuera de esta versión.","fr":"CalorieApp est l’aperçu du projet dans le navigateur pour rechercher des aliments, consulter leur valeur nutritionnelle et tenir un journal personnel avec Open Food Facts. La connexion au site est reliée par Xaman. Les paiements en magasin, un service d’échange intégré et la gestion des dépenses professionnelles ne font pas partie de cet aperçu.","pt":"O CalorieApp é a prévia do projeto no navegador para pesquisar alimentos, consultar informação nutricional e manter um diário pessoal com Open Food Facts. O acesso ao site está ligado através do Xaman. Pagamentos no comércio, uma plataforma de troca integrada e gestão de despesas empresariais estão fora desta prévia.","id":"CalorieApp adalah pratinjau proyek berbasis peramban untuk pencarian makanan, informasi gizi, dan catatan makanan pribadi menggunakan Open Food Facts. Login situs terhubung melalui Xaman. Pembayaran ritel, layanan pertukaran terintegrasi, dan pengelolaan biaya bisnis berada di luar pratinjau saat ini.","zh-Hans":"CalorieApp 是项目的浏览器预览版,使用 Open Food Facts 提供食品搜索、营养信息和个人饮食记录。网站登录通过 Xaman 关联。零售支付、内置兑换服务和企业支出管理不属于当前预览版。","hi":"CalorieApp परियोजना का ब्राउज़र प्रीव्यू है, जिसमें Open Food Facts की मदद से भोजन खोज, पोषण जानकारी और व्यक्तिगत भोजन डायरी मिलती है। वेबसाइट का लॉगिन Xaman से जुड़ा है। खुदरा भुगतान, एकीकृत एक्सचेंज और व्यावसायिक खर्च प्रबंधन वर्तमान प्रीव्यू में शामिल नहीं हैं।","ar":"CalorieApp هو معاينة المشروع عبر المتصفح للبحث عن الطعام والمعلومات الغذائية والسجل الغذائي الشخصي باستخدام Open Food Facts. يرتبط تسجيل دخول الموقع عبر Xaman. مدفوعات المتاجر والصرافة المدمجة وإدارة نفقات الشركات ليست ضمن المعاينة الحالية.","bn":"CalorieApp হলো প্রকল্পের ব্রাউজারভিত্তিক প্রিভিউ, যা Open Food Facts দিয়ে খাবার খোঁজা, পুষ্টির তথ্য ও ব্যক্তিগত খাদ্য ডায়েরি দেয়। ওয়েবসাইটে লগইন Xaman-এর মাধ্যমে সংযুক্ত। দোকানে পেমেন্ট, সমন্বিত এক্সচেঞ্জ ও ব্যবসায়িক ব্যয় পরিচালনা বর্তমান প্রিভিউয়ের বাইরে।","ur":"CalorieApp منصوبے کی براؤزر میں پیشگی جھلک ہے، جو Open Food Facts کے ذریعے خوراک کی تلاش، غذائی معلومات اور ذاتی خوراک ڈائری دیتی ہے۔ ویب سائٹ کا لاگ ان Xaman سے منسلک ہے۔ دکانوں کی ادائیگیاں، مربوط ایکسچینج اور کاروباری اخراجات کا انتظام موجودہ پیشگی جھلک میں شامل نہیں۔"},"pages":[6855]},{"source":"Can you explain the phased buildup of CalorieApp's release?","translations":{"nl":"Hoe verloopt de gefaseerde introductie van CalorieApp?","es":"¿Cómo se desarrolla el lanzamiento por fases de CalorieApp?","fr":"Comment se déroule le lancement progressif de CalorieApp ?","pt":"Como funciona o lançamento faseado do CalorieApp?","id":"Bagaimana tahapan peluncuran CalorieApp?","zh-Hans":"CalorieApp 的分阶段发布如何进行?","hi":"CalorieApp चरणों में कैसे जारी किया जा रहा है?","ar":"كيف يتم إطلاق CalorieApp على مراحل؟","bn":"CalorieApp কীভাবে ধাপে ধাপে প্রকাশ করা হচ্ছে?","ur":"CalorieApp کی مرحلہ وار ریلیز کیسے ہوتی ہے؟"},"pages":[6855]},{"source":"The browser preview can already be explored from the CalorieApp page. The current roadmap records eight milestones and their status. Earlier Android and Testnet prototypes remain development history; additional services will need their own implementation and release announcement.","translations":{"nl":"De browserpreview is al te bekijken via de CalorieApp-pagina. De huidige roadmap bevat acht mijlpalen en hun status. Eerdere Android- en Testnet-prototypes blijven onderdeel van de ontwikkelgeschiedenis; extra diensten hebben hun eigen implementatie en aankondiging nodig.","es":"La vista previa en el navegador ya puede explorarse desde la página CalorieApp. La hoja de ruta actual registra ocho hitos y su estado. Los prototipos anteriores de Android y Testnet son parte de la historia del desarrollo; los servicios adicionales requieren su propia implementación y anuncio de lanzamiento.","fr":"L’aperçu dans le navigateur est déjà accessible depuis la page CalorieApp. La feuille de route actuelle présente huit étapes et leur état. Les anciens prototypes Android et Testnet restent des éléments historiques du développement ; les services supplémentaires nécessiteront leur propre réalisation et annonce de lancement.","pt":"A prévia no navegador já pode ser explorada na página CalorieApp. O roteiro atual regista oito marcos e o seu estado. Os protótipos anteriores de Android e Testnet fazem parte da história do desenvolvimento; serviços adicionais precisarão da sua própria implementação e anúncio de lançamento.","id":"Pratinjau peramban sudah dapat dijelajahi melalui halaman CalorieApp. Peta jalan saat ini mencatat delapan tonggak beserta statusnya. Prototipe Android dan Testnet terdahulu tetap menjadi sejarah pengembangan; layanan tambahan memerlukan implementasi dan pengumuman rilis tersendiri.","zh-Hans":"目前可通过 CalorieApp 页面体验浏览器预览版。当前路线图列出八个里程碑及其状态。早期 Android 和 Testnet 原型属于开发历史;新增服务需要各自的实施与发布公告。","hi":"ब्राउज़र प्रीव्यू को अभी CalorieApp पृष्ठ से देखा जा सकता है। वर्तमान रोडमैप में आठ पड़ाव और उनकी स्थिति दर्ज हैं। पुराने Android और Testnet प्रोटोटाइप विकास के इतिहास का हिस्सा हैं; अतिरिक्त सेवाओं का अलग कार्यान्वयन और रिलीज़ घोषणा जरूरी होगी।","ar":"يمكن بالفعل استكشاف معاينة المتصفح من صفحة CalorieApp. تسجل خارطة الطريق الحالية ثماني مراحل وحالتها. تظل نماذج Android وTestnet السابقة جزءًا من تاريخ التطوير؛ وستحتاج الخدمات الإضافية إلى تنفيذها وإعلان إطلاقها الخاص.","bn":"CalorieApp পৃষ্ঠা থেকে এখনই ব্রাউজার প্রিভিউ দেখা যায়। বর্তমান রোডম্যাপে আটটি মাইলফলক ও তাদের অবস্থা আছে। আগের Android ও Testnet প্রোটোটাইপ উন্নয়নের ইতিহাসের অংশ; নতুন পরিষেবার নিজস্ব বাস্তবায়ন ও প্রকাশের ঘোষণা প্রয়োজন হবে।","ur":"براؤزر کی پیشگی جھلک CalorieApp صفحے سے ابھی دیکھی جا سکتی ہے۔ موجودہ روڈمیپ میں آٹھ سنگِ میل اور ان کی حالت درج ہے۔ پرانے Android اور Testnet نمونے ترقی کی تاریخ کا حصہ ہیں؛ اضافی خدمات کی اپنی تکمیل اور ریلیز کا اعلان ضروری ہوگا۔"},"pages":[6855]},{"source":"Donations","translations":{"nl":"Donaties","es":"Donaciones","fr":"Dons","pt":"Donativos","id":"Donasi","zh-Hans":"捐赠","hi":"दान","ar":"التبرعات","bn":"অনুদান","ur":"عطیات"},"pages":[6897,6914,6926,6937]},{"source":"Donation In","translations":{"nl":"Donatie in","es":"Donación en","fr":"Don en","pt":"Donativo em","id":"Donasi dalam","zh-Hans":"捐赠币种","hi":"दान की मुद्रा","ar":"التبرع بعملة","bn":"অনুদানের মুদ্রা","ur":"عطیے کی کرنسی"},"pages":[6897,6914,6926,6937]},{"source":"Free choice of the donation amount in XRP","translations":{"nl":"Kies zelf je donatiebedrag in XRP","es":"Elige tu importe de donación en XRP","fr":"Choisissez le montant de votre don en XRP","pt":"Escolha o valor do seu donativo em XRP","id":"Pilih sendiri jumlah donasi dalam XRP","zh-Hans":"自行选择 XRP 捐赠金额","hi":"XRP में अपना दान मूल्य चुनें","ar":"اختر مبلغ تبرعك بعملة XRP","bn":"XRP-এ অনুদানের পরিমাণ নিজে বেছে নিন","ur":"XRP میں عطیے کی رقم خود منتخب کریں"},"pages":[6897,6914,6926,6937]},{"source":"Free choice of the donation amount in XRP quantity","translations":{"nl":"Aantal voor je donatiebedrag in XRP","es":"Cantidad de la donación en XRP","fr":"Quantité du don en XRP","pt":"Quantidade do donativo em XRP","id":"Kuantitas donasi dalam XRP","zh-Hans":"XRP 捐赠数量","hi":"XRP दान की संख्या","ar":"كمية التبرع بعملة XRP","bn":"XRP অনুদানের সংখ্যা","ur":"XRP عطیے کی تعداد"},"pages":[6897,6914,6926,6937]},{"source":"Donate to CalorieToken","translations":{"nl":"Doneer aan CalorieToken","es":"Donar a CalorieToken","fr":"Faire un don à CalorieToken","pt":"Doar ao CalorieToken","id":"Donasi ke CalorieToken","zh-Hans":"向 CalorieToken 捐赠","hi":"CalorieToken को दान दें","ar":"تبرع لـ CalorieToken","bn":"CalorieToken-কে অনুদান দিন","ur":"CalorieToken کو عطیہ دیں"},"pages":[6897,6914,6926,6937]},{"source":"Add Your Donation","translations":{"nl":"Voeg je donatie toe","es":"Añadir tu donación","fr":"Ajouter votre don","pt":"Adicionar o seu donativo","id":"Tambahkan donasi Anda","zh-Hans":"添加捐赠","hi":"अपना दान जोड़ें","ar":"أضف تبرعك","bn":"আপনার অনুদান যোগ করুন","ur":"اپنا عطیہ شامل کریں"},"pages":[6897,6914,6926,6937]},{"source":"No products in the cart.","translations":{"nl":"Geen producten in de winkelwagen.","es":"No hay productos en el carrito.","fr":"Aucun produit dans le panier.","pt":"Não há produtos no carrinho.","id":"Tidak ada produk dalam keranjang.","zh-Hans":"购物车中没有商品。","hi":"कार्ट में कोई उत्पाद नहीं है।","ar":"لا توجد منتجات في السلة.","bn":"কার্টে কোনো পণ্য নেই।","ur":"کارٹ میں کوئی چیز نہیں۔"},"pages":[6897,6914,6926,6937]},{"source":"No Donations Yet","translations":{"nl":"Nog geen donaties","es":"Todavía no hay donaciones","fr":"Aucun don pour le moment","pt":"Ainda não há donativos","id":"Belum ada donasi","zh-Hans":"暂无捐赠","hi":"अभी कोई दान नहीं","ar":"لا توجد تبرعات بعد","bn":"এখনো কোনো অনুদান নেই","ur":"ابھی کوئی عطیہ نہیں"},"pages":[6897,6914,6926,6937]},{"source":"Choose your donation amount in XRP. You can review the amount and quantity in your cart before continuing to payment.","translations":{"nl":"Kies je donatiebedrag in XRP. Je kunt het bedrag en het aantal in je winkelwagen controleren voordat je doorgaat naar de betaling.","es":"Elige el importe de tu donación en XRP. Puedes revisar el importe y la cantidad en el carrito antes de continuar al pago.","fr":"Choisissez le montant de votre don en XRP. Vous pourrez vérifier le montant et la quantité dans le panier avant de passer au paiement.","pt":"Escolha o valor do donativo em XRP. Pode verificar o valor e a quantidade no carrinho antes de continuar para o pagamento.","id":"Pilih jumlah donasi dalam XRP. Anda dapat memeriksa jumlah dan kuantitas di keranjang sebelum melanjutkan pembayaran.","zh-Hans":"选择您的 XRP 捐赠金额。继续付款前,可在购物车中核对金额和数量。","hi":"XRP में दान की राशि चुनें। भुगतान करने से पहले आप कार्ट में राशि और संख्या जाँच सकते हैं।","ar":"اختر مبلغ تبرعك بعملة XRP. يمكنك مراجعة المبلغ والكمية في السلة قبل المتابعة إلى الدفع.","bn":"XRP-এ অনুদানের পরিমাণ বেছে নিন। পেমেন্টের আগে কার্টে পরিমাণ ও সংখ্যা যাচাই করতে পারবেন।","ur":"XRP میں عطیے کی رقم منتخب کریں۔ ادائیگی سے پہلے آپ کارٹ میں رقم اور تعداد چیک کر سکتے ہیں۔"},"pages":[6897,6914,6926,6937]},{"source":"Voluntary contributions support work on CalorieApp and the wider CalorieToken project, including improvements to the food and nutrition experience.","translations":{"nl":"Vrijwillige bijdragen ondersteunen het werk aan CalorieApp en het bredere CalorieToken-project, waaronder verbeteringen aan de ervaring rond voeding en voedingsinformatie.","es":"Las contribuciones voluntarias apoyan el trabajo en CalorieApp y el proyecto CalorieToken, incluidas mejoras en la experiencia alimentaria y nutricional.","fr":"Les contributions volontaires soutiennent CalorieApp et le projet CalorieToken, notamment l’amélioration de l’expérience alimentaire et nutritionnelle.","pt":"As contribuições voluntárias apoiam o trabalho no CalorieApp e no projeto CalorieToken, incluindo melhorias na experiência alimentar e nutricional.","id":"Kontribusi sukarela mendukung pengembangan CalorieApp dan proyek CalorieToken, termasuk perbaikan pengalaman terkait makanan dan gizi.","zh-Hans":"自愿捐赠支持 CalorieApp 及更广泛的 CalorieToken 项目,包括改善食品与营养体验。","hi":"स्वैच्छिक योगदान CalorieApp और व्यापक CalorieToken परियोजना का समर्थन करते हैं, जिसमें भोजन और पोषण के अनुभव को बेहतर बनाना शामिल है।","ar":"تدعم المساهمات الطوعية العمل على CalorieApp ومشروع CalorieToken الأوسع، بما في ذلك تحسين تجربة الطعام والمعلومات الغذائية.","bn":"স্বেচ্ছা অনুদান CalorieApp ও বৃহত্তর CalorieToken প্রকল্পের কাজকে সাহায্য করে, যার মধ্যে খাদ্য ও পুষ্টির অভিজ্ঞতা উন্নত করাও রয়েছে।","ur":"رضاکارانہ تعاون CalorieApp اور وسیع تر CalorieToken منصوبے کے کام کی مدد کرتا ہے، جس میں خوراک اور غذائیت کے تجربے میں بہتری شامل ہے۔"},"pages":[6897,6914,6926,6937]},{"source":"Disclaimer: While your generous support drives our efforts, we cannot guarantee specific outcomes or features as a result of donations. Your contribution helps advance our mission, but the nature and timeline of developments remain subject to various factors and unforeseen circumstances.","translations":{"nl":"Toelichting: je steun helpt ons werk, maar een donatie garandeert geen specifieke resultaten of functies. Je bijdrage ondersteunt onze missie; de aard en planning van ontwikkelingen blijven afhankelijk van uiteenlopende factoren en onvoorziene omstandigheden.","es":"Aviso: tu apoyo impulsa nuestro trabajo, pero las donaciones no garantizan resultados ni funciones concretas. Tu contribución ayuda a nuestra misión; la naturaleza y los plazos de los desarrollos dependen de diversos factores y circunstancias imprevistas.","fr":"Précision : votre soutien aide notre travail, mais les dons ne garantissent aucun résultat ni aucune fonction précise. Votre contribution soutient notre mission ; la nature et le calendrier des développements dépendent de différents facteurs et de circonstances imprévues.","pt":"Aviso: o seu apoio ajuda o nosso trabalho, mas os donativos não garantem resultados ou funcionalidades específicas. A sua contribuição apoia a nossa missão; a natureza e os prazos dos desenvolvimentos dependem de vários fatores e circunstâncias imprevistas.","id":"Keterangan: dukungan Anda membantu pekerjaan kami, tetapi donasi tidak menjamin hasil atau fitur tertentu. Kontribusi Anda mendukung misi kami; sifat dan jadwal pengembangan bergantung pada berbagai faktor serta keadaan tak terduga.","zh-Hans":"说明:您的支持有助于我们的工作,但捐赠不保证特定成果或功能。您的捐赠支持我们的使命;开发内容和时间安排仍取决于各种因素及不可预见的情况。","hi":"स्पष्टीकरण: आपका सहयोग हमारे काम में मदद करता है, लेकिन दान से किसी खास परिणाम या फीचर की गारंटी नहीं मिलती। आपका योगदान हमारे मिशन को आगे बढ़ाता है; विकास का स्वरूप और समय विभिन्न कारकों तथा अप्रत्याशित परिस्थितियों पर निर्भर रहता है।","ar":"توضيح: يساعد دعمك عملنا، لكن التبرعات لا تضمن نتائج أو ميزات محددة. تسهم مساهمتك في مهمتنا؛ وتبقى طبيعة التطوير ومواعيده مرتبطة بعوامل مختلفة وظروف غير متوقعة.","bn":"ব্যাখ্যা: আপনার সহযোগিতা আমাদের কাজে সাহায্য করে, কিন্তু অনুদান নির্দিষ্ট ফলাফল বা বৈশিষ্ট্যের নিশ্চয়তা দেয় না। আপনার অবদান আমাদের লক্ষ্যকে এগিয়ে নেয়; উন্নয়নের ধরন ও সময় বিভিন্ন বিষয় এবং অপ্রত্যাশিত পরিস্থিতির ওপর নির্ভর করে।","ur":"وضاحت: آپ کا تعاون ہمارے کام میں مدد دیتا ہے، لیکن عطیات کسی خاص نتیجے یا فیچر کی ضمانت نہیں دیتے۔ آپ کی شراکت ہمارے مقصد کی مدد کرتی ہے؛ ترقی کی نوعیت اور وقت مختلف عوامل اور غیر متوقع حالات پر منحصر رہتے ہیں۔"},"pages":[6897,6914,6926,6937]},{"source":"Discover CalorieApp","translations":{"nl":"Ontdek CalorieApp","es":"Descubre CalorieApp","fr":"Découvrez CalorieApp","pt":"Descubra o CalorieApp","id":"Jelajahi CalorieApp","zh-Hans":"探索 CalorieApp","hi":"CalorieApp देखें","ar":"اكتشف CalorieApp","bn":"CalorieApp দেখুন","ur":"CalorieApp دیکھیں"},"pages":[5580,6897,6914,6926,6937]},{"source":"Search food, explore nutrition and keep your own food diary. Try the current webapp.","translations":{"nl":"Zoek voeding, bekijk voedingsinformatie en houd je eigen voedingsdagboek bij. Probeer de huidige webapp.","es":"Busca alimentos, consulta su información nutricional y lleva tu diario alimentario. Prueba la aplicación web actual.","fr":"Recherchez des aliments, consultez leur valeur nutritionnelle et tenez votre journal alimentaire. Essayez l’application web actuelle.","pt":"Pesquise alimentos, consulte informação nutricional e mantenha o seu diário alimentar. Experimente a aplicação web atual.","id":"Cari makanan, lihat informasi gizi, dan buat catatan makanan pribadi. Coba aplikasi web saat ini.","zh-Hans":"搜索食品、了解营养并记录个人饮食。体验当前的网页应用。","hi":"भोजन खोजें, पोषण जानें और अपनी भोजन डायरी रखें। वर्तमान वेब ऐप आज़माएँ।","ar":"ابحث عن الطعام واستكشف التغذية واحتفظ بسجلك الغذائي. جرّب تطبيق الويب الحالي.","bn":"খাবার খুঁজুন, পুষ্টি সম্পর্কে জানুন এবং নিজের খাদ্য ডায়েরি রাখুন। বর্তমান ওয়েব অ্যাপটি ব্যবহার করুন।","ur":"خوراک تلاش کریں، غذائی معلومات جانیں اور اپنی خوراک ڈائری رکھیں۔ موجودہ ویب ایپ آزمائیں۔"},"pages":[5580,6897,6914,6926,6937]},{"source":"Historical video · watch on YouTube","translations":{"nl":"Historische video · bekijk op YouTube","es":"Vídeo histórico · ver en YouTube","fr":"Vidéo historique · regarder sur YouTube","pt":"Vídeo histórico · ver no YouTube","id":"Video lama · tonton di YouTube","zh-Hans":"历史视频 · 在 YouTube 观看","hi":"पुराना वीडियो · YouTube पर देखें","ar":"فيديو تاريخي · شاهد على YouTube","bn":"পুরোনো ভিডিও · YouTube-এ দেখুন","ur":"پرانا ویڈیو · YouTube پر دیکھیں"},"pages":[5580,6897,6914,6926,6937]},{"source":"Instructions for XRPToolkit:","translations":{"nl":"Instructies voor XRP Toolkit:","es":"Instrucciones para XRP Toolkit:","fr":"Instructions pour XRP Toolkit :","pt":"Instruções para XRP Toolkit:","id":"Petunjuk XRP Toolkit:","zh-Hans":"XRP Toolkit 使用说明:","hi":"XRP Toolkit के निर्देश:","ar":"تعليمات XRP Toolkit:","bn":"XRP Toolkit-এর নির্দেশনা:","ur":"XRP Toolkit کی ہدایات:"},"pages":[1205,6897,6914,6926,6937]},{"source":"Issuer Account:","translations":{"nl":"Uitgeversaccount:","es":"Cuenta del emisor:","fr":"Compte de l’émetteur :","pt":"Conta do emissor:","id":"Akun penerbit:","zh-Hans":"发行方账户:","hi":"जारीकर्ता का खाता:","ar":"حساب الجهة المصدرة:","bn":"ইস্যুকারীর অ্যাকাউন্ট:","ur":"جاری کنندہ کا اکاؤنٹ:"},"pages":[1205,6897,6914,6926,6937]},{"source":"Currency (HEX-code):","translations":{"nl":"Valuta (HEX-code):","es":"Moneda (código HEX):","fr":"Devise (code HEX) :","pt":"Moeda (código HEX):","id":"Mata uang (kode HEX):","zh-Hans":"货币(HEX 编码):","hi":"मुद्रा (HEX कोड):","ar":"العملة (رمز HEX):","bn":"মুদ্রা (HEX কোড):","ur":"کرنسی (HEX کوڈ):"},"pages":[1205,6897,6914,6926,6937]},{"source":"Limit:","translations":{"nl":"Limiet:","es":"Límite:","fr":"Limite :","pt":"Limite:","id":"Batas:","zh-Hans":"限额:","hi":"सीमा:","ar":"الحد:","bn":"সীমা:","ur":"حد:"},"pages":[1205,6897,6914,6926,6937]},{"source":"Set your trustline with Xaman:","translations":{"nl":"Stel je trustline in met Xaman:","es":"Configura tu trustline con Xaman:","fr":"Configurez votre trustline avec Xaman :","pt":"Configure a sua trustline com Xaman:","id":"Atur trustline Anda dengan Xaman:","zh-Hans":"使用 Xaman 设置信任线:","hi":"Xaman से अपनी ट्रस्टलाइन सेट करें:","ar":"أنشئ خط الثقة باستخدام Xaman:","bn":"Xaman দিয়ে ট্রাস্টলাইন তৈরি করুন:","ur":"Xaman سے اپنی ٹرسٹ لائن بنائیں:"},"pages":[1205,6897,6914,6926,6937]},{"source":"FAQ CalorieToken","translations":{"nl":"Veelgestelde vragen over CalorieToken","es":"Preguntas frecuentes sobre CalorieToken","fr":"Questions fréquentes sur CalorieToken","pt":"Perguntas frequentes sobre CalorieToken","id":"Pertanyaan umum CalorieToken","zh-Hans":"CalorieToken 常见问题","hi":"CalorieToken के सामान्य प्रश्न","ar":"أسئلة CalorieToken الشائعة","bn":"CalorieToken-এর সাধারণ প্রশ্ন","ur":"CalorieToken کے عام سوالات"},"pages":[6855,6897,6914,6926,6937]},{"source":"Team updates","translations":{"nl":"Teamnieuws","es":"Novedades del equipo","fr":"Actualités de l’équipe","pt":"Novidades da equipa","id":"Kabar tim","zh-Hans":"团队动态","hi":"टीम की खबरें","ar":"أخبار الفريق","bn":"দলের খবর","ur":"ٹیم کی خبریں"},"pages":[1207,6897,6914,6926,6937]},{"source":"Billing details","translations":{"nl":"Factuurgegevens","es":"Datos de facturación","fr":"Coordonnées de facturation","pt":"Dados de faturação","id":"Rincian tagihan","zh-Hans":"账单信息","hi":"बिलिंग विवरण","ar":"تفاصيل الفاتورة","bn":"বিলের বিবরণ","ur":"بل کی تفصیلات"},"pages":[6897,6914,6926,6937]},{"source":"First name","translations":{"nl":"Voornaam","es":"Nombre","fr":"Prénom","pt":"Nome","id":"Nama depan","zh-Hans":"名字","hi":"पहला नाम","ar":"الاسم الأول","bn":"নামের প্রথম অংশ","ur":"پہلا نام"},"pages":[6897,6914,6926,6937]},{"source":"Last name","translations":{"nl":"Achternaam","es":"Apellidos","fr":"Nom","pt":"Apelido","id":"Nama belakang","zh-Hans":"姓氏","hi":"अंतिम नाम","ar":"اسم العائلة","bn":"পদবি","ur":"آخری نام"},"pages":[6897,6914,6926,6937]},{"source":"Company name","translations":{"nl":"Bedrijfsnaam","es":"Empresa","fr":"Entreprise","pt":"Empresa","id":"Nama perusahaan","zh-Hans":"公司名称","hi":"कंपनी का नाम","ar":"اسم الشركة","bn":"প্রতিষ্ঠানের নাম","ur":"کمپنی کا نام"},"pages":[6897,6914,6926,6937]},{"source":"Country / Region","translations":{"nl":"Land / regio","es":"País / Región","fr":"Pays / Région","pt":"País / Região","id":"Negara / Wilayah","zh-Hans":"国家/地区","hi":"देश / क्षेत्र","ar":"الدولة / المنطقة","bn":"দেশ / অঞ্চল","ur":"ملک / علاقہ"},"pages":[6897,6914,6926,6937]},{"source":"Street address","translations":{"nl":"Straat en huisnummer","es":"Dirección","fr":"Adresse","pt":"Morada","id":"Alamat jalan","zh-Hans":"街道地址","hi":"सड़क का पता","ar":"عنوان الشارع","bn":"রাস্তার ঠিকানা","ur":"گلی کا پتہ"},"pages":[6897,6914,6926,6937]},{"source":"Town / City","translations":{"nl":"Plaats","es":"Localidad","fr":"Ville","pt":"Localidade","id":"Kota","zh-Hans":"城镇/城市","hi":"शहर","ar":"المدينة","bn":"শহর","ur":"شہر"},"pages":[6897,6914,6926,6937]},{"source":"State / County","translations":{"nl":"Provincie","es":"Provincia","fr":"Région","pt":"Distrito","id":"Provinsi","zh-Hans":"州/省","hi":"राज्य / प्रांत","ar":"المحافظة","bn":"রাজ্য / জেলা","ur":"صوبہ / ضلع"},"pages":[6897,6914,6926,6937]},{"source":"Postcode / ZIP","translations":{"nl":"Postcode","es":"Código postal","fr":"Code postal","pt":"Código postal","id":"Kode pos","zh-Hans":"邮政编码","hi":"पिन कोड","ar":"الرمز البريدي","bn":"পোস্ট কোড","ur":"پوسٹ کوڈ"},"pages":[6897,6914,6926,6937]},{"source":"Phone","translations":{"nl":"Telefoon","es":"Teléfono","fr":"Téléphone","pt":"Telefone","id":"Telepon","zh-Hans":"电话","hi":"फ़ोन","ar":"الهاتف","bn":"ফোন","ur":"فون"},"pages":[6897,6914,6926,6937]},{"source":"Email address","translations":{"nl":"E-mailadres","es":"Correo electrónico","fr":"Adresse e-mail","pt":"E-mail","id":"Alamat email","zh-Hans":"电子邮箱","hi":"ईमेल पता","ar":"البريد الإلكتروني","bn":"ইমেইল ঠিকানা","ur":"ای میل پتہ"},"pages":[6897,6914,6926,6937]},{"source":"Additional information","translations":{"nl":"Aanvullende informatie","es":"Información adicional","fr":"Informations complémentaires","pt":"Informações adicionais","id":"Informasi tambahan","zh-Hans":"其他信息","hi":"अतिरिक्त जानकारी","ar":"معلومات إضافية","bn":"অতিরিক্ত তথ্য","ur":"مزید معلومات"},"pages":[6897,6914,6926,6937]},{"source":"Order notes","translations":{"nl":"Opmerkingen bij je bestelling","es":"Notas del pedido","fr":"Notes de commande","pt":"Notas da encomenda","id":"Catatan pesanan","zh-Hans":"订单备注","hi":"ऑर्डर संबंधी नोट","ar":"ملاحظات الطلب","bn":"অর্ডারের মন্তব্য","ur":"آرڈر کے نوٹس"},"pages":[6897,6914,6926,6937]},{"source":"Your order","translations":{"nl":"Je bestelling","es":"Tu pedido","fr":"Votre commande","pt":"A sua encomenda","id":"Pesanan Anda","zh-Hans":"您的订单","hi":"आपका ऑर्डर","ar":"طلبك","bn":"আপনার অর্ডার","ur":"آپ کا آرڈر"},"pages":[6897,6914,6926,6937]},{"source":"Product","translations":{"nl":"Product","es":"Producto","fr":"Produit","pt":"Produto","id":"Produk","zh-Hans":"商品","hi":"उत्पाद","ar":"المنتج","bn":"পণ্য","ur":"مصنوعہ"},"pages":[6897,6914,6926,6937]},{"source":"Subtotal","translations":{"nl":"Subtotaal","es":"Subtotal","fr":"Sous-total","pt":"Subtotal","id":"Subtotal","zh-Hans":"小计","hi":"उप-योग","ar":"المجموع الفرعي","bn":"উপমোট","ur":"ذیلی مجموعہ"},"pages":[6897,6914,6926,6937]},{"source":"Total","translations":{"nl":"Totaal","es":"Total","fr":"Total","pt":"Total","id":"Total","zh-Hans":"合计","hi":"कुल","ar":"الإجمالي","bn":"মোট","ur":"کل"},"pages":[6897,6914,6926,6937]},{"source":"Quantity","translations":{"nl":"Aantal","es":"Cantidad","fr":"Quantité","pt":"Quantidade","id":"Kuantitas","zh-Hans":"数量","hi":"संख्या","ar":"الكمية","bn":"সংখ্যা","ur":"تعداد"},"pages":[6897,6914,6926,6937]},{"source":"Update cart","translations":{"nl":"Winkelwagen bijwerken","es":"Actualizar carrito","fr":"Mettre à jour le panier","pt":"Atualizar carrinho","id":"Perbarui keranjang","zh-Hans":"更新购物车","hi":"कार्ट अपडेट करें","ar":"تحديث السلة","bn":"কার্ট হালনাগাদ করুন","ur":"کارٹ تازہ کریں"},"pages":[6897,6914,6926,6937]},{"source":"Coupon code","translations":{"nl":"Kortingscode","es":"Código de cupón","fr":"Code promo","pt":"Código de cupão","id":"Kode kupon","zh-Hans":"优惠码","hi":"कूपन कोड","ar":"رمز القسيمة","bn":"কুপন কোড","ur":"کوپن کوڈ"},"pages":[6897,6914,6926,6937]},{"source":"Apply coupon","translations":{"nl":"Kortingscode toepassen","es":"Aplicar cupón","fr":"Appliquer le code","pt":"Aplicar cupão","id":"Gunakan kupon","zh-Hans":"使用优惠码","hi":"कूपन लगाएँ","ar":"تطبيق القسيمة","bn":"কুপন প্রয়োগ করুন","ur":"کوپن لگائیں"},"pages":[6897,6914,6926,6937]},{"source":"Cart totals","translations":{"nl":"Winkelwagentotaal","es":"Totales del carrito","fr":"Total du panier","pt":"Totais do carrinho","id":"Total keranjang","zh-Hans":"购物车合计","hi":"कार्ट का कुल","ar":"إجمالي السلة","bn":"কার্টের মোট","ur":"کارٹ کا کل"},"pages":[6897,6914,6926,6937]},{"source":"Proceed to checkout","translations":{"nl":"Doorgaan naar afrekenen","es":"Continuar al pago","fr":"Passer au paiement","pt":"Continuar para pagamento","id":"Lanjut ke pembayaran","zh-Hans":"继续结账","hi":"भुगतान की ओर बढ़ें","ar":"المتابعة إلى الدفع","bn":"পেমেন্টে এগিয়ে যান","ur":"ادائیگی کی طرف جائیں"},"pages":[6897,6914,6926,6937]},{"source":"Place order","translations":{"nl":"Bestelling plaatsen","es":"Realizar pedido","fr":"Commander","pt":"Fazer encomenda","id":"Buat pesanan","zh-Hans":"提交订单","hi":"ऑर्डर दें","ar":"إرسال الطلب","bn":"অর্ডার করুন","ur":"آرڈر دیں"},"pages":[6897,6914,6926,6937]},{"source":"Return to shop","translations":{"nl":"Terug naar de winkel","es":"Volver a la tienda","fr":"Retour à la boutique","pt":"Voltar à loja","id":"Kembali ke toko","zh-Hans":"返回商店","hi":"दुकान पर लौटें","ar":"العودة إلى المتجر","bn":"দোকানে ফিরুন","ur":"دکان پر واپس جائیں"},"pages":[6897,6914,6926,6937]},{"source":"Your cart is currently empty.","translations":{"nl":"Je winkelwagen is momenteel leeg.","es":"Tu carrito está vacío.","fr":"Votre panier est vide.","pt":"O seu carrinho está vazio.","id":"Keranjang Anda kosong.","zh-Hans":"您的购物车目前为空。","hi":"आपका कार्ट अभी खाली है।","ar":"سلتك فارغة حاليًا.","bn":"আপনার কার্ট এখন খালি।","ur":"آپ کا کارٹ ابھی خالی ہے۔"},"pages":[6897,6914,6926,6937]},{"source":"Read more","translations":{"nl":"Lees meer","es":"Leer más","fr":"Lire la suite","pt":"Ler mais","id":"Baca lebih lanjut","zh-Hans":"阅读更多","hi":"और पढ़ें","ar":"اقرأ المزيد","bn":"আরও পড়ুন","ur":"مزید پڑھیں"},"pages":["*"]},{"source":"READ MORE","translations":{"nl":"LEES MEER","es":"LEER MÁS","fr":"LIRE LA SUITE","pt":"LER MAIS","id":"BACA LEBIH LANJUT","zh-Hans":"阅读更多","hi":"और पढ़ें","ar":"اقرأ المزيد","bn":"আরও পড়ুন","ur":"مزید پڑھیں"},"pages":["*"]},{"source":"Previous","translations":{"nl":"Vorige","es":"Anterior","fr":"Précédent","pt":"Anterior","id":"Sebelumnya","zh-Hans":"上一页","hi":"पिछला","ar":"السابق","bn":"আগের","ur":"پچھلا"},"pages":["*"]},{"source":"Recent posts","translations":{"nl":"Recente berichten","es":"Publicaciones recientes","fr":"Articles récents","pt":"Publicações recentes","id":"Kiriman terbaru","zh-Hans":"最新文章","hi":"हाल की पोस्ट","ar":"أحدث المنشورات","bn":"সাম্প্রতিক পোস্ট","ur":"حالیہ پوسٹس"},"pages":["*"]},{"source":"Category","translations":{"nl":"Categorie","es":"Categoría","fr":"Catégorie","pt":"Categoria","id":"Kategori","zh-Hans":"分类","hi":"श्रेणी","ar":"التصنيف","bn":"বিভাগ","ur":"زمرہ"},"pages":["*"]},{"source":"Categories","translations":{"nl":"Categorieën","es":"Categorías","fr":"Catégories","pt":"Categorias","id":"Kategori","zh-Hans":"分类","hi":"श्रेणियाँ","ar":"التصنيفات","bn":"বিভাগসমূহ","ur":"زمرے"},"pages":["*"]},{"source":"Search","translations":{"nl":"Zoeken","es":"Buscar","fr":"Rechercher","pt":"Pesquisar","id":"Cari","zh-Hans":"搜索","hi":"खोजें","ar":"بحث","bn":"খুঁজুন","ur":"تلاش کریں"},"pages":["*"]},{"source":"Required","translations":{"nl":"Verplicht","es":"Obligatorio","fr":"Obligatoire","pt":"Obrigatório","id":"Wajib","zh-Hans":"必填","hi":"आवश्यक","ar":"مطلوب","bn":"আবশ্যক","ur":"لازمی"},"pages":[6897,6914,6926,6937]},{"source":"optional","translations":{"nl":"optioneel","es":"opcional","fr":"facultatif","pt":"opcional","id":"opsional","zh-Hans":"选填","hi":"वैकल्पिक","ar":"اختياري","bn":"ঐচ্ছিক","ur":"اختیاری"},"pages":[6897,6914,6926,6937]},{"source":"Tackling Frontrunning on the XRPL: Our Journey to a Practical Solution","translations":{"nl":"Frontrunning op de XRPL aanpakken: onze weg naar een praktische oplossing"},"pages":[7545]},{"source":"Introduction","translations":{"nl":"Inleiding"},"pages":[7545,7608]},{"source":"As an early XRP holder and now a developer, I've seen the XRPL ecosystem evolve. Alongside two friends, I founded CalorieToken to create a real use case for tokens on the XRPL. Our journey led us to confront a significant issue: frontrunning on the XRPL order books. This blog post outlines our solution to this problem.","translations":{"nl":"Als vroege XRP-bezitter en inmiddels ontwikkelaar heb ik het XRPL-ecosysteem zien groeien. Samen met twee vrienden richtte ik CalorieToken op om een echte toepassing voor tokens op de XRPL te maken. Onderweg kwamen we een belangrijk probleem tegen: frontrunning in de XRPL-orderboeken. In dit blog beschrijven we onze oplossing voor dit probleem."},"pages":[7545]},{"source":"The Problem: Unfunded Orders and Negative Spread","translations":{"nl":"Het probleem: ongedekte orders en een negatieve spread"},"pages":[7545]},{"source":"During our development phase, we noticed an issue when one of my friends heavily traded EQ for XRP. Despite his efforts to accumulate more XRP, he ended up with less XRP and more EQ. He then sent his EQ to Bitrue for staking, aiming to earn some APY. However, his EQ sell orders remained open on the XRPL, even though his EQ was no longer in his XRPL wallet. This anomaly, known as negative spread, raised concerns about the integrity of the XRPL trading environment.","translations":{"nl":"Tijdens de ontwikkeling merkten we een probleem op toen een vriend veel EQ tegen XRP verhandelde. Ondanks zijn pogingen meer XRP te verzamelen, eindigde hij met minder XRP en meer EQ. Hij stuurde zijn EQ vervolgens naar Bitrue om te staken en daarmee rendement te verdienen. Zijn EQ-verkooporders bleven echter openstaan op de XRPL, hoewel zijn EQ niet meer in zijn XRPL-wallet stond. Deze afwijking, bekend als een negatieve spread, riep vragen op over de integriteit van de XRPL-handelsomgeving."},"pages":[7545]},{"source":"Understanding Frontrunning and Unfunded Offers","translations":{"nl":"Frontrunning en ongedekte orders begrijpen"},"pages":[7545]},{"source":"Frontrunning involves exploiting the time gap between when a trade is initiated and when it is executed. A frontrunner can place their orders ahead of others to profit from price movements. On the XRPL, unfunded offers exacerbate this issue:","translations":{"nl":"Bij frontrunning wordt het tijdsverschil tussen het starten en uitvoeren van een transactie uitgebuit. Een frontrunner kan orders vóór anderen plaatsen om van prijsbewegingen te profiteren. Op de XRPL versterken ongedekte orders dit probleem:"},"pages":[7545]},{"source":"Placing Unfunded Offers: Frontrunners place large buy or sell orders without the actual funds to back them up. These offers appear in the order book, influencing the market price and misleading genuine traders.","translations":{"nl":"Ongedekte orders plaatsen: frontrunners plaatsen grote koop- of verkooporders zonder de middelen om ze te dekken. Deze orders verschijnen in het orderboek, beïnvloeden de marktprijs en misleiden gewone handelaren."},"pages":[7545]},{"source":"Manipulating Market Perception: By placing unfunded offers, frontrunners create a false sense of supply or demand, causing prices to move in their favor.","translations":{"nl":"Het marktbeeld manipuleren: door ongedekte orders te plaatsen, wekken frontrunners een onjuist beeld van vraag of aanbod, waardoor prijzen in hun voordeel bewegen."},"pages":[7545]},{"source":"Profiting from the Spread: Once the price has moved as intended, the frontrunner cancels their unfunded offers and places real orders to exploit the price difference.","translations":{"nl":"Profiteren van de spread: zodra de prijs naar wens is veranderd, annuleert de frontrunner de ongedekte orders en plaatst echte orders om het prijsverschil te benutten."},"pages":[7545]},{"source":"This practice undermines the fairness and transparency of the trading environment, making it crucial to address unfunded offers on the XRPL.","translations":{"nl":"Deze werkwijze ondermijnt de eerlijkheid en transparantie van de handelsomgeving. Daarom is het belangrijk ongedekte orders op de XRPL aan te pakken."},"pages":[7545]},{"source":"Apex 2023: A Keynote that Sparked an Idea","translations":{"nl":"Apex 2023: een presentatie die een idee opriep"},"pages":[7545]},{"source":"At the Apex 2023 conference in Amsterdam, I attended Vito Tumas' keynote on frontrunning on the XRPL. After the session, I proposed a solution: preventing orders from remaining open without the necessary funds. Vito's interest in this idea encouraged me to pursue it further.","translations":{"nl":"Tijdens de Apex-conferentie van 2023 in Amsterdam bezocht ik de presentatie van Vito Tumas over frontrunning op de XRPL. Na afloop stelde ik voor te voorkomen dat orders zonder voldoende dekking open blijven staan. Vito’s belangstelling moedigde mij aan het idee verder uit te werken."},"pages":[7545]},{"source":"Discussing with David Schwartz","translations":{"nl":"In gesprek met David Schwartz"},"pages":[7545]},{"source":"I also shared my thoughts with David Schwartz, suggesting the need to cancel unfunded offers. David highlighted the XRPL's resource constraints, explaining that continuously checking for unfunded offers would be too resource-intensive. His feedback, while sobering, pushed me to think outside the box for a more efficient solution.","translations":{"nl":"Ik deelde mijn gedachten ook met David Schwartz en stelde voor ongedekte orders te annuleren. David wees op de beperkte verwerkingscapaciteit van de XRPL: voortdurend controleren op ongedekte orders zou te veel capaciteit vragen. Zijn nuchtere reactie zette mij aan om verder te denken over een efficiëntere oplossing."},"pages":[7545]},{"source":"Crafting a Resource-Efficient Solution","translations":{"nl":"Een oplossing met beperkt middelengebruik ontwerpen"},"pages":[7545]},{"source":"Determined to resolve the issue without compromising the XRPL's performance, I collaborated with other developers and devised a novel approach:","translations":{"nl":"Vastbesloten het probleem op te lossen zonder de prestaties van de XRPL te schaden, werkte ik met andere ontwikkelaars aan een nieuwe aanpak:"},"pages":[7545]},{"source":"Real-Time Oracle: We proposed running an oracle parallel to the XRPL, capable of monitoring offers in real-time.","translations":{"nl":"Realtime oracle: we stelden een oracle naast de XRPL voor die orders in realtime kan volgen."},"pages":[7545]},{"source":"Indexing Unfunded Offers: This oracle would specifically index unfunded offers, minimizing the resource load.","translations":{"nl":"Ongedekte orders indexeren: deze oracle zou specifiek ongedekte orders indexeren, om de belasting zo klein mogelijk te houden."},"pages":[7545]},{"source":"Cancellation Service: A separate service would then cancel these indexed offers on the XRPL, ensuring the process remains efficient and minimally intrusive.","translations":{"nl":"Annuleringsdienst: een aparte dienst zou deze geïndexeerde orders vervolgens op de XRPL annuleren, zodat het proces efficiënt blijft en zo min mogelijk ingrijpt."},"pages":[7545]},{"source":"Why This Solution Works","translations":{"nl":"Waarom deze oplossing werkt"},"pages":[7545]},{"source":"Resource Efficiency: By offloading the real-time monitoring to an oracle, we avoid overburdening the XRPL.","translations":{"nl":"Efficiënt middelengebruik: door realtime monitoring aan een oracle over te laten, voorkomen we extra belasting van de XRPL."},"pages":[7545]},{"source":"Focused Action: Only the unfunded offers are targeted for cancellation, streamlining the process.","translations":{"nl":"Gerichte actie: alleen ongedekte orders komen in aanmerking voor annulering, wat het proces vereenvoudigt."},"pages":[7545]},{"source":"Preserving Speed: The XRPL's renowned speed remains unaffected, maintaining its core advantage.","translations":{"nl":"Behoud van snelheid: de bekende snelheid van de XRPL blijft behouden, net als dit belangrijke voordeel."},"pages":[7545]},{"source":"A Step Towards a Cleaner XRPL","translations":{"nl":"Een stap naar een schonere XRPL"},"pages":[7545]},{"source":"Implementing this solution underscores our commitment to maintaining a transparent and reliable environment for CalorieToken and the broader XRPL community. While David Schwartz suggested relying on third-party software, our oracle-based approach offers a direct and efficient method to tackle unfunded offers, enhancing the overall integrity of the XRPL.","translations":{"nl":"Het uitvoeren van deze oplossing onderstreept ons streven naar een transparante en betrouwbare omgeving voor CalorieToken en de bredere XRPL-gemeenschap. David Schwartz stelde voor software van derden te gebruiken; onze aanpak met een oracle biedt een directe en efficiënte manier om ongedekte orders aan te pakken en zo de integriteit van de XRPL te verbeteren."},"pages":[7545]},{"source":"Conclusion","translations":{"nl":"Conclusie"},"pages":[7545,7564,7575,7587,7608]},{"source":"Our journey from identifying a problem to proposing a viable solution reflects our dedication to the XRPL ecosystem. By addressing the issue of unfunded offers, we aim to contribute to a more secure and efficient trading environment. Stay tuned as we continue to develop and refine our solutions for the betterment of the XRPL community.","translations":{"nl":"Onze weg van het herkennen van een probleem naar het voorstellen van een haalbare oplossing toont onze inzet voor het XRPL-ecosysteem. Door ongedekte orders aan te pakken, willen we bijdragen aan een veiligere en efficiëntere handelsomgeving. Volg onze verdere ontwikkeling en verfijning van oplossingen voor de XRPL-gemeenschap."},"pages":[7545]},{"source":"CalorieToken: An Update on Our Journey and Future Plans","translations":{"nl":"CalorieToken: een update over onze voortgang en toekomstplannen"},"pages":[7564]},{"source":"Commitment to Transparency and Honesty","translations":{"nl":"Onze inzet voor transparantie en eerlijkheid"},"pages":[7564]},{"source":"At CalorieToken, we have always prioritized transparency and honesty. Our commitment to openness extends to our team and project progress, and we believe it’s important to keep our community informed about our journey. Here’s a detailed update on where we stand and what’s ahead for us.","translations":{"nl":"Bij CalorieToken staan transparantie en eerlijkheid altijd voorop. Onze openheid gaat ook over het team en de voortgang van het project. We vinden het belangrijk onze gemeenschap op de hoogte te houden. Hier volgt een uitgebreide update over waar we staan en wat er voor ons ligt."},"pages":[7564]},{"source":"Our Team","translations":{"nl":"Ons team"},"pages":[7564]},{"source":"We are a small but dedicated team based in the Netherlands. The core members include:","translations":{"nl":"We zijn een klein, toegewijd team in Nederland. De kernleden zijn:"},"pages":[7564]},{"source":"Pieter: Our developer, who is instrumental in bringing our vision to life.","translations":{"nl":"Pieter: onze ontwikkelaar, die een centrale rol speelt bij het realiseren van onze visie."},"pages":[7564]},{"source":"Leroy: Our creative designer, who has been contributing his artistic talents to the project.","translations":{"nl":"Leroy: onze creatieve ontwerper, die zijn artistieke talenten inzet voor het project."},"pages":[7564]},{"source":"Unfortunately, due to personal circumstances, Sam is no longer part of the team, and Leroy’s contributions are temporarily limited. Despite these challenges, we remain committed to advancing CalorieToken and its associated projects.","translations":{"nl":"Door persoonlijke omstandigheden maakt Sam helaas geen deel meer uit van het team en is Leroy’s bijdrage tijdelijk beperkt. Ondanks deze uitdagingen blijven we werken aan CalorieToken en de bijbehorende projecten."},"pages":[7564]},{"source":"Transparency in Token Handling","translations":{"nl":"Transparantie over het gebruik van tokens"},"pages":[7564]},{"source":"We want to clarify that we have never sold any $CAL tokens. This commitment to transparency can be verified on blockchain explorers such as","translations":{"nl":"We willen duidelijk maken dat we nooit $CAL-tokens hebben verkocht. Deze transparantie is te controleren via blockchainverkenners zoals"},"pages":[7564]},{"source":"and","translations":{"nl":"en"},"pages":[7564]},{"source":". Our personal funds were used to list CalorieToken on the","translations":{"nl":". Onze eigen middelen zijn gebruikt om CalorieToken te laten noteren op de"},"pages":[7564]},{"source":"exchange, and we take pride in these accomplishments, no matter how small they may seem.","translations":{"nl":"-exchange. We zijn trots op deze resultaten, hoe klein ze misschien ook lijken."},"pages":[7564]},{"source":"Future Listing Aspirations","translations":{"nl":"Ambities voor toekomstige noteringen"},"pages":[7564]},{"source":"While we aspire to be listed on major platforms like Binance and Bitrue in the future, our current focus is on building the CalorieApp and expanding our brand. We believe that laying a solid foundation is crucial for long-term success.","translations":{"nl":"Hoewel we hopen in de toekomst op grote platforms zoals Binance en Bitrue te komen, ligt onze huidige aandacht bij het bouwen van CalorieApp en het uitbreiden van ons merk. Volgens ons is een stevige basis essentieel voor succes op langere termijn."},"pages":[7564]},{"source":"Navigating Regulatory Landscapes","translations":{"nl":"Omgaan met regelgeving"},"pages":[7564]},{"source":"Regulatory clarity remains a significant factor in our operations. Although there has been some progress in Europe, uncertainties persist. We continue to navigate this landscape with caution and adaptability to ensure compliance and sustainability.","translations":{"nl":"Duidelijkheid over regelgeving blijft belangrijk voor ons werk. Hoewel Europa vooruitgang heeft geboekt, blijven onzekerheden bestaan. We bewegen voorzichtig en flexibel door deze omgeving om naleving en continuïteit te ondersteunen."},"pages":[7564]},{"source":"Development Journey","translations":{"nl":"Ons ontwikkelingstraject"},"pages":[7564]},{"source":"Since September 2021, we have been diligently working on our unique idea for CalorieToken. For more details on our vision and plans, you can check out our","translations":{"nl":"Sinds september 2021 werken we aan ons eigen idee voor CalorieToken. Meer over onze visie en plannen lees je in ons"},"pages":[7564]},{"source":"CalorieApp Development","translations":{"nl":"Ontwikkeling van CalorieApp"},"pages":[7564]},{"source":"The development of the CalorieApp is ongoing. You can explore our code and follow our progress on our","translations":{"nl":"De ontwikkeling van CalorieApp gaat door. Je kunt onze code bekijken en de voortgang volgen op onze"},"pages":[7564]},{"source":"GitHub page","translations":{"nl":"GitHub-pagina"},"pages":[7564]},{"source":". Pieter, our main developer, runs his own IT business five days a week but remains dedicated to building our app. The CalorieApp leverages advanced technologies, including:","translations":{"nl":". Pieter, onze hoofdontwikkelaar, runt vijf dagen per week zijn eigen IT-bedrijf en blijft daarnaast aan de app werken. CalorieApp gebruikt geavanceerde technologieën, waaronder:"},"pages":[7564]},{"source":"IPFS for decentralized storage","translations":{"nl":"IPFS voor gedecentraliseerde opslag"},"pages":[7564]},{"source":"BigchainDB as a decentralized database","translations":{"nl":"BigchainDB als gedecentraliseerde database"},"pages":[7564]},{"source":"FoodRepo as the initial data source","translations":{"nl":"FoodRepo als eerste gegevensbron"},"pages":[7564]},{"source":"Challenges and Progress","translations":{"nl":"Uitdagingen en voortgang"},"pages":[7564]},{"source":"The progress of the CalorieApp is steady but not as fast as we would like due to time and resource constraints. Our priority is to deliver a functional and secure product that meets the needs of our users.","translations":{"nl":"De ontwikkeling van CalorieApp vordert gestaag, maar door beperkte tijd en middelen minder snel dan we zouden willen. Onze prioriteit is een werkend en veilig product dat aansluit bij de behoeften van gebruikers."},"pages":[7564]},{"source":"Community Engagement","translations":{"nl":"Betrokkenheid van de gemeenschap"},"pages":[7564]},{"source":"We encourage the Calorie community to use and build applications on our platform. Our goal is for CalorieToken to evolve and thrive long after us, leaving a lasting legacy. Your support is crucial, and we owe our current position to the unwavering support of the Calorie community.","translations":{"nl":"We moedigen de Calorie-gemeenschap aan ons platform te gebruiken en er toepassingen voor te bouwen. Ons doel is dat CalorieToken ook na ons blijft groeien en voortbestaan. Jullie steun is essentieel; onze huidige positie danken we aan de blijvende steun van de Calorie-gemeenschap."},"pages":[7564]},{"source":"Despite recent changes in our team, we remain steadfast in our mission. Pieter, our main developer, is handling most of the work during this transition period, and we appreciate your understanding and support.","translations":{"nl":"Ondanks recente veranderingen in ons team blijven we vasthouden aan onze missie. Pieter, onze hoofdontwikkelaar, doet tijdens deze overgang het grootste deel van het werk. We waarderen jullie begrip en steun."},"pages":[7564]},{"source":"Thank you for being a part of our journey. Together, we will continue to work towards conquering the Food","translations":{"nl":"Bedankt dat je deel uitmaakt van onze reis. Samen blijven we werken aan het veroveren van de voedings-"},"pages":[7564]},{"source":"Beverage industry with CalorieToken and the CalorieApp!","translations":{"nl":"drankensector met CalorieToken en CalorieApp!"},"pages":[7564]},{"source":"Exciting News: First Release of the CalorieApp Test APK on Android!","translations":{"nl":"Mooi nieuws: de eerste CalorieApp-test-APK voor Android!"},"pages":[7569]},{"source":"We are thrilled to announce a significant milestone in our journey: the first release of the test APK for our CalorieApp on Android! 📱 This achievement is a testament to the dedication and hard work of our solo developer, who has been juggling this project alongside his main job. We invite you to dive into the app on your Android phone and be part of this exciting milestone! 🎉","translations":{"nl":"Met plezier kondigen we een belangrijke mijlpaal aan: de eerste test-APK van CalorieApp voor Android! 📱 Dit resultaat is te danken aan de inzet van onze enige ontwikkelaar, die dit project naast zijn hoofdberoep uitvoert. We nodigen je uit om de app op je Android-telefoon te proberen en deze mijlpaal mee te beleven! 🎉"},"pages":[7569]},{"source":"Download the Test APK Here:","translations":{"nl":"Download de test-APK hier:"},"pages":[7569]},{"source":"Download the Test APK","translations":{"nl":"Download de test-APK"},"pages":[7569]},{"source":"Disclaimer:","translations":{"nl":"Toelichting:"},"pages":[7569,7583]},{"source":"This application is currently in the testing phase and is not yet ready for the Play Store. Install at your own risk.","translations":{"nl":"Deze toepassing bevindt zich in de testfase en is nog niet klaar voor de Play Store. Installeren gebeurt op eigen risico."},"pages":[7569]},{"source":"Test XRPL Wallet:","translations":{"nl":"XRPL-testwallet:"},"pages":[7569]},{"source":"This version features a simple XRPL wallet running on the testnet. Upon generating a test wallet within the app, you will receive 1000 test XRP for experimenting with transactions and exploring the features. This allows you to familiarize yourself with the app's functionality in a risk-free environment.","translations":{"nl":"Deze versie bevat een eenvoudige XRPL-wallet op Testnet. Wanneer je in de app een testwallet maakt, ontvang je 1000 test-XRP om transacties en functies uit te proberen. Zo kun je de werking van de app leren kennen in een omgeving zonder financieel risico."},"pages":[7569]},{"source":"Important:","translations":{"nl":"Belangrijk:"},"pages":[7569]},{"source":"This application is intended for testing and showcase purposes only. It is not suitable for production use, and any transactions made with the test XRP are not real and will not impact your actual XRPL account or holdings.","translations":{"nl":"Deze toepassing is uitsluitend bedoeld voor tests en demonstraties. Ze is niet geschikt voor productiegebruik. Transacties met test-XRP zijn niet echt en beïnvloeden je echte XRPL-account of bezit niet."},"pages":[7569]},{"source":"Explore Our OpenSource Code:","translations":{"nl":"Bekijk onze opensourcecode:"},"pages":[7569]},{"source":"For those interested in the technical details and ongoing development, you can check out our open-source code on GitHub:","translations":{"nl":"Wie belangstelling heeft voor de techniek en de verdere ontwikkeling, kan onze opensourcecode op GitHub bekijken:"},"pages":[7569]},{"source":"CalorieApp Testnet on GitHub","translations":{"nl":"CalorieApp Testnet op GitHub"},"pages":[7569]},{"source":"Our Journey Ahead:","translations":{"nl":"Het vervolg:"},"pages":[7569]},{"source":"This marks the first step towards an incredible journey. Following this milestone, our developer is set to enhance and expand the CalorieApp with more features. While we aim to get it ready for the Play Store, we acknowledge that there's still a lot of work to be done before the official launch.","translations":{"nl":"Dit is de eerste stap in een bijzondere ontwikkeling. Na deze mijlpaal gaat onze ontwikkelaar CalorieApp verder verbeteren en uitbreiden. We willen de app geschikt maken voor de Play Store, maar erkennen dat voor de officiële introductie nog veel werk nodig is."},"pages":[7569]},{"source":"Our Ultimate Goal:","translations":{"nl":"Ons uiteindelijke doel:"},"pages":[7569]},{"source":"We are committed to building a revolutionary food tracking system based on blockchain technology. This ambitious goal will be pursued gradually as we continue to develop the basic CalorieApp.","translations":{"nl":"We willen een vernieuwend systeem voor het volgen van voeding bouwen op basis van blockchaintechnologie. Dat ambitieuze doel streven we geleidelijk na terwijl we de basis van CalorieApp ontwikkelen."},"pages":[7569]},{"source":"Get Involved:","translations":{"nl":"Doe mee:"},"pages":[7569]},{"source":"If you want to test the app with real test currencies, feel free to reach out for Lipisa and CalorieTest! 🔄 Just drop us a message, and we'll provide you with what you need to explore the app further.","translations":{"nl":"Wil je de app met testmunten uitproberen? Vraag gerust naar Lipisa en CalorieTest! 🔄 Stuur een bericht en we geven je wat je nodig hebt om de app verder te verkennen."},"pages":[7569]},{"source":"Stay Tuned:","translations":{"nl":"Blijf op de hoogte:"},"pages":[7569,7583]},{"source":"This release is just the beginning. We appreciate your support and enthusiasm as we continue to innovate and improve. Stay tuned for more updates and exciting developments! 🍏🔗💡","translations":{"nl":"Deze release is pas het begin. We waarderen jullie steun en enthousiasme terwijl we blijven vernieuwen en verbeteren. Volg ons voor verdere updates en nieuwe ontwikkelingen! 🍏🔗💡"},"pages":[7569]},{"source":"Exciting News: Our Donations Page is LIVE!","translations":{"nl":"Mooi nieuws: onze donatiepagina is live!"},"pages":[7573]},{"source":"We are thrilled to announce that our donations page is now live! This is an incredible opportunity for you to fuel the future of finance and shape the revolutionary CalorieApp ecosystem with your XRP contributions. Your support is invaluable to us as we continue to build and innovate within the blockchain space.","translations":{"nl":"We zijn blij te melden dat onze donatiepagina nu live is! Met je bijdrage in XRP kun je de toekomst van financiën en het vernieuwende CalorieApp-ecosysteem helpen vormgeven. Je steun is van grote waarde terwijl we verder bouwen en vernieuwen binnen de blockchainwereld."},"pages":[7573]},{"source":"Join Us in Building the Future","translations":{"nl":"Bouw met ons aan de toekomst"},"pages":[7573]},{"source":"At CalorieToken, we believe in the power of community and collective effort. Our journey has been shaped by the support and enthusiasm of our amazing community, and we are excited to offer a new way for you to get involved. Whether your donation is small, big, or somewhere in between, every bit counts and goes directly into crafting an amazing app experience.","translations":{"nl":"Bij CalorieToken geloven we in de kracht van de gemeenschap en gezamenlijke inzet. Onze ontwikkeling is gevormd door jullie steun en enthousiasme. We bieden graag een nieuwe manier om mee te doen. Of je donatie klein, groot of daartussenin is: elke bijdrage telt en gaat rechtstreeks naar het maken van een mooie app-ervaring."},"pages":[7573]},{"source":"Your contributions will help us:","translations":{"nl":"Je bijdrage helpt ons om:"},"pages":[7573]},{"source":"Enhance the CalorieApp: Develop new features and improve existing ones to create a seamless and innovative user experience.","translations":{"nl":"CalorieApp te verbeteren: nieuwe functies ontwikkelen en bestaande functies verbeteren voor een soepele en vernieuwende gebruikerservaring."},"pages":[7573]},{"source":"Expand Our Ecosystem: Integrate more functionalities and services that align with our vision of revolutionizing the food and beverage industry using blockchain technology.","translations":{"nl":"Ons ecosysteem uit te breiden: meer functies en diensten integreren die passen bij onze visie om de voedings- en drankensector met blockchaintechnologie te vernieuwen."},"pages":[7573]},{"source":"Support Development Costs: Cover the costs associated with ongoing development, maintenance, and scaling of our infrastructure.","translations":{"nl":"Ontwikkelkosten te dragen: de kosten van doorlopende ontwikkeling, onderhoud en uitbreiding van onze infrastructuur betalen."},"pages":[7573]},{"source":"A Heartfelt Thank You to Our Community","translations":{"nl":"Een hartelijk dankjewel aan onze gemeenschap"},"pages":[7573]},{"source":"We extend our heartfelt gratitude to our incredible community. Your unwavering support and belief in our vision drive us to push the boundaries of what is possible. We are committed to transparency and ensuring that every penny donated is used effectively to bring our ideas to life.","translations":{"nl":"We bedanken onze gemeenschap van harte. Jullie blijvende steun en vertrouwen in onze visie moedigen ons aan de grenzen van het mogelijke te verkennen. We zetten ons in voor transparantie en willen elke gedoneerde cent effectief gebruiken om onze ideeën te realiseren."},"pages":[7573]},{"source":"To show our appreciation, we have some exciting plans in the pipeline:","translations":{"nl":"Als blijk van waardering werken we aan enkele mooie plannen:"},"pages":[7573]},{"source":"Merch & NFTs: We will soon be launching exclusive merchandise and NFTs for our supporters. Stay tuned for updates on how you can get your hands on these unique items!","translations":{"nl":"Merchandise en NFT’s: binnenkort willen we exclusieve merchandise en NFT’s voor onze supporters introduceren. Houd onze updates in de gaten om te ontdekken hoe je deze bijzondere artikelen kunt verkrijgen!"},"pages":[7573]},{"source":"Support Us Today","translations":{"nl":"Steun ons vandaag"},"pages":[7573]},{"source":"Ready to contribute? Visit our","translations":{"nl":"Wil je bijdragen? Bezoek onze"},"pages":[7573]},{"source":"donations page","translations":{"nl":"donatiepagina"},"pages":[7573]},{"source":"and make your XRP donation today. Your support will make a significant impact on our ability to deliver a top-notch app and continue our mission of innovation.","translations":{"nl":"en doneer vandaag XRP. Je steun helpt ons om een goede app te leveren en onze vernieuwingsmissie voort te zetten."},"pages":[7573]},{"source":"Important Note","translations":{"nl":"Belangrijke toelichting"},"pages":[7573]},{"source":"While your generous support drives our efforts, we cannot guarantee specific outcomes or features as a result of donations. However, we assure you that every penny will go towards the development of CalorieApp and Calorie Token. Your trust and contributions are the foundation of our progress, and we are committed to using these resources wisely and transparently.","translations":{"nl":"Je steun helpt ons werk, maar donaties garanderen geen specifieke resultaten of functies. Wel verzekeren we je dat elke cent naar de ontwikkeling van CalorieApp en CalorieToken gaat. Jullie vertrouwen en bijdragen vormen de basis van onze voortgang. We willen deze middelen verstandig en transparant gebruiken."},"pages":[7573]},{"source":"Stay Connected","translations":{"nl":"Blijf verbonden"},"pages":[7573,7591]},{"source":"We are just getting started, and there is so much more to come. Follow us on our journey as we work tirelessly to create a better, more efficient ecosystem. Stay connected with us for the latest updates, new features, and exciting announcements.","translations":{"nl":"We zijn pas begonnen en er komt nog veel meer. Volg ons terwijl we hard werken aan een beter en efficiënter ecosysteem. Blijf op de hoogte van updates, nieuwe functies en aankondigingen."},"pages":[7573]},{"source":"Thank you for being an integral part of our journey. Together, we can make a difference and revolutionize the way we think about finance and technology.","translations":{"nl":"Bedankt voor jouw belangrijke bijdrage aan onze ontwikkeling. Samen kunnen we verschil maken en onze kijk op financiën en technologie vernieuwen."},"pages":[7573]},{"source":"Enhancing Trust in the XRPL Ecosystem: The Importance of Verified Domains","translations":{"nl":"Vertrouwen in het XRPL-ecosysteem vergroten: het belang van geverifieerde domeinen"},"pages":[7575]},{"source":"At CalorieToken, we believe that trust and credibility are the cornerstones of any successful blockchain project. That’s why we are excited to extend our heartfelt thanks to","translations":{"nl":"Bij CalorieToken zien we vertrouwen en geloofwaardigheid als de basis van een succesvol blockchainproject. Daarom bedanken we"},"pages":[7575]},{"source":"for providing the XRPL verified domains list! This initiative is a significant step towards fostering a more secure and trustworthy environment within the XRPL community.","translations":{"nl":"van harte voor de lijst met geverifieerde XRPL-domeinen! Dit initiatief is een belangrijke stap naar een veiligere en betrouwbaardere omgeving binnen de XRPL-gemeenschap."},"pages":[7575]},{"source":"Why Verified Domains Matter","translations":{"nl":"Waarom geverifieerde domeinen belangrijk zijn"},"pages":[7575]},{"source":"Verified domains are crucial for several reasons:","translations":{"nl":"Geverifieerde domeinen zijn om verschillende redenen belangrijk:"},"pages":[7575]},{"source":"Enhanced Credibility: Verification helps distinguish legitimate projects and businesses from fraudulent ones. It assures users that they are interacting with genuine entities.","translations":{"nl":"Meer geloofwaardigheid: verificatie helpt legitieme projecten en bedrijven van frauduleuze partijen te onderscheiden. Ze geeft gebruikers vertrouwen dat ze met echte organisaties te maken hebben."},"pages":[7575]},{"source":"Building Trust: Trust is fundamental in the crypto space. By verifying your domain, you demonstrate transparency and commitment to integrity.","translations":{"nl":"Vertrouwen opbouwen: vertrouwen is essentieel in de cryptowereld. Door je domein te verifiëren, laat je transparantie en inzet voor integriteit zien."},"pages":[7575]},{"source":"Increased Security: Verified domains reduce the risk of phishing and other malicious activities, creating a safer environment for users and developers.","translations":{"nl":"Meer veiligheid: geverifieerde domeinen verkleinen het risico op phishing en andere schadelijke activiteiten, wat gebruikers en ontwikkelaars een veiligere omgeving biedt."},"pages":[7575]},{"source":"Join the Movement: Verify Your Domain","translations":{"nl":"Doe mee: verifieer je domein"},"pages":[7575]},{"source":"We encourage all projects and businesses in the XRPL community to follow this lead. Verifying your domain is a straightforward process that can significantly boost your project's credibility and trustworthiness.","translations":{"nl":"We moedigen alle projecten en bedrijven binnen de XRPL-gemeenschap aan dit voorbeeld te volgen. Je domein verifiëren is een eenvoudig proces dat de geloofwaardigheid en betrouwbaarheid van je project kan vergroten."},"pages":[7575]},{"source":"You can check out the list of verified domains here:","translations":{"nl":"Bekijk hier de lijst met geverifieerde domeinen:"},"pages":[7575]},{"source":"Bithomp Verified Domains","translations":{"nl":"Geverifieerde domeinen bij Bithomp"},"pages":[7575]},{"source":"Taking Verification a Step Further: The TOML File","translations":{"nl":"Een extra verificatiestap: het TOML-bestand"},"pages":[7575]},{"source":"For those looking to enhance their verification, consider hosting a TOML file on your domain. This adds an extra layer of authenticity and provides additional information about your project.","translations":{"nl":"Wil je je verificatie uitbreiden? Overweeg een TOML-bestand op je domein te plaatsen. Dat voegt een extra laag van herkomstinformatie toe en geeft meer informatie over je project."},"pages":[7575]},{"source":"Learn How to Create a TOML File: Follow this guide on the XRPL website to create your own TOML file.","translations":{"nl":"Leer een TOML-bestand maken: volg deze handleiding op de XRPL-website om je eigen TOML-bestand te maken."},"pages":[7575]},{"source":"Validate Your TOML: Once created, ensure your TOML file is correctly formatted and validated by using the TOML checker.","translations":{"nl":"Valideer je TOML: controleer na het maken met de TOML-checker of je bestand juist is opgemaakt en geldig is."},"pages":[7575]},{"source":"Our Example","translations":{"nl":"Ons voorbeeld"},"pages":[7575]},{"source":"To provide a clear example, here is our verified domain TOML file:","translations":{"nl":"Als duidelijk voorbeeld vind je hier het TOML-bestand van ons geverifieerde domein:"},"pages":[7575]},{"source":". By hosting this file, we aim to set a standard and inspire others to take similar steps towards building a more secure XRPL ecosystem.","translations":{"nl":". Door dit bestand te hosten, willen we een voorbeeld geven en anderen inspireren soortgelijke stappen te zetten naar een veiliger XRPL-ecosysteem."},"pages":[7575]},{"source":"Together, we can enhance the credibility, trust, and security of the XRPL community. We encourage every project and business to verify their domains and consider the additional step of hosting a TOML file. These efforts will collectively strengthen the entire ecosystem.","translations":{"nl":"Samen kunnen we de geloofwaardigheid, het vertrouwen en de veiligheid binnen de XRPL-gemeenschap vergroten. We moedigen ieder project en bedrijf aan zijn domein te verifiëren en ook een TOML-bestand te overwegen. Samen versterken die inspanningen het hele ecosysteem."},"pages":[7575]},{"source":"Thank you for being a part of this journey towards a more transparent and trustworthy blockchain environment. Let’s build an even more secure and trusted XRPL ecosystem together! 💼🌐","translations":{"nl":"Bedankt dat je meewerkt aan een transparantere en betrouwbaardere blockchainomgeving. Laten we samen een nog veiliger en betrouwbaarder XRPL-ecosysteem bouwen! 💼🌐"},"pages":[7575]},{"source":"Unlock Seamless Trading with Our Integrated Exchange","translations":{"nl":"Soepel handelen met onze geïntegreerde exchange"},"pages":[7578]},{"source":"At CalorieToken, we're thrilled to unveil one of our most powerful features: our integrated exchange, powered by","translations":{"nl":"Bij CalorieToken presenteren we graag een van onze krachtigste functies: onze geïntegreerde exchange, mogelijk gemaakt door"},"pages":[7578]},{"source":". This cutting-edge tool revolutionizes your trading experience by providing seamless connectivity with 44 popular self-custody wallets and access to hundreds of cryptocurrencies, including a special focus on XRP through the Xaman wallet.","translations":{"nl":". Dit vernieuwende hulpmiddel biedt verbinding met 44 populaire wallets in eigen beheer en toegang tot honderden cryptomunten, met bijzondere aandacht voor XRP via de Xaman-wallet."},"pages":[7578]},{"source":"Why Our Integrated Exchange is a Game-Changer","translations":{"nl":"Waarom onze geïntegreerde exchange een verschil maakt"},"pages":[7578]},{"source":"Our integrated exchange is designed to deliver a streamlined and efficient trading experience. Here’s what sets it apart:","translations":{"nl":"Onze geïntegreerde exchange is ontworpen voor eenvoudig en efficiënt handelen. Dit maakt de dienst bijzonder:"},"pages":[7578]},{"source":"Broad Wallet Compatibility: Connect effortlessly with 44 self-custody wallets, including MetaMask, Coinbase Wallet, Trust Wallet, WalletConnect, and Xaman. Xaman, formerly known as Xumm, is tailored specifically for XRP holders, offering enhanced features for managing XRP transactions and assets.","translations":{"nl":"Brede walletcompatibiliteit: verbind eenvoudig met 44 wallets in eigen beheer, waaronder MetaMask, Coinbase Wallet, Trust Wallet, WalletConnect en Xaman. Xaman, eerder Xumm, is speciaal ontwikkeld voor XRP-bezitters en biedt uitgebreide functies voor XRP-transacties en bezittingen."},"pages":[7578]},{"source":"Extensive Cryptocurrency Support: Access a vast range of cryptocurrencies, with hundreds of options available for trading, giving you ample opportunities to diversify and manage your portfolio.","translations":{"nl":"Ondersteuning voor veel cryptomunten: krijg toegang tot honderden verschillende cryptomunten om te verhandelen, met ruime mogelijkheden om je portefeuille te spreiden en te beheren."},"pages":[7578]},{"source":"User-Friendly Interface: Our platform is designed for ease of use, ensuring a smooth experience whether you’re a newcomer or a seasoned trader.","translations":{"nl":"Gebruiksvriendelijke interface: ons platform is ontworpen voor eenvoudig gebruik, zowel voor beginners als ervaren handelaren."},"pages":[7578]},{"source":"Enhanced Security: Self-custody wallets provide you with full control over your assets, ensuring high levels of security and peace of mind.","translations":{"nl":"Meer veiligheid: wallets in eigen beheer geven je volledige controle over je bezittingen, voor een hoog veiligheidsniveau en meer gemoedsrust."},"pages":[7578]},{"source":"How to Get Started","translations":{"nl":"Zo begin je"},"pages":[7578]},{"source":"Getting started with our integrated exchange is simple. Here’s how you can begin:","translations":{"nl":"Beginnen met onze geïntegreerde exchange is eenvoudig. Dit zijn de stappen:"},"pages":[7578]},{"source":"Connect Your Wallet Choose from 44 supported self-custody wallets to link with our exchange. This includes popular options like MetaMask, Trust Wallet, Coinbase Wallet, and Xaman—ideal for XRP enthusiasts who need specialized features. Follow the easy setup instructions to connect your wallet.","translations":{"nl":"Verbind je wallet. Kies uit 44 ondersteunde wallets in eigen beheer om met onze exchange te verbinden. Daarbij zijn bekende opties zoals MetaMask, Trust Wallet, Coinbase Wallet en Xaman, gericht op XRP-gebruikers die speciale functies nodig hebben. Volg de eenvoudige instructies om je wallet te verbinden."},"pages":[7578]},{"source":"Swap Your Assets Once your wallet is connected, you can seamlessly swap a wide range of cryptocurrencies. Trade assets such as $MATIC, $Ethereum, $XRP, and $Calorie quickly and securely, leveraging our platform’s robust features.","translations":{"nl":"Wissel je bezittingen. Zodra je wallet verbonden is, kun je allerlei cryptomunten omwisselen. Verhandel bijvoorbeeld $MATIC, $Ethereum, $XRP en $Calorie snel en veilig met de functies van ons platform."},"pages":[7578]},{"source":"Explore and Trade With access to hundreds of cryptocurrencies, you have extensive options for managing and diversifying your portfolio. Make informed trading decisions and explore various market opportunities with ease.","translations":{"nl":"Verken en handel. Met toegang tot honderden cryptomunten heb je ruime mogelijkheden om je portefeuille te beheren en te spreiden. Neem geïnformeerde handelsbeslissingen en verken verschillende marktmogelijkheden."},"pages":[7578]},{"source":"Supported Wallets and Cryptocurrencies","translations":{"nl":"Ondersteunde wallets en cryptomunten"},"pages":[7578]},{"source":"Our integrated exchange supports a diverse array of wallets and cryptocurrencies, including:","translations":{"nl":"Onze geïntegreerde exchange ondersteunt allerlei wallets en cryptomunten, waaronder:"},"pages":[7578]},{"source":"Wallets: Connect to 44 self-custody wallets, including MetaMask, Coinbase Wallet, Trust Wallet, WalletConnect, and Xaman (formerly Xumm). Xaman is specially designed for XRP users, offering unique tools and features for managing XRP transactions.","translations":{"nl":"Wallets: verbind met 44 wallets in eigen beheer, waaronder MetaMask, Coinbase Wallet, Trust Wallet, WalletConnect en Xaman (voorheen Xumm). Xaman is speciaal ontworpen voor XRP-gebruikers en biedt bijzondere hulpmiddelen voor het beheren van XRP-transacties."},"pages":[7578]},{"source":"Cryptocurrencies: Trade a broad spectrum of assets, from popular tokens like $MATIC, $Ethereum, and $XRP to many other emerging cryptocurrencies. For a full list of supported cryptocurrencies, visit our exchange page.","translations":{"nl":"Cryptomunten: verhandel een breed scala aan bezittingen, van bekende tokens zoals $MATIC, $Ethereum en $XRP tot andere opkomende cryptomunten. Kijk op onze exchangepagina voor de volledige lijst met ondersteunde munten."},"pages":[7578]},{"source":"Experience the Future of Crypto Trading","translations":{"nl":"Ervaar de toekomst van cryptohandel"},"pages":[7578]},{"source":"Our integrated exchange is crafted to enhance your trading experience with its comprehensive wallet compatibility and extensive cryptocurrency support. Whether you're managing a diverse portfolio or executing specific trades, our platform is designed to meet your needs.","translations":{"nl":"Onze geïntegreerde exchange is bedoeld om je handelservaring te verbeteren met brede walletcompatibiliteit en ondersteuning voor veel cryptomunten. Of je nu een gevarieerde portefeuille beheert of specifieke transacties uitvoert, ons platform is ontworpen voor jouw behoeften."},"pages":[7578]},{"source":"Key Features:","translations":{"nl":"Belangrijkste functies:"},"pages":[7578]},{"source":"Connect to 44 Wallets: Including top choices like MetaMask, Trust Wallet, Coinbase Wallet, and Xaman (optimized for XRP).","translations":{"nl":"Verbind met 44 wallets: waaronder bekende keuzes zoals MetaMask, Trust Wallet, Coinbase Wallet en Xaman (geoptimaliseerd voor XRP)."},"pages":[7578]},{"source":"Trade Hundreds of Cryptocurrencies: From well-known assets to new tokens.","translations":{"nl":"Verhandel honderden cryptomunten: van bekende bezittingen tot nieuwe tokens."},"pages":[7578]},{"source":"Fast and Secure: Enjoy efficient transactions with robust security measures.","translations":{"nl":"Snel en veilig: profiteer van efficiënte transacties met stevige beveiligingsmaatregelen."},"pages":[7578]},{"source":"User-Friendly: Navigate the platform with ease, whether you’re new to crypto or a veteran trader.","translations":{"nl":"Gebruiksvriendelijk: navigeer eenvoudig, of je nu begint met crypto of al ervaren bent."},"pages":[7578]},{"source":"Get Started Today","translations":{"nl":"Begin vandaag"},"pages":[7578]},{"source":"Ready to experience the benefits of our integrated exchange? Visit our platform to connect your preferred wallet and start trading today. Discover the convenience and power of a tool designed to elevate your crypto journey:","translations":{"nl":"Wil je de voordelen van onze geïntegreerde exchange ervaren? Bezoek ons platform, verbind je favoriete wallet en begin vandaag met handelen. Ontdek het gemak en de mogelijkheden van een hulpmiddel dat jouw crypto-ervaring wil verbeteren:"},"pages":[7578]},{"source":"Visit the Integrated Exchange","translations":{"nl":"Bezoek de geïntegreerde exchange"},"pages":[7578]},{"source":"Looking Ahead","translations":{"nl":"Vooruitblik"},"pages":[7578]},{"source":"At CalorieToken, we are committed to delivering innovative solutions that enhance your crypto experience. Our integrated exchange is just one example of how we are pushing the boundaries of trading technology. Stay tuned for more updates and features as we continue to advance the future of crypto.","translations":{"nl":"Bij CalorieToken werken we aan vernieuwende oplossingen die jouw crypto-ervaring verbeteren. Onze geïntegreerde exchange is één voorbeeld van hoe we handelstechnologie verder willen brengen. Volg onze updates en nieuwe functies terwijl we verder werken aan de toekomst van crypto."},"pages":[7578]},{"source":"Thank you for being a vital part of our community. Together, let’s shape the future of crypto trading! 🚀💼","translations":{"nl":"Bedankt voor jouw belangrijke rol in onze gemeenschap. Samen geven we de toekomst van cryptohandel vorm! 🚀💼"},"pages":[7578]},{"source":"CalorieToken Hackathon: Building the Future of Health and Blockchain","translations":{"nl":"CalorieToken Hackathon: bouwen aan de toekomst van gezondheid en blockchain"},"pages":[7583]},{"source":"At CalorieToken, we are dedicated to pushing the boundaries of blockchain technology in the health and nutrition sector. Our recent hackathon was a pivotal step in this journey, aimed at fostering innovation and exploring new use cases. Although we faced challenges in participation, the hackathon underscored the importance of such initiatives and their role in driving our mission forward.","translations":{"nl":"Bij CalorieToken willen we de mogelijkheden van blockchaintechnologie voor gezondheid en voeding verder brengen. Onze recente hackathon was daarin een belangrijke stap, gericht op vernieuwing en het verkennen van nieuwe toepassingen. Hoewel de deelname tegenviel, liet de hackathon zien hoe belangrijk zulke initiatieven zijn om onze missie vooruit te helpen."},"pages":[7583]},{"source":"The Essence of the Hackathon","translations":{"nl":"De kern van de hackathon"},"pages":[7583]},{"source":"Our hackathon, held in 2023, was designed to bring together creative minds and technical experts to explore the potential of the Calorie platform. We focused on several key areas to drive development and innovation:","translations":{"nl":"Onze hackathon in 2023 moest creatieve mensen en technische experts samenbrengen om de mogelijkheden van het Calorie-platform te onderzoeken. We richtten ons op enkele belangrijke ontwikkelgebieden:"},"pages":[7583]},{"source":"Encryption Cracking: Enhance the security of login systems.","translations":{"nl":"Versleuteling kraken: de beveiliging van inlogsystemen verbeteren."},"pages":[7583]},{"source":"Food Track App Integration: Develop a comprehensive food tracking system with advanced features.","translations":{"nl":"Integratie van een voedingsregistratie-app: een compleet systeem voor het volgen van voeding ontwikkelen, met uitgebreide functies."},"pages":[7583]},{"source":"DEX Development: Build a customized decentralized exchange on XRPL.","translations":{"nl":"DEX-ontwikkeling: een eigen gedecentraliseerde exchange op de XRPL bouwen."},"pages":[7583]},{"source":"These challenges were aimed at unlocking new possibilities and refining the Calorie ecosystem. Despite the limited participation, the event was a valuable learning experience, highlighting areas for improvement and future focus.","translations":{"nl":"Deze uitdagingen moesten nieuwe mogelijkheden openen en het Calorie-ecosysteem verfijnen. Ondanks de beperkte deelname was het evenement leerzaam en maakte het duidelijk waar verbetering en verdere aandacht nodig zijn."},"pages":[7583]},{"source":"Preparing for the Hackathon: Resources and Tools","translations":{"nl":"Voorbereiding op de hackathon: middelen en hulpmiddelen"},"pages":[7583]},{"source":"To facilitate engagement and ensure participants were well-prepared, we provided a comprehensive setup guide:","translations":{"nl":"Om deelname makkelijker te maken en deelnemers goed voor te bereiden, boden we een uitgebreide installatiehandleiding:"},"pages":[7583]},{"source":"Setup Video Tutorial: We created a step-by-step video guide for installing essential tools like Python, Visual Studio Code, KivyMD, and xrpl-py. This guide was intended to make the setup process accessible to everyone.📹 Watch the Setup Video","translations":{"nl":"Installatievideo: we maakten een stapsgewijze video over het installeren van hulpmiddelen zoals Python, Visual Studio Code, KivyMD en xrpl-py. De handleiding moest de installatie voor iedereen toegankelijk maken. 📹 Bekijk de installatievideo"},"pages":[7583]},{"source":"Installation Steps: Python: Download Python Visual Studio Code: Install VS Code Project Setup: Create a folder and download files from GitHub Install Dependencies: Follow video instructions for installation. Run and Debug: Explore the app’s features using the guide.","translations":{"nl":"Installatiestappen: Python: download Python. Visual Studio Code: installeer VS Code. Projectinrichting: maak een map en download bestanden van GitHub. Afhankelijkheden installeren: volg de video. Starten en fouten onderzoeken: verken de functies van de app met de handleiding."},"pages":[7583]},{"source":"Python: Download Python","translations":{"nl":"Python: download Python"},"pages":[7583]},{"source":"Visual Studio Code: Install VS Code","translations":{"nl":"Visual Studio Code: installeer VS Code"},"pages":[7583]},{"source":"Project Setup: Create a folder and download files from GitHub","translations":{"nl":"Projectinrichting: maak een map en download bestanden van GitHub"},"pages":[7583]},{"source":"Install Dependencies: Follow video instructions for installation.","translations":{"nl":"Afhankelijkheden installeren: volg de installatie-instructies in de video."},"pages":[7583]},{"source":"Run and Debug: Explore the app’s features using the guide.","translations":{"nl":"Starten en fouten onderzoeken: verken de functies van de app met de handleiding."},"pages":[7583]},{"source":"New Developments: Android Integration","translations":{"nl":"Nieuwe ontwikkelingen: Android-integratie"},"pages":[7583]},{"source":"We are excited to announce that the CalorieAppTestnet is now compatible with Android devices. This advancement is a significant step towards making our platform more accessible and user-friendly.","translations":{"nl":"We kondigen met plezier aan dat CalorieAppTestnet nu geschikt is voor Android-apparaten. Dit is een belangrijke stap naar een toegankelijker en gebruiksvriendelijker platform."},"pages":[7583]},{"source":"Download the Test APK:","translations":{"nl":"Download de test-APK:"},"pages":[7583]},{"source":"This APK is for testing purposes only. It is not available on the Play Store, and transactions with test currencies are not real.","translations":{"nl":"Deze APK is uitsluitend bedoeld om te testen. Ze staat niet in de Play Store en transacties met testmunten zijn niet echt."},"pages":[7583]},{"source":"Looking Ahead: Future Hackathons and Use Cases","translations":{"nl":"Vooruitblik: toekomstige hackathons en toepassingen"},"pages":[7583]},{"source":"The experience from this hackathon has reinforced the value of such events in fostering innovation and driving progress. As we look to the future, we are committed to organizing more hackathons and exploring new use cases for the Calorie platform. These events are crucial for gathering insights, testing new ideas, and pushing the boundaries of blockchain technology in health and nutrition.","translations":{"nl":"De ervaring met deze hackathon heeft de waarde van zulke evenementen voor vernieuwing en vooruitgang bevestigd. We willen meer hackathons organiseren en nieuwe toepassingen voor het Calorie-platform verkennen. Die evenementen helpen inzichten te verzamelen, ideeën te testen en de mogelijkheden van blockchain voor gezondheid en voeding te vergroten."},"pages":[7583]},{"source":"Follow us on social media for updates on future hackathons and developments.","translations":{"nl":"Volg ons op sociale media voor updates over toekomstige hackathons en ontwikkelingen."},"pages":[7583]},{"source":"Explore our GitHub repository for the latest on our projects.","translations":{"nl":"Bekijk onze GitHub-repository voor het laatste nieuws over onze projecten."},"pages":[7583]},{"source":"Join Us in Shaping the Future","translations":{"nl":"Geef samen met ons de toekomst vorm"},"pages":[7583]},{"source":"The journey of innovation and development is ongoing. We invite you to stay engaged and be part of our mission to transform health and nutrition through blockchain technology. Your support and involvement are key to making this vision a reality.","translations":{"nl":"Vernieuwing en ontwikkeling gaan door. We nodigen je uit betrokken te blijven en mee te werken aan onze missie om gezondheid en voeding met blockchaintechnologie te veranderen. Jouw steun en betrokkenheid zijn belangrijk om deze visie waar te maken."},"pages":[7583]},{"source":"Let’s continue to explore, innovate, and build the future together! 🌟💡🚀","translations":{"nl":"Laten we samen blijven verkennen, vernieuwen en bouwen aan de toekomst! 🌟💡🚀"},"pages":[7583]},{"source":"The Importance of Test Currency and Testing in Blockchain Development","translations":{"nl":"Het belang van testmunten en testen bij blockchainontwikkeling"},"pages":[7587]},{"source":"In the world of blockchain technology, testing is crucial for ensuring the stability, security, and functionality of new applications and features. At CalorieToken, we recently introduced the \"CalorieTest\" currency, a dedicated test asset designed to facilitate rigorous testing and experimentation. Here’s why having a test currency and robust testing practices are essential in blockchain development.","translations":{"nl":"Bij blockchaintechnologie zijn tests essentieel om de stabiliteit, veiligheid en werking van nieuwe toepassingen en functies te controleren. Bij CalorieToken introduceerden we onlangs CalorieTest, een speciale testmunt voor grondige tests en experimenten. Hieronder leggen we uit waarom een testmunt en goede testmethoden belangrijk zijn bij blockchainontwikkeling."},"pages":[7587]},{"source":"The Role of Test Currency","translations":{"nl":"De rol van testmunten"},"pages":[1207,7587]},{"source":"The introduction of CalorieTest, our exclusive test currency, played a vital role in our development and testing processes. Here's why test currencies are so valuable:","translations":{"nl":"De introductie van onze eigen testmunt CalorieTest speelde een belangrijke rol bij ontwikkeling en tests. Daarom zijn testmunten zo waardevol:"},"pages":[7587]},{"source":"Safe Experimentation: Test currencies allow developers and users to experiment with new features and functionalities without risking real assets. This safe environment encourages creativity and innovation, enabling developers to test new ideas and features thoroughly before they are deployed on the main network.","translations":{"nl":"Veilig experimenteren: met testmunten kunnen ontwikkelaars en gebruikers nieuwe functies proberen zonder echte bezittingen te riskeren. Deze veilige omgeving moedigt creativiteit en vernieuwing aan. Ontwikkelaars kunnen ideeën en functies grondig testen voordat ze op het hoofdnetwerk worden uitgerold."},"pages":[7587]},{"source":"Realistic Testing: By integrating a test currency into our test environment, we can simulate real-world scenarios and interactions. This realistic approach helps identify potential issues and ensures that the application behaves as expected in various conditions.","translations":{"nl":"Realistische tests: door een testmunt in onze testomgeving op te nemen, kunnen we praktijksituaties en interacties nabootsen. Dat helpt mogelijke problemen te herkennen en te controleren of de toepassing onder verschillende omstandigheden werkt zoals verwacht."},"pages":[7587]},{"source":"Improved Feedback: Test currencies facilitate detailed feedback from developers and users. Participants can explore the application, report bugs, and suggest improvements based on their experience with the test currency, leading to more effective refinements and enhancements.","translations":{"nl":"Betere feedback: testmunten maken gedetailleerde feedback van ontwikkelaars en gebruikers mogelijk. Deelnemers kunnen de toepassing verkennen, fouten melden en verbeteringen voorstellen vanuit hun ervaring met de testmunt. Zo kunnen we gerichter verbeteren."},"pages":[7587]},{"source":"Controlled Development: Using a test currency helps manage potential risks by isolating experimental activities from the main network. This controlled environment allows developers to address issues and make adjustments without impacting real transactions or assets.","translations":{"nl":"Gecontroleerde ontwikkeling: een testmunt helpt risico’s te beheersen door experimenten van het hoofdnetwerk te scheiden. In die gecontroleerde omgeving kunnen ontwikkelaars problemen oplossen en aanpassingen doen zonder echte transacties of bezittingen te beïnvloeden."},"pages":[7587]},{"source":"The Broader Importance of Testing","translations":{"nl":"Het bredere belang van testen"},"pages":[7587]},{"source":"Testing is not limited to just the use of test currencies. It encompasses a range of practices and methodologies that are crucial for the successful development and deployment of blockchain applications:","translations":{"nl":"Testen omvat meer dan testmunten gebruiken. Het gaat om verschillende werkwijzen die belangrijk zijn voor het succesvol ontwikkelen en uitrollen van blockchaintoepassingen:"},"pages":[7587]},{"source":"Ensuring Quality: Comprehensive testing is essential for identifying and resolving issues before they affect end users. This includes testing for functionality, performance, security, and usability to ensure that the final product meets high standards of quality.","translations":{"nl":"Kwaliteit bewaken: uitgebreide tests zijn essentieel om problemen te vinden en op te lossen voordat eindgebruikers er last van krijgen. Daarbij horen tests op werking, prestaties, veiligheid en gebruiksgemak, zodat het eindproduct aan hoge kwaliteitseisen voldoet."},"pages":[7587]},{"source":"Fostering Innovation: Testing encourages innovation by providing a space for developers to experiment with new features and technologies. It allows for the exploration of new ideas and solutions, contributing to the evolution and advancement of blockchain technology.","translations":{"nl":"Vernieuwing stimuleren: tests bieden ruimte om met nieuwe functies en technologieën te experimenteren. Ze helpen nieuwe ideeën en oplossingen te verkennen en dragen zo bij aan de verdere ontwikkeling van blockchaintechnologie."},"pages":[7587]},{"source":"Managing Risks: Testing helps manage risks by identifying potential vulnerabilities and weaknesses in the system. By addressing these issues before deployment, developers can minimize the impact of security threats and operational failures.","translations":{"nl":"Risico’s beheersen: tests helpen kwetsbaarheden en zwakke plekken in het systeem te herkennen. Door ze vóór de uitrol op te lossen, kunnen ontwikkelaars de gevolgen van beveiligingsproblemen en storingen beperken."},"pages":[7587]},{"source":"Improving User Experience: Rigorous testing ensures that applications are user-friendly and function smoothly. It helps identify and address any usability issues, leading to a better overall experience for users.","translations":{"nl":"De gebruikerservaring verbeteren: grondige tests helpen toepassingen gebruiksvriendelijk en soepel te laten werken. Ze brengen gebruiksproblemen aan het licht, zodat die kunnen worden opgelost."},"pages":[7587]},{"source":"Supporting Continuous Improvement: Testing is an ongoing process that supports continuous improvement. Regular updates, bug fixes, and feature enhancements are driven by feedback and insights gained from testing, ensuring that the application remains relevant and effective over time.","translations":{"nl":"Voortdurend verbeteren: testen is een doorlopend proces. Regelmatige updates, foutoplossingen en functieverbeteringen worden gevoed door feedback en inzichten uit tests, zodat de toepassing ook later nuttig en effectief blijft."},"pages":[7587]},{"source":"The introduction of CalorieTest and the emphasis on rigorous testing highlight the importance of these practices in blockchain development. By providing a safe and controlled environment for experimentation, test currencies and comprehensive testing methodologies play a crucial role in ensuring the success and reliability of blockchain applications.","translations":{"nl":"De introductie van CalorieTest en de nadruk op grondig testen laten zien hoe belangrijk deze werkwijzen zijn bij blockchainontwikkeling. Een veilige, gecontroleerde experimenteeromgeving met testmunten en uitgebreide tests draagt bij aan het succes en de betrouwbaarheid van blockchaintoepassingen."},"pages":[7587]},{"source":"As we continue to advance in the field of blockchain technology, we remain committed to maintaining high standards of testing and quality assurance. We believe that by prioritizing these practices, we can drive innovation, enhance user experiences, and build robust and secure applications that meet the needs of our community.","translations":{"nl":"Terwijl we verder werken aan blockchaintechnologie, blijven we hoge eisen stellen aan testen en kwaliteitsbewaking. Door deze werkwijzen voorrang te geven, kunnen we vernieuwen, de gebruikerservaring verbeteren en stevige, veilige toepassingen bouwen die aansluiten bij onze gemeenschap."},"pages":[7587]},{"source":"Stay tuned for more updates and developments as we continue to push the boundaries of blockchain technology and strive for excellence in every aspect of our work.","translations":{"nl":"Volg onze updates en ontwikkelingen terwijl we de mogelijkheden van blockchaintechnologie verder verkennen en in elk onderdeel van ons werk naar kwaliteit streven."},"pages":[7587]},{"source":"Discover the Future of Food with Calorie Token: A Comprehensive Overview","translations":{"nl":"Ontdek de toekomst van voeding met CalorieToken: een uitgebreid overzicht"},"pages":[7591]},{"source":"Welcome to the future of food and finance with Calorie Token! Developed by the visionary team of P.H.J. Hendrikse, L.A. Visscher, and S. Zoetman, Calorie Token is poised to revolutionize the food and beverage (F","translations":{"nl":"Welkom bij de toekomst van voeding en financiën met CalorieToken! Het team van P.H.J. Hendrikse, L.A. Visscher en S. Zoetman ontwikkelde CalorieToken om de voedings-"},"pages":[7591]},{"source":"B) industry by seamlessly integrating advanced cryptocurrency technology into everyday transactions. This blog provides an in-depth look at the principles, goals, and innovative features that underpin our groundbreaking initiative.","translations":{"nl":"drankensector te vernieuwen door cryptotechnologie met alledaagse transacties te verbinden. Dit blog beschrijft de uitgangspunten, doelen en vernieuwende functies achter ons initiatief."},"pages":[7591]},{"source":"Introducing Calorie Token","translations":{"nl":"Kennismaking met CalorieToken"},"pages":[7591]},{"source":"Calorie Token is a sub-token built on the XRP Ledger (XRPL), designed specifically for the food and beverage sector. As a digital currency, Calorie Token is set to transform how payments are processed, provide new opportunities for businesses, and enhance consumer experiences in the F","translations":{"nl":"CalorieToken is een subtoken op de XRP Ledger (XRPL), speciaal ontworpen voor de voedings- en drankensector. Als digitale munt wil CalorieToken betalingen veranderen, nieuwe mogelijkheden voor bedrijven bieden en de ervaring van consumenten in de voedings-"},"pages":[7591]},{"source":"B industry. Leveraging the XRPL’s strengths—speed, scalability, and low cost—Calorie Token aims to become the premier digital currency for the global food and beverage market.","translations":{"nl":"drankensector verbeteren. Met de snelheid, schaalbaarheid en lage kosten van de XRPL wil CalorieToken een toonaangevende digitale munt voor de wereldwijde voedings- en drankenmarkt worden."},"pages":[7591]},{"source":"Why Calorie Token?","translations":{"nl":"Waarom CalorieToken?"},"pages":[7591]},{"source":"The transition from traditional fiat money to cryptocurrencies offers numerous advantages, particularly in terms of transaction efficiency, cost-effectiveness, and sustainability. The XRPL’s technology provides a robust foundation for our token, facilitating rapid and secure transactions while minimizing environmental impact. With these benefits, Calorie Token is uniquely positioned to drive innovation and adoption across the F","translations":{"nl":"De overgang van traditioneel geld naar cryptomunten biedt verschillende voordelen, vooral voor de efficiëntie, kosten en duurzaamheid van transacties. De XRPL-technologie vormt een stevige basis voor onze token en maakt snelle, veilige transacties mogelijk met beperkte milieubelasting. Daarmee wil CalorieToken vernieuwing en gebruik binnen de voedings-"},"pages":[7591]},{"source":"B industry.","translations":{"nl":"drankensector stimuleren."},"pages":[7591]},{"source":"Our Purpose: Why We’re Doing This","translations":{"nl":"Ons doel: waarom we dit doen"},"pages":[7591]},{"source":"Calorie Token addresses several pressing needs within the food and beverage sector:","translations":{"nl":"CalorieToken richt zich op verschillende dringende behoeften binnen de voedings- en drankensector:"},"pages":[7591]},{"source":"Access and Convenience: Digital tokens can simplify transactions at various food distributors, including supermarkets, restaurants, and home delivery services.","translations":{"nl":"Toegang en gemak: digitale tokens kunnen betalingen vereenvoudigen bij onder meer supermarkten, restaurants en bezorgdiensten."},"pages":[7591]},{"source":"Government Support: Digital currency solutions like Calorie Token offer governments a new tool to support citizens in accessing essential food resources efficiently.","translations":{"nl":"Overheidssteun: digitale muntoplossingen zoals CalorieToken bieden overheden een nieuw hulpmiddel om burgers efficiënt toegang tot noodzakelijke voeding te geven."},"pages":[7591]},{"source":"Future-Readiness: As the world moves towards digital solutions, Calorie Token is at the forefront of this shift, providing a practical and scalable solution to meet modern needs.","translations":{"nl":"Klaar voor de toekomst: terwijl de wereld overstapt op digitale oplossingen, wil CalorieToken daarin vooroplopen met een praktische, schaalbare oplossing voor moderne behoeften."},"pages":[7591]},{"source":"Calorie Token’s Mission and Vision","translations":{"nl":"De missie en visie van CalorieToken"},"pages":[7591]},{"source":"Mission:","translations":{"nl":"Missie:"},"pages":[7591]},{"source":"Establish a Diverse Network: Forge partnerships within the F&B industry to enhance payment solutions and expand brand presence.","translations":{"nl":"Een breed netwerk opbouwen: samenwerkingen in de voedings- en drankensector aangaan om betaaloplossingen en merkbekendheid te versterken."},"pages":[7591]},{"source":"Integrate Payment Applications: Develop and implement payment systems for businesses and consumers, contributing to a healthier financial ecosystem.","translations":{"nl":"Betaaltoepassingen integreren: betaalsystemen voor bedrijven en consumenten ontwikkelen en invoeren, als bijdrage aan een gezonder financieel ecosysteem."},"pages":[7591]},{"source":"Promote Corporate Social Responsibility: Leverage the XRPL’s advantages to support sustainable practices and corporate social responsibility.","translations":{"nl":"Maatschappelijk verantwoord ondernemen bevorderen: de voordelen van de XRPL gebruiken om duurzame werkwijzen en maatschappelijke verantwoordelijkheid te ondersteunen."},"pages":[7591]},{"source":"Encourage Healthier Lifestyles: Offer tools to help users track their calorie intake and manage their food expenses more effectively.","translations":{"nl":"Een gezondere leefstijl stimuleren: hulpmiddelen bieden om calorie-inname bij te houden en voedingsuitgaven beter te beheren."},"pages":[7591]},{"source":"Vision:","translations":{"nl":"Visie:"},"pages":[7591]},{"source":"Global Integration: Achieve widespread adoption of Calorie Token as a standard currency within the global F&B sector.","translations":{"nl":"Wereldwijde integratie: breed gebruik van CalorieToken als standaardmunt binnen de wereldwijde voedings- en drankensector bereiken."},"pages":[7591]},{"source":"Innovative Solutions: Develop advanced payment systems and applications that streamline transactions and enhance user experiences for consumers, businesses, and investors.","translations":{"nl":"Vernieuwende oplossingen: geavanceerde betaalsystemen en toepassingen ontwikkelen die transacties vereenvoudigen en de ervaring van consumenten, bedrijven en investeerders verbeteren."},"pages":[7591]},{"source":"Currency Details:","translations":{"nl":"Muntgegevens:"},"pages":[7591]},{"source":"Name: Calorie","translations":{"nl":"Naam: Calorie"},"pages":[7591]},{"source":"HEX Code: 43616C6F72696500000000000000000000000000","translations":{"nl":"HEX-code: 43616C6F72696500000000000000000000000000"},"pages":[7591]},{"source":"Issuing Address: rNqGa93B8ewQP9mUwpwqA19SApbf62U7PY","translations":{"nl":"Uitgeversadres: rNqGa93B8ewQP9mUwpwqA19SApbf62U7PY"},"pages":[7591]},{"source":"Initial Supply: 100,000,000,000 Calorie (Fixed Supply)","translations":{"nl":"Oorspronkelijk aanbod: 100.000.000.000 Calorie (vast aanbod)"},"pages":[7591]},{"source":"Distribution Breakdown:","translations":{"nl":"Verdeling:"},"pages":[7591]},{"source":"Community (60%): To foster engagement and build a strong user base through airdrops, giveaways, and community events.","translations":{"nl":"Gemeenschap (60%): om betrokkenheid en een sterke gebruikersbasis op te bouwen via airdrops, weggeefacties en gemeenschapsevenementen."},"pages":[7591]},{"source":"Escrow (20%): Reserved for future needs, including token burns and exchange listings to support long-term growth and liquidity.","translations":{"nl":"Escrow (20%): gereserveerd voor toekomstige behoeften, waaronder het vernietigen van tokens en exchangenoteringen om groei en liquiditeit op langere termijn te ondersteunen."},"pages":[7591]},{"source":"Branding (10%): Dedicated to enhancing brand awareness, developing applications, and securing strategic partnerships.","translations":{"nl":"Merkontwikkeling (10%): voor merkbekendheid, het ontwikkelen van toepassingen en strategische samenwerkingen."},"pages":[7591]},{"source":"Developers (10%): Allocated for future development and innovation to keep the Calorie Token ecosystem at the cutting edge.","translations":{"nl":"Ontwikkelaars (10%): bestemd voor toekomstige ontwikkeling en vernieuwing van het CalorieToken-ecosysteem."},"pages":[7591]},{"source":"CalorieApp: A Comprehensive F&B Solution","translations":{"nl":"CalorieApp: een complete oplossing voor voeding en dranken"},"pages":[7591]},{"source":"CalorieApp is designed to be the ultimate platform for all participants in the F","translations":{"nl":"CalorieApp is bedoeld als breed platform voor alle deelnemers in het voedings-"},"pages":[7591]},{"source":"B ecosystem, offering a range of features tailored to different user needs:","translations":{"nl":"drankenecosysteem, met functies voor verschillende gebruikers:"},"pages":[7591]},{"source":"Consumers:","translations":{"nl":"Consumenten:"},"pages":[7591]},{"source":"Payment System: Easily make and receive payments using Calorie Token, with options to exchange it for other currencies on the XRPL.","translations":{"nl":"Betaalsysteem: eenvoudig betalen en ontvangen met CalorieToken, met mogelijkheden om de token op de XRPL tegen andere munten te wisselen."},"pages":[7591]},{"source":"Food Tracker System (FTS): Track nutritional values and calorie intake, view spending patterns, and manage food expenses effectively.","translations":{"nl":"Voedingsregistratiesysteem (FTS): voedingswaarden en calorie-inname volgen, uitgavenpatronen bekijken en voedingsuitgaven effectief beheren."},"pages":[7591]},{"source":"Custom NFTs: Create and trade NFTs representing unique recipes, preserving and sharing culinary creations.","translations":{"nl":"Eigen NFT’s: NFT’s van bijzondere recepten maken en verhandelen om culinaire creaties te bewaren en te delen."},"pages":[7591]},{"source":"Merchants/Producers:","translations":{"nl":"Winkeliers en producenten:"},"pages":[7591]},{"source":"Integrated Payment System: Manage transactions seamlessly with connections to inventory management, accounting systems, and cash register systems.","translations":{"nl":"Geïntegreerd betaalsysteem: transacties beheren met koppelingen naar voorraadbeheer, boekhouding en kassasystemen."},"pages":[7591]},{"source":"Inventory Management: Real-time updates and notifications for stock levels, with secure data storage and retrieval.","translations":{"nl":"Voorraadbeheer: actuele updates en meldingen over voorraadniveaus, met veilige opslag en teruglezing van gegevens."},"pages":[7591]},{"source":"Custom NFTs: Mint NFTs for menu items, allowing for automated royalty payments and increased revenue through digital assets.","translations":{"nl":"Eigen NFT’s: NFT’s van menugerechten maken, voor automatische royaltybetalingen en extra inkomsten via digitale bezittingen."},"pages":[7591]},{"source":"Investors:","translations":{"nl":"Investeerders:"},"pages":[7591]},{"source":"Payment System: Send and receive payments with comprehensive financial tracking linked to accounting and analysis systems.","translations":{"nl":"Betaalsysteem: betalingen versturen en ontvangen met uitgebreide financiële registratie, gekoppeld aan boekhoud- en analysesystemen."},"pages":[7591]},{"source":"Industry News: Stay updated with the latest developments in the F&B industry, crypto markets, and financial news.","translations":{"nl":"Sectornieuws: op de hoogte blijven van ontwikkelingen in de voedings- en drankensector, cryptomarkten en financieel nieuws."},"pages":[7591]},{"source":"Network Platform: Connect with businesses using Calorie Token, explore investment opportunities, and engage with potential partners.","translations":{"nl":"Netwerkplatform: contact leggen met bedrijven die CalorieToken gebruiken, investeringsmogelijkheden verkennen en mogelijke partners ontmoeten."},"pages":[7591]},{"source":"CalorieApp Network","translations":{"nl":"CalorieApp-netwerk"},"pages":[7591]},{"source":"The CalorieApp Network is designed to enhance online sales and streamline operations for F","translations":{"nl":"Het CalorieApp-netwerk is bedoeld om online verkoop en bedrijfsprocessen voor voedings-"},"pages":[7591]},{"source":"B businesses:","translations":{"nl":"drankenbedrijven te verbeteren:"},"pages":[7591]},{"source":"Online Sales Platform: Enables companies to manage and sell their products online, including takeaway and delivery services.","translations":{"nl":"Online verkoopplatform: bedrijven hun producten online laten beheren en verkopen, waaronder afhaal- en bezorgdiensten."},"pages":[7591]},{"source":"Inventory and Accounting Integration: Supports seamless management of stock and financial records, with tools to improve efficiency and accuracy.","translations":{"nl":"Integratie van voorraad en boekhouding: voorraad en financiële administratie gezamenlijk beheren, met hulpmiddelen voor meer efficiëntie en nauwkeurigheid."},"pages":[7591]},{"source":"Web3 Integration: CalorieDB","translations":{"nl":"Web3-integratie: CalorieDB"},"pages":[7591]},{"source":"At the heart of Calorie Token's infrastructure is CalorieDB, a sophisticated Web3 solution that ensures a seamless and secure user experience:","translations":{"nl":"CalorieDB staat centraal in de infrastructuur van CalorieToken: een uitgebreide Web3-oplossing voor een soepele en veilige gebruikerservaring."},"pages":[7591]},{"source":"On-Chain Data Handling: CalorieDB leverages the XRPL for transaction processing, utilizing private keys to access payment systems, Calorie trustlines, and other XRPL functionalities. Each transaction is linked to a unique hash that connects on-chain activities with off-chain records.","translations":{"nl":"Gegevens op de blockchain: CalorieDB gebruikt de XRPL voor transactieverwerking en privésleutels voor toegang tot betaalsystemen, Calorie-trustlines en andere XRPL-functies. Elke transactie krijgt een unieke hash die activiteiten op de blockchain aan externe gegevens koppelt."},"pages":[7591]},{"source":"Off-Chain Database Layer: CalorieDB utilizes BigchainDB for decentralized data storage, handling extensive transactional data such as purchase details, expenses, and seller information. This off-chain layer integrates with XRPL transactions, ensuring comprehensive data management while preserving privacy and security.","translations":{"nl":"Databaselaag buiten de blockchain: CalorieDB gebruikt BigchainDB voor gedecentraliseerde opslag van uitgebreide transactiegegevens, zoals aankopen, uitgaven en verkoperinformatie. Deze laag is gekoppeld aan XRPL-transacties voor volledig gegevensbeheer met behoud van privacy en veiligheid."},"pages":[7591]},{"source":"Food Product Integration: Data from the Open Food Repo is incorporated into CalorieDB, providing a robust database of nutritional information. This integration allows users to access detailed food product data while maintaining privacy through the use of unique transaction hashes.","translations":{"nl":"Integratie van voedingsproducten: gegevens uit Open Food Repo worden opgenomen in CalorieDB als uitgebreide bron van voedingsinformatie. Daarmee kunnen gebruikers gedetailleerde productgegevens raadplegen, terwijl unieke transactiehashes de privacy ondersteunen."},"pages":[7591]},{"source":"Application Layer: Powered by IPFS services, the CalorieDB application layer ensures decentralization and security. It supports various functions including the Food Tracker System, Inventory Management System, and Analysis System, operating through a Client/Server Model for optimal performance.","translations":{"nl":"Toepassingslaag: de CalorieDB-toepassingslaag gebruikt IPFS-diensten voor decentralisatie en veiligheid. Ze ondersteunt onder meer voedingsregistratie, voorraadbeheer en analyse, via een client-servermodel voor goede prestaties."},"pages":[7591]},{"source":"Security and Privacy","translations":{"nl":"Veiligheid en privacy"},"pages":[7591]},{"source":"At Calorie Token, we prioritize user privacy and data security:","translations":{"nl":"Bij CalorieToken geven we voorrang aan privacy en gegevensbeveiliging:"},"pages":[7591]},{"source":"Private Keys: Access to your Calorie Database is controlled by private keys, ensuring secure and exclusive control over your data.","translations":{"nl":"Privésleutels: toegang tot je Calorie Database wordt door privésleutels geregeld, zodat jij veilig en exclusief controle over je gegevens hebt."},"pages":[7591]},{"source":"Data Management: Users can manage, back up, and delete their data as desired. We adhere to strict legal standards for data protection and privacy.","translations":{"nl":"Gegevensbeheer: gebruikers kunnen hun gegevens naar wens beheren, opslaan als reservekopie en verwijderen. We volgen strenge wettelijke eisen voor gegevensbescherming en privacy."},"pages":[7591]},{"source":"Stay informed and join the conversation as we reshape the future of food with Calorie Token. Follow us on:","translations":{"nl":"Blijf op de hoogte en praat mee terwijl we met CalorieToken de toekomst van voeding vormgeven. Volg ons op:"},"pages":[7591]},{"source":"LinkedIn: Calorie Token","translations":{"nl":"LinkedIn: CalorieToken"},"pages":[7591]},{"source":"Embrace the future of food and finance with Calorie Token, and be part of a revolutionary journey towards a more efficient, sustainable, and healthier F","translations":{"nl":"Omarm de toekomst van voeding en financiën met CalorieToken en werk mee aan een efficiënter, duurzamer en gezonder voedings-"},"pages":[7591]},{"source":"Title: Embracing the Future: The Potential of CalorieToken as a DAO","translations":{"nl":"Titel: de toekomst omarmen: de mogelijkheden van CalorieToken als DAO"},"pages":[7608]},{"source":"We are absolutely thrilled with the recent outcome of the Ripple vs. SEC case, which concluded after a long wait of 3 years and 7 months. This victory marks a significant milestone for the XRPL ecosystem, and we couldn’t be happier. In addition to this exciting development, the Inclusive Financial Technology Foundation (@IncFinTech), formerly known as the XRPL Foundation, has announced their rebranding and is exploring the concept of a Decentralized Autonomous Organization (DAO). This exploration resonates with our own ongoing considerations about transitioning CalorieToken into a DAO.","translations":{"nl":"We zijn erg blij met de recente uitkomst van de rechtszaak tussen Ripple en de SEC, die na drie jaar en zeven maanden is afgerond. Deze overwinning is een belangrijke mijlpaal voor het XRPL-ecosysteem. Daarnaast heeft de Inclusive Financial Technology Foundation (@IncFinTech), voorheen XRPL Foundation, een nieuwe naam aangekondigd en onderzoekt ze het concept van een gedecentraliseerde autonome organisatie (DAO). Dat sluit aan bij onze eigen overwegingen om CalorieToken om te vormen tot een DAO."},"pages":[7608]},{"source":"The Vision for CalorieToken as a DAO","translations":{"nl":"De visie op CalorieToken als DAO"},"pages":[7608]},{"source":"As an officially registered company with the Chamber of Commerce, CalorieToken is committed to exploring the transition to a DAO. We believe that shifting our current responsibilities to a DAO structure could significantly benefit our ecosystem. Here’s why we think a DAO is an important future development:","translations":{"nl":"Als officieel bij de Kamer van Koophandel geregistreerd bedrijf onderzoekt CalorieToken een overgang naar een DAO. We denken dat het overdragen van onze huidige verantwoordelijkheden aan een DAO ons ecosysteem veel kan opleveren. Daarom zien we een DAO als een belangrijke toekomstige ontwikkeling:"},"pages":[7608]},{"source":"Community Involvement Moving to a DAO would enable every participant in the $CAL ecosystem to have a voice in key decisions. This democratic approach aims to involve the community directly in shaping the project's future. By transitioning responsibilities to a DAO, we ensure that decisions reflect the collective input of all stakeholders, not just a central authority.","translations":{"nl":"Betrokkenheid van de gemeenschap: een DAO zou iedere deelnemer aan het $CAL-ecosysteem een stem in belangrijke besluiten geven. Deze democratische aanpak wil de gemeenschap rechtstreeks betrekken bij de toekomst van het project. Door verantwoordelijkheden aan een DAO over te dragen, laten we besluiten aansluiten bij de gezamenlijke inbreng van belanghebbenden en niet alleen bij een centrale partij."},"pages":[7608]},{"source":"Transparency DAOs operate on blockchain technology, allowing all transactions and decisions to be recorded on a public ledger. This transparency could greatly enhance trust within our community. By managing responsibilities through a DAO, we can ensure that all actions are visible and verifiable, promoting openness and accountability.","translations":{"nl":"Transparantie: DAO’s werken met blockchaintechnologie, waardoor transacties en besluiten in een openbaar register kunnen worden vastgelegd. Die transparantie kan het vertrouwen binnen onze gemeenschap versterken. Als een DAO de verantwoordelijkheden beheert, kunnen handelingen zichtbaar en controleerbaar zijn, wat openheid en verantwoording bevordert."},"pages":[7608]},{"source":"Democratic Governance A DAO structure could provide a more democratic governance model, where stakeholders have the power to propose and vote on changes. This could lead to more balanced and well-considered decisions, as opposed to a centralized model. Transitioning our company’s responsibilities to a DAO would involve our community in governance, making the decision-making process more inclusive.","translations":{"nl":"Democratisch bestuur: een DAO kan een democratischer bestuursmodel bieden, waarin belanghebbenden wijzigingen kunnen voorstellen en erover stemmen. Dat kan leiden tot evenwichtigere en beter overwogen besluiten dan bij een centraal model. Het overdragen van bedrijfsverantwoordelijkheden aan een DAO zou onze gemeenschap bij het bestuur betrekken en de besluitvorming toegankelijker maken."},"pages":[7608]},{"source":"Efficient Fund Management Smart contracts within a DAO could automate the allocation and spending of funds, potentially reducing overhead costs and improving efficiency. While not all features might be feasible with current technology, the use of smart contracts could streamline financial management and ensure resources are used effectively for the development of the $CAL ecosystem.","translations":{"nl":"Efficiënt beheer van middelen: slimme contracten binnen een DAO kunnen de verdeling en besteding van geld automatiseren, mogelijk met lagere overheadkosten en meer efficiëntie. Niet alle functies zijn met de huidige technologie haalbaar, maar slimme contracten kunnen financieel beheer vereenvoudigen en middelen effectief inzetten voor het $CAL-ecosysteem."},"pages":[7608]},{"source":"Challenges and Considerations","translations":{"nl":"Uitdagingen en overwegingen"},"pages":[7608]},{"source":"Transitioning to a DAO involves several challenges. Developing robust smart contracts, ensuring secure voting mechanisms, and establishing a comprehensive governance framework are crucial steps. Additionally, educating our community about the DAO model and encouraging active participation will be essential for its success.","translations":{"nl":"Een overgang naar een DAO brengt uitdagingen mee. Betrouwbare slimme contracten, veilige stemmechanismen en een volledig bestuurskader zijn essentiële stappen. Ook moeten we de gemeenschap uitleg geven over het DAO-model en actieve deelname aanmoedigen."},"pages":[7608]},{"source":"The Path Forward","translations":{"nl":"De weg vooruit"},"pages":[7608]},{"source":"As we explore the transition to a DAO, we recognize the complexities involved but also the significant potential benefits. Moving our company’s responsibilities to a DAO could enhance how we manage operations, engage with our community, and ensure transparency and efficiency. We are committed to carefully evaluating this transition and working towards a model that benefits all participants in our ecosystem.","translations":{"nl":"We erkennen zowel de complexiteit als de mogelijke voordelen van een overgang naar een DAO. Het overdragen van bedrijfsverantwoordelijkheden kan onze bedrijfsvoering, betrokkenheid van de gemeenschap, transparantie en efficiëntie verbeteren. We willen die overgang zorgvuldig beoordelen en werken aan een model dat alle deelnemers helpt."},"pages":[7608]},{"source":"The recent Ripple vs. SEC case outcome and the evolving focus of organizations like @IncFinTech (Inclusive Financial Technology Foundation) highlight the dynamic nature of the blockchain space. At CalorieToken, we are excited about the potential of a DAO structure to transform how we manage our responsibilities and involve our community. We will continue to explore this promising direction and keep you updated on our progress.","translations":{"nl":"De recente uitkomst van Ripple tegen de SEC en de veranderende aandacht van organisaties zoals @IncFinTech laten zien hoe dynamisch de blockchainwereld is. Bij CalorieToken zijn we enthousiast over de mogelijkheden van een DAO om onze verantwoordelijkheden anders te beheren en de gemeenschap te betrekken. We blijven deze richting onderzoeken en houden jullie op de hoogte."},"pages":[7608]},{"source":"For more detailed insights and updates, follow our blog and join us on this exciting journey towards decentralization and community empowerment.","translations":{"nl":"Volg ons blog voor meer inzichten en updates en ga met ons mee op weg naar decentralisatie en invloed voor de gemeenschap."},"pages":[7608]},{"source":"Read more on our blog:","translations":{"nl":"Lees meer op ons blog:"},"pages":[7608]},{"source":"This article records a discussion from August 2024 about a possible DAO structure. A full transfer of the project to a DAO remains a future direction, not an announced completed transition. The legal and organisational references below belong to the article’s original period. For the current product scope, see the project FAQ.","translations":{"nl":"Dit artikel beschrijft een discussie uit augustus 2024 over een mogelijke DAO-structuur. Volledige overdracht van het project aan een DAO blijft een toekomstige richting en is niet als voltooid aangekondigd. De juridische en organisatorische verwijzingen horen bij de oorspronkelijke periode van het artikel. Bekijk de project-FAQ voor de huidige mogelijkheden."},"pages":[7608]},{"source":"This article records a proposed approach to unfunded XRPL offers. The XRP Ledger can remove unfunded offers when it encounters them during transaction processing; an OfferCancel transaction cancels an offer owned by its sending account. An external monitoring service does not automatically gain permission to cancel other accounts’ offers. The proposal below should not be read as evidence of a deployed CalorieApp service.","translations":{"nl":"Dit artikel beschrijft een voorgestelde aanpak voor ongedekte XRPL-orders. De XRP Ledger kan ongedekte orders verwijderen wanneer hij die tegenkomt tijdens transactieverwerking. Een OfferCancel-transactie annuleert een order van de rekening die de transactie verstuurt. Een externe controledienst krijgt niet automatisch toestemming om orders van andere accounts te annuleren. Het voorstel hieronder bewijst niet dat zo’n CalorieApp-dienst is uitgerold."},"pages":[7545]},{"source":"This is a historical project update from 2024. Today, CalorieApp provides food search, nutrition information and a personal food log using Open Food Facts. The FoodRepo, BigchainDB and IPFS descriptions below record earlier development plans; they are not a description of the current web app’s deployed architecture. Team circumstances and listing aspirations should also be read in their original context.","translations":{"nl":"Dit is een historische projectupdate uit 2024. CalorieApp biedt nu voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek met Open Food Facts. De beschrijvingen van FoodRepo, BigchainDB en IPFS hieronder gaan over eerdere ontwikkelplannen en niet over de uitgerolde architectuur van de huidige webapp. Ook de teamsituatie en noteringsambities horen bij hun oorspronkelijke context."},"pages":[7564]},{"source":"This article concerns the earlier CalorieAppTestnet Android prototype. The APK link below is retained as development history and is not the current web app or a Play Store release. For the current food search, nutrition information and personal food log, open CalorieApp in your browser.","translations":{"nl":"Dit artikel gaat over het eerdere Android-prototype CalorieAppTestnet. De APK-link blijft bewaard als ontwikkelgeschiedenis en is niet de huidige webapp of een Play Store-release. Open CalorieApp in je browser voor de huidige voedingszoekfunctie, voedingsinformatie en het persoonlijke voedingsdagboek."},"pages":[7569]},{"source":"This article describes the former embedded exchange experience. That integration is outside the current CalorieApp web version, which focuses on food search, nutrition information and a personal food log. The wallet counts, supported assets and trading instructions below are historical descriptions, not a current service list. For today’s product scope, see the project FAQ.","translations":{"nl":"Dit artikel beschrijft de vroegere ingebedde exchange. Die integratie valt buiten de huidige CalorieApp-webversie, die zich richt op voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek. De aantallen wallets, ondersteunde munten en handelsinstructies hieronder zijn historische beschrijvingen en geen actuele dienstenlijst. Bekijk de project-FAQ voor de huidige mogelijkheden."},"pages":[7578]},{"source":"This article announced the donation page in 2023. Voluntary contributions support work on CalorieApp and the wider project. The merchandise and NFT launch language below belongs to that historical announcement and does not confirm current availability. Read the current donation page and its terms before contributing.","translations":{"nl":"Dit artikel kondigde in 2023 de donatiepagina aan. Vrijwillige bijdragen ondersteunen het werk aan CalorieApp en het bredere project. De tekst over de introductie van merchandise en NFT’s hoort bij die historische aankondiging en bevestigt geen huidige beschikbaarheid. Lees de actuele donatiepagina en voorwaarden voordat je bijdraagt."},"pages":[7573]},{"source":"This article records the 2023 CalorieToken hackathon and early development experiments. It is not an open invitation to a current competition, and the APK links refer to the earlier testnet prototype. New events will have their own announcements. The current browser app is available from the CalorieApp page.","translations":{"nl":"Dit artikel beschrijft de CalorieToken-hackathon van 2023 en vroege ontwikkelproeven. Het is geen uitnodiging voor een huidige wedstrijd. De APK-links verwijzen naar het eerdere Testnet-prototype. Nieuwe evenementen krijgen een eigen aankondiging. De huidige browserapp is bereikbaar via de CalorieApp-pagina."},"pages":[7583]},{"source":"This article records the project’s work on domain and account information. Domain verification establishes a specific link between a domain and an account or validator; it is not an endorsement of every claim, service or token associated with that domain. A TOML entry claiming an account must be checked against that account’s on-ledger Domain field.","translations":{"nl":"Dit artikel beschrijft het werk van het project aan domein- en accountinformatie. Domeinverificatie bevestigt een specifieke koppeling tussen een domein en een account of validator; ze keurt niet iedere claim, dienst of token bij dat domein goed. Een accountvermelding in een TOML-bestand moet worden vergeleken met het Domain-veld van dat account op de ledger."},"pages":[7575]},{"source":"This article explains the role of test assets in the project’s early experiments. CalorieTest and Lipisa belong to that development context. Their mention does not mean that the current CalorieApp browser experience includes a test wallet, token faucet or payment service. Its current focus is food search, nutrition information and a personal food log.","translations":{"nl":"Dit artikel legt de rol van testmiddelen in de vroege experimenten uit. CalorieTest en Lipisa horen bij die ontwikkelcontext. Hun vermelding betekent niet dat de huidige CalorieApp-browserervaring een testwallet, tokenfaucet of betaaldienst bevat. De huidige aandacht ligt bij voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek."},"pages":[7587]},{"source":"This is the project’s original 2023 overview. The allocation percentages describe the initial plan, not current wallet balances. The payment, NFT, inventory, private-key and BigchainDB/IPFS sections describe that early vision. Today, CalorieApp provides food search, nutrition information and a personal food log using Open Food Facts. The project FAQ explains the current scope.","translations":{"nl":"Dit is het oorspronkelijke projectoverzicht uit 2023. De verdelingspercentages beschrijven het eerste plan en niet de huidige wallettegoeden. De onderdelen over betalingen, NFT’s, voorraad, privésleutels en BigchainDB/IPFS beschrijven die vroege visie. CalorieApp biedt nu voeding zoeken, voedingsinformatie en een persoonlijk voedingsdagboek met Open Food Facts. De project-FAQ licht de huidige mogelijkheden toe."},"pages":[7591]},{"source":"Tackling Frontrunning on the XRPL","translations":{"nl":"Frontrunning op de XRPL aanpakken"},"pages":[1207,7545]},{"source":"An Update on Our Journey and Future Plans","translations":{"nl":"Een update over onze voortgang en toekomstplannen"},"pages":[1207,7564]},{"source":"CalorieApp Test APK on Android!","translations":{"nl":"CalorieApp-test-APK voor Android!"},"pages":[1207,7569]},{"source":"Our Donations Page is LIVE!","translations":{"nl":"Onze donatiepagina is live!"},"pages":[1207,7573]},{"source":"The Importance of Verified Domains","translations":{"nl":"Het belang van geverifieerde domeinen"},"pages":[1207,7575]},{"source":"Our Integrated Exchange","translations":{"nl":"Onze geïntegreerde exchange"},"pages":[1207,7578]},{"source":"CalorieToken Hackathon","translations":{"nl":"CalorieToken-hackathon"},"pages":[1207,7583]},{"source":"A Comprehensive Overview","translations":{"nl":"Een uitgebreid overzicht"},"pages":[1207,7591]},{"source":"The Potential of CalorieToken as a DAO","translations":{"nl":"De mogelijkheden van CalorieToken als DAO"},"pages":[1207,7608]},{"source":"About this article — September 2026","translations":{"nl":"Over dit artikel — september 2026"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]},{"source":"Project FAQ","translations":{"nl":"Project-FAQ"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]},{"source":"Current donation information","translations":{"nl":"Actuele donatie-informatie"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]},{"source":"XRPL offer lifecycle","translations":{"nl":"Levenscyclus van XRPL-orders"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]},{"source":"XRPL TOML specification","translations":{"nl":"XRPL TOML-specificatie"},"pages":[7545,7564,7569,7573,7575,7578,7583,7587,7591,7608]},{"source":"CalorieToken explores how digital tools and the XRP Ledger can support the food and beverage sector.","translations":{"nl":"CalorieToken onderzoekt hoe digitale hulpmiddelen en de XRP Ledger de voedings- en drankensector kunnen ondersteunen.","es":"CalorieToken explora cómo las herramientas digitales y el XRP Ledger pueden apoyar al sector de alimentos y bebidas.","fr":"CalorieToken explore comment les outils numériques et le XRP Ledger peuvent soutenir le secteur de l’alimentation et des boissons.","pt":"CalorieToken explora como ferramentas digitais e o XRP Ledger podem apoiar o setor de alimentos e bebidas.","zh-Hans":"CalorieToken 探索数字工具和 XRP Ledger 如何支持食品饮料行业。","id":"CalorieToken menjajaki bagaimana alat digital dan XRP Ledger dapat mendukung sektor makanan dan minuman.","hi":"CalorieToken जाँचता है कि डिजिटल उपकरण और XRP Ledger खाद्य व पेय क्षेत्र की मदद कैसे कर सकते हैं।","bn":"CalorieToken অনুসন্ধান করে কীভাবে ডিজিটাল সরঞ্জাম ও XRP Ledger খাদ্য ও পানীয় খাতকে সহায়তা করতে পারে।","ar":"يستكشف CalorieToken كيف يمكن للأدوات الرقمية وXRP Ledger دعم قطاع الأغذية والمشروبات.","ur":"CalorieToken دریافت کرتا ہے کہ ڈیجیٹل اوزار اور XRP Ledger خوراک اور مشروبات کے شعبے کی مدد کیسے کر سکتے ہیں۔"},"pages":[1090]},{"source":"CalorieApp is the project's current web app for finding food products, understanding nutrition and keeping a personal food log.","translations":{"nl":"CalorieApp is de huidige webapp van het project om voedingsproducten te zoeken, voedingsinformatie te begrijpen en een eigen eetdagboek bij te houden.","es":"CalorieApp es la aplicación web actual del proyecto para buscar alimentos, entender su nutrición y llevar un diario personal.","fr":"CalorieApp est l’application web actuelle du projet pour trouver des aliments, comprendre leur nutrition et tenir un journal alimentaire personnel.","pt":"CalorieApp é o aplicativo web atual do projeto para encontrar alimentos, entender sua nutrição e manter um diário alimentar pessoal.","zh-Hans":"CalorieApp 是本项目当前的网页应用,用于查找食品、了解营养并记录个人饮食。","id":"CalorieApp adalah aplikasi web proyek saat ini untuk mencari makanan, memahami gizi, dan membuat catatan makanan pribadi.","hi":"CalorieApp परियोजना का वर्तमान वेब ऐप है, जिसमें खाद्य उत्पाद खोज सकते हैं, पोषण समझ सकते हैं और निजी भोजन डायरी रख सकते हैं।","bn":"CalorieApp প্রকল্পের বর্তমান ওয়েব অ্যাপ, যেখানে খাবার খোঁজা, পুষ্টি বোঝা ও ব্যক্তিগত খাবারের ডায়েরি রাখা যায়।","ar":"CalorieApp هو تطبيق الويب الحالي للمشروع للعثور على الأطعمة وفهم التغذية والاحتفاظ بسجل طعام شخصي.","ur":"CalorieApp منصوبے کی موجودہ ویب ایپ ہے جس سے غذائی مصنوعات تلاش، غذائیت کی معلومات سمجھ اور ذاتی خوراک کی ڈائری رکھی جا سکتی ہے۔"},"pages":[1090]},{"source":"CalorieApp is the project’s current web app for finding food products, understanding nutrition and keeping a personal food log.","translations":{"nl":"CalorieApp is de huidige webapp van het project om voedingsproducten te zoeken, voedingsinformatie te begrijpen en een eigen eetdagboek bij te houden.","es":"CalorieApp es la aplicación web actual del proyecto para buscar alimentos, entender su nutrición y llevar un diario personal.","fr":"CalorieApp est l’application web actuelle du projet pour trouver des aliments, comprendre leur nutrition et tenir un journal alimentaire personnel.","pt":"CalorieApp é o aplicativo web atual do projeto para encontrar alimentos, entender sua nutrição e manter um diário alimentar pessoal.","zh-Hans":"CalorieApp 是本项目当前的网页应用,用于查找食品、了解营养并记录个人饮食。","id":"CalorieApp adalah aplikasi web proyek saat ini untuk mencari makanan, memahami gizi, dan membuat catatan makanan pribadi.","hi":"CalorieApp परियोजना का वर्तमान वेब ऐप है, जिसमें खाद्य उत्पाद खोज सकते हैं, पोषण समझ सकते हैं और निजी भोजन डायरी रख सकते हैं।","bn":"CalorieApp প্রকল্পের বর্তমান ওয়েব অ্যাপ, যেখানে খাবার খোঁজা, পুষ্টি বোঝা ও ব্যক্তিগত খাবারের ডায়েরি রাখা যায়।","ar":"CalorieApp هو تطبيق الويب الحالي للمشروع للعثور على الأطعمة وفهم التغذية والاحتفاظ بسجل طعام شخصي.","ur":"CalorieApp منصوبے کی موجودہ ویب ایپ ہے جس سے غذائی مصنوعات تلاش، غذائیت کی معلومات سمجھ اور ذاتی خوراک کی ڈائری رکھی جا سکتی ہے۔"},"pages":[1090]},{"source":"Our wider vision includes practical food data tools and possible future payment applications for consumers and businesses.","translations":{"nl":"Onze bredere visie omvat praktische hulpmiddelen voor voedingsgegevens en mogelijke toekomstige betaaltoepassingen voor consumenten en bedrijven.","es":"Nuestra visión más amplia incluye herramientas prácticas de datos alimentarios y posibles aplicaciones futuras de pago para consumidores y empresas.","fr":"Notre vision élargie comprend des outils pratiques de données alimentaires et d’éventuelles applications de paiement pour les particuliers et les entreprises.","pt":"Nossa visão mais ampla inclui ferramentas práticas de dados alimentares e possíveis aplicações futuras de pagamento para consumidores e empresas.","zh-Hans":"我们的广泛愿景包括实用的食品数据工具,以及未来可能面向消费者和企业的支付应用。","id":"Visi kami yang lebih luas mencakup alat data makanan praktis dan kemungkinan aplikasi pembayaran masa depan bagi konsumen dan bisnis.","hi":"हमारी व्यापक दृष्टि में व्यावहारिक खाद्य डेटा उपकरण और उपभोक्ताओं व व्यवसायों के लिए भविष्य के संभावित भुगतान ऐप शामिल हैं।","bn":"আমাদের বৃহত্তর পরিকল্পনায় ব্যবহারিক খাদ্য-তথ্যের সরঞ্জাম এবং ভবিষ্যতে ভোক্তা ও ব্যবসার জন্য সম্ভাব্য পেমেন্ট অ্যাপ রয়েছে।","ar":"تشمل رؤيتنا الأوسع أدوات عملية لبيانات الغذاء وتطبيقات دفع مستقبلية محتملة للمستهلكين والشركات.","ur":"ہمارے وسیع تر تصور میں عملی غذائی ڈیٹا کے اوزار اور صارفین و کاروبار کے لیے مستقبل کی ممکنہ ادائیگی کی ایپس شامل ہیں۔"},"pages":[1090]},{"source":"Calorie aims to be the world’s food token","translations":{"nl":"Calorie streeft ernaar de foodtoken van de wereld te worden","es":"Calorie aspira a ser el token alimentario del mundo","fr":"Calorie ambitionne de devenir le jeton alimentaire mondial","pt":"Calorie pretende ser o token alimentar do mundo","zh-Hans":"Calorie 致力于成为全球食品代币","id":"Calorie bercita-cita menjadi token makanan dunia","hi":"Calorie का लक्ष्य दुनिया का खाद्य टोकन बनना है","bn":"Calorie-এর লক্ষ্য বিশ্বের খাদ্য টোকেন হওয়া","ar":"يطمح Calorie إلى أن يكون رمز الغذاء العالمي","ur":"Calorie کا مقصد دنیا کا فوڈ ٹوکن بننا ہے"},"pages":["*"]},{"source":"Market cap","translations":{"nl":"Marktkapitalisatie","es":"Capitalización","fr":"Capitalisation","pt":"Capitalização","zh-Hans":"市值","id":"Kapitalisasi pasar","hi":"बाज़ार पूँजीकरण","bn":"বাজার মূলধন","ar":"القيمة السوقية","ur":"مارکیٹ کیپ"},"pages":["*"]},{"source":"Rank","translations":{"nl":"Rang","es":"Posición","fr":"Rang","pt":"Posição","zh-Hans":"排名","id":"Peringkat","hi":"स्थान","bn":"অবস্থান","ar":"الترتيب","ur":"درجہ"},"pages":["*"]},{"source":"Holders","translations":{"nl":"Houders","es":"Titulares","fr":"Détenteurs","pt":"Detentores","zh-Hans":"持有人","id":"Pemegang","hi":"धारक","bn":"ধারক","ar":"الحاملون","ur":"ہولڈرز"},"pages":["*"]},{"source":"XPMarket data","translations":{"nl":"XPMarket-gegevens","es":"Datos de XPMarket","fr":"Données XPMarket","pt":"Dados do XPMarket","zh-Hans":"XPMarket 数据","id":"Data XPMarket","hi":"XPMarket डेटा","bn":"XPMarket তথ্য","ar":"بيانات XPMarket","ur":"XPMarket ڈیٹا"},"pages":["*"]},{"source":"View CAL on XPMarket","translations":{"nl":"Bekijk CAL op XPMarket","es":"Ver CAL en XPMarket","fr":"Voir CAL sur XPMarket","pt":"Ver CAL no XPMarket","zh-Hans":"在 XPMarket 查看 CAL","id":"Lihat CAL di XPMarket","hi":"XPMarket पर CAL देखें","bn":"XPMarket-এ CAL দেখুন","ar":"عرض CAL على XPMarket","ur":"XPMarket پر CAL دیکھیں"},"pages":["*"]},{"source":"Posts on X","translations":{"nl":"Berichten op X","es":"Publicaciones en X","fr":"Publications sur X","pt":"Publicações no X","zh-Hans":"X 上的帖子","id":"Postingan di X","hi":"X पर पोस्ट","bn":"X-এর পোস্ট","ar":"منشورات X","ur":"X پر پوسٹس"},"pages":["*"]},{"source":"Shopping Cart","translations":{"nl":"Winkelwagen","es":"Carrito","fr":"Panier","pt":"Carrinho","zh-Hans":"购物车","id":"Keranjang","hi":"खरीदारी की टोकरी","bn":"কেনাকাটার ঝুড়ি","ar":"سلة التسوق","ur":"خریداری کی ٹوکری"},"pages":["*"]},{"source":"Order Complete","translations":{"nl":"Bestelling afgerond","es":"Pedido completado","fr":"Commande terminée","pt":"Pedido concluído","zh-Hans":"订单完成","id":"Pesanan selesai","hi":"ऑर्डर पूरा हुआ","bn":"অর্ডার সম্পূর্ণ","ar":"اكتمل الطلب","ur":"آرڈر مکمل"},"pages":["*"]}]} diff --git a/wordpress-plugins/calorietoken-site-style/assets/content-language.js b/wordpress-plugins/calorietoken-site-style/assets/content-language.js index e5f58ae0..d11a4a76 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/content-language.js +++ b/wordpress-plugins/calorietoken-site-style/assets/content-language.js @@ -6,7 +6,7 @@ var lookup=new Map(), records=new WeakMap(), active=[], observer=null, queued=false, locale='en'; var excluded='script,style,template,svg,iframe,input,textarea,select,[contenteditable],.xl-card,[data-calorieapp-account],[data-calorieapp-embed],#ctstyle-testnet-secret,.woocommerce-customer-details,.woocommerce-order-details,.woocommerce-order-overview,.comment-content,.comment-list,.cmplz-cookiebanner,.ctstyle-help-reply'; var owned='[data-cal-buy-copy],#ctstyle-account-app,#ctstyle-app-launcher,.ctstyle-discovery,.ctstyle-discovery-card:not(.ctstyle-faq-hub),.calorieapp-tokenomics-note,[data-calorieapp-trustline-ui],[data-ctstyle-app-info],.ctstyle-faq-hub'; - var candidates='.brz-rich-text p,.brz-rich-text li,.brz-rich-text h1,.brz-rich-text h2,.brz-rich-text h3,.brz-rich-text h4,.brz-rich-text h5,.brz-rich-text summary,.entry-content p,.entry-content li,.entry-content h1,.entry-content h2,.entry-content h3,.entry-title,.ctstyle-title h1,.ctstyle-title h2,.ctstyle-roadmap-preview p,.ctstyle-roadmap-preview h3,.ctstyle-roadmap-preview a,.calorieapp-context-note p,.calorieapp-context-note strong,.calorieapp-context-note a,.woocommerce label,.woocommerce button,.woocommerce th,.woocommerce h2,.woocommerce h3,.woocommerce .product_title,.woocommerce a,.woocommerce-notices-wrapper,.brz-posts p,.brz-posts h2,.brz-posts a'; + var candidates='.ctstyle-legal p,.ctstyle-market-card h2,.ctstyle-market-card p,.brz-rich-text p,.brz-rich-text li,.brz-rich-text h1,.brz-rich-text h2,.brz-rich-text h3,.brz-rich-text h4,.brz-rich-text h5,.brz-rich-text summary,.entry-content p,.entry-content li,.entry-content h1,.entry-content h2,.entry-content h3,.entry-title,.ctstyle-title h1,.ctstyle-title h2,.ctstyle-roadmap-preview p,.ctstyle-roadmap-preview h3,.ctstyle-roadmap-preview a,.calorieapp-context-note p,.calorieapp-context-note strong,.calorieapp-context-note a,.woocommerce label,.woocommerce button,.woocommerce th,.woocommerce h2,.woocommerce h3,.woocommerce .product_title,.woocommerce a,.woocommerce-notices-wrapper,.brz-posts p,.brz-posts h2,.brz-posts a'; function norm(v){return String(v||'').replace(/\s+/g,' ').trim();} cfg.entries.forEach(function(row){if(row&&typeof row.source==='string'&&row.translations)lookup.set(norm(row.source),row);}); function allowed(){return document.body&&document.body.matches('.ctstyle-enabled,.ctstyle-footer-only')&&!document.body.matches('.page-id-8001,.brz-ed')&&!document.querySelector('.brz-ed,#brz-ed-iframe')&&['https://calorietoken.net','https://www.calorietoken.net'].includes(window.location.origin)&&!/\/wp-admin\//.test(window.location.pathname)&&!Array.from(new URL(window.location.href).searchParams.keys()).some(function(k){return /^(?:preview|customize_changeset_uuid|brizy-edit|brizy-edit-iframe|brz-edit|brz-edit-iframe|xl-)/.test(k);});} @@ -60,7 +60,7 @@ ['CalorieAppTokenomics'].forEach(function(key){if(window[key]&&typeof window[key].setLocale==='function')window[key].setLocale(locale);}); active=active.filter(function(r){return r.node.isConnected;});active.forEach(paint); document.querySelectorAll(candidates).forEach(block); - document.querySelectorAll('.brz-rich-text,.entry-content,.woocommerce,.woocommerce-notices-wrapper,.ctstyle-roadmap-preview,.calorieapp-context-note,.brz-posts').forEach(function(root){if(!records.has(root))fragments(root);}); + document.querySelectorAll('.ctstyle-legal,.ctstyle-market-card,.brz-rich-text,.entry-content,.woocommerce,.woocommerce-notices-wrapper,.ctstyle-roadmap-preview,.calorieapp-context-note,.brz-posts').forEach(function(root){if(!records.has(root))fragments(root);}); if(observer)observer.observe(document.body,{subtree:true,childList:true,characterData:true}); } window.CalorieTokenContentLanguageUI={refresh:refresh,getLocale:function(){return locale;},coverage:function(){return {locale:locale,translated:active.filter(function(r){return r.node.isConnected&&locale!=='en'&&!!r.row.translations[locale];}).length};}}; diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.css b/wordpress-plugins/calorietoken-site-style/assets/discovery.css index 5707fba8..da3c1866 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.css +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.css @@ -167,7 +167,7 @@ body.ctstyle-enabled .ctstyle-testnet-import li {padding-inline-start:8px;margin :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is([data-calorieapp-shortcut="top"],[data-calorieapp-shortcut="app"],.ctstyle-app-launcher) { --ct-float-bg:#008d36;--ct-float-fg:#fff;--ct-float-hover:#505ba9;--ct-float-hover-fg:#fff; } -:is(body.ctstyle-enabled,body.ctstyle-footer-only) [data-calorieapp-shortcut="exchange"] { +:is(body.ctstyle-enabled,body.ctstyle-footer-only) :is([data-calorieapp-shortcut="exchange"],[data-ctstyle-exchange-shortcut]) { --ct-float-bg:#f9b233;--ct-float-fg:#303449;--ct-float-hover:#008d36;--ct-float-hover-fg:#fff; } :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.calorieapp-page-tool,.ctstyle-app-launcher > details > summary) { @@ -214,6 +214,20 @@ body.ctstyle-enabled .ctstyle-testnet-help summary {cursor:pointer;min-height:44 body.ctstyle-enabled .ctstyle-faq-item {padding:8px 0;border-top:1px solid #d4d8e7;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-help-widget,.ctstyle-faq-hub) :is(input,button,a,summary):focus-visible {outline:3px solid #008d36;outline-offset:3px;} /* S27: visible app/language controls inside the existing Xaman card. */ +/* A single compact mobile row leaves forms and footer links usable. */ +@media(max-width:767px){ + :is(body.ctstyle-enabled,body.ctstyle-footer-only) {padding-bottom:calc(64px + env(safe-area-inset-bottom))!important;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .calorieapp-page-tools {box-sizing:border-box;flex-direction:row-reverse!important;width:auto!important;max-width:calc(100vw - 76px);gap:4px!important;right:max(8px,env(safe-area-inset-right))!important;bottom:calc(8px + env(safe-area-inset-bottom))!important;z-index:10000;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .calorieapp-page-tool-position {width:44px!important;flex:0 0 44px;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .calorieapp-page-tool {width:44px;height:44px;padding:7px;border-radius:12px;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .calorieapp-page-tool-icon {width:26px;height:26px;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-app-launcher {inset-inline-start:max(8px,env(safe-area-inset-left));bottom:calc(8px + env(safe-area-inset-bottom));max-width:44px;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-app-launcher>details>summary {width:44px;min-width:44px;height:44px;min-height:44px;padding:6px;gap:0;border-radius:12px;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-app-launcher>details>summary>span {position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-app-launcher>details>summary::after {display:none;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-app-launcher>details>summary>img {width:28px;height:28px;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-app-launcher-panel {width:340px;max-width:calc(100vw - 16px);max-height:calc(100dvh - 100px);} +} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-account-app { box-sizing:border-box;width:100%;min-width:0;padding:12px;border-top:2px solid #f9b233; background:#fff;color:#303449;font:16px/1.45 Amiri,Georgia,serif;text-align:start;border-radius:0 0 12px 12px; @@ -302,3 +316,5 @@ body.ctstyle-enabled #ctstyle-testnet>.ctstyle-test-steps {padding-top:10px;bord body.ctstyle-enabled .ctstyle-trustline-option>.brz-row>.brz-columns {border:0!important;border-radius:0!important;background:transparent!important;box-shadow:none!important;} body.ctstyle-enabled .ctstyle-trustline-option>.brz-row>.brz-columns>.brz-bg {display:none!important;} body.ctstyle-enabled .ctstyle-trustline-option h2::before {display:none!important;} + +:is(body.ctstyle-enabled,body.ctstyle-footer-only):has(.cmplz-cookiebanner.cmplz-show) .calorieapp-page-tools {visibility:hidden;} diff --git a/wordpress-plugins/calorietoken-site-style/assets/refinements.css b/wordpress-plugins/calorietoken-site-style/assets/refinements.css index a8045be9..4b806389 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/refinements.css +++ b/wordpress-plugins/calorietoken-site-style/assets/refinements.css @@ -2,6 +2,38 @@ :is(body.ctstyle-enabled,body.ctstyle-footer-only).ctstyle-paper-continuous { background:#fff var(--ctstyle-paper-image) center top/960px auto repeat!important; } +/* S34: mobile screenshots — keep the original art while restoring readable surfaces. */ +body.ctstyle-footer-only .ctstyle-home-panel {background:#fff!important;isolation:isolate;} +body.ctstyle-footer-only .ctstyle-home-panel>.brz-bg {background:#fff!important;} +body.ctstyle-footer-only .ctstyle-home-panel>.brz-bg :is(.brz-bg-image,.brz-bg-color) {background:#fff!important;opacity:1!important;} +body.ctstyle-footer-only .ctstyle-home-panel .brz-menu-simple {position:relative;z-index:1;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-footer-tail {display:block;box-sizing:border-box;width:100%;height:26px;min-height:26px;background:#f9b233 var(--ctstyle-header-image) 47% 69%/cover;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-replaced-empty-tail {display:none!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-footer :is(.ctstyle-legal,.ctstyle-legal p,.ctstyle-legal-links) {box-sizing:border-box;min-width:0;max-width:100%;white-space:normal!important;overflow-wrap:anywhere;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-footer-cookies {display:inline-flex;align-items:center;justify-content:center;position:static!important;min-height:44px;max-width:100%;margin:0;padding:4px 8px;border:0;background:transparent;color:#fff!important;font:inherit;font-weight:700;text-decoration:underline;white-space:normal;cursor:pointer;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-footer-cookies:focus-visible {outline:3px solid #f9b233;outline-offset:2px;} +body.ctstyle-enabled.page-id-1209 #calorieapp-consolidation-wallet {width:calc(100% - 32px)!important;max-width:500px!important;gap:16px!important;margin:32px auto 40px!important;padding:22px!important;border:0!important;border-radius:14px 14px 58px 58px!important;background:#dedede var(--ctstyle-paper-image) center/960px auto!important;box-shadow:0 8px 18px #30344940;} +body.ctstyle-enabled.page-id-1209 #calorieapp-consolidation-wallet h2 {padding:8px 10px!important;border:3px solid #505ba9;background:#008d36;color:#fff!important;font:700 24px/1.25 Amiri,Georgia,serif!important;overflow-wrap:anywhere;} +body.ctstyle-enabled.page-id-1209 #calorieapp-consolidation-wallet h2::before {display:none!important;} +body.ctstyle-enabled.page-id-1209 #calorieapp-consolidation-wallet p {font-size:17px!important;line-height:1.5!important;} +body.ctstyle-enabled.page-id-1209 #calorieapp-consolidation-wallet code {background:#fffffff0;border-color:#adb2cc;font-size:14px!important;padding:10px;} +body.ctstyle-enabled.page-id-1209 #calorieapp-consolidation-wallet a {width:100%!important;box-sizing:border-box;font-size:18px!important;padding:10px 14px!important;border-radius:16px!important;} +body.ctstyle-enabled.page-id-1213 .ctstyle-after-team {margin-top:48px!important;} +body.ctstyle-enabled .ctstyle-roadmap-preview {padding:16px 0!important;border:0!important;border-radius:0!important;background:transparent!important;box-shadow:none!important;} +body.ctstyle-enabled .ctstyle-roadmap-preview>img {display:none;} +body.ctstyle-enabled .ctstyle-roadmap-preview :is(h3,p,a) {overflow-wrap:anywhere;min-width:0;} +@media(max-width:767px){ + body.ctstyle-enabled .ctstyle-title :is(h1,h2) {letter-spacing:0!important;} + body.ctstyle-enabled .ctstyle-roadmap-timeline {width:100%!important;max-width:100%!important;min-width:0!important;} + body.ctstyle-enabled .ctstyle-roadmap-timeline>.brz-timeline__tab {box-sizing:border-box;display:grid!important;grid-template-columns:44px minmax(0,1fr)!important;gap:12px!important;width:100%!important;min-width:0!important;margin:0 0 24px!important;padding:16px!important;background:#ffffffef;border:1px solid #d7daea;border-radius:20px;} + body.ctstyle-enabled .ctstyle-roadmap-timeline>.brz-timeline__tab::before,body.ctstyle-enabled .ctstyle-roadmap-timeline>.brz-timeline__tab::after {display:none!important;} + body.ctstyle-enabled .ctstyle-roadmap-timeline .brz-timeline__nav--icon {grid-column:1;grid-row:1;position:static!important;width:44px!important;height:44px!important;transform:none!important;margin:0!important;} + body.ctstyle-enabled .ctstyle-roadmap-timeline .brz-timeline__nav--title {grid-column:2;grid-row:1;align-self:center;position:static!important;width:auto!important;max-width:100%!important;transform:none!important;margin:0!important;text-align:start!important;} + body.ctstyle-enabled .ctstyle-roadmap-timeline .brz-timeline__content {grid-column:1 / -1;position:static!important;box-sizing:border-box;width:100%!important;max-width:100%!important;min-width:0!important;padding:0!important;margin:0!important;border:0!important;transform:none!important;text-align:start!important;} + body.ctstyle-enabled .ctstyle-roadmap-timeline .brz-timeline__content::before,body.ctstyle-enabled .ctstyle-roadmap-timeline .brz-timeline__content::after {display:none!important;} + body.ctstyle-enabled .ctstyle-roadmap-timeline .brz-timeline__content>.brz-wrapper {width:100%!important;max-width:100%!important;min-width:0!important;padding:0!important;margin:0!important;} + body.ctstyle-enabled .ctstyle-roadmap-timeline .brz-timeline__content p {white-space:normal!important;overflow-wrap:anywhere;} +} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-paper-layer,.ctstyle-paper-host,.ctstyle-paper-tint) {background:none!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only).ctstyle-paper-continuous :is(.ctstyle-app-ending,.ctstyle-discovery-card,#ctstyle-testnet,.calorieapp-app-info) {background-image:none!important;background-color:transparent!important;} body.ctstyle-enabled .ctstyle-trustline-methods {width:calc(100% - 32px);max-width:1100px;margin:24px auto 40px;} diff --git a/wordpress-plugins/calorietoken-site-style/assets/refinements.js b/wordpress-plugins/calorietoken-site-style/assets/refinements.js index c98ff5ff..1e949844 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/refinements.js +++ b/wordpress-plugins/calorietoken-site-style/assets/refinements.js @@ -64,6 +64,8 @@ } function roadmap(){ if(!/\/(?:index.php\/)?roadmap\/$/.test(window.location.pathname))return; + var timeline=one('[data-brz-custom-id="fywltezyscgeklkmjuvcoesosmptkgaloptn"].brz-timeline__tabs'); + if(timeline)timeline.classList.add('ctstyle-roadmap-timeline'); var video=one('[data-brz-custom-id="nuyzubccqediuokhizydlyfagzwueddoqrnd"]'); if(!video||video.closest(blocked)||!video.querySelector('[data-src*="youtube.com/embed/a5mwDKsWrA8"]'))return; if(document.getElementById('ctstyle-roadmap-preview'))return; @@ -97,10 +99,18 @@ var team=one('.ctstyle-contact-team');if(!team)return; team.classList.add('ctstyle-team-flow'); var market=one('[data-brz-custom-id="qnfxijkgbfdhagolgnoklsbijcstdnpoowkg"]'); - if(market&&market.querySelector('.ctstyle-market-card')&&market.parentElement===team.parentElement){ - market.classList.add('ctstyle-after-team');team.parentElement.classList.add('ctstyle-team-container'); + if(market&&market.querySelector('.ctstyle-market-card')&&!team.contains(market)){ + market.classList.add('ctstyle-after-team'); + if(market.parentElement===team.parentElement)team.parentElement.classList.add('ctstyle-team-container'); } } + function homeMenu(){ + if(!document.body.matches('.home.ctstyle-footer-only,.page-id-1090.ctstyle-footer-only'))return; + ['vzcdxvzyscymovatjslzsgqaarqlecarnfro','tirkcgzevvwvvoohirqhwhjxmgrflwjgoyrc'].forEach(function(id){ + var n=one('[data-brz-custom-id="'+id+'"]'); + if(n&&n.matches('.brz-columns')&&!n.querySelector('.xl-card'))n.classList.add('ctstyle-home-panel'); + }); + } function sharedLayout(){ // The accepted Home header stays untouched. Only recognised native columns // on other pages adopt the compact usecase proportions, without moving them. @@ -120,12 +130,19 @@ } } var footer=one('.ctstyle-footer'); - if(footer&&[1119,1121,1123,1125,1127,1129].includes(Number(cfg.page))){ + if(footer){ + if(!footer.querySelector('.ctstyle-footer-cookies')){ + var links=footer.querySelector('.ctstyle-legal-links'); + if(links){var cookies=label('button','cookies','ctstyle-footer-cookies cmplz-manage-consent');cookies.type='button';links.append(cookies);} + } + if(!document.querySelector('.ctstyle-footer-tail')){ + var tail=make('div','ctstyle-footer-tail');tail.setAttribute('aria-hidden','true');footer.after(tail); + } var siblings=Array.from(footer.parentElement.children),after=siblings.slice(siblings.indexOf(footer)+1); after.forEach(function(section){ if(!section.matches('.brz-section')||section.querySelector('img,a,button,form,iframe,video')||text(section.textContent))return; section.querySelectorAll('.brz-bg-image').forEach(function(n){ - if(/\/Caloriefoto2(?:-\d+x\d+)?\.jpg(?:["')?]|$)/i.test(window.getComputedStyle(n).backgroundImage||''))n.classList.add('ctstyle-tail-background'); + if(/\/(?:Caloriefoto2(?:-\d+x\d+)?\.jpg|achtergrondbannersitea1\.png)(?:["')?]|$)/i.test(window.getComputedStyle(n).backgroundImage||''))section.classList.add('ctstyle-replaced-empty-tail'); }); }); } @@ -133,7 +150,7 @@ function refresh(tag){ if(!allowed())return;if(observer)observer.disconnect(); locale=cfg.copy[tag]?tag:(window.CalorieTokenDiscoveryUI?window.CalorieTokenDiscoveryUI.getLocale():'en'); - routes();paper();trustline();roadmap();faq();contact();sharedLayout(); + routes();paper();trustline();roadmap();faq();contact();homeMenu();sharedLayout(); labels.forEach(function(x){var value=cfg.copy[locale][x.key];if(x.node.textContent!==value)x.node.textContent=value;x.node.lang=locale;x.node.dir=['ar','ur'].includes(locale)?'rtl':'ltr';}); if(observer)observer.observe(document.body,{childList:true,subtree:true}); } diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index 2c43ec62..be1d020d 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.3 + * Version: 1.4.4 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.3'; + const VERSION = '1.4.4'; private static function json_asset($name) { // Request-local cache only: plugin updates never need a persistent cache purge. From d9f26d3a2c18dcbee2d9ce2ba0ea8b2472745294 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:35:29 +0200 Subject: [PATCH 002/368] Expose the colourful footer asset in Home footer mode --- wordpress-plugins/calorietoken-site-style/assets/style.js | 1 + 1 file changed, 1 insertion(+) diff --git a/wordpress-plugins/calorietoken-site-style/assets/style.js b/wordpress-plugins/calorietoken-site-style/assets/style.js index cba58c1e..38c7c00c 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/style.js +++ b/wordpress-plugins/calorietoken-site-style/assets/style.js @@ -310,6 +310,7 @@ return /^(preview|customize_changeset_uuid|brizy-edit|brizy-edit-iframe|brz-edit|brz-edit-iframe)$/.test(key); })) { imageVar('--ctstyle-paper-image', cfg.paperImage); + imageVar('--ctstyle-header-image', cfg.headerImage); styleFooter(); return; } From f6583ee91016d33ebfcb4af7e78f99c332b53a8e Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:07:34 +0200 Subject: [PATCH 003/368] Fix both X anchor states and synchronize controls with cookie-banner visibility --- .github/workflows/ci.yml | 2 +- .../tests/wordpress_cookie_controls.test.mjs | 123 ++++++++++++++++++ .../calorietoken-site-style/LEESMIJ.md | 2 + .../assets/blog-timeline.js | 2 +- .../assets/discovery.css | 3 +- .../assets/discovery.js | 41 ++++-- 6 files changed, 159 insertions(+), 14 deletions(-) create mode 100644 tools/tests/wordpress_cookie_controls.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 237cfd6b..54b31ab7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: run: find wordpress-plugins -type f -name '*.php' -print0 | xargs -0 -n1 php -l - name: Validate standalone Site Style package - run: node --test tools/tests/wordpress_site_style_package.test.mjs tools/tests/wordpress_app_origin.test.mjs + run: node --test tools/tests/wordpress_site_style_package.test.mjs tools/tests/wordpress_app_origin.test.mjs tools/tests/wordpress_cookie_controls.test.mjs - name: Test legal footer compatibility shell: bash diff --git a/tools/tests/wordpress_cookie_controls.test.mjs b/tools/tests/wordpress_cookie_controls.test.mjs new file mode 100644 index 00000000..991d00eb --- /dev/null +++ b/tools/tests/wordpress_cookie_controls.test.mjs @@ -0,0 +1,123 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import vm from 'node:vm'; + +const assets = new URL('../../wordpress-plugins/calorietoken-site-style/assets/', import.meta.url); +const source = name => readFileSync(new URL(name, assets), 'utf8'); +const handlers = () => { + const events = new Map(); + return { + addEventListener(name, fn) { events.set(name, [...(events.get(name) || []), fn]); }, + emit(name, event = {}) { for (const fn of events.get(name) || []) fn(event); }, + }; +}; +const tokens = (...initial) => { + const values = new Set(initial); + return {contains: value => values.has(value), toggle(value, enabled) { + if (enabled) values.add(value); else values.delete(value); + }}; +}; + +function discoveryFixture() { + const observers = [], banners = []; + class Observer { + constructor(callback) { this.callback = callback; observers.push(this); } + observe(target, options) { this.target = target; this.options = options; this.active = true; } + disconnect() { this.active = false; } + } + const body = {classList: tokens(), matches: s => s === '.ctstyle-enabled,.ctstyle-footer-only'}; + const document = { + ...handlers(), body, readyState: 'loading', documentElement: {lang: 'en'}, + querySelector: () => null, + querySelectorAll: selector => selector === '.cmplz-cookiebanner' ? banners : [], + // Existing host widgets bypass unrelated creation in this page fixture. + getElementById: () => ({}), + }; + const window = { + ...handlers(), location: new URL('https://calorietoken.net/index.php/groceries/'), + CalorieTokenDiscovery: {page: 1119, copy: {en: {}}}, + localStorage: {getItem: () => null}, + getComputedStyle: node => node.style, + }; + vm.runInNewContext(source('discovery.js'), {window, document, URL, MutationObserver: Observer}); + document.emit('DOMContentLoaded'); + function mutate(record) { + record = {addedNodes: [], removedNodes: [], ...record}; + for (const observer of observers) if (observer.active && + (record.type === 'attributes' ? observer.options.attributes : observer.options.childList)) { + observer.callback([record]); + } + } + function banner() { + return {nodeType: 1, hidden: false, classList: tokens('cmplz-cookiebanner'), + style: {display: 'block', visibility: 'visible', opacity: '1'}, + matches: selector => selector.includes('.cmplz-cookiebanner'), querySelector: () => null, + closest: () => null}; + } + return {window, document, body, observers, banners, mutate, banner, + open: () => body.classList.contains('ctstyle-cookie-banner-open')}; +} + +test('Late cookie banners hide controls and dismissal, hiding or removal restores them', () => { + const h = discoveryFixture(); + assert.equal(h.open(), false); + const banner = h.banner(); h.banners.push(banner); + h.mutate({type: 'childList', addedNodes: [banner]}); + assert.equal(h.open(), true); + banner.hidden = true; h.mutate({type: 'attributes', target: banner}); + assert.equal(h.open(), false); + banner.hidden = false; h.mutate({type: 'attributes', target: banner}); + assert.equal(h.open(), true); + banner.style.display = 'none'; h.mutate({type: 'attributes', target: banner}); + assert.equal(h.open(), false); + banner.style.display = 'block'; h.mutate({type: 'attributes', target: banner}); + banner.classList.toggle('cmplz-dismissed', true); h.mutate({type: 'attributes', target: banner}); + assert.equal(h.open(), false); + banner.classList.toggle('cmplz-dismissed', false); h.document.emit('cmplz_cookie_warning_loaded'); + assert.equal(h.open(), true); + h.banners.pop(); h.mutate({type: 'childList', removedNodes: [banner]}); + assert.equal(h.open(), false); + const css = source('discovery.css'); + assert.match(css, /body\.ctstyle-enabled\.ctstyle-cookie-banner-open \.calorieapp-page-tools/); + assert.match(css, /body\.ctstyle-footer-only\.ctstyle-cookie-banner-open \.calorieapp-page-tools\s*\{visibility:hidden;/); + assert.doesNotMatch(css, /:has\([^)]*cmplz/); +}); + +test('Cookie visibility resumes after back/forward restoration without accumulating observers', () => { + const h = discoveryFixture(); + assert.equal(h.observers.filter(o => o.active).length, 2); + h.window.emit('pagehide'); + assert.equal(h.observers.filter(o => o.active).length, 0); + h.banners.push(h.banner()); + h.window.emit('pageshow'); + assert.equal(h.open(), true); + assert.equal(h.observers.filter(o => o.active).length, 2); + h.window.emit('pageshow'); + assert.equal(h.observers.filter(o => o.active).length, 2); + h.banners[0].hidden = true; h.document.emit('cmplz_status_change'); + assert.equal(h.open(), false); +}); + +test('X consent stays on Blog for both native and managed anchors without intercepting ordinary links', () => { + for (const managed of [false, true]) { + class Element { constructor(closest) { this.closest = closest; } } + const document = {...handlers(), readyState: 'loading', querySelector: () => null, + body: {matches: selector => selector === '.ctstyle-enabled.page-id-1207'}}; + const window = {...handlers(), Element, location: new URL('https://calorietoken.net/index.php/blog/'), + setTimeout: () => 0}; + vm.runInNewContext(source('blog-timeline.js'), {window, document, URL}); + const link = new Element(() => ({})); link.tagName = 'A'; + link.getAttribute = () => 'https://x.com/CalorieToken'; + const button = new Element(selector => selector.split(',').includes(managed ? 'a[data-ctstyle-x-anchor]' : 'a.twitter-timeline') ? link : null); + const target = new Element(() => button); + let prevented = false; + const event = {target, preventDefault: () => { prevented = true; }, + stopPropagation: () => assert.fail('The native consent handler must receive the click')}; + document.emit('click', event); assert.equal(prevented, true); + prevented = false; event.target = new Element(() => null); + document.emit('click', event); assert.equal(prevented, false); + event.target = target; link.getAttribute = () => 'https://x.com/another-profile'; + document.emit('click', event); assert.equal(prevented, false); + } +}); diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index 0c67ed5d..0e1a185c 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -8,6 +8,8 @@ WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorieto ## Correctieronde 1.4.4 — 29 screenshots en negen opmerkingen +De laatste reviewcorrectie herkent beide bestaande toestanden van de X-profielkoppeling. De zwevende knoppen volgen de zichtbaarheid van de cookiemelding via een expliciete paginaklasse, ook bij laat laden en terugkeren met de browser. Dit gebruikt de bestaande toestemmingskeuze en vereist geen CSS `:has()`. + - Home-menu en logovlak krijgen een ondoorzichtige achtergrond. De bestaande Home-titelbanner blijft behouden. - De consolidatiewallet gebruikt de grijze papierkaart, groene titelstrook en afgeronde onderkant van de bestaande walletpresentatie. Adres, explorerlink en volledige toelichting blijven staan. - De acht roadmaponderdelen krijgen op mobiel dezelfde volledige breedte. De appverwijzing past binnen de tijdlijn en is geen afwijkende ingesloten kaart meer. diff --git a/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js b/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js index cbf76d2b..e96761f4 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js +++ b/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js @@ -125,7 +125,7 @@ document.addEventListener('click', function (event) { if (!allowed() || !(event.target instanceof window.Element)) return; var button = event.target.closest('button.cmplz-accept-service[data-service="twitter"]'); - var link = button && button.closest('a.twitter-timeline'); + var link = button && button.closest('a.twitter-timeline,a[data-ctstyle-x-anchor]'); if (link && profile(link) && link.closest('[data-brz-custom-id="amfuxnhsfmkknesyuldlbdorcvqsardaetus"]')) { event.preventDefault(); window.setTimeout(refresh, 0); diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.css b/wordpress-plugins/calorietoken-site-style/assets/discovery.css index da3c1866..77bbb6b7 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.css +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.css @@ -317,4 +317,5 @@ body.ctstyle-enabled .ctstyle-trustline-option>.brz-row>.brz-columns {border:0!i body.ctstyle-enabled .ctstyle-trustline-option>.brz-row>.brz-columns>.brz-bg {display:none!important;} body.ctstyle-enabled .ctstyle-trustline-option h2::before {display:none!important;} -:is(body.ctstyle-enabled,body.ctstyle-footer-only):has(.cmplz-cookiebanner.cmplz-show) .calorieapp-page-tools {visibility:hidden;} +body.ctstyle-enabled.ctstyle-cookie-banner-open .calorieapp-page-tools, +body.ctstyle-footer-only.ctstyle-cookie-banner-open .calorieapp-page-tools {visibility:hidden;} diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.js b/wordpress-plugins/calorietoken-site-style/assets/discovery.js index 0793034a..abdc717a 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.js +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.js @@ -9,7 +9,7 @@ var helpURL = 'https://help.xaman.app/app/learning-more-about-xaman/how-to-access-testnet-on-xrp-ledger'; var nodes = [], hub = null, launcher = null, test = null, trust = null, frame = null, controls = null, requestedLocale = null, revoked = false, picker = null, preferenceRead = false; var preferenceKey = 'calorieapp.display-language.v1'; - var pickers = [], accountApp = null, accountWatcher = null; + var pickers = [], accountApp = null, accountWatcher = null, cookieWatcher = null; var menuLabels = new WeakMap(); function allowed() { return document.body && document.body.matches('.ctstyle-enabled,.ctstyle-footer-only') && @@ -180,6 +180,28 @@ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; }); } + function syncCookieVisibility() { + if (!allowed()) return; + var visible = cookieVisible(); + if (document.body.classList.contains('ctstyle-cookie-banner-open') !== visible) { + document.body.classList.toggle('ctstyle-cookie-banner-open', visible); + } + if (launcher && launcher.hidden !== visible) launcher.hidden = visible; + } + function watchCookieBanner() { + if (cookieWatcher || typeof MutationObserver !== 'function') return; + var selector = '#cmplz-cookiebanner-container,.cmplz-cookiebanner'; + cookieWatcher = new MutationObserver(function (records) { + if (records.some(function (record) { + if (record.type === 'attributes') return record.target.matches(selector); + return Array.from(record.addedNodes).concat(Array.from(record.removedNodes)).some(function (node) { + return node.nodeType === 1 && (node.matches(selector) || node.querySelector(selector)); + }); + })) syncCookieVisibility(); + }); + // Complianz may insert its banner after this script has initialized. + cookieWatcher.observe(document.body,{attributes:true,childList:true,subtree:true,attributeFilter:['class','style','hidden']}); + } function websiteLanguage(tag, remember) { if (!allowed() || !cfg.copy[tag]) return; if (remember) { @@ -261,14 +283,8 @@ if (Number(cfg.page) !== 7880) panel.append(link('openApp',cfg.appURL),link('testTitle',cfg.appURL+'#ctstyle-testnet')); picker = languagePicker('ctstyle-language-select',panel); details.append(panel); launcher.append(details); document.body.append(launcher); - launcher.hidden = cookieVisible(); + syncCookieVisibility(); details.addEventListener('keydown',function (event) { if (event.key === 'Escape' && details.open) { details.open = false; summary.focus(); } }); - if (typeof MutationObserver === 'function') { - var banners = document.querySelectorAll('#cmplz-cookiebanner-container,.cmplz-cookiebanner'); - var observer = new MutationObserver(function () { launcher.hidden = cookieVisible(); }); - banners.forEach(function (node) { observer.observe(node,{attributes:true,childList:true,subtree:true,attributeFilter:['class','style','hidden']}); }); - window.addEventListener('pagehide',function () { observer.disconnect(); },{once:true}); - } } function navigation() { // Change only the known label at the existing destination. No menu, URL or metadata rewrite. @@ -318,6 +334,7 @@ } if (locale) requestedLocale = locale; buyingPage(); trustPage(); appPage(); widget(); accountWidget(); watchAccount(); navigation(); + syncCookieVisibility(); watchCookieBanner(); var tag = lang(); sharedLanguage(tag); nodes.forEach(function (entry) { @@ -332,14 +349,16 @@ } window.CalorieTokenDiscoveryUI = {refresh:refresh,getLocale:lang,setLocale:websiteLanguage}; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded',function () { refresh(); },{once:true}); else refresh(); - window.addEventListener('load',function () { websiteLanguage(lang(),false); syncConsent(); },{once:true}); + window.addEventListener('load',function () { websiteLanguage(lang(),false); syncConsent(); syncCookieVisibility(); },{once:true}); window.addEventListener('pagehide',unload); window.addEventListener('pagehide',function () {if (accountWatcher) {accountWatcher.disconnect();accountWatcher=null;}}); - window.addEventListener('pageshow',function () {if (allowed()) {accountWidget();watchAccount();}}); + window.addEventListener('pagehide',function () {if (cookieWatcher) {cookieWatcher.disconnect();cookieWatcher=null;}}); + window.addEventListener('pageshow',function () {if (allowed()) {accountWidget();watchAccount();syncCookieVisibility();watchCookieBanner();}}); + document.addEventListener('cmplz_cookie_warning_loaded',syncCookieVisibility); ['cmplz_status_change','cmplz_service_status_change','cmplz_revoke','cmplz_enable_category','cmplz_enable_service'].forEach(function (name) { document.addEventListener(name,function () { revoked = name === 'cmplz_revoke'; - syncConsent(); if (launcher) launcher.hidden = cookieVisible(); + syncConsent(); syncCookieVisibility(); }); }); document.addEventListener('change',function (event) { From e595fe26a0ea3622d8322c6aa8b77bf39e7973a1 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:03:20 +0200 Subject: [PATCH 004/368] Scope cookie observation and release completed search probes in Site Style 1.4.5 --- frontend/lib/foodSearchReadiness.ts | 2 +- tools/tests/food_search_readiness.test.mjs | 7 ++++ .../tests/wordpress_cookie_controls.test.mjs | 36 ++++++++++++++----- .../calorietoken-site-style/LEESMIJ.md | 8 +++-- .../assets/discovery.js | 23 ++++++++---- .../calorietoken-site-style.php | 4 +-- 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/frontend/lib/foodSearchReadiness.ts b/frontend/lib/foodSearchReadiness.ts index d6b57069..6752abac 100644 --- a/frontend/lib/foodSearchReadiness.ts +++ b/frontend/lib/foodSearchReadiness.ts @@ -18,7 +18,7 @@ export function createFoodSearchReadiness() { controller = new AbortController(); pending = waitForBackendReady(BACKEND_WAKE_BASE_URL, controller.signal) .then(() => { if (!disposed) readyUntil = Date.now() + READY_TTL_MS; }) - .finally(() => { pending = null; }); + .finally(() => { pending = null; controller = null; }); } const preparation = pending; if (!signal) return preparation; diff --git a/tools/tests/food_search_readiness.test.mjs b/tools/tests/food_search_readiness.test.mjs index 5dcab200..76a30b48 100644 --- a/tools/tests/food_search_readiness.test.mjs +++ b/tools/tests/food_search_readiness.test.mjs @@ -19,3 +19,10 @@ test('Cancelling a search preserves page preparation and rejects the cancelled c test('Failed readiness is not cached and unmount stops the outstanding probe',async()=>{ const h=fixture(),first=h.api.prepare();h.calls[0].reject(new Error('not ready'));await assert.rejects(first);const retry=h.api.prepare();assert.equal(h.calls.length,2);h.api.dispose();await assert.rejects(retry);assert.equal(h.calls[1].signal.aborted,true);await assert.rejects(h.api.prepare());assert.equal(h.calls.length,2); }); +test('Disposal does not abort a health probe that already settled',async()=>{ + for(const success of [true,false]){ + const h=fixture(),probe=h.api.prepare(); + if(success){h.calls[0].resolve();await probe;}else{h.calls[0].reject(new Error('not ready'));await assert.rejects(probe);} + h.api.dispose();assert.equal(h.calls[0].signal.aborted,false); + } +}); diff --git a/tools/tests/wordpress_cookie_controls.test.mjs b/tools/tests/wordpress_cookie_controls.test.mjs index 991d00eb..0e2a91dc 100644 --- a/tools/tests/wordpress_cookie_controls.test.mjs +++ b/tools/tests/wordpress_cookie_controls.test.mjs @@ -22,15 +22,15 @@ const tokens = (...initial) => { function discoveryFixture() { const observers = [], banners = []; class Observer { - constructor(callback) { this.callback = callback; observers.push(this); } - observe(target, options) { this.target = target; this.options = options; this.active = true; } - disconnect() { this.active = false; } + constructor(callback) { this.callback = callback; this.targets = new Map(); this.calls = 0; observers.push(this); } + observe(target, options) { this.targets.set(target, options); this.active = true; } + disconnect() { this.active = false; this.targets.clear(); } } const body = {classList: tokens(), matches: s => s === '.ctstyle-enabled,.ctstyle-footer-only'}; const document = { ...handlers(), body, readyState: 'loading', documentElement: {lang: 'en'}, querySelector: () => null, - querySelectorAll: selector => selector === '.cmplz-cookiebanner' ? banners : [], + querySelectorAll: selector => ['.cmplz-cookiebanner', '#cmplz-cookiebanner-container,.cmplz-cookiebanner'].includes(selector) ? banners : [], // Existing host widgets bypass unrelated creation in this page fixture. getElementById: () => ({}), }; @@ -43,9 +43,11 @@ function discoveryFixture() { vm.runInNewContext(source('discovery.js'), {window, document, URL, MutationObserver: Observer}); document.emit('DOMContentLoaded'); function mutate(record) { - record = {addedNodes: [], removedNodes: [], ...record}; - for (const observer of observers) if (observer.active && - (record.type === 'attributes' ? observer.options.attributes : observer.options.childList)) { + record = {target: body, addedNodes: [], removedNodes: [], ...record}; + for (const observer of observers) if (observer.active && Array.from(observer.targets).some(([target, options]) => + (target === record.target || target === body && options.subtree) && + (record.type === 'attributes' ? options.attributes : options.childList))) { + observer.calls++; observer.callback([record]); } } @@ -92,13 +94,29 @@ test('Cookie visibility resumes after back/forward restoration without accumulat h.banners.push(h.banner()); h.window.emit('pageshow'); assert.equal(h.open(), true); - assert.equal(h.observers.filter(o => o.active).length, 2); + assert.equal(h.observers.filter(o => o.active).length, 3); h.window.emit('pageshow'); - assert.equal(h.observers.filter(o => o.active).length, 2); + assert.equal(h.observers.filter(o => o.active).length, 3); h.banners[0].hidden = true; h.document.emit('cmplz_status_change'); assert.equal(h.open(), false); }); +test('Cookie attribute observation ignores Brizy changes and follows banner replacement', () => { + const h = discoveryFixture(), first = h.banner(); h.banners.push(first); + h.mutate({type: 'childList', addedNodes: [first]}); + const before = h.observers.reduce((sum, o) => sum + o.calls, 0); + const unrelated = {matches: () => false}; + for (let i = 0; i < 20; i++) h.mutate({type: 'attributes', target: unrelated}); + assert.equal(h.observers.reduce((sum, o) => sum + o.calls, 0), before); + const replacement = h.banner(); replacement.hidden = true; + h.banners.splice(0, 1, replacement); + h.mutate({type: 'childList', addedNodes: [replacement], removedNodes: [first]}); + assert.equal(h.open(), false); + assert.ok(h.observers.every(o => !o.targets.has(first))); + replacement.hidden = false; h.mutate({type: 'attributes', target: replacement}); + assert.equal(h.open(), true); +}); + test('X consent stays on Blog for both native and managed anchors without intercepting ordinary links', () => { for (const managed of [false, true]) { class Element { constructor(closest) { this.closest = closest; } } diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index 0e1a185c..4d324dde 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,10 +1,14 @@ -# CalorieToken Site Style 1.4.4 +# CalorieToken Site Style 1.4.5 Eén complete vervangende ZIP, 10 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.4.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.4** actief is en leeg de websitecache. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.5.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.5** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. + +## Reviewafronding 1.4.5 + +Deze versie bevat alle correcties van 1.4.4. De cookiebewaking volgt klasse- en stijlwijzigingen uitsluitend op de herkende Complianz-elementen; wijzigingen elders op Brizy-pagina's activeren die bewaking niet. Laat laden, vervangen en terugkeren via de browser blijven ondersteund. Een afgeronde openbare zoekvoorbereiding geeft in de afzonderlijke appupdate ook zijn annuleringcontroller vrij. Zoeken en login behouden hun bestaande verzoeken en limieten. De pluginupload rolt de appupdate niet uit. ## Correctieronde 1.4.4 — 29 screenshots en negen opmerkingen diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.js b/wordpress-plugins/calorietoken-site-style/assets/discovery.js index abdc717a..93b16a8f 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.js +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.js @@ -9,7 +9,7 @@ var helpURL = 'https://help.xaman.app/app/learning-more-about-xaman/how-to-access-testnet-on-xrp-ledger'; var nodes = [], hub = null, launcher = null, test = null, trust = null, frame = null, controls = null, requestedLocale = null, revoked = false, picker = null, preferenceRead = false; var preferenceKey = 'calorieapp.display-language.v1'; - var pickers = [], accountApp = null, accountWatcher = null, cookieWatcher = null; + var pickers = [], accountApp = null, accountWatcher = null, cookieWatcher = null, cookieAttributeWatcher = null; var menuLabels = new WeakMap(); function allowed() { return document.body && document.body.matches('.ctstyle-enabled,.ctstyle-footer-only') && @@ -191,16 +191,24 @@ function watchCookieBanner() { if (cookieWatcher || typeof MutationObserver !== 'function') return; var selector = '#cmplz-cookiebanner-container,.cmplz-cookiebanner'; + cookieAttributeWatcher = new MutationObserver(syncCookieVisibility); + function trackBanners() { + cookieAttributeWatcher.disconnect(); + document.querySelectorAll(selector).forEach(function (node) { + cookieAttributeWatcher.observe(node,{attributes:true,attributeFilter:['class','style','hidden']}); + }); + syncCookieVisibility(); + } cookieWatcher = new MutationObserver(function (records) { if (records.some(function (record) { - if (record.type === 'attributes') return record.target.matches(selector); return Array.from(record.addedNodes).concat(Array.from(record.removedNodes)).some(function (node) { return node.nodeType === 1 && (node.matches(selector) || node.querySelector(selector)); }); - })) syncCookieVisibility(); + })) trackBanners(); }); - // Complianz may insert its banner after this script has initialized. - cookieWatcher.observe(document.body,{attributes:true,childList:true,subtree:true,attributeFilter:['class','style','hidden']}); + // Observe only structure here; Brizy class/style changes are unrelated. + cookieWatcher.observe(document.body,{childList:true,subtree:true}); + trackBanners(); } function websiteLanguage(tag, remember) { if (!allowed() || !cfg.copy[tag]) return; @@ -352,7 +360,10 @@ window.addEventListener('load',function () { websiteLanguage(lang(),false); syncConsent(); syncCookieVisibility(); },{once:true}); window.addEventListener('pagehide',unload); window.addEventListener('pagehide',function () {if (accountWatcher) {accountWatcher.disconnect();accountWatcher=null;}}); - window.addEventListener('pagehide',function () {if (cookieWatcher) {cookieWatcher.disconnect();cookieWatcher=null;}}); + window.addEventListener('pagehide',function () { + if (cookieWatcher) {cookieWatcher.disconnect();cookieWatcher=null;} + if (cookieAttributeWatcher) {cookieAttributeWatcher.disconnect();cookieAttributeWatcher=null;} + }); window.addEventListener('pageshow',function () {if (allowed()) {accountWidget();watchAccount();syncCookieVisibility();watchCookieBanner();}}); document.addEventListener('cmplz_cookie_warning_loaded',syncCookieVisibility); ['cmplz_status_change','cmplz_service_status_change','cmplz_revoke','cmplz_enable_category','cmplz_enable_service'].forEach(function (name) { diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index be1d020d..c8732c87 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.4 + * Version: 1.4.5 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.4'; + const VERSION = '1.4.5'; private static function json_asset($name) { // Request-local cache only: plugin updates never need a persistent cache purge. From 242a083718cac35fc3b00087217f51d897e2a4b5 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:18:24 +0200 Subject: [PATCH 005/368] Fix translated Xaman continue link in embedded login panel --- frontend/components/XamanLoginPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/XamanLoginPanel.tsx b/frontend/components/XamanLoginPanel.tsx index bec15521..97c49539 100644 --- a/frontend/components/XamanLoginPanel.tsx +++ b/frontend/components/XamanLoginPanel.tsx @@ -1705,7 +1705,7 @@ export function XamanLoginPanel() { onClick={handleLoginClick} className="mt-4 inline-flex items-center justify-center rounded-full bg-brand-primary px-6 py-2.5 text-sm font-semibold text-white transition hover:opacity-90" > - Continue in Xaman + {authCopy.continueXaman} ) : ( + +
+ ); +} diff --git a/frontend/components/FoodSearchPlaceholder.tsx b/frontend/components/FoodSearchPlaceholder.tsx index f83c6a45..af9582ac 100644 --- a/frontend/components/FoodSearchPlaceholder.tsx +++ b/frontend/components/FoodSearchPlaceholder.tsx @@ -1,5 +1,8 @@ "use client"; +import { FoodBarcodeScanner } from "@/components/FoodBarcodeScanner"; +import barcodeTranslations from "@/config/barcode-copy.json"; +import { validFoodBarcode } from "@/lib/foodBarcode"; import { createFoodSearchReadiness } from "@/lib/foodSearchReadiness"; import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { EmptyState } from "@/components/EmptyState"; @@ -214,6 +217,8 @@ export function FoodSearchPlaceholder() { const [error, setError] = useState(null); const [logError, setLogError] = useState(SIGN_IN_REQUIRED_LOG_MESSAGE); const [didSearch, setDidSearch] = useState(false); + const [barcodeSearch, setBarcodeSearch] = useState(false); + const barcodeCopy = barcodeTranslations[locale as keyof typeof barcodeTranslations] ?? barcodeTranslations.en; const [searchStatus, setSearchStatus] = useState(null); const [searchRetryAt, setSearchRetryAt] = useState(0); const [searchWaitSeconds, setSearchWaitSeconds] = useState(0); @@ -364,12 +369,17 @@ export function FoodSearchPlaceholder() { async function onSearch(event: FormEvent) { event.preventDefault(); + await runSearch(query); + } + + async function runSearch(searchQuery: string, barcode = false) { // Enter-key submissions and rapid clicks must not cancel/restart a cold start. if (logMutationInFlightRef.current || searchInFlightRef.current || Date.now() < searchRetryAtRef.current) return; cancelPortionLogging(); setLogFeedback(null); - const trimmedQuery = query.trim(); + const trimmedQuery = searchQuery.trim(); + if (barcode && !validFoodBarcode(trimmedQuery)) return; if (!trimmedQuery) { searchAbortControllerRef.current?.abort(); searchRequestIdRef.current += 1; @@ -387,6 +397,8 @@ export function FoodSearchPlaceholder() { setIsLoading(true); setError(null); + setBarcodeSearch(barcode); + if (barcode) setQuery(trimmedQuery); setDidSearch(true); setSearchStatus( "Preparing food search. This can take a moment after inactivity." @@ -397,7 +409,7 @@ export function FoodSearchPlaceholder() { setSearchStatus("Searching foods. This can take up to 45 seconds."); const response = await backendRequest( - `${BACKEND_BASE_URL}/search-food?q=${encodeURIComponent(trimmedQuery)}`, + `${BACKEND_BASE_URL}/search-food?q=${encodeURIComponent(trimmedQuery)}${barcode ? "&mode=barcode" : ""}`, { signal: controller.signal }, FOOD_SEARCH_TIMEOUT_MS ); @@ -421,7 +433,11 @@ export function FoodSearchPlaceholder() { if (requestId !== searchRequestIdRef.current) { return; } - setResults(normalizeFoodItems(data.results ?? [])); + const items = normalizeFoodItems(data.results ?? []); + setResults(barcode ? items.filter(item => { + const code = typeof item.barcode === "string" ? validFoodBarcode(item.barcode) : null; + return code !== null && code.padStart(14, "0") === trimmedQuery.padStart(14, "0"); + }) : items); } catch (requestError) { if (!controller.signal.aborted && requestId === searchRequestIdRef.current) { pauseSearch(foodSearchRetryAt(503, null)); @@ -765,6 +781,9 @@ export function FoodSearchPlaceholder() { onSubmit={onSearch} /> + 0 || isLogging !== null} + onLookup={code => { void runSearch(code, true); }} /> + {error ?
: null} {searchWaitSeconds > 0 ?

{copy.searchCooldownNotice}

: null} @@ -788,7 +807,7 @@ export function FoodSearchPlaceholder() {
) : null} diff --git a/frontend/components/UsdaReferenceFoods.tsx b/frontend/components/UsdaReferenceFoods.tsx index 2eeef3b3..df13f416 100644 --- a/frontend/components/UsdaReferenceFoods.tsx +++ b/frontend/components/UsdaReferenceFoods.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { parseReferenceGrams, referenceNutrientAmount } from "@/lib/usdaReference"; import reference from "@/data/usda-reference-foods.json"; import translations from "@/config/usda-reference-copy.json"; import { localeDirection, resolveLocale } from "@/lib/locales"; @@ -10,6 +11,8 @@ import { useDisplayLanguage } from "@/components/DisplayLanguageProvider"; // private diary. It has no provider requests, account state or logging action. export function UsdaReferenceFoods() { const [initialLocale, setLocale] = useState("en"); + const [weight, setWeight] = useState("100"); + const grams = parseReferenceGrams(weight); const display = useDisplayLanguage(); const locale = display.enabled ? display.locale : initialLocale; useEffect(() => { @@ -39,7 +42,20 @@ export function UsdaReferenceFoods() {

{copy.scope}

-

{copy.basis}

+

{grams === 100 ? copy.basis : grams !== null ? copy.calculated.replace("{grams}", number.format(grams)) : copy.invalidWeight}

+
+ +
+ setWeight(event.target.value)} maxLength={8} dir="ltr" + aria-invalid={grams === null} aria-describedby="usda-reference-weight-note" + className="min-h-11 w-32 max-w-full rounded-lg border border-brand-secondary/40 px-3 text-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary" /> + +
+

{grams === null ? copy.invalidWeight : copy.calculationNote}

+ {grams !== null && grams !== 100 ?

{copy.calculated.replace("{grams}", number.format(grams))}

: null} +
    {reference.foods.map((food) => (
  • @@ -49,14 +65,12 @@ export function UsdaReferenceFoods() {
    {metrics.map((metric) => { const nutrient = food.nutrients.find((item) => item.id === metric.id); - const known = nutrient && nutrient.unit === metric.unit && - Number.isFinite(nutrient.amount) && nutrient.amount >= 0 && - !(nutrient.amount === 0 && nutrient.loq !== null); + const amount = referenceNutrientAmount(nutrient, metric.unit, grams); return (
    {metric.label}
    - {known ? {number.format(nutrient.amount)} {metric.unit} : copy.unavailable} + {amount !== null ? {amount > 0 && amount < 0.01 ? `<${number.format(0.01)}` : number.format(amount)} {metric.unit} : copy.unavailable}
    ); diff --git a/frontend/config/barcode-copy.json b/frontend/config/barcode-copy.json new file mode 100644 index 00000000..91730eca --- /dev/null +++ b/frontend/config/barcode-copy.json @@ -0,0 +1,200 @@ +{ + "en": { + "title": "Scan a food barcode", + "intro": "Scan the bars on packaged food or enter the digits below.", + "manual": "Food barcode", + "lookup": "Find product", + "scan": "Start camera", + "stop": "Stop camera", + "opening": "Opening camera…", + "scanning": "Centre the barcode in the frame. Use good light and hold steady.", + "denied": "Camera access was not allowed. Enter the barcode below.", + "unavailable": "Camera unavailable. Enter the barcode below.", + "invalid": "Enter a valid barcode with 8, 12, 13 or 14 digits.", + "found": "Barcode read. Check the product below before logging it.", + "paused": "Camera stopped. Try again or enter the barcode.", + "cameraError": "The camera could not start. Try again or enter the barcode.", + "privacy": "Camera images stay on your device. Only the barcode is used to look up the food in Open Food Facts.", + "notFound": "No product with complete nutrition was found for this barcode. Try searching by food name." + }, + "nl": { + "title": "Scan een voedselbarcode", + "intro": "Scan de streepjes op de verpakking of vul hieronder de cijfers in.", + "manual": "Voedselbarcode", + "lookup": "Zoek product", + "scan": "Start camera", + "stop": "Stop camera", + "opening": "Camera openen…", + "scanning": "Houd de barcode in het kader. Zorg voor voldoende licht en houd de camera stil.", + "denied": "Cameratoegang is niet toegestaan. Vul de barcode hieronder in.", + "unavailable": "Camera niet beschikbaar. Vul de barcode hieronder in.", + "invalid": "Vul een geldige barcode met 8, 12, 13 of 14 cijfers in.", + "found": "Barcode gelezen. Controleer het product hieronder voordat je het toevoegt.", + "paused": "Camera gestopt. Probeer opnieuw of vul de barcode in.", + "cameraError": "De camera kon niet starten. Probeer opnieuw of vul de barcode in.", + "privacy": "Camerabeelden blijven op je apparaat. Alleen de barcode wordt gebruikt om het voedsel in Open Food Facts op te zoeken.", + "notFound": "Voor deze barcode is geen product met volledige voedingswaarden gevonden. Zoek op de naam van het voedsel." + }, + "zh-Hans": { + "title": "扫描食品条码", + "intro": "扫描包装上的条码,或在下方输入数字。", + "manual": "食品条码", + "lookup": "查找产品", + "scan": "启动相机", + "stop": "停止相机", + "opening": "正在打开相机…", + "scanning": "将条码置于框内,保持光线充足和相机稳定。", + "denied": "相机访问未获允许。请在下方输入条码。", + "unavailable": "相机不可用。请在下方输入条码。", + "invalid": "请输入有效的 8、12、13 或 14 位条码。", + "found": "已读取条码。记录前请核对下方产品。", + "paused": "相机已停止。请重试或输入条码。", + "cameraError": "相机无法启动。请重试或输入条码。", + "privacy": "相机图像保留在您的设备上。仅使用条码在 Open Food Facts 中查询食品。", + "notFound": "未找到具有完整营养数据的对应产品。请尝试按食品名称搜索。" + }, + "hi": { + "title": "भोजन का बारकोड स्कैन करें", + "intro": "पैकेट पर बने बारकोड को स्कैन करें या नीचे अंक दर्ज करें।", + "manual": "भोजन का बारकोड", + "lookup": "उत्पाद खोजें", + "scan": "कैमरा शुरू करें", + "stop": "कैमरा रोकें", + "opening": "कैमरा खुल रहा है…", + "scanning": "बारकोड को फ्रेम के बीच रखें। अच्छी रोशनी में कैमरा स्थिर रखें।", + "denied": "कैमरे की अनुमति नहीं मिली। नीचे बारकोड दर्ज करें।", + "unavailable": "कैमरा उपलब्ध नहीं है। नीचे बारकोड दर्ज करें।", + "invalid": "8, 12, 13 या 14 अंकों का मान्य बारकोड दर्ज करें।", + "found": "बारकोड पढ़ लिया गया। दर्ज करने से पहले नीचे उत्पाद जाँचें।", + "paused": "कैमरा बंद है। फिर कोशिश करें या बारकोड दर्ज करें।", + "cameraError": "कैमरा शुरू नहीं हो सका। फिर कोशिश करें या बारकोड दर्ज करें।", + "privacy": "कैमरे की छवियाँ आपके डिवाइस पर रहती हैं। Open Food Facts में भोजन खोजने के लिए केवल बारकोड का उपयोग होता है।", + "notFound": "इस बारकोड का पूरा पोषण डेटा वाला उत्पाद नहीं मिला। भोजन के नाम से खोजें।" + }, + "es": { + "title": "Escanea un código de alimentos", + "intro": "Escanea las barras del envase o introduce los dígitos abajo.", + "manual": "Código de barras del alimento", + "lookup": "Buscar producto", + "scan": "Iniciar cámara", + "stop": "Detener cámara", + "opening": "Abriendo la cámara…", + "scanning": "Centra el código en el recuadro. Usa buena luz y mantén la cámara estable.", + "denied": "No se permitió el acceso a la cámara. Introduce el código abajo.", + "unavailable": "Cámara no disponible. Introduce el código abajo.", + "invalid": "Introduce un código válido de 8, 12, 13 o 14 dígitos.", + "found": "Código leído. Revisa el producto abajo antes de registrarlo.", + "paused": "Cámara detenida. Vuelve a intentarlo o introduce el código.", + "cameraError": "La cámara no pudo iniciarse. Reinténtalo o introduce el código.", + "privacy": "Las imágenes permanecen en tu dispositivo. Solo se usa el código para buscar el alimento en Open Food Facts.", + "notFound": "No se encontró un producto con nutrición completa para este código. Busca por el nombre del alimento." + }, + "ar": { + "title": "امسح رمز الطعام الشريطي", + "intro": "امسح الخطوط على العبوة أو أدخل الأرقام أدناه.", + "manual": "الرمز الشريطي للطعام", + "lookup": "ابحث عن المنتج", + "scan": "تشغيل الكاميرا", + "stop": "إيقاف الكاميرا", + "opening": "جارٍ فتح الكاميرا…", + "scanning": "ضع الرمز في منتصف الإطار مع إضاءة جيدة وثبّت الكاميرا.", + "denied": "لم يُسمح بالوصول إلى الكاميرا. أدخل الرمز أدناه.", + "unavailable": "الكاميرا غير متاحة. أدخل الرمز أدناه.", + "invalid": "أدخل رمزًا صالحًا من 8 أو 12 أو 13 أو 14 رقمًا.", + "found": "تمت قراءة الرمز. تحقق من المنتج أدناه قبل تسجيله.", + "paused": "توقفت الكاميرا. حاول مجددًا أو أدخل الرمز.", + "cameraError": "تعذر تشغيل الكاميرا. حاول مجددًا أو أدخل الرمز.", + "privacy": "تبقى صور الكاميرا على جهازك. يُستخدم الرمز فقط للبحث عن الطعام في Open Food Facts.", + "notFound": "لم يُعثر على منتج ببيانات غذائية مكتملة لهذا الرمز. جرّب البحث باسم الطعام." + }, + "fr": { + "title": "Scanner un code-barres alimentaire", + "intro": "Scannez les barres de l’emballage ou saisissez les chiffres ci-dessous.", + "manual": "Code-barres alimentaire", + "lookup": "Trouver le produit", + "scan": "Démarrer la caméra", + "stop": "Arrêter la caméra", + "opening": "Ouverture de la caméra…", + "scanning": "Centrez le code dans le cadre, avec un bon éclairage et une caméra stable.", + "denied": "L’accès à la caméra n’a pas été autorisé. Saisissez le code ci-dessous.", + "unavailable": "Caméra indisponible. Saisissez le code ci-dessous.", + "invalid": "Saisissez un code valide de 8, 12, 13 ou 14 chiffres.", + "found": "Code lu. Vérifiez le produit ci-dessous avant de l’enregistrer.", + "paused": "Caméra arrêtée. Réessayez ou saisissez le code.", + "cameraError": "La caméra n’a pas pu démarrer. Réessayez ou saisissez le code.", + "privacy": "Les images restent sur votre appareil. Seul le code sert à rechercher l’aliment dans Open Food Facts.", + "notFound": "Aucun produit avec des données nutritionnelles complètes n’a été trouvé pour ce code. Recherchez par nom d’aliment." + }, + "bn": { + "title": "খাবারের বারকোড স্ক্যান করুন", + "intro": "প্যাকেটের বারকোড স্ক্যান করুন অথবা নিচে সংখ্যাগুলো লিখুন।", + "manual": "খাবারের বারকোড", + "lookup": "পণ্য খুঁজুন", + "scan": "ক্যামেরা চালু করুন", + "stop": "ক্যামেরা বন্ধ করুন", + "opening": "ক্যামেরা খুলছে…", + "scanning": "বারকোড ফ্রেমের মাঝখানে রাখুন। পর্যাপ্ত আলোতে ক্যামেরা স্থির রাখুন।", + "denied": "ক্যামেরার অনুমতি মেলেনি। নিচে বারকোড লিখুন।", + "unavailable": "ক্যামেরা নেই। নিচে বারকোড লিখুন।", + "invalid": "৮, ১২, ১৩ বা ১৪ সংখ্যার বৈধ বারকোড লিখুন।", + "found": "বারকোড পড়া হয়েছে। লগ করার আগে নিচের পণ্যটি যাচাই করুন।", + "paused": "ক্যামেরা বন্ধ। আবার চেষ্টা করুন অথবা বারকোড লিখুন।", + "cameraError": "ক্যামেরা চালু হয়নি। আবার চেষ্টা করুন অথবা বারকোড লিখুন।", + "privacy": "ক্যামেরার ছবি আপনার ডিভাইসেই থাকে। শুধু বারকোড দিয়ে Open Food Facts-এ খাবার খোঁজা হয়।", + "notFound": "এই বারকোডে সম্পূর্ণ পুষ্টিতথ্যসহ পণ্য পাওয়া যায়নি। খাবারের নাম দিয়ে খুঁজুন।" + }, + "pt": { + "title": "Digitalizar um código alimentar", + "intro": "Digitalize as barras da embalagem ou introduza os dígitos abaixo.", + "manual": "Código de barras do alimento", + "lookup": "Encontrar produto", + "scan": "Iniciar câmara", + "stop": "Parar câmara", + "opening": "A abrir a câmara…", + "scanning": "Centre o código na moldura. Use boa iluminação e mantenha a câmara estável.", + "denied": "O acesso à câmara não foi permitido. Introduza o código abaixo.", + "unavailable": "Câmara indisponível. Introduza o código abaixo.", + "invalid": "Introduza um código válido com 8, 12, 13 ou 14 dígitos.", + "found": "Código lido. Verifique o produto abaixo antes de o registar.", + "paused": "Câmara parada. Tente novamente ou introduza o código.", + "cameraError": "Não foi possível iniciar a câmara. Tente novamente ou introduza o código.", + "privacy": "As imagens ficam no seu dispositivo. Só o código é usado para procurar o alimento no Open Food Facts.", + "notFound": "Não foi encontrado um produto com dados nutricionais completos para este código. Pesquise pelo nome do alimento." + }, + "id": { + "title": "Pindai kode batang makanan", + "intro": "Pindai garis pada kemasan atau masukkan angkanya di bawah.", + "manual": "Kode batang makanan", + "lookup": "Cari produk", + "scan": "Mulai kamera", + "stop": "Hentikan kamera", + "opening": "Membuka kamera…", + "scanning": "Posisikan kode di tengah bingkai. Gunakan cahaya yang cukup dan pegang dengan stabil.", + "denied": "Akses kamera tidak diizinkan. Masukkan kode di bawah.", + "unavailable": "Kamera tidak tersedia. Masukkan kode di bawah.", + "invalid": "Masukkan kode yang valid dengan 8, 12, 13, atau 14 digit.", + "found": "Kode terbaca. Periksa produk di bawah sebelum mencatatnya.", + "paused": "Kamera berhenti. Coba lagi atau masukkan kode.", + "cameraError": "Kamera tidak dapat dimulai. Coba lagi atau masukkan kode.", + "privacy": "Gambar kamera tetap di perangkat Anda. Hanya kode yang digunakan untuk mencari makanan di Open Food Facts.", + "notFound": "Tidak ditemukan produk dengan gizi lengkap untuk kode ini. Coba cari berdasarkan nama makanan." + }, + "ur": { + "title": "کھانے کا بارکوڈ اسکین کریں", + "intro": "پیکٹ کی لکیریں اسکین کریں یا نیچے ہندسے درج کریں۔", + "manual": "کھانے کا بارکوڈ", + "lookup": "پروڈکٹ تلاش کریں", + "scan": "کیمرہ شروع کریں", + "stop": "کیمرہ بند کریں", + "opening": "کیمرہ کھل رہا ہے…", + "scanning": "بارکوڈ فریم کے بیچ میں رکھیں۔ مناسب روشنی میں کیمرہ ساکن رکھیں۔", + "denied": "کیمرے کی اجازت نہیں ملی۔ نیچے بارکوڈ درج کریں۔", + "unavailable": "کیمرہ دستیاب نہیں۔ نیچے بارکوڈ درج کریں۔", + "invalid": "8، 12، 13 یا 14 ہندسوں کا درست بارکوڈ درج کریں۔", + "found": "بارکوڈ پڑھ لیا گیا۔ لاگ میں شامل کرنے سے پہلے نیچے پروڈکٹ چیک کریں۔", + "paused": "کیمرہ بند ہے۔ دوبارہ کوشش کریں یا بارکوڈ درج کریں۔", + "cameraError": "کیمرہ شروع نہیں ہو سکا۔ دوبارہ کوشش کریں یا بارکوڈ درج کریں۔", + "privacy": "کیمرے کی تصاویر آپ کے آلے پر رہتی ہیں۔ Open Food Facts میں کھانا تلاش کرنے کے لیے صرف بارکوڈ استعمال ہوتا ہے۔", + "notFound": "اس بارکوڈ کے لیے مکمل غذائی معلومات والی پروڈکٹ نہیں ملی۔ کھانے کے نام سے تلاش کریں۔" + } +} diff --git a/frontend/config/usda-reference-copy.json b/frontend/config/usda-reference-copy.json index 77a60f63..71d454b3 100644 --- a/frontend/config/usda-reference-copy.json +++ b/frontend/config/usda-reference-copy.json @@ -10,7 +10,12 @@ "source": "Find at USDA", "snapshot": "Data retrieved:", "method": "Energy uses Atwater general factors. Carbohydrates are calculated by difference and include fibre. Values vary between foods and are not medical advice.", - "unavailable": "Not supplied" + "unavailable": "Not supplied", + "weight": "Reference weight (g)", + "calculated": "Calculated for {grams} g of edible food", + "invalidWeight": "Enter a weight from 0.1 to 5,000 g.", + "resetWeight": "Reset to 100 g", + "calculationNote": "Calculated from the dated USDA values per 100 g. This reference does not add food to your diary." }, "zh-Hans": { "title": "基础食物参考", @@ -23,7 +28,12 @@ "source": "在 USDA 中查找", "snapshot": "数据获取日期:", "method": "能量采用 Atwater 通用系数。碳水化合物采用差减法计算,包含膳食纤维。不同食物的数值会有差异,这些信息不构成医疗建议。", - "unavailable": "未提供" + "unavailable": "未提供", + "weight": "参考重量(克)", + "calculated": "按 {grams} 克可食用部分计算", + "invalidWeight": "请输入 0.1 至 5,000 克的重量。", + "resetWeight": "重置为 100 克", + "calculationNote": "根据有日期记录的 USDA 每 100 克数据计算。此参考不会向日记添加食物。" }, "hi": { "title": "सामान्य खाद्य संदर्भ", @@ -36,7 +46,12 @@ "source": "USDA पर खोजें", "snapshot": "डेटा प्राप्त करने की तारीख:", "method": "ऊर्जा के लिए Atwater के सामान्य गुणक उपयोग किए जाते हैं। कार्बोहाइड्रेट की गणना अंतर विधि से होती है और इसमें फ़ाइबर शामिल है। मान खाद्य पदार्थों के बीच बदलते हैं और यह चिकित्सा सलाह नहीं है।", - "unavailable": "उपलब्ध नहीं" + "unavailable": "उपलब्ध नहीं", + "weight": "संदर्भ वजन (ग्राम)", + "calculated": "खाने योग्य भोजन के {grams} ग्राम के लिए गणना", + "invalidWeight": "0.1 से 5,000 ग्राम के बीच वजन दर्ज करें।", + "resetWeight": "100 ग्राम पर रीसेट करें", + "calculationNote": "दिनांकित USDA प्रति 100 ग्राम मूल्यों से गणना। यह संदर्भ आपकी डायरी में भोजन नहीं जोड़ता।" }, "es": { "title": "Referencia de alimentos básicos", @@ -49,7 +64,12 @@ "source": "Buscar en USDA", "snapshot": "Datos consultados:", "method": "La energía utiliza los factores generales de Atwater. Los carbohidratos se calculan por diferencia e incluyen fibra. Los valores varían entre alimentos y no constituyen consejo médico.", - "unavailable": "No disponible" + "unavailable": "No disponible", + "weight": "Peso de referencia (g)", + "calculated": "Calculado para {grams} g de parte comestible", + "invalidWeight": "Introduce un peso entre 0,1 y 5.000 g.", + "resetWeight": "Restablecer a 100 g", + "calculationNote": "Calculado a partir de los valores USDA por 100 g de la fecha indicada. Esta referencia no añade alimentos a tu diario." }, "ar": { "title": "مرجع الأغذية الأساسية", @@ -62,7 +82,12 @@ "source": "البحث لدى USDA", "snapshot": "تاريخ جلب البيانات:", "method": "تستخدم الطاقة عوامل أتواتر العامة. تُحسب الكربوهيدرات بطريقة الفرق وتشمل الألياف. تختلف القيم بين الأغذية ولا تُعد نصيحة طبية.", - "unavailable": "غير متوفر" + "unavailable": "غير متوفر", + "weight": "الوزن المرجعي (غ)", + "calculated": "محسوب لـ {grams} غ من الجزء الصالح للأكل", + "invalidWeight": "أدخل وزنًا من 0.1 إلى 5,000 غ.", + "resetWeight": "إعادة الضبط إلى 100 غ", + "calculationNote": "محسوب من قيم USDA المؤرخة لكل 100 غ. لا يضيف هذا المرجع طعامًا إلى يومياتك." }, "fr": { "title": "Référence d’aliments de base", @@ -75,7 +100,12 @@ "source": "Rechercher sur USDA", "snapshot": "Données consultées le :", "method": "L’énergie utilise les facteurs généraux d’Atwater. Les glucides sont calculés par différence et comprennent les fibres. Les valeurs varient selon les aliments et ne constituent pas un conseil médical.", - "unavailable": "Non fourni" + "unavailable": "Non fourni", + "weight": "Poids de référence (g)", + "calculated": "Calculé pour {grams} g de partie comestible", + "invalidWeight": "Saisissez un poids entre 0,1 et 5 000 g.", + "resetWeight": "Revenir à 100 g", + "calculationNote": "Calculé à partir des valeurs USDA datées pour 100 g. Cette référence n’ajoute pas d’aliment à votre journal." }, "bn": { "title": "সাধারণ খাবারের তথ্য", @@ -88,7 +118,12 @@ "source": "USDA-তে খুঁজুন", "snapshot": "তথ্য সংগ্রহের তারিখ:", "method": "শক্তির জন্য Atwater-এর সাধারণ গুণক ব্যবহার করা হয়। শর্করা বিয়োগ পদ্ধতিতে গণনা করা হয় এবং এতে আঁশ অন্তর্ভুক্ত থাকে। খাবারভেদে মান আলাদা হয় এবং এটি চিকিৎসা পরামর্শ নয়।", - "unavailable": "দেওয়া হয়নি" + "unavailable": "দেওয়া হয়নি", + "weight": "রেফারেন্স ওজন (গ্রাম)", + "calculated": "খাওয়ার উপযোগী অংশের {grams} গ্রামের হিসাব", + "invalidWeight": "০.১ থেকে ৫,০০০ গ্রামের মধ্যে ওজন লিখুন।", + "resetWeight": "১০০ গ্রামে ফিরুন", + "calculationNote": "তারিখযুক্ত USDA প্রতি ১০০ গ্রামের মান থেকে হিসাব করা হয়েছে। এই রেফারেন্স ডায়েরিতে খাবার যোগ করে না।" }, "pt": { "title": "Referência de alimentos básicos", @@ -101,7 +136,12 @@ "source": "Pesquisar no USDA", "snapshot": "Dados consultados em:", "method": "A energia utiliza os fatores gerais de Atwater. Os carboidratos são calculados por diferença e incluem fibras. Os valores variam entre alimentos e não constituem aconselhamento médico.", - "unavailable": "Não fornecido" + "unavailable": "Não fornecido", + "weight": "Peso de referência (g)", + "calculated": "Calculado para {grams} g de parte comestível", + "invalidWeight": "Introduza um peso entre 0,1 e 5.000 g.", + "resetWeight": "Repor 100 g", + "calculationNote": "Calculado a partir dos valores USDA datados por 100 g. Esta referência não adiciona alimentos ao diário." }, "id": { "title": "Referensi bahan pangan dasar", @@ -114,7 +154,12 @@ "source": "Cari di USDA", "snapshot": "Data diambil:", "method": "Energi menggunakan faktor umum Atwater. Karbohidrat dihitung dengan metode selisih dan mencakup serat. Nilai bervariasi antar makanan dan bukan nasihat medis.", - "unavailable": "Tidak tersedia" + "unavailable": "Tidak tersedia", + "weight": "Berat acuan (g)", + "calculated": "Dihitung untuk {grams} g bagian yang dapat dimakan", + "invalidWeight": "Masukkan berat dari 0,1 hingga 5.000 g.", + "resetWeight": "Kembali ke 100 g", + "calculationNote": "Dihitung dari nilai USDA bertanggal per 100 g. Acuan ini tidak menambahkan makanan ke jurnal Anda." }, "ur": { "title": "عام غذاؤں کا حوالہ", @@ -127,7 +172,12 @@ "source": "USDA پر تلاش کریں", "snapshot": "ڈیٹا حاصل کرنے کی تاریخ:", "method": "توانائی کے لیے Atwater کے عمومی عوامل استعمال ہوتے ہیں۔ کاربوہائیڈریٹس کا حساب فرق کے طریقے سے لگایا جاتا ہے اور اس میں فائبر شامل ہے۔ غذاؤں کے درمیان قدریں مختلف ہوتی ہیں اور یہ طبی مشورہ نہیں ہے۔", - "unavailable": "فراہم نہیں کیا گیا" + "unavailable": "فراہم نہیں کیا گیا", + "weight": "حوالہ وزن (گرام)", + "calculated": "کھانے کے قابل حصے کے {grams} گرام کے لیے حساب", + "invalidWeight": "0.1 سے 5,000 گرام تک وزن درج کریں۔", + "resetWeight": "100 گرام پر واپس جائیں", + "calculationNote": "تاریخ شدہ USDA فی 100 گرام اقدار سے حساب کیا گیا ہے۔ یہ حوالہ آپ کی ڈائری میں کھانا شامل نہیں کرتا۔" }, "nl": { "title": "Basisvoeding als naslag", @@ -140,6 +190,11 @@ "source": "Opzoeken bij USDA", "snapshot": "Gegevens opgehaald:", "method": "Energie gebruikt de algemene Atwater-factoren. Koolhydraten zijn berekend volgens de verschilmethode en bevatten vezels. Waarden verschillen per voedingsmiddel en zijn geen medisch advies.", - "unavailable": "Niet opgegeven" + "unavailable": "Niet opgegeven", + "weight": "Referentiegewicht (g)", + "calculated": "Berekend voor {grams} g eetbaar voedsel", + "invalidWeight": "Vul een gewicht van 0,1 tot 5.000 g in.", + "resetWeight": "Terug naar 100 g", + "calculationNote": "Berekend uit de gedateerde USDA-waarden per 100 g. Deze referentie voegt geen voedsel toe aan je logboek." } } diff --git a/frontend/lib/foodBarcode.ts b/frontend/lib/foodBarcode.ts new file mode 100644 index 00000000..52214633 --- /dev/null +++ b/frontend/lib/foodBarcode.ts @@ -0,0 +1,82 @@ +/** Preserve leading zeros; accept checked GTIN-8/12/13/14 food identifiers only. */ +export function validFoodBarcode(value: string): string | null { + const code = value.trim(); + if (!/^(?:\d{8}|\d{12}|\d{13}|\d{14})$/.test(code) || /^0+$/.test(code)) return null; + let total = 0; + for (let i = code.length - 2, weight = 3; i >= 0; i--, weight = 4 - weight) { + total += Number(code[i]) * weight; + } + return (10 - total % 10) % 10 === Number(code.at(-1)) ? code : null; +} + +type Reader = { decode: (video: HTMLVideoElement) => string | null }; +type CameraDependencies = { + openStream: () => Promise; + createReader: () => Promise; +}; +const cameraDependencies: CameraDependencies = { + openStream: () => navigator.mediaDevices.getUserMedia({ + audio: false, video: { facingMode: { ideal: "environment" }, width: { ideal: 1280 }, height: { ideal: 720 } }, + }), + async createReader() { + // Bundled by Next; no CDN, remote decoding or camera image upload. + const [{ BrowserMultiFormatReader }, { BarcodeFormat, DecodeHintType, NotFoundException, ChecksumException, FormatException }] = await Promise.all([ + import("@zxing/browser"), import("@zxing/library"), + ]); + const hints = new Map(); + hints.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.EAN_13, BarcodeFormat.EAN_8, BarcodeFormat.UPC_A, BarcodeFormat.ITF]); + const reader = new BrowserMultiFormatReader(hints); + return { decode(video) { + try { return reader.decode(video).getText(); } + catch (error) { + if (error instanceof NotFoundException || error instanceof ChecksumException || error instanceof FormatException) return null; + throw error; + } + } }; + }, +}; + +/** Own and release this session's tracks, including a permission grant after cancellation. */ +export async function startBarcodeCamera( + video: HTMLVideoElement, signal: AbortSignal, onCode: (code: string) => void, + onReady: () => void, dependencies: CameraDependencies = cameraDependencies, +): Promise { + let stream: MediaStream | undefined, stopped = false; + let timer: ReturnType | undefined, finish: (() => void) | undefined; + function stop() { + stopped = true; + if (timer !== undefined) clearTimeout(timer); + stream?.getTracks().forEach(track => track.stop()); + if (stream && video.srcObject === stream) { video.pause(); video.srcObject = null; } + signal.removeEventListener("abort", stop); + finish?.(); + } + if (signal.aborted) return; + signal.addEventListener("abort", stop, { once: true }); + try { + const reader = await dependencies.createReader(); + if (stopped) return; + stream = await dependencies.openStream(); + if (stopped) { stop(); return; } + video.srcObject = stream; + await video.play(); + if (stopped) { stop(); return; } + onReady(); + await new Promise((resolve, reject) => { + finish = resolve; + const tick = () => { + if (stopped) { resolve(); return; } + try { + const raw = video.readyState >= 2 ? reader.decode(video) : null; + const code = raw === null ? null : validFoodBarcode(raw); + if (code) { stop(); onCode(code); return; } + timer = setTimeout(tick, 250); + } catch (error) { reject(error); stop(); } + }; + tick(); + }); + } catch (error) { + stop(); + if (!signal.aborted) throw error; + } +} diff --git a/frontend/lib/usdaReference.ts b/frontend/lib/usdaReference.ts new file mode 100644 index 00000000..4c2b77e3 --- /dev/null +++ b/frontend/lib/usdaReference.ts @@ -0,0 +1,19 @@ +export function parseReferenceGrams(value: string): number | null { + const normalized = value.trim().replace(",", "."); + if (!/^\d+(?:\.\d{1,2})?$/.test(normalized)) return null; + const grams = Number(normalized); + return Number.isFinite(grams) && grams >= 0.1 && grams <= 5000 ? grams : null; +} + +export function referenceNutrientAmount( + nutrient: { amount: unknown; unit: string; loq: number | null } | undefined, + unit: string, + grams: number | null, +): number | null { + if (!nutrient || nutrient.unit !== unit || typeof nutrient.amount !== "number" || + !Number.isFinite(nutrient.amount) || nutrient.amount < 0 || + (nutrient.amount === 0 && nutrient.loq !== null) || + grams === null || !Number.isFinite(grams) || grams < 0.1 || grams > 5000) return null; + // Scale the original precision, rounding only when presenting the result. + return nutrient.amount * grams / 100; +} diff --git a/frontend/next.config.js b/frontend/next.config.js index b2d742d4..2d114643 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -19,7 +19,7 @@ const nextConfig = { { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, { key: "Permissions-Policy", - value: "camera=(), microphone=(), geolocation=(), payment=()", + value: "camera=(self), microphone=(), geolocation=(), payment=()", }, ], }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 39b99711..9683332e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,8 @@ "version": "0.2.0", "license": "UNLICENSED", "dependencies": { + "@zxing/browser": "0.1.5", + "@zxing/library": "0.21.3", "next": "15.5.25", "react": "18.3.1", "react-dom": "18.3.1" @@ -1614,6 +1616,40 @@ "win32" ] }, + "node_modules/@zxing/browser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.1.5.tgz", + "integrity": "sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==", + "license": "MIT", + "optionalDependencies": { + "@zxing/text-encoding": "^0.9.0" + }, + "peerDependencies": { + "@zxing/library": "^0.21.0" + } + }, + "node_modules/@zxing/library": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz", + "integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==", + "license": "MIT", + "dependencies": { + "ts-custom-error": "^3.2.1" + }, + "engines": { + "node": ">= 10.4.0" + }, + "optionalDependencies": { + "@zxing/text-encoding": "~0.9.0" + } + }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "license": "(Unlicense OR Apache-2.0)", + "optional": true + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -6399,6 +6435,15 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index 40070843..39e32077 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,8 @@ "lint": "next lint" }, "dependencies": { + "@zxing/browser": "0.1.5", + "@zxing/library": "0.21.3", "next": "15.5.25", "react": "18.3.1", "react-dom": "18.3.1" diff --git a/frontend/public/barcode-licenses.txt b/frontend/public/barcode-licenses.txt new file mode 100644 index 00000000..2a25d056 --- /dev/null +++ b/frontend/public/barcode-licenses.txt @@ -0,0 +1,536 @@ +CalorieApp barcode decoder dependencies — pinned build components. +Upstream copyright and licence texts retained verbatim below. + + +@zxing/browser 0.1.5 +MIT License + +Copyright (c) 2018 ZXing for JS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +@zxing/library 0.21.3 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +jai-imageio +======================================================================== + +Copyright (c) 2005 Sun Microsystems, Inc. +Copyright © 2010-2014 University of Manchester +Copyright © 2010-2015 Stian Soiland-Reyes +Copyright © 2015 Peter Hull +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +- Redistribution of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistribution in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +Neither the name of Sun Microsystems, Inc. or the names of +contributors may be used to endorse or promote products derived +from this software without specific prior written permission. + +This software is provided "AS IS," without a warranty of any +kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND +WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY +EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL +NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF +USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS +DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR +ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, +CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND +REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR +INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +You acknowledge that this software is not designed or intended for +use in the design, construction, operation or maintenance of any +nuclear facility. + +ts-custom-error 3.3.1 +The MIT License + +Copyright (c) 2019 Adrien Gibrat https://github.com/adriengibrat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +@zxing/text-encoding 0.9.0 +The encoding indexes, algorithms, and many comments in the code +derive from the Encoding Standard https://encoding.spec.whatwg.org/ + +Otherwise, the code of this repository is released under the Unlicense +license and is also dual-licensed under an Apache 2.0 license. Both +are included below. + +# Unlicense + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +# Apache 2.0 License + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tools/tests/food_barcode.test.mjs b/tools/tests/food_barcode.test.mjs new file mode 100644 index 00000000..cf3534c5 --- /dev/null +++ b/tools/tests/food_barcode.test.mjs @@ -0,0 +1,92 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import { readFileSync } from 'node:fs'; +import vm from 'node:vm'; +import test from 'node:test'; + +const require = createRequire(new URL('../../frontend/package.json', import.meta.url)); +const ts = require('typescript'); +const source = readFileSync(new URL('../../frontend/lib/foodBarcode.ts', import.meta.url), 'utf8'); +const module = { exports: {} }; +vm.runInNewContext(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText, + { module, exports: module.exports, setTimeout, clearTimeout, require: () => { throw Error('Unexpected remote/dependency load'); } }); +const { validFoodBarcode, startBarcodeCamera } = module.exports; +const valid = ['3017620422003', '034000470693', '0034000470693', '96385074', '10012345000017']; + +test('GTIN validation preserves leading zeros and rejects URLs, malformed codes and incorrect checksums', () => { + valid.forEach(code => assert.equal(validFoodBarcode(' ' + code + ' '), code)); + ['', '00000000', '3017620422004', '1e12', '123', '../3017620422003', 'https://example.com'].forEach(code => assert.equal(validFoodBarcode(code), null)); +}); + +function camera() { + let stopped = 0, paused = 0, decoded = 0, opened = 0; + const stream = { getTracks: () => [{ stop: () => stopped++ }] }; + const video = { srcObject: null, readyState: 2, play: async () => {}, pause: () => paused++ }; + const dependencies = { openStream: async () => { opened++; return stream; }, createReader: async () => ({ decode: () => { decoded++; return null; } }) }; + return { stream, video, dependencies, counts: () => ({ stopped, paused, decoded, opened }) }; +} + +test('A found code stops tracks before publishing exactly one result', async () => { + const c = camera(), controller = new AbortController(), results = []; + c.dependencies.createReader = async () => ({ decode: () => valid[0] }); + await startBarcodeCamera(c.video, controller.signal, code => { + assert.ok(c.counts().stopped > 0); assert.equal(c.video.srcObject, null); results.push(code); + }, () => {}, c.dependencies); + controller.abort(); + assert.deepEqual(results, [valid[0]]); +}); + +test('Cancellation during permission prompt releases a late stream without decoding or publishing', async () => { + const c = camera(), controller = new AbortController(); let grant; + c.dependencies.openStream = () => new Promise(resolve => { grant = resolve; }); + const pending = startBarcodeCamera(c.video, controller.signal, () => assert.fail('late result'), () => assert.fail('late ready'), c.dependencies); + await new Promise(setImmediate); controller.abort(); grant(c.stream); await pending; + assert.ok(c.counts().stopped > 0); assert.equal(c.counts().decoded, 0); assert.equal(c.video.srcObject, null); +}); + +test('Cancellation during decoder loading does not request a camera', async () => { + const c = camera(), controller = new AbortController(); let loaded; + c.dependencies.createReader = () => new Promise(resolve => { loaded = resolve; }); + const pending = startBarcodeCamera(c.video, controller.signal, () => assert.fail(), () => assert.fail(), c.dependencies); + controller.abort(); loaded({ decode: () => valid[0] }); await pending; + assert.equal(c.counts().opened, 0); +}); + +test('Stopping an old playback request never clears a newer camera stream', async () => { + const c = camera(), controller = new AbortController(); let playing; + c.video.play = () => new Promise(resolve => { playing = resolve; }); + const pending = startBarcodeCamera(c.video, controller.signal, () => assert.fail(), () => assert.fail(), c.dependencies); + await new Promise(setImmediate); controller.abort(); + const newerStream = {}; c.video.srcObject = newerStream; + playing(); await pending; + assert.equal(c.video.srcObject, newerStream); +}); + +test('Denied camera and decoder failures remain recoverable and release owned resources', async () => { + const c = camera(), controller = new AbortController(); + c.dependencies.openStream = async () => { throw Object.assign(new Error('blocked'), { name: 'NotAllowedError' }); }; + await assert.rejects(startBarcodeCamera(c.video, controller.signal, () => assert.fail(), () => {}, c.dependencies), { name: 'NotAllowedError' }); + c.dependencies.openStream = async () => c.stream; + c.dependencies.createReader = async () => ({ decode: () => { throw new Error('decoder failed'); } }); + await assert.rejects(startBarcodeCamera(c.video, controller.signal, () => assert.fail(), () => {}, c.dependencies), /decoder failed/); + assert.ok(c.counts().stopped > 0); assert.equal(c.video.srcObject, null); +}); + +test('Invalid detections keep scanning locally and abort clears the loop', async () => { + const c = camera(), controller = new AbortController(); let attempted = 0; + c.dependencies.createReader = async () => ({ decode: () => { attempted++; return 'https://example.com'; } }); + const pending = startBarcodeCamera(c.video, controller.signal, () => assert.fail('QR or malformed code must not be looked up'), () => {}, c.dependencies); + await new Promise(setImmediate); assert.equal(attempted, 1); + controller.abort(); await pending; + assert.ok(c.counts().stopped > 0); +}); + +test('All scanner copy covers the eleven existing languages', () => { + const copy = JSON.parse(readFileSync(new URL('../../frontend/config/barcode-copy.json', import.meta.url), 'utf8')); + const locales = JSON.parse(readFileSync(new URL('../../frontend/config/locales.json', import.meta.url), 'utf8')); + assert.deepEqual(Object.keys(copy).sort(), locales.locales.map(x => x.tag).sort()); + Object.values(copy).forEach(values => { + assert.deepEqual(Object.keys(values), Object.keys(copy.en)); + assert.ok(Object.values(values).every(value => typeof value === 'string' && value.trim())); + }); +}); diff --git a/tools/tests/food_logging_ui.test.mjs b/tools/tests/food_logging_ui.test.mjs index 98f12065..fdb4ebb9 100644 --- a/tools/tests/food_logging_ui.test.mjs +++ b/tools/tests/food_logging_ui.test.mjs @@ -35,6 +35,9 @@ const foodUi = await loadLibrary("foodUi", { "@/config/food-ui-copy.json": { default: foodUiCopy }, "@/lib/locales": locales, }); +const usdaMath = await loadLibrary("usdaReference", {}); +const barcode = await loadLibrary("foodBarcode", {}); +const barcodeCopy = JSON.parse(await readFile(new URL("../../frontend/config/barcode-copy.json", import.meta.url), "utf8")); const searchAvailability = await loadLibrary("foodSearchAvailability", {}); const AUTH_EVENT = "test-auth-state-changed"; const foods = Array.from({ length: 30 }, (_, index) => ({ @@ -135,6 +138,9 @@ async function harness(componentName = "FoodSearchPlaceholder", postResponse, lo if (specifier === "@/components/authEvents") return { AUTH_STATE_CHANGED_EVENT: AUTH_EVENT }; if (specifier === "@/lib/foodLogFilter") return foodLogFilter; if (specifier === "@/lib/foodUi") return foodUi; + if (specifier === "@/lib/usdaReference") return usdaMath; + if (specifier === "@/lib/foodBarcode") return barcode; + if (specifier === "@/config/barcode-copy.json") return { default: barcodeCopy }; if (specifier === "@/lib/foodSearchReadiness") return readiness; if (specifier === "@/lib/foodSearchAvailability") return { foodSearchRetryAt: (status, header) => searchAvailability.foodSearchRetryAt(status, header, now), @@ -275,7 +281,7 @@ test("USDA reference displays source values without combining energy methods or assert.ok(text(tree).includes(usdaCopy.en.method)); assert.equal(nodes(tree, (node) => node.type === "time")[0].props.dateTime, usdaReference.retrieved_on); assert.ok(nodes(tree, (node) => node.type === "a").some((node) => node.props.href === usdaReference.licence_url)); - assert.equal(nodes(tree, (node) => ["button", "form", "iframe", "img", "script", "FoodCard"].includes(node.type)).length, 0); + assert.equal(nodes(tree, (node) => ["form", "iframe", "img", "script", "FoodCard"].includes(node.type)).length, 0); await app.flush(); app.login(); app.logout(); @@ -895,3 +901,52 @@ test("Nutri-Score colors accept recorded A–E grades and never invent missing s assert.equal(foodUi.recordedGradeStyle(grade), undefined); } }); + +test('USDA grams scale original precision, accept decimal comma and never change provider records', async () => { + const before = JSON.stringify(usdaReference), h = await harness('UsdaReferenceFoods'); + let tree = h.render(); + const weight = () => nodes(tree, n => n.type === 'input' && n.props.id === 'usda-reference-weight')[0]; + weight().props.onChange({ target: { value: '12,5' } }); tree = h.render(); + assert.ok(text(tree).includes('12.5 g')); + assert.equal(text(nodes(tree, n => n.type === 'dd')[0]).replace(/\s+/g, ' ').trim(), '6 kcal'); + weight().props.onChange({ target: { value: '0.1' } }); tree = h.render(); + assert.equal(text(nodes(tree, n => n.type === 'dd')[2]).replace(/\s+/g, ' ').trim(), '<0.01 g'); + weight().props.onChange({ target: { value: '' } }); tree = h.render(); + assert.equal(weight().props['aria-invalid'], true); + assert.ok(nodes(tree, n => n.type === 'dd').every(n => text(n) === usdaCopy.en.unavailable)); + button(tree, usdaCopy.en.resetWeight).props.onClick(); tree = h.render(); + assert.equal(weight().props.value, '100'); assert.equal(JSON.stringify(usdaReference), before); + assert.deepEqual(h.requests, []); +}); + +test('USDA weight validation rejects coercions, invalid ranges and unknown nutrition', () => { + for (const input of ['', 'NaN', 'Infinity', '1e3', '-1', '0', '0.01', '5001', '100g', '1,000.5']) assert.equal(usdaMath.parseReferenceGrams(input), null, input); + assert.equal(usdaMath.parseReferenceGrams('5000'), 5000); + assert.equal(usdaMath.parseReferenceGrams('0,1'), .1); + assert.equal(usdaMath.referenceNutrientAmount({ amount: 0, unit: 'g', loq: null }, 'g', 250), 0); + assert.equal(usdaMath.referenceNutrientAmount({ amount: 0, unit: 'g', loq: .2 }, 'g', 250), null); + assert.equal(usdaMath.referenceNutrientAmount({ amount: 10, unit: 'kJ', loq: null }, 'kcal', 250), null); +}); + +test('Barcode lookup uses the existing search guard and shows only the matching product, without logging', async () => { + const item = { ...foods[0], barcode: '0034000470693' }; + const h = await harness('FoodSearchPlaceholder', undefined, undefined, async () => ({ ok: true, json: async () => ({ results: [item, { ...foods[1], barcode: '3017620422003' }] }) })); + const scanner = () => nodes(h.tree, n => n.type === 'FoodBarcodeScanner')[0]; + scanner().props.onLookup('034000470693'); scanner().props.onLookup('034000470693'); + await h.flush(); + assert.equal(h.requests.filter(r => r.url.includes('search-food')).length, 1); + assert.ok(h.requests.some(r => r.url.endsWith('q=034000470693&mode=barcode'))); + assert.equal(h.cards().length, 1); assert.equal(h.cards()[0].props.item.barcode, '0034000470693'); + assert.equal(h.requests.some(r => r.url.endsWith('/log-food')), false); +}); + +test('Barcode absence has useful translated fallback text and name search restores ordinary empty state', async () => { + const h = await harness('FoodSearchPlaceholder', undefined, undefined, async () => ({ ok: true, json: async () => ({ results: [] }) })); + nodes(h.tree, n => n.type === 'FoodBarcodeScanner')[0].props.onLookup('034000470693'); await h.flush(); + for (const locale of ['en', 'nl', 'ar']) { + h.setDisplayLanguage(locale); + assert.ok(nodes(h.tree, n => n.type === 'EmptyState').some(n => n.props.description === barcodeCopy[locale].notFound)); + } + await h.search('oats'); + assert.ok(nodes(h.tree, n => n.type === 'EmptyState').some(n => n.props.description === foodUiCopy.ar.noResultsDescription)); +}); diff --git a/tools/tests/wordpress_app_origin.test.mjs b/tools/tests/wordpress_app_origin.test.mjs index 9433e9ab..2bd35bb6 100644 --- a/tools/tests/wordpress_app_origin.test.mjs +++ b/tools/tests/wordpress_app_origin.test.mjs @@ -6,9 +6,10 @@ import test from 'node:test'; const source = readFileSync(new URL('../../wordpress-plugins/calorietoken-site-style/assets/app-integration.js', import.meta.url), 'utf8'); const origins = ['https://app.calorietoken.net', 'https://calorieapp-frontend.onrender.com']; -function adapter(src, count = 1) { +function adapter(src, count = 1, initialPermission = "") { const events = new Map(), sent = [], frameWindow = {}; - const frame = { src, contentWindow: frameWindow, closest: () => null }; + const attributes = new Map(initialPermission ? [["allow", initialPermission]] : []); + const frame = { src, contentWindow: frameWindow, closest: () => null, getAttribute: key => attributes.get(key) ?? null, setAttribute: (key, value) => attributes.set(key, value) }; frameWindow.postMessage = (data, origin) => sent.push({ data, origin }); let host, scrolls = 0; const guide = { scrollIntoView: () => scrolls++, querySelector: () => null }; @@ -85,3 +86,18 @@ test('A known origin still needs the actual current iframe window and exact guid h.message(origins[0]); assert.equal(h.scrolls(), 0, 'retired iframe window is rejected'); }); + +for (const origin of origins) test(`Camera delegation is limited to ${origin}`, () => { + const h = adapter(origin + '/?embedded=1'); + assert.equal(h.frame.getAttribute('allow'), 'camera ' + origin); + const restricted = adapter(origin + '/', 1, "camera 'none'; fullscreen 'self'"); + assert.equal(restricted.frame.getAttribute('allow'), "camera 'none'; fullscreen 'self'"); + const other = adapter(origin + '/', 1, "fullscreen 'self'"); + assert.equal(other.frame.getAttribute('allow'), "fullscreen 'self'; camera " + origin); +}); + +test('An unrelated or duplicate iframe never receives camera permission', () => { + for (const h of [adapter('https://evil.example/'), adapter(origins[0] + '/', 2)]) { + assert.equal(h.frame.getAttribute('allow'), null); + } +}); diff --git a/tools/tests/wordpress_cookie_controls.test.mjs b/tools/tests/wordpress_cookie_controls.test.mjs index 94d96bd3..c696367d 100644 --- a/tools/tests/wordpress_cookie_controls.test.mjs +++ b/tools/tests/wordpress_cookie_controls.test.mjs @@ -230,13 +230,16 @@ test('Native menu accessibility preserves the existing checkbox and restores foc nextElementSibling:{matches:selector=>selector==='.brz-menu-simple__icon',getAttribute:()=> 'native-menu'}}; input.addEventListener('change',()=>changes++); const menu = {...menuEvents,querySelector:()=>input,closest:()=>null}; - const header = {querySelector:selector=>selector==='.brz-menu-simple'?menu:null,classList:tokens()}; + const current = new Map(); + const navLink = {href:'https://calorietoken.net/index.php/whitepaper/',setAttribute:(key,value)=>current.set(key,value)}; + const header = {querySelectorAll:()=>[navLink],querySelector:selector=>selector==='.brz-menu-simple'?menu:null,classList:tokens()}; const document = {...handlers(),readyState:'loading',body:{matches:s=>s==='.ctstyle-enabled,.ctstyle-footer-only'}, querySelector:()=>null,querySelectorAll:s=>s==='.ctstyle-header,.ctstyle-native-header'?[header]:[],getElementById:()=>null}; const window = {...handlers(),location:new URL('https://calorietoken.net/index.php/whitepaper/'), CalorieTokenDiscovery:{page:1210,copy},Event:class {constructor(type){this.type=type;}}}; vm.runInNewContext(source('refinements.js'),{window,document,URL});document.emit('DOMContentLoaded'); window.CalorieTokenRefinements.refresh('nl');window.CalorieTokenRefinements.refresh('nl'); + assert.equal(current.get('aria-current'),'page'); assert.equal(attributes.get('aria-label'),'Hoofdnavigatie');assert.equal(attributes.get('aria-expanded'),'false'); input.checked=true;input.emit('change');assert.equal(attributes.get('aria-expanded'),'true'); let prevented=false;menu.emit('keydown',{key:'Escape',preventDefault:()=>prevented=true}); diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index c923d5d7..28b47871 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,24 @@ -# CalorieToken Site Style 1.4.7 +# CalorieToken Site Style 1.4.8 Eén complete vervangende ZIP, 10 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.7.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.7** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.8.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.8** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## Auditronde 1.4.7 — basis behouden, review eenvoudiger +## Reviewronde 1.4.8 + +- Brizy- en usecase-headers delen nu dezelfde logobreedte, menuruimte, buitenmarges en mobiele afronding. Het usecase-menu opent in de pagina en bedekt de accountwidget niet meer. +- De volledige Xaman-/CalorieApp-kaart kan meegroeien; lange tekst, adresregels, appknop en taalkeuze blijven binnen de beschikbare breedte. De oorspronkelijke inlogbediening blijft intact. +- Footerlinks en cookieknop delen lettertype, regelhoogte en aanraakruimte. De huidige navigatiepagina is herkenbaar. +- CAL & Crypto krijgt drie korte stappen, een ingang naar CalorieApp en de whitepaper. Externe markten blijven herkenbaar; er wordt geen eigen orderdienst geactiveerd. +- Alleen de ene bestaande iframe op de echte CalorieApp-pagina krijgt cameradelegatie voor zijn gecontroleerde app-oorsprong. Er wordt geen camera gestart en een expliciete bestaande camerabeperking wordt niet overschreven. + +**Afzonderlijke app-uitrol:** de nieuwe barcodescanner, exacte Open Food Facts-opvraag, USDA-gewichtberekening en Nutri-Score-kleuren vragen uitrol van de bijbehorende appcode. De barcodefunctie vereist zowel de frontend als de backend uit deze PR. Deze ZIP installeert uitsluitend Site Style. De Identity Bridge en accountkoppeling zijn niet gewijzigd. + +Test na upload Home, Whitepaper en Cafés op desktop en telefoon, met open/dicht menu en ingelogd/uitgelogd account; controleer ook footer, Richlist, Trustline, CAL & Crypto, Tokenomics, Roadmap, Blog en donaties. Controleer de app pas op de nieuwe functies nadat de afzonderlijke uitrol bevestigd is. + +## Historisch: auditronde 1.4.7 — basis behouden, review eenvoudiger Deze versie bevat alle eerdere screenshotcorrecties van 1.4.6, inclusief de gekleurde footer-terugval en het CalorieHelp-vraagteken. Je hoeft 1.4.6 niet eerst te installeren. diff --git a/wordpress-plugins/calorietoken-site-style/assets/app-integration.js b/wordpress-plugins/calorietoken-site-style/assets/app-integration.js index 0b7d9ef1..4e42e2d7 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/app-integration.js +++ b/wordpress-plugins/calorietoken-site-style/assets/app-integration.js @@ -1,4 +1,4 @@ -/* Public display language and an explicit guide-navigation message only. */ +/* Public display language, explicit guide navigation and scoped camera delegation. */ (function () { 'use strict'; var cfg = window.CalorieTokenDiscovery, runtime = window.CalorieAppDisplayLanguage; @@ -21,7 +21,17 @@ var url = new URL(all[0].src); if (!appOrigins.includes(url.origin) || url.pathname !== '/' || url.username || url.password || !all[0].contentWindow) return []; } catch (_) { return []; } - return [{window:all[0].contentWindow,origin:url.origin}]; + return [{window:all[0].contentWindow,origin:url.origin,node:all[0]}]; + } + function cameraPermission() { + // Only the one verified app may ask the visitor to use its camera. Keep all + // existing directives, including an explicit operator camera restriction. + frames().forEach(function (frame) { + var permission=frame.node.getAttribute('allow')||''; + if (!permission.split(';').some(function (part) {return /^camera(?:\s|$)/i.test(part.trim());})) { + frame.node.setAttribute('allow',permission+(permission.trim()?'; ':'')+'camera '+frame.origin); + } + }); } function trusted(event) { return frames().some(function (frame) { return frame.window === event.source && frame.origin === event.origin; }); @@ -38,7 +48,9 @@ if (heading) { heading.tabIndex = -1; heading.focus({preventScroll:true}); } } function init() { - if (!allowed() || started || !window.CalorieTokenDiscoveryUI) return; + if (!allowed()) return; + cameraPermission(); + if (started || !window.CalorieTokenDiscoveryUI) return; started = true; // A native display controller, when already active, remains the sole host. if (!document.querySelector('[data-calorieapp-display-language][data-ready="1"]') && diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery-data.json b/wordpress-plugins/calorietoken-site-style/assets/discovery-data.json index e5eef630..8a5553bf 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery-data.json +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery-data.json @@ -41,7 +41,15 @@ "manualRoute": "Manual setup with XRP Toolkit", "historicalVideo": "Historical video on YouTube", "roadmapRead": "Roadmap explained", - "menuLabel": "Menu" + "menuLabel": "Menu", + "exploreApp": "Explore CalorieApp", + "readWhitepaper": "Read the whitepaper", + "journeyWallet": "Prepare your wallet", + "journeyWalletText": "Use Xaman and check the network, issuer and CAL trustline before continuing.", + "journeyRoute": "Choose your route", + "journeyRouteText": "Follow the setup guide or open the external CAL/XRP order book or swap.", + "journeyReview": "Review in your wallet", + "journeyReviewText": "Check the asset, amount, fees and destination before approving a request." }, "nl": { "intro": "Ontdek CAL op de XRP Ledger. Volg de uitleg, open de CAL/XRP-markten of bekijk andere cryptoroutes.", @@ -85,7 +93,15 @@ "manualRoute": "Handmatig instellen met XRP Toolkit", "historicalVideo": "Historische video op YouTube", "roadmapRead": "Roadmap toegelicht", - "menuLabel": "Menu" + "menuLabel": "Menu", + "exploreApp": "Ontdek CalorieApp", + "readWhitepaper": "Lees de whitepaper", + "journeyWallet": "Bereid je wallet voor", + "journeyWalletText": "Gebruik Xaman en controleer het netwerk, de issuer en de CAL-trustline voordat je verdergaat.", + "journeyRoute": "Kies je route", + "journeyRouteText": "Volg de installatiegids of open het externe CAL/XRP-orderboek of de swap.", + "journeyReview": "Controleer in je wallet", + "journeyReviewText": "Controleer de munt, het bedrag, de kosten en de bestemming voordat je een verzoek goedkeurt." }, "zh-Hans": { "intro": "探索 XRP Ledger 上的 CAL。阅读指南、打开 CAL/XRP 市场,或了解其他加密资产兑换途径。", @@ -129,7 +145,15 @@ "manualRoute": "使用 XRP Toolkit 手动设置", "historicalVideo": "YouTube 上的历史视频", "roadmapRead": "路线图详解", - "menuLabel": "菜单" + "menuLabel": "菜单", + "exploreApp": "探索 CalorieApp", + "readWhitepaper": "阅读白皮书", + "journeyWallet": "准备钱包", + "journeyWalletText": "使用 Xaman,继续前请核对网络、发行方和 CAL 信任线。", + "journeyRoute": "选择方式", + "journeyRouteText": "按照设置指南操作,或打开外部 CAL/XRP 订单簿或兑换页面。", + "journeyReview": "在钱包中核对", + "journeyReviewText": "批准请求前,请核对资产、金额、费用和目标地址。" }, "hi": { "intro": "XRP Ledger पर CAL के बारे में जानें। गाइड पढ़ें, CAL/XRP बाज़ार खोलें या अन्य क्रिप्टो विकल्प देखें।", @@ -173,7 +197,15 @@ "manualRoute": "XRP Toolkit से मैन्युअल सेटअप", "historicalVideo": "YouTube पर ऐतिहासिक वीडियो", "roadmapRead": "रोडमैप का विवरण", - "menuLabel": "मेन्यू" + "menuLabel": "मेन्यू", + "exploreApp": "CalorieApp देखें", + "readWhitepaper": "श्वेतपत्र पढ़ें", + "journeyWallet": "वॉलेट तैयार करें", + "journeyWalletText": "Xaman का उपयोग करें और आगे बढ़ने से पहले नेटवर्क, जारीकर्ता और CAL ट्रस्टलाइन जाँचें।", + "journeyRoute": "अपना तरीका चुनें", + "journeyRouteText": "सेटअप गाइड अपनाएँ या बाहरी CAL/XRP ऑर्डर बुक या स्वैप खोलें।", + "journeyReview": "वॉलेट में जाँचें", + "journeyReviewText": "अनुरोध स्वीकार करने से पहले संपत्ति, राशि, शुल्क और गंतव्य जाँचें।" }, "es": { "intro": "Descubre CAL en XRP Ledger. Sigue la guía, abre los mercados CAL/XRP o explora otras opciones cripto.", @@ -217,7 +249,15 @@ "manualRoute": "Configuración manual con XRP Toolkit", "historicalVideo": "Vídeo histórico en YouTube", "roadmapRead": "Hoja de ruta explicada", - "menuLabel": "Menú" + "menuLabel": "Menú", + "exploreApp": "Explora CalorieApp", + "readWhitepaper": "Lee el documento técnico", + "journeyWallet": "Prepara tu cartera", + "journeyWalletText": "Usa Xaman y comprueba la red, el emisor y la línea de confianza de CAL antes de continuar.", + "journeyRoute": "Elige tu ruta", + "journeyRouteText": "Sigue la guía de configuración o abre el libro de órdenes o el intercambio externo CAL/XRP.", + "journeyReview": "Revisa en tu cartera", + "journeyReviewText": "Comprueba el activo, el importe, las comisiones y el destino antes de aprobar una solicitud." }, "ar": { "intro": "اكتشف CAL على XRP Ledger. اتبع الدليل، أو افتح أسواق CAL/XRP، أو استكشف مسارات أخرى للعملات المشفرة.", @@ -261,7 +301,15 @@ "manualRoute": "إعداد يدوي باستخدام XRP Toolkit", "historicalVideo": "فيديو تاريخي على YouTube", "roadmapRead": "شرح خارطة الطريق", - "menuLabel": "القائمة" + "menuLabel": "القائمة", + "exploreApp": "استكشف CalorieApp", + "readWhitepaper": "اقرأ الورقة البيضاء", + "journeyWallet": "جهّز محفظتك", + "journeyWalletText": "استخدم Xaman وتحقق من الشبكة والجهة المُصدرة وخط الثقة لـ CAL قبل المتابعة.", + "journeyRoute": "اختر المسار", + "journeyRouteText": "اتبع دليل الإعداد أو افتح دفتر أوامر CAL/XRP أو المبادلة لدى الخدمة الخارجية.", + "journeyReview": "راجع في محفظتك", + "journeyReviewText": "تحقق من الأصل والمبلغ والرسوم والوجهة قبل الموافقة على الطلب." }, "fr": { "intro": "Découvrez CAL sur XRP Ledger. Suivez le guide, ouvrez les marchés CAL/XRP ou explorez d’autres options crypto.", @@ -305,7 +353,15 @@ "manualRoute": "Configuration manuelle avec XRP Toolkit", "historicalVideo": "Vidéo historique sur YouTube", "roadmapRead": "La feuille de route expliquée", - "menuLabel": "Menu" + "menuLabel": "Menu", + "exploreApp": "Découvrir CalorieApp", + "readWhitepaper": "Lire le livre blanc", + "journeyWallet": "Préparez votre portefeuille", + "journeyWalletText": "Utilisez Xaman et vérifiez le réseau, l’émetteur et la ligne de confiance CAL avant de continuer.", + "journeyRoute": "Choisissez votre parcours", + "journeyRouteText": "Suivez le guide de configuration ou ouvrez le carnet d’ordres ou l’échange externe CAL/XRP.", + "journeyReview": "Vérifiez dans votre portefeuille", + "journeyReviewText": "Vérifiez l’actif, le montant, les frais et la destination avant d’approuver une demande." }, "bn": { "intro": "XRP Ledger-এ CAL সম্পর্কে জানুন। নির্দেশিকা পড়ুন, CAL/XRP বাজার খুলুন অথবা অন্য ক্রিপ্টো বিকল্প দেখুন।", @@ -349,7 +405,15 @@ "manualRoute": "XRP Toolkit দিয়ে ম্যানুয়াল সেটআপ", "historicalVideo": "YouTube-এ ঐতিহাসিক ভিডিও", "roadmapRead": "রোডম্যাপের ব্যাখ্যা", - "menuLabel": "মেনু" + "menuLabel": "মেনু", + "exploreApp": "CalorieApp দেখুন", + "readWhitepaper": "হোয়াইটপেপার পড়ুন", + "journeyWallet": "ওয়ালেট প্রস্তুত করুন", + "journeyWalletText": "Xaman ব্যবহার করুন এবং এগোনোর আগে নেটওয়ার্ক, ইস্যুকারী ও CAL ট্রাস্টলাইন যাচাই করুন।", + "journeyRoute": "পদ্ধতি বেছে নিন", + "journeyRouteText": "সেটআপ নির্দেশিকা অনুসরণ করুন অথবা বাহ্যিক CAL/XRP অর্ডার বুক বা সোয়াপ খুলুন।", + "journeyReview": "ওয়ালেটে যাচাই করুন", + "journeyReviewText": "অনুরোধ অনুমোদনের আগে সম্পদ, পরিমাণ, ফি ও গন্তব্য যাচাই করুন।" }, "pt": { "intro": "Explore CAL no XRP Ledger. Siga o guia, abra os mercados CAL/XRP ou veja outras opções cripto.", @@ -393,7 +457,15 @@ "manualRoute": "Configuração manual com XRP Toolkit", "historicalVideo": "Vídeo histórico no YouTube", "roadmapRead": "Roteiro explicado", - "menuLabel": "Menu" + "menuLabel": "Menu", + "exploreApp": "Explorar CalorieApp", + "readWhitepaper": "Ler o whitepaper", + "journeyWallet": "Prepare a sua carteira", + "journeyWalletText": "Use Xaman e verifique a rede, o emissor e a linha de confiança CAL antes de continuar.", + "journeyRoute": "Escolha o seu caminho", + "journeyRouteText": "Siga o guia de configuração ou abra o livro de ordens ou a troca externa CAL/XRP.", + "journeyReview": "Reveja na carteira", + "journeyReviewText": "Verifique o ativo, o montante, as taxas e o destino antes de aprovar um pedido." }, "id": { "intro": "Jelajahi CAL di XRP Ledger. Ikuti panduan, buka pasar CAL/XRP, atau lihat pilihan kripto lainnya.", @@ -437,7 +509,15 @@ "manualRoute": "Pengaturan manual dengan XRP Toolkit", "historicalVideo": "Video historis di YouTube", "roadmapRead": "Penjelasan peta jalan", - "menuLabel": "Menu" + "menuLabel": "Menu", + "exploreApp": "Jelajahi CalorieApp", + "readWhitepaper": "Baca whitepaper", + "journeyWallet": "Siapkan dompet", + "journeyWalletText": "Gunakan Xaman dan periksa jaringan, penerbit, serta trustline CAL sebelum melanjutkan.", + "journeyRoute": "Pilih cara Anda", + "journeyRouteText": "Ikuti panduan penyiapan atau buka buku pesanan atau swap CAL/XRP di layanan eksternal.", + "journeyReview": "Periksa di dompet", + "journeyReviewText": "Periksa aset, jumlah, biaya, dan tujuan sebelum menyetujui permintaan." }, "ur": { "intro": "XRP Ledger پر CAL کے بارے میں جانیں۔ رہنما پڑھیں، CAL/XRP مارکیٹ کھولیں یا دوسرے کرپٹو راستے دیکھیں۔", @@ -481,6 +561,14 @@ "manualRoute": "XRP Toolkit سے دستی ترتیب", "historicalVideo": "YouTube پر تاریخی ویڈیو", "roadmapRead": "روڈمیپ کی وضاحت", - "menuLabel": "مینو" + "menuLabel": "مینو", + "exploreApp": "CalorieApp دیکھیں", + "readWhitepaper": "وائٹ پیپر پڑھیں", + "journeyWallet": "اپنا والیٹ تیار کریں", + "journeyWalletText": "Xaman استعمال کریں اور آگے بڑھنے سے پہلے نیٹ ورک، جاری کنندہ اور CAL ٹرسٹ لائن چیک کریں۔", + "journeyRoute": "اپنا طریقہ منتخب کریں", + "journeyRouteText": "سیٹ اپ گائیڈ پر عمل کریں یا بیرونی CAL/XRP آرڈر بک یا سویپ کھولیں۔", + "journeyReview": "والیٹ میں جائزہ لیں", + "journeyReviewText": "درخواست منظور کرنے سے پہلے اثاثہ، رقم، فیس اور منزل چیک کریں۔" } } diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.js b/wordpress-plugins/calorietoken-site-style/assets/discovery.js index c716d605..2a9f8eda 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.js +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.js @@ -118,7 +118,12 @@ hub = element('div','ctstyle-discovery'); hub.id = 'ctstyle-cal-crypto'; var intro=element('header','ctstyle-crypto-intro'); intro.append(element('p','ctstyle-crypto-kicker','CalorieToken · XRP Ledger'),label('h2','exchangeLabel'),label('p','intro','ctstyle-discovery-intro')); - hub.append(intro); + var actions=element('div','ctstyle-discovery-actions'); + actions.append(link('exploreApp',window.location.origin+'/index.php/calorieapp/',false),link('readWhitepaper',window.location.origin+'/index.php/whitepaper/',false)); + intro.append(actions); + var journey=element('ol','ctstyle-crypto-journey'); + ['Wallet','Route','Review'].forEach(function(key){var step=element('li');step.append(label('h3','journey'+key),label('p','journey'+key+'Text'));journey.append(step);}); + intro.append(journey);hub.append(intro); var nav = element('nav','ctstyle-discovery-tabs'); nav.setAttribute('aria-label','CAL & Crypto'); if (!guide.id) guide.id = 'ctstyle-cal-options'; nav.append(link('dexTitle','#ctstyle-own-dex'),link('guideTitle','#'+guide.id),link('bridgeTitle','#ctstyle-external-exchange')); diff --git a/wordpress-plugins/calorietoken-site-style/assets/refinements.css b/wordpress-plugins/calorietoken-site-style/assets/refinements.css index d12a4d3a..050067ea 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/refinements.css +++ b/wordpress-plugins/calorietoken-site-style/assets/refinements.css @@ -87,14 +87,14 @@ body.ctstyle-enabled .ctstyle-public-update{margin-top:30px;padding-top:10px;bor Account, checkbox and menu nodes retain their original handlers. */ :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-tail-background {background-image:var(--ctstyle-header-image)!important;background-position:47% 69%!important;background-size:cover!important;} body.ctstyle-footer-only .ctstyle-home-panel {isolation:isolate;} -:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid {display:grid!important;grid-template-columns:173px minmax(0,1fr) 324px!important;gap:0!important;align-items:stretch!important;max-width:1170px!important;margin:0 auto!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid {box-sizing:border-box;width:100%!important;display:grid!important;grid-template-columns:236px minmax(0,1fr) 324px!important;gap:0!important;align-items:stretch!important;max-width:1170px!important;margin:0 auto!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.brz-columns {box-sizing:border-box!important;width:auto!important;max-width:none!important;min-width:0!important;flex:none!important;min-height:0!important;margin:0!important;padding:0!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-logo-column {grid-column:1;grid-row:1;border-radius:100px 0 0 100px!important;background:#fff!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-menu-column {grid-column:2;grid-row:1;border-radius:0 100px 100px 0!important;background:#fff!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-account-column {grid-column:3;grid-row:1;padding-inline-start:24px!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.brz-columns>.brz-bg {display:none!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.brz-columns>.brz-column__items {box-sizing:border-box!important;display:flex!important;justify-content:center!important;width:100%!important;height:100%!important;min-height:205px!important;margin:0!important;padding:20px!important;} -:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-menu-column>.brz-column__items {padding-inline:10px 35px!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-menu-column>.brz-column__items {padding-inline:0 35px!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-account-column>.brz-column__items {padding:0!important;min-height:0!important;height:auto!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid .brz-wrapper.menu {margin:0!important;padding:0!important;width:100%!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid .brz-menu-simple a {font:700 24px/1.4 Amiri,Georgia,serif!important;letter-spacing:-1px!important;padding-block:5px!important;} @@ -109,11 +109,11 @@ body.ctstyle-footer-only .ctstyle-home-panel {isolation:isolate;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card {box-sizing:border-box!important;width:100%!important;max-width:300px!important;min-height:0!important;height:auto!important;max-height:none!important;margin:0 auto!important;overflow:visible!important;background:#505ba9!important;color:#fff!important;border:2px solid #fff!important;border-radius:12px;box-shadow:0 2px 8px #1e265038;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) :is(.xl-card-header,.xl-card-wallet) {box-sizing:border-box;min-width:0;max-width:100%;height:auto!important;max-height:none!important;white-space:normal!important;overflow:visible!important;overflow-wrap:anywhere!important;text-overflow:clip!important;font-size:13px!important;line-height:1.5!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-header {padding:6px 10px!important;background:#f9b233!important;color:#173c24!important;border-radius:10px 10px 0 0;} -:is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-body {box-sizing:border-box!important;display:grid!important;grid-template-columns:52px minmax(0,1fr)!important;gap:5px 10px!important;align-items:start!important;padding:12px!important;min-height:0!important;height:auto!important;max-height:none!important;overflow:visible!important;background:transparent!important;color:#fff!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-body {box-sizing:border-box!important;display:grid!important;grid-template-columns:65px minmax(0,1fr)!important;gap:5px 10px!important;align-items:start!important;padding:12px!important;min-height:0!important;height:auto!important;max-height:none!important;overflow:visible!important;background:transparent!important;color:#fff!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-body>div {box-sizing:border-box!important;position:static!important;float:none!important;width:auto!important;min-width:0!important;height:auto!important;max-height:none!important;margin:0!important;padding:0!important;white-space:normal!important;overflow:visible!important;overflow-wrap:anywhere!important;font:14px/1.5 Arial,sans-serif!important;color:inherit!important;} -:is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-body>div.xl-card-avatar {grid-column:1;grid-row:1 / span 3;width:52px!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-body>div.xl-card-avatar {grid-column:1;grid-row:1 / span 3;width:65px!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-body>:is(.xl-card-balance,.xl-card-rank,.xl-card-note) {grid-column:2;} -:is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-avatar img {display:block!important;width:52px!important;height:52px!important;max-width:100%!important;margin:0!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-avatar img {display:block!important;width:65px!important;height:65px!important;max-width:100%!important;margin:0!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-body a {color:#fff!important;font:inherit!important;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-footer {height:auto!important;max-height:none!important;white-space:normal!important;overflow-wrap:anywhere;color:#fff!important;background:transparent!important;} @media(max-width:1050px){ @@ -234,3 +234,44 @@ body.ctstyle-enabled.ctstyle-donation-page :is(.woocommerce-info,.woocommerce-me body.ctstyle-enabled :is(.ctstyle-roadmap-link,.ctstyle-wallet-details>summary,#calorieapp-consolidation-wallet>a,.ctstyle-manual-trustline>summary):focus-visible {outline:3px solid #008d36;outline-offset:4px;} /* The helper is visually distinct from the actual CalorieApp shortcut. */ :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-app-launcher>details>summary>.ctstyle-help-icon {position:static!important;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:28px!important;height:28px!important;min-width:28px;padding:0!important;border:2px solid currentColor;border-radius:50%;overflow:visible!important;clip-path:none!important;color:inherit;font:700 22px/1 Arial,sans-serif;} + +/* 1.4.8: match native and fallback header geometry; keep menus in document flow. */ +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-logo-column>.brz-column__items {padding-inline:35px 28px!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-account-column :is(.brz-wrapper,.brz-wp-shortcode) {box-sizing:border-box!important;min-width:0!important;max-width:100%!important;height:auto!important;max-height:none!important;overflow:visible!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) .xl-card-body :is(strong,span,a) {max-width:100%!important;white-space:normal!important;overflow-wrap:anywhere!important;line-height:inherit!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-account-app :is(strong,label,a) {min-width:0;max-width:100%;white-space:normal!important;overflow-wrap:anywhere;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-footer {font-family:Amiri,Georgia,serif;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-legal-links :is(a,button) {display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;min-height:44px;padding:6px 8px;font:700 17px/1.5 Amiri,Georgia,serif!important;} +body.ctstyle-enabled .ctstyle-header-fallback {padding:50px 24px;} +body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-header-nav a {white-space:normal;overflow-wrap:anywhere;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-header,.ctstyle-native-header) a[aria-current="page"] {text-decoration:underline!important;text-underline-offset:5px;text-decoration-color:#008d36!important;} +@media(max-width:1050px){ + :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-compact-header .ctstyle-header-content,.ctstyle-native-header>.brz-section__content) {padding:30px 18px!important;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid {grid-template-columns:132px minmax(0,1fr)!important;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-logo-column>.brz-column__items {padding:20px 16px 20px 20px!important;min-height:128px!important;justify-content:flex-start!important;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid>.ctstyle-menu-column>.brz-column__items {padding:20px 20px 20px 0!important;min-height:128px!important;justify-content:flex-start!important;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid .brz-menu-simple a {min-height:44px!important;box-sizing:border-box;} + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid .brz-menu-simple__icon {margin-inline:auto 0!important;} + body.ctstyle-enabled .ctstyle-header-fallback {padding:30px 18px;} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-header-inner {max-width:760px;gap:20px;} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-header-brand {grid-template-columns:96px minmax(0,1fr);align-items:start;gap:0 16px;padding:20px;min-height:128px;border-radius:26px;} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-logo img {width:96px;} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-mobile-nav {width:100%;min-width:0;} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-mobile-nav summary {margin-inline:auto 0;} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-mobile-nav nav {position:static;inset:auto;margin:16px 0 0;padding:0;border:0;border-radius:0;box-shadow:none;} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-mobile-nav ul {display:block;} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-mobile-nav a {min-height:44px;font-size:20px;line-height:1.45;} + body.ctstyle-enabled .ctstyle-header-account {width:100%;min-width:0;max-width:300px;} +} +@media(max-width:380px){ + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .ctstyle-header-grid {grid-template-columns:112px minmax(0,1fr)!important;} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-header-brand {grid-template-columns:76px minmax(0,1fr);} + body.ctstyle-enabled .ctstyle-header-fallback .ctstyle-logo img {width:76px;} +} +body.ctstyle-enabled .ctstyle-crypto-journey {display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;list-style:none;margin:24px 0 0;padding:0;counter-reset:cal-step;} +body.ctstyle-enabled .ctstyle-crypto-journey li {min-width:0;padding:20px;background:#fff;border:1px solid #d7daea;border-radius:18px;counter-increment:cal-step;} +body.ctstyle-enabled .ctstyle-crypto-journey li::before {content:counter(cal-step);display:flex;align-items:center;justify-content:center;width:34px;height:34px;margin-bottom:12px;border-radius:50%;background:#505ba9;color:#fff;font:700 18px/1 Arial,sans-serif;} +body.ctstyle-enabled .ctstyle-crypto-journey h3 {margin:0 0 8px;color:#008d36;font:700 24px/1.35 Amiri,Georgia,serif;} +body.ctstyle-enabled .ctstyle-crypto-journey p {margin:0;font:400 19px/1.55 Amiri,Georgia,serif;} +body.ctstyle-enabled .ctstyle-crypto-intro .ctstyle-discovery-actions {margin-top:22px;} +@media(max-width:767px){body.ctstyle-enabled .ctstyle-crypto-journey {grid-template-columns:minmax(0,1fr);}} diff --git a/wordpress-plugins/calorietoken-site-style/assets/refinements.js b/wordpress-plugins/calorietoken-site-style/assets/refinements.js index d6d38ebb..899fc8ec 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/refinements.js +++ b/wordpress-plugins/calorietoken-site-style/assets/refinements.js @@ -155,7 +155,14 @@ } } } + // Mark only known same-site navigation links; never touch account actions. + header.querySelectorAll('.brz-menu-simple a,.ctstyle-header-nav a').forEach(function(a){ + try {var target=new URL(a.href,window.location.href); + if(target.origin===window.location.origin&&target.pathname===window.location.pathname&&!target.hash&&!target.search)a.setAttribute('aria-current','page'); + } catch(_) { /* Keep an unrecognized native link. */ } + }); var fallbackMenu=header.querySelector('details.ctstyle-mobile-nav'); + if(fallbackMenu){var fallbackSummary=fallbackMenu.querySelector('summary');if(fallbackSummary)fallbackSummary.setAttribute('aria-label',cfg.copy[locale].menuLabel);} if(fallbackMenu&&!menuBindings.has(fallbackMenu)){ menuBindings.add(fallbackMenu); fallbackMenu.addEventListener('keydown',function(event){ diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index 19f7b954..e0d52609 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.7 + * Version: 1.4.8 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.7'; + const VERSION = '1.4.8'; private static function json_asset($name) { // Request-local cache only: plugin updates never need a persistent cache purge. From 2ad5d87e74a0169dfa069fb77babe27d592c9f8b Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:15:42 +0200 Subject: [PATCH 013/368] =?UTF-8?q?Harden=20Step=201=E2=80=933=20integrati?= =?UTF-8?q?on=20and=20automate=20regression=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 27 +- backend/app/services/open_food_facts.py | 31 +- .../test_open_food_facts_normalization.py | 26 +- docs/BARCODE_SCANNING.md | 8 + docs/public/step123-review-2026-09-10.md | 32 +- frontend/components/FoodSearchPlaceholder.tsx | 12 + frontend/package-lock.json | 302 ++++++++++++++++++ frontend/package.json | 1 + tools/tests/food_barcode.test.mjs | 44 +++ tools/tests/food_barcode_ui.test.mjs | 112 +++++++ tools/tests/food_logging_ui.test.mjs | 42 ++- tools/tests/wordpress_site_style_dom.test.mjs | 143 +++++++++ .../wordpress_site_style_review.test.php | 8 +- .../calorietoken-site-style/LEESMIJ.md | 19 +- .../assets/content-language.js | 20 +- .../assets/discovery.js | 15 +- .../calorietoken-site-style.php | 6 +- 17 files changed, 792 insertions(+), 56 deletions(-) create mode 100644 tools/tests/food_barcode_ui.test.mjs create mode 100644 tools/tests/wordpress_site_style_dom.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e6e1b75..e46dc471 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -320,29 +320,10 @@ jobs: working-directory: frontend run: npm run lint - - name: Test embedded login and private account controls - run: >- - node --test - tools/tests/auth_ui.test.mjs - tools/tests/food_search_readiness.test.mjs - tools/tests/auth_callback_return.test.mjs - tools/tests/account_data_import_ui.test.mjs - tools/tests/account_data_export_validation.test.mjs - tools/tests/account_erasure_ui.test.mjs - tools/tests/account_privacy_locales.test.mjs - tools/tests/account_privacy_display.test.mjs - tools/tests/display_language_protocol.test.mjs - tools/tests/display_language_ui.test.mjs - tools/tests/testnet_entry.test.mjs - tools/tests/backend_proxy_logout_fallback.test.mjs - tools/tests/backend_warmup_rate_limit.test.mjs - tools/tests/calorieapp_embed_readiness.test.mjs - tools/tests/food_logging_ui.test.mjs - tools/tests/food_barcode.test.mjs - tools/tests/food_search_deadline.test.mjs - tools/tests/identity_locales.test.mjs - tools/tests/xaman_logout_request.test.mjs - tools/tests/xaman_login_start_retry.test.mjs + - name: Test all frontend and WordPress user flows + # Discover new test files automatically so follow-up checks cannot be + # forgotten in a manually maintained list. + run: node --test tools/tests/*.test.mjs - name: Build frontend working-directory: frontend diff --git a/backend/app/services/open_food_facts.py b/backend/app/services/open_food_facts.py index f5dab81a..7ad75e67 100644 --- a/backend/app/services/open_food_facts.py +++ b/backend/app/services/open_food_facts.py @@ -14,10 +14,11 @@ from urllib.error import HTTPError as UrllibHTTPError, URLError import httpx +from pydantic import ValidationError from app.database import engine from app.provider_rate_governor import build_provider_rate_governor -from app.schemas import FoodSearchResult +from app.schemas import FoodLogCreate, FoodSearchResult from app.services.food_search_availability import ( FoodSearchAvailability, FoodSearchUnavailable, @@ -95,7 +96,7 @@ def _repair_common_mojibake(text: str) -> str: def _to_float(value: Any) -> float | None: - if value is None: + if value is None or isinstance(value, bool): return None try: result = float(value) @@ -104,7 +105,7 @@ def _to_float(value: Any) -> float | None: if result < 0: return None return round(result, 2) - except (TypeError, ValueError): + except (TypeError, ValueError, OverflowError): return None @@ -269,19 +270,26 @@ async def _fetch_product(code: str) -> dict[str, Any]: def _normalize_products(payload: dict[str, Any]) -> list[FoodSearchResult]: results: list[FoodSearchResult] = [] + products = payload.get("products", []) if isinstance(payload, dict) else None + if not isinstance(products, list): + raise httpx.HTTPError("Invalid Open Food Facts product list") nutrient_fields = { "calories": "energy-kcal", "protein": "proteins", "fat": "fat", "carbohydrates": "carbohydrates", } - for product in payload.get("products", []): - raw_product_name = (product.get("product_name") or "").strip() + for product in products: + if not isinstance(product, dict) or not isinstance(product.get("product_name"), str): + continue + raw_product_name = product["product_name"].strip() product_name = _repair_common_mojibake(raw_product_name) if not product_name: continue - nutriments = product.get("nutriments") or {} + nutriments = product.get("nutriments") + if not isinstance(nutriments, dict): + continue serving_size = _to_optional_text(product.get("serving_size")) nutrition = { name: _to_float(nutriments.get(f"{field}_serving")) @@ -307,8 +315,7 @@ def _normalize_products(payload: dict[str, Any]) -> list[FoodSearchResult]: if any(value is None for value in nutrition.values()): continue - results.append( - FoodSearchResult( + result = FoodSearchResult( product_name=product_name, calories=nutrition["calories"], protein=nutrition["protein"], @@ -319,8 +326,14 @@ def _normalize_products(payload: dict[str, Any]) -> list[FoodSearchResult]: brand=_extract_brand(product), serving_size=serving_size, nutri_score=_extract_nutri_score(product), - ) ) + try: + # Every offered result must fit the existing diary contract. Do not + # silently truncate a provider's product identity or source fields. + FoodLogCreate.model_validate(result.model_dump()) + except ValidationError: + continue + results.append(result) return results diff --git a/backend/tests/test_open_food_facts_normalization.py b/backend/tests/test_open_food_facts_normalization.py index b224c56c..a5895183 100644 --- a/backend/tests/test_open_food_facts_normalization.py +++ b/backend/tests/test_open_food_facts_normalization.py @@ -57,7 +57,7 @@ def test_open_food_facts_admission_configuration_is_bounded() -> None: @pytest.mark.parametrize( "value", - [None, float("inf"), float("-inf"), float("nan"), "Infinity", "NaN", -1], + [None, True, False, [], {}, float("inf"), float("-inf"), float("nan"), "Infinity", "NaN", -1], ) def test_to_float_marks_missing_or_invalid_upstream_values_as_unknown(value: object) -> None: assert _to_float(value) is None @@ -110,6 +110,30 @@ def test_search_omits_products_with_unknown_nutrition(primary: AsyncMock) -> Non assert [result.product_name for result in results] == ["Complete oats"] +@patch("app.services.open_food_facts._fetch_primary", new_callable=AsyncMock) +def test_one_malformed_provider_record_does_not_discard_valid_foods(primary: AsyncMock) -> None: + good = {"product_name": "Oats", "nutriments": { + "energy-kcal_100g": 375, "proteins_100g": 13, "fat_100g": 7, "carbohydrates_100g": 60, + }} + primary.return_value = {"products": [None, "wrong type", {"product_name": 12}, + {"product_name": "Invalid nutrients", "nutriments": []}, good]} + results = asyncio.run(search_food_products("oats")) + assert [result.product_name for result in results] == ["Oats"] + + +@patch("app.services.open_food_facts._fetch_primary", new_callable=AsyncMock) +def test_search_results_can_be_saved_without_truncating_provider_identity(primary: AsyncMock) -> None: + from app.schemas import FoodLogCreate + good = {"product_name": "Oats", "nutriments": { + "energy-kcal_100g": 375, "proteins_100g": 13, "fat_100g": 7, "carbohydrates_100g": 60, + }} + primary.return_value = {"products": [good, {**good, "product_name": "x" * 121}, + {**good, "brands": "x" * 161}, {**good, "image_url": "https://example.test/" + "x" * 500}]} + results = asyncio.run(search_food_products("oats")) + assert [result.product_name for result in results] == ["Oats"] + assert FoodLogCreate.model_validate(results[0].model_dump()).product_name == "Oats" + + @patch("app.services.open_food_facts._fetch_fallback", new_callable=AsyncMock) @patch("app.services.open_food_facts._fetch_primary", new_callable=AsyncMock) def test_expected_fallback_failure_becomes_upstream_http_error( diff --git a/docs/BARCODE_SCANNING.md b/docs/BARCODE_SCANNING.md index d558e6b9..ad0e03e8 100644 --- a/docs/BARCODE_SCANNING.md +++ b/docs/BARCODE_SCANNING.md @@ -67,6 +67,14 @@ required; synthetic camera tests are not hardware or focus/lighting evidence. ## Acceptance pass +The follow-up automated audit runs the actual bundled decoder against synthetic +EAN-8, EAN-13, UPC-A and ITF-14 raster images. It also exercises the scanner +component's controls through eleven languages, duplicate clicks, refusal, +cancel/close/hide/unmount/timeout and stale responses from an older session. +These cover decoding and software lifecycle; the canvas input and camera stream +remain synthetic and do not establish optical recognition or installed hosting +permissions on a real phone. No intermediate owner debugging pass is requested. + - Open the disclosure: no camera prompt until Start camera. - Allow a rear-camera scan: one product lookup, correct packaging identity and nutrition basis, no diary entry until explicit confirmation. diff --git a/docs/public/step123-review-2026-09-10.md b/docs/public/step123-review-2026-09-10.md index 1d5c5dc4..05c59e9c 100644 --- a/docs/public/step123-review-2026-09-10.md +++ b/docs/public/step123-review-2026-09-10.md @@ -12,7 +12,37 @@ The older baseline and synthetic-provider evidence files describe dated observations. Their historical statements must not be read as a current live deployment status. No new provider operation, migration, credential handling, transaction or deployment is authorized by a styling release. -## Current follow-up: Site Style 1.4.8 and food barcode scanning +## Additional automated audit: Site Style 1.4.9 + +The owner requested fewer manual test/debug cycles. This round reproduces and +fixes additional integration failures while keeping the accepted identity +protocol, database schemas, artwork, CSS layout and existing dependency versions +unchanged: + +- Reattach the existing app/language widget after native account-footer replacement. +- Rebase public translation records after a CMS text replacement, preserving the + new English content and unknown custom edits. +- Exclude Home previews consistently on the server. +- Ignore stale single/bulk deletion responses after logout or unmount; an old + authorization failure cannot clear a newly authenticated diary. +- Isolate malformed food records and exclude results that would fail the existing + diary schema. Boolean values are unknown nutrition, not measured zero/one. +- Reset empty-query search flags, addressing the later automated review note. + +CI now discovers every `tools/tests/*.test.mjs` file instead of relying on a +manually maintained list. A development-only DOM dependency supports interaction +tests of the actual WordPress scripts; it is not shipped in the plugin or app +bundle. Additional scanner UI and actual ZXing raster-decoding tests cover all +four advertised formats. No new production package version, provider request, +paid service, signing action or deployment is introduced. + +Live Home was rechecked and still loaded Site Style 1.4.5; the public app lacked +the new scanner. The review requires no intermediate owner testing. Source/CI +completion remains distinct from final installed layout, camera hardware, +historical content coverage and deployed build verification. The outstanding +Step 3 requirements below are retained rather than silently signed off. + +## Included: Site Style 1.4.8 and food barcode scanning All earlier corrections below remain included. A deeper CSS review found that the native header's 173 px column left a smaller logo than the fallback header's diff --git a/frontend/components/FoodSearchPlaceholder.tsx b/frontend/components/FoodSearchPlaceholder.tsx index af9582ac..4e7c23ce 100644 --- a/frontend/components/FoodSearchPlaceholder.tsx +++ b/frontend/components/FoodSearchPlaceholder.tsx @@ -191,6 +191,7 @@ export function FoodSearchPlaceholder() { const searchInFlightRef = useRef(false); const searchRetryAtRef = useRef(0); const logsRequestIdRef = useRef(0); + const privateStateVersionRef = useRef(0); const logMutationInFlightRef = useRef(false); const logSelectionIdRef = useRef(0); const deleteMutationInFlightRef = useRef(false); @@ -281,6 +282,7 @@ export function FoodSearchPlaceholder() { // Invalidate any request that began under the previous authentication // state so a late response cannot repopulate another session's logs. logsRequestIdRef.current += 1; + privateStateVersionRef.current += 1; logSelectionIdRef.current += 1; setLogs([]); setSelectedLogId(null); @@ -344,6 +346,8 @@ export function FoodSearchPlaceholder() { useEffect(() => { return () => { searchAbortControllerRef.current?.abort(); + logsRequestIdRef.current += 1; + privateStateVersionRef.current += 1; }; }, []); @@ -384,6 +388,8 @@ export function FoodSearchPlaceholder() { searchAbortControllerRef.current?.abort(); searchRequestIdRef.current += 1; setResults([]); + setDidSearch(false); + setBarcodeSearch(false); setError("Enter a food name to search."); return; } @@ -560,6 +566,7 @@ export function FoodSearchPlaceholder() { return; } + const privateVersion = privateStateVersionRef.current; deleteMutationInFlightRef.current = true; setDeletingLogId(logId); setLogError(null); @@ -567,6 +574,7 @@ export function FoodSearchPlaceholder() { const response = await backendRequest(`${BACKEND_BASE_URL}/logs/${logId}`, { method: "DELETE", }); + if (privateVersion !== privateStateVersionRef.current) return; if (response.status === 401) { clearPrivateLogState(); return; @@ -582,6 +590,7 @@ export function FoodSearchPlaceholder() { } await fetchLogs(); } catch (requestError) { + if (privateVersion !== privateStateVersionRef.current) return; setLogError( backendUnavailableMessage( requestError, @@ -604,6 +613,7 @@ export function FoodSearchPlaceholder() { return; } + const privateVersion = privateStateVersionRef.current; deleteMutationInFlightRef.current = true; setIsClearingAll(true); setLogError(null); @@ -611,6 +621,7 @@ export function FoodSearchPlaceholder() { const response = await backendRequest(`${BACKEND_BASE_URL}/logs`, { method: "DELETE", }); + if (privateVersion !== privateStateVersionRef.current) return; if (response.status === 401) { clearPrivateLogState(); return; @@ -621,6 +632,7 @@ export function FoodSearchPlaceholder() { setSelectedLogId(null); await fetchLogs(); } catch (requestError) { + if (privateVersion !== privateStateVersionRef.current) return; setLogError( backendUnavailableMessage( requestError, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9683332e..a379dba7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "autoprefixer": "10.4.19", "eslint": "8.57.0", "eslint-config-next": "14.2.5", + "linkedom": "0.18.13", "postcss": "8.5.23", "tailwindcss": "3.4.7", "typescript": "5.5.4" @@ -2055,6 +2056,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boolbase": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz", + "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/brace-expansion": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -2316,6 +2331,41 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz", + "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0", + "css-what": "^8.0.0", + "domhandler": "^6.0.1", + "domutils": "^4.0.2", + "nth-check": "^3.0.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz", + "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2329,6 +2379,13 @@ "node": ">=4" } }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2508,6 +2565,77 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2544,6 +2672,19 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3867,6 +4008,118 @@ "node": ">= 0.4" } }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4564,6 +4817,31 @@ "dev": true, "license": "MIT" }, + "node_modules/linkedom": { + "version": "0.18.13", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.13.tgz", + "integrity": "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "css-select": "^7.0.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^10.1.0", + "uhyphen": "^0.2.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": ">= 2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4841,6 +5119,23 @@ "node": ">=0.10.0" } }, + "node_modules/nth-check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz", + "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6588,6 +6883,13 @@ "node": ">=14.17" } }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "dev": true, + "license": "ISC" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 39e32077..55b41933 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "autoprefixer": "10.4.19", "eslint": "8.57.0", "eslint-config-next": "14.2.5", + "linkedom": "0.18.13", "postcss": "8.5.23", "tailwindcss": "3.4.7", "typescript": "5.5.4" diff --git a/tools/tests/food_barcode.test.mjs b/tools/tests/food_barcode.test.mjs index cf3534c5..ed8baa6b 100644 --- a/tools/tests/food_barcode.test.mjs +++ b/tools/tests/food_barcode.test.mjs @@ -90,3 +90,47 @@ test('All scanner copy covers the eleven existing languages', () => { assert.ok(Object.values(values).every(value => typeof value === 'string' && value.trim())); }); }); + +test('The actual bundled decoder reads EAN-8, EAN-13, UPC-A and ITF-14 raster fixtures', async () => { + const zxing = require('@zxing/library'); + const {BrowserMultiFormatReader} = require('@zxing/browser'); + // Only the DOM canvas capture is substituted. Format selection, luminance + // binarization, real ZXing decoding, validation and session cleanup run as shipped. + class RasterReader extends BrowserMultiFormatReader { + decode(video) { return this.decodeBitmap(video.bitmap); } + } + const rasterModule = {exports: {}}; + let nextStream; + vm.runInNewContext(ts.transpileModule(source, {compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022}}).outputText, { + module: rasterModule, exports: rasterModule.exports, setTimeout, clearTimeout, + navigator: {mediaDevices: {getUserMedia: async () => nextStream}}, + require(name) { return name === '@zxing/browser' ? {BrowserMultiFormatReader: RasterReader} : require(name); }, + }); + const left = ['0001101','0011001','0010011','0111101','0100011','0110001','0101111','0111011','0110111','0001011']; + const parity = ['LLLLLL','LLGLGG','LLGGLG','LLGGGL','LGLLGG','LGGLLG','LGGGLL','LGLGLG','LGLGGL','LGGLGL']; + const invert = bits => bits.replace(/[01]/g, bit => bit === '1' ? '0' : '1'); + function ean(code) { + if (code.length === 12) code = '0' + code; + const half = code.length === 8 ? 4 : 6, lhs = code.length === 8 ? code.slice(0, 4) : code.slice(1, 7); + return '101' + [...lhs].map((digit, i) => code.length === 8 || parity[code[0]][i] === 'L' ? left[digit] : invert([...left[digit]].reverse().join(''))).join('') + + '01010' + [...code.slice(-half)].map(digit => invert(left[digit])).join('') + '101'; + } + function itf(code) { + const patterns = ['11331','31113','13113','33111','11313','31311','13311','11133','31131','13131']; + let bars = '1010'; + for (let i = 0; i < code.length; i += 2) for (let j = 0; j < 5; j++) bars += '1'.repeat(Number(patterns[code[i]][j])) + '0'.repeat(Number(patterns[code[i + 1]][j])); + return bars + '11101'; + } + for (const code of [valid[0], valid[1], valid[3], valid[4]]) { + const bars = '0'.repeat(20) + (code.length === 14 ? itf(code) : ean(code)) + '0'.repeat(20); + const width = bars.length * 3, height = 100, pixels = new Uint8ClampedArray(width * height); + for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) pixels[y * width + x] = bars[Math.floor(x / 3)] === '1' ? 0 : 255; + const c = camera(); nextStream = c.stream; + c.video.bitmap = new zxing.BinaryBitmap(new zxing.HybridBinarizer(new zxing.RGBLuminanceSource(pixels, width, height))); + const controller = new AbortController(), found = []; + const timeout = setTimeout(() => controller.abort(), 2_000); + try { await rasterModule.exports.startBarcodeCamera(c.video, controller.signal, value => found.push(value), () => {}); } + finally { clearTimeout(timeout); controller.abort(); } + assert.deepEqual(found, [code]); assert.ok(c.counts().stopped > 0); assert.equal(c.video.srcObject, null); + } +}); diff --git a/tools/tests/food_barcode_ui.test.mjs b/tools/tests/food_barcode_ui.test.mjs new file mode 100644 index 00000000..942e2291 --- /dev/null +++ b/tools/tests/food_barcode_ui.test.mjs @@ -0,0 +1,112 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import {readFileSync} from 'node:fs'; +import {createRequire} from 'node:module'; +import vm from 'node:vm'; + +const require = createRequire(new URL('../../frontend/package.json', import.meta.url)); +const ts = require('typescript'); +const source = readFileSync(new URL('../../frontend/components/FoodBarcodeScanner.tsx', import.meta.url), 'utf8'); +const copy = JSON.parse(readFileSync(new URL('../../frontend/config/barcode-copy.json', import.meta.url), 'utf8')); +const library = {exports: {}}; +vm.runInNewContext(ts.transpileModule(readFileSync(new URL('../../frontend/lib/foodBarcode.ts', import.meta.url), 'utf8'), + {compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022}}).outputText, {module: library, exports: library.exports}); +const nodes = (tree, type) => Array.isArray(tree) ? tree.flatMap(n => nodes(n, type)) : !tree || typeof tree !== 'object' ? [] : [...(tree.type === type ? [tree] : []), ...nodes(tree.props?.children, type)]; +const text = tree => Array.isArray(tree) ? tree.map(text).join(' ') : tree == null || typeof tree === 'boolean' ? '' : typeof tree === 'object' ? text(tree.props?.children) : String(tree); +function events() { + const handlers = new Map(); + return {handlers, addEventListener(name, fn) {handlers.set(name, fn);}, removeEventListener(name, fn) {if (handlers.get(name) === fn) handlers.delete(name);}, emit(name, event) {handlers.get(name)?.(event);}}; +} +function harness() { + const hooks = [], sessions = [], results = [], timers = new Map(); + const window = {...events(), isSecureContext: true}, document = {...events(), hidden: false}; + const navigator = {mediaDevices: {getUserMedia() {throw Error('Only the lifecycle service may request a camera');}}}; + let cursor = 0, effects = [], tree, timerId = 0, props = {locale: 'en', disabled: false, onLookup: code => results.push(code)}; + const hook = initial => {const i = cursor++; if (!(i in hooks)) hooks[i] = initial(); return i;}; + const same = (a, b) => a && b && a.length === b.length && a.every((x, i) => Object.is(x, b[i])); + const react = { + useState(initial) {const i = hook(() => initial); return [hooks[i], value => {hooks[i] = typeof value === 'function' ? value(hooks[i]) : value;}];}, + useRef(initial) {return hooks[hook(() => ({current: initial}))];}, + useCallback(fn, deps) {const i = hook(() => null); if (!same(hooks[i]?.deps, deps)) hooks[i] = {deps, fn}; return hooks[i].fn;}, + useEffect(effect, deps) {const i = hook(() => null); if (!same(hooks[i]?.deps, deps)) effects.push(() => {hooks[i]?.cleanup?.(); hooks[i] = {deps, cleanup: effect()};});}, + }; + const module = {exports: {}}; + const jsx = (type, props) => ({type, props}); + vm.runInNewContext(ts.transpileModule(source, {compilerOptions: {jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022}}).outputText, { + module, exports: module.exports, window, document, navigator, AbortController, + setTimeout(fn, delay) {timers.set(++timerId, {fn, delay}); return timerId;}, clearTimeout(id) {timers.delete(id);}, + require(name) { + if (name === 'react') return react; + if (name === 'react/jsx-runtime') return {jsx, jsxs: jsx}; + if (name === '@/config/barcode-copy.json') return {default: copy}; + if (name === '@/lib/foodBarcode') return {validFoodBarcode: library.exports.validFoodBarcode, + startBarcodeCamera(video, signal, onCode, onReady) {return new Promise((resolve, reject) => sessions.push({video, signal, onCode, onReady, resolve, reject}));}}; + throw Error(`Unexpected import ${name}`); + }, + }); + function render(patch = {}) { + props = {...props, ...patch}; cursor = 0; effects = []; + tree = module.exports.FoodBarcodeScanner(props); + const video = nodes(tree, 'video')[0]; if (!video.props.ref.current) video.props.ref.current = {}; + for (const effect of effects) effect(); + return tree; + } + render(); + return {window, document, navigator, sessions, results, timers, render, + get tree() {return tree;}, + click() {const result = nodes(tree, 'button')[0].props.onClick(); render(); return result;}, + type(value) {nodes(tree, 'input')[0].props.onChange({target: {value}}); render();}, + submit() {nodes(tree, 'form')[0].props.onSubmit({preventDefault() {}}); render();}, + unmount() {for (const h of hooks) h?.cleanup?.();}, + }; +} + +test('Scanner is idle on mount; manual entry validates and preserves zeros without requesting a camera', () => { + const h = harness(); assert.equal(h.sessions.length, 0); assert.equal(h.timers.size, 0); + h.type('https://example.test'); h.submit(); assert.equal(h.results.length, 0); + assert.ok(text(h.tree).includes(copy.en.invalid)); + h.type('0034000470693'); h.submit(); assert.deepEqual(h.results, ['0034000470693']); + assert.equal(h.sessions.length, 0); +}); + +test('Rapid scan clicks share a session; language changes and one result preserve exactly one lookup', async () => { + const h = harness(), click = nodes(h.tree, 'button')[0].props.onClick; + const pending = click(); void click(); h.render(); assert.equal(h.sessions.length, 1); + for (const locale of Object.keys(copy)) {h.render({locale}); assert.ok(text(h.tree).includes(copy[locale].opening));} + const s = h.sessions[0]; s.onReady(); h.render(); assert.ok(text(h.tree).includes(copy.ur.scanning)); + s.onCode('3017620422003'); s.onCode('3017620422003'); h.render(); + assert.deepEqual(h.results, ['3017620422003']); assert.equal(s.signal.aborted, true); assert.equal(h.timers.size, 0); + s.resolve(); await pending; +}); + +test('Stop, Escape, closing the panel, leaving/hiding, parent activity and timeout all cancel scanning', () => { + for (const reason of ['stop', 'escape', 'close', 'leave', 'hide', 'disabled', 'timeout', 'unmount']) { + const h = harness(); void h.click(); const s = h.sessions[0]; + if (reason === 'stop') void h.click(); + if (reason === 'escape') h.window.emit('keydown', {key: 'Escape'}); + if (reason === 'close') h.tree.props.onToggle({currentTarget: {open: false}}); + if (reason === 'leave') h.window.emit('pagehide'); + if (reason === 'hide') {h.document.hidden = true; h.document.emit('visibilitychange');} + if (reason === 'disabled') h.render({disabled: true}); + if (reason === 'timeout') {const timer = [...h.timers.values()][0]; assert.equal(timer.delay, 60_000); timer.fn();} + if (reason === 'unmount') {h.unmount(); assert.equal(h.window.handlers.size, 0); assert.equal(h.document.handlers.size, 0);} + assert.equal(s.signal.aborted, true, reason); assert.equal(h.timers.size, 0, reason); + s.onCode('3017620422003'); assert.equal(h.results.length, 0, 'Cancelled results must be ignored'); s.resolve(); + } +}); + +test('A late error from a stopped session cannot stop a newer session', async () => { + const h = harness(); const old = h.click(); void h.click(); const newer = h.click(); + assert.equal(h.sessions.length, 2); + h.sessions[0].reject(Object.assign(new Error('Late refusal'), {name: 'NotAllowedError'})); await old; h.render(); + assert.equal(h.sessions[1].signal.aborted, false); assert.ok(text(h.tree).includes(copy.en.opening)); + void h.click(); h.sessions[1].resolve(); await newer; +}); + +test('Camera refusal and unavailable contexts retain a working manual fallback; disabled lookup sends nothing', async () => { + const h = harness(); const pending = h.click(); + h.sessions[0].reject(Object.assign(new Error('Denied'), {name: 'NotAllowedError'})); await pending; h.render(); + assert.ok(text(h.tree).includes(copy.en.denied)); h.type('034000470693'); h.submit(); assert.equal(h.results.length, 1); + h.window.isSecureContext = false; await h.click(); h.render(); assert.ok(text(h.tree).includes(copy.en.unavailable)); + h.render({disabled: true}); h.submit(); await h.click(); assert.equal(h.sessions.length, 1); assert.equal(h.results.length, 1); +}); diff --git a/tools/tests/food_logging_ui.test.mjs b/tools/tests/food_logging_ui.test.mjs index fdb4ebb9..2ca6003f 100644 --- a/tools/tests/food_logging_ui.test.mjs +++ b/tools/tests/food_logging_ui.test.mjs @@ -65,7 +65,7 @@ function button(tree, label) { return result[0]; } -async function harness(componentName = "FoodSearchPlaceholder", postResponse, logsResponse, searchResponse, warmupResponse) { +async function harness(componentName = "FoodSearchPlaceholder", postResponse, logsResponse, searchResponse, warmupResponse, deleteResponse) { const source = await readFile(new URL(`../../frontend/components/${componentName}.tsx`, import.meta.url), "utf8"); const compiled = typescript.transpileModule(source, { compilerOptions: { jsx: typescript.JsxEmit.ReactJSX, module: typescript.ModuleKind.CommonJS, target: typescript.ScriptTarget.ES2022 }, @@ -168,11 +168,12 @@ async function harness(componentName = "FoodSearchPlaceholder", postResponse, lo if (response.ok) saved.push({ ...JSON.parse(options.body), id: saved.length + 1 }); return response; } + if (options?.method === "DELETE" && /\/logs(?:\/\d+)?$/.test(url)) { + const response = deleteResponse ? await deleteResponse() : { ok: true, status: 204 }; + if (response.ok) saved = url.endsWith('/logs') ? [] : saved.filter(item => item.id !== Number(url.split('/').at(-1))); + return response; + } if (url.endsWith("/logs")) { - if (options?.method === "DELETE") { - saved = []; - return { ok: true, status: 204 }; - } const response = logsResponse ? await logsResponse() : null; return response ?? { ok: true, json: async () => saved }; } @@ -478,6 +479,37 @@ test("a response arriving after logout cannot restore the former selection or su assert.equal(h.requests.filter((request) => request.url.endsWith("/logs")).length, 0); }); +test('Late single/bulk delete replies and failures cannot replace the logged-out state or issue another private read', async () => { + for (const bulk of [false, true]) for (const outcome of [204, 401, 500, 'network']) { + let finish, reject; + const h = await harness('FoodSearchPlaceholder', undefined, undefined, undefined, undefined, + () => new Promise((resolve, fail) => { finish = resolve; reject = fail; })); + await h.search(); h.choose(0); h.submit(); await h.flush(); h.answerConfirmation(true); + const list = nodes(h.tree, node => node.type === 'FoodLogList')[0]; + const pending = bulk ? list.props.onDeleteAllLogs() : list.props.onDeleteLog(1); + h.logout(); const count = h.requests.length; + if (outcome === 'network') reject(new Error('Offline')); else finish({ok: outcome === 204, status: outcome}); + await pending; await h.flush(); + assert.equal(h.requests.length, count, 'A reply from the signed-out session must not fetch private logs again'); + assert.ok(text(h.tree).includes(foodUiCopy.en.signInTitle)); + assert.equal(nodes(h.tree, node => node.type === 'FoodLogList').length, 0); + assert.equal(nodes(h.tree, node => node.type === 'ErrorBanner').length, 0); + } +}); + +test('An old delete authorization failure does not clear a newly signed-in diary', async () => { + let finish; + const h = await harness('FoodSearchPlaceholder', undefined, undefined, undefined, undefined, + () => new Promise(resolve => { finish = resolve; })); + await h.search(); h.choose(0); h.submit(); await h.flush(); + const pending = nodes(h.tree, node => node.type === 'FoodLogList')[0].props.onDeleteLog(1); + h.logout(); h.login(); await h.flush(); + assert.equal(nodes(h.tree, node => node.type === 'FoodLogList').length, 1); + finish({ok: false, status: 401}); await pending; await h.flush(); + assert.equal(nodes(h.tree, node => node.type === 'FoodLogList').length, 1); + assert.ok(!text(h.tree).includes(foodUiCopy.en.signInTitle)); +}); + test("expanding a card reveals its portion region once and restores keyboard focus on cancel", async () => { const h = await harness("FoodCard"); const props = { item: foods[0], isLogging: false, onLog() {}, formatNumber: String }; diff --git a/tools/tests/wordpress_site_style_dom.test.mjs b/tools/tests/wordpress_site_style_dom.test.mjs new file mode 100644 index 00000000..8f6aea43 --- /dev/null +++ b/tools/tests/wordpress_site_style_dom.test.mjs @@ -0,0 +1,143 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import {createRequire} from 'node:module'; +import vm from 'node:vm'; + +// A real DOM implementation exercises selectors, preserved nodes and interacting +// scripts. It does not measure pixels or claim a browser/device visual pass. +const require = createRequire(new URL('../../frontend/package.json', import.meta.url)); +const {parseHTML} = require('linkedom'); +const assets = new URL('../../wordpress-plugins/calorietoken-site-style/assets/', import.meta.url); +const source = name => readFileSync(new URL(name, assets), 'utf8'); +const copy = JSON.parse(source('discovery-data.json')); +const menu = JSON.parse(source('menu-data.json')); +function fixture(html, {page = 1210, route = 'whitepaper', home = false} = {}) { + const {document, window: dom} = parseHTML(`${html}`); + const observers = [], frames = [], events = new Map(); + class Observer { + constructor(callback) { this.callback = callback; observers.push(this); } + observe(target, options) { this.target = target; this.options = options; this.active = true; } + disconnect() { this.active = false; } + } + // linkedom intentionally omits layout and writable select.value behavior. + // Supply only the native control property; no CSS/layout results are invented. + Object.defineProperty(dom.HTMLSelectElement.prototype, 'value', {configurable: true, + get() { return this.querySelector('option[selected]')?.value ?? this.querySelector('option')?.value ?? ''; }, + set(value) { this.querySelectorAll('option').forEach(option => option.toggleAttribute('selected', option.value === value)); }, + }); + const window = { + Event: dom.Event, CustomEvent: dom.CustomEvent, Element: dom.Element, MutationObserver: Observer, + location: new URL(`https://calorietoken.net/${home ? '' : 'index.php/' + route + '/'}`), + CalorieTokenDiscovery: {page, copy, appURL: 'https://calorietoken.net/index.php/calorieapp/', appLogo: '/logo.svg'}, + CalorieTokenSiteStyleMenu: menu, + localStorage: {getItem: () => null, setItem() {}}, + getComputedStyle: node => ({display: node.style.display || 'block', visibility: node.style.visibility || 'visible', backgroundImage: node.style.backgroundImage || '', opacity: node.style.opacity || '1'}), + requestAnimationFrame: fn => frames.push(fn), + addEventListener(name, fn) { events.set(name, [...events.get(name) || [], fn]); }, + dispatchEvent(event) { for (const fn of events.get(event.type) || []) fn(event); }, + }; + const context = {window, document, URL, MutationObserver: Observer}; + return {document, window, observers, frames, + run(name) { vm.runInNewContext(source(name), context); document.dispatchEvent(new dom.Event('DOMContentLoaded')); }, + emit(name) { for (const fn of events.get(name) || []) fn(); }, + mutate(record) { for (const o of [...observers]) if (o.active && o.options.childList) o.callback([{type: 'childList', addedNodes: [], removedNodes: [], ...record}]); }, + flush() { let budget = 20; while (frames.length && budget--) frames.shift()(); assert.equal(frames.length, 0, 'Observers must settle without a refresh loop'); }, + }; +} +const account = '
    Public account placeholder
    '; +const footer = ''; +const native = `
    ${account}
    `; + +test('Native headers keep menu/login nodes and handlers through eleven language changes', () => { + for (const home of [false, true]) { + const h = fixture(native + footer, {home, page: home ? 1090 : 1210}); + const input = h.document.querySelector('#native-menu'), login = h.document.querySelector('#native-login'); + let changes = 0, clicks = 0, focuses = 0; + input.checked = false; input.focus = () => focuses++; + input.addEventListener('change', () => changes++); login.addEventListener('click', () => clicks++); + h.run('discovery.js'); h.run('refinements.js'); + for (const tag of [...Object.keys(copy), 'en']) h.window.CalorieTokenDiscoveryUI.setLocale(tag, false); + assert.equal(h.document.querySelectorAll('.ctstyle-header-grid').length, 1); + assert.equal(h.document.querySelectorAll('.ctstyle-footer-tail').length, 1); + assert.equal(h.document.querySelectorAll('#ctstyle-account-language').length, 1); + assert.equal(h.document.querySelector('#native-menu'), input); + assert.equal(h.document.querySelector('#native-login'), login); + login.click(); assert.equal(clicks, 1); + input.checked = true; input.dispatchEvent(new h.window.Event('change', {bubbles: true})); + const escape = new h.window.Event('keydown', {bubbles: true, cancelable: true}); escape.key = 'Escape'; + input.dispatchEvent(escape); + assert.equal(input.checked, false); assert.equal(input.getAttribute('aria-expanded'), 'false'); + assert.equal(changes, 2); assert.equal(focuses, 1); + } +}); + +test('The combined account controls recover after a native footer refresh without cloning login', () => { + const h = fixture(native + footer); h.run('discovery.js'); + const card = h.document.querySelector('.xl-card'), widget = h.document.querySelector('#ctstyle-account-app'); + const login = h.document.querySelector('#native-login'), target = h.document.querySelector('.xl-card-footer'); + target.replaceChildren(h.document.createTextNode('Updated sign-in note')); + h.mutate({target, addedNodes: [...target.childNodes], removedNodes: [widget]}); h.flush(); + assert.ok(card.contains(widget), 'The existing app/language controls should reattach after footer replacement'); + assert.equal(h.document.querySelector('#native-login'), login); + assert.equal(h.document.querySelectorAll('#ctstyle-account-app').length, 1); +}); + +test('Translated public text can be replaced by a new known CMS message and translated again', () => { + const h = fixture('

    Welcome

    Welcome

    '); + h.window.CalorieTokenContentLanguage = {locales: ['en', 'nl'], entries: [ + {source: 'Welcome', translations: {nl: 'Welkom'}}, {source: 'No Donations Yet', translations: {nl: 'Nog geen donaties'}}, + ]}; + h.run('content-language.js'); h.window.CalorieTokenContentLanguageUI.refresh('nl'); + const node = h.document.querySelector('#message'); assert.equal(node.textContent, 'Welkom'); + const removed = [...node.childNodes]; node.replaceChildren(h.document.createTextNode('No Donations Yet')); + h.mutate({target: node, addedNodes: [...node.childNodes], removedNodes: removed}); h.flush(); + assert.equal(node.textContent, 'Nog geen donaties'); + h.window.CalorieTokenContentLanguageUI.refresh('en'); assert.equal(node.textContent, 'No Donations Yet'); + assert.equal(h.document.querySelector('.xl-card p').textContent, 'Welcome'); +}); + +test('A CMS edit to unrecognized public copy is preserved during later language changes', () => { + const h = fixture('

    Welcome

    '); + h.window.CalorieTokenContentLanguage = {locales: ['en', 'nl'], entries: [{source: 'Welcome', translations: {nl: 'Welkom'}}]}; + h.run('content-language.js'); h.window.CalorieTokenContentLanguageUI.refresh('nl'); + const node = h.document.querySelector('#message'); node.textContent = 'New custom content'; + h.window.CalorieTokenContentLanguageUI.refresh('en'); h.window.CalorieTokenContentLanguageUI.refresh('nl'); + assert.equal(node.textContent, 'New custom content'); +}); + +test('Trustline alternatives preserve native links/copy controls and do not duplicate on refresh', () => { + const h = fixture('
    ', {page: 1205, route: 'trustline'}); + const controls = [...h.document.querySelectorAll('a,button')]; const links = controls.map(n => n.getAttribute('href')); + h.run('refinements.js'); for (const tag of Object.keys(copy)) h.window.CalorieTokenRefinements.refresh(tag); + assert.equal(h.document.querySelectorAll('.ctstyle-manual-trustline').length, 2); + assert.equal(h.document.querySelectorAll('#ctstyle-trustline-methods').length, 1); + controls.forEach((node, index) => {assert.ok(node.isConnected); assert.equal(node.getAttribute('href'), links[index]);}); +}); + +test('The full CAL guide/discovery/translation sequence retains exact routes and all eleven languages', () => { + const ts = require('typescript'); + const parsed = ts.createSourceFile('menu-pages.js', source('menu-pages.js'), ts.ScriptTarget.Latest, true); + let legacy; + function visit(node) { + if (ts.isVariableDeclaration(node) && node.name.getText(parsed) === 'legacyBuyGuideMarkup') legacy = vm.runInNewContext(node.initializer.getText(parsed)); + ts.forEachChild(node, visit); + } + visit(parsed); assert.equal(typeof legacy, 'string'); + const h = fixture(`
    ${legacy}
    `, {page: 4205, route: 'how-to-buy-calorie'}); + h.run('menu-pages.js'); h.run('ready-languages.js'); h.run('discovery.js'); h.run('refinements.js'); + const guide = h.document.querySelector('.cal-buy-guide'); + assert.equal(guide.getAttribute('data-ctstyle-buy-routes'), '1'); + const controls = [...guide.querySelectorAll('a')], destinations = controls.map(a => a.getAttribute('href')); + assert.equal(destinations.length, 7); + for (const locale of [...Object.keys(copy), 'en']) { + h.window.CalorieTokenDiscoveryUI.setLocale(locale, false); + assert.equal(guide.lang, locale); + for (const field of guide.querySelectorAll('[data-cal-buy-copy]')) assert.equal(field.textContent, menu.buyGuide.copy[locale][field.getAttribute('data-cal-buy-copy')]); + controls.forEach((node, index) => {assert.ok(node.isConnected); assert.equal(node.getAttribute('href'), destinations[index]);}); + assert.equal(h.document.querySelectorAll('.ctstyle-crypto-journey li').length, 3); + assert.equal(h.document.querySelectorAll('#ctstyle-own-dex').length, 1); + assert.equal(h.document.querySelectorAll('#ctstyle-external-exchange').length, 1); + assert.equal(h.document.querySelectorAll('iframe').length, 0, 'An unopened external exchange must not load a provider'); + } +}); diff --git a/tools/tests/wordpress_site_style_review.test.php b/tools/tests/wordpress_site_style_review.test.php index adc4cbc7..6dea648c 100644 --- a/tools/tests/wordpress_site_style_review.test.php +++ b/tools/tests/wordpress_site_style_review.test.php @@ -3,11 +3,11 @@ define('ABSPATH', __DIR__); function add_action(...$args) {} function add_filter(...$args) {} -$allowed = true; $fixture = array(); $queries = 0; $preview = false; +$allowed = true; $fixture = array(); $queries = 0; $preview = false; $front_page = false; function is_admin() { return false; } function is_feed() { return false; } function is_embed() { return false; } -function is_front_page() { return false; } +function is_front_page() { global $front_page; return $front_page; } function is_page($ids) { return false; } function is_preview() { global $preview; return $preview; } function current_user_can($capability) { global $allowed; return $allowed && $capability === 'manage_options'; } @@ -28,7 +28,11 @@ function post_fixture($id, $status, $content) { } $preview = true; check(!\CalorieToken\SiteStyle\Plugin::enabled(), 'Preview must not load public styling'); +$front_page = true; +check(!\CalorieToken\SiteStyle\Plugin::footer_only(), 'Front-page preview must not load public styling'); $preview = false; +check(\CalorieToken\SiteStyle\Plugin::footer_only(), 'Ordinary front page should retain shared styling'); +$front_page = false; check(\CalorieToken\SiteStyle\Plugin::enabled(), 'Ordinary public page should retain styling'); foreach (array('preview','customize_changeset_uuid','brizy-edit') as $query) { $_GET[$query] = '1'; diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index 28b47871..f9c0a749 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,23 @@ -# CalorieToken Site Style 1.4.8 +# CalorieToken Site Style 1.4.9 Eén complete vervangende ZIP, 10 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.8.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.8** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.9.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.9** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## Reviewronde 1.4.8 +## Extra regressie-audit 1.4.9 + +- De bestaande gecombineerde CalorieApp-/taalbediening wordt teruggeplaatst als de native accountwidget zijn footer vernieuwt. De inlogknop en zijn handlers blijven dezelfde nodes. +- Een nieuwe bekende CMS- of donatiemelding wordt opnieuw vertaald, ook als de vorige tekst al vertaald was. Terug naar Engels herstelt de nieuwe tekst; onbekende eigen wijzigingen blijven behouden. +- Ook een WordPress-voorbeeld van Home krijgt geen publieke opmaakcorrecties. +- De bijbehorende appcorrectie negeert late verwijderantwoorden na uitloggen of verlaten van de pagina. Een oude foutmelding kan een nieuwe ingelogde sessie niet meer wissen. +- Voedselzoeken slaat verkeerd gevormde bronrecords over. Aangeboden producten moeten binnen de bestaande dagboekregels passen; de bronidentiteit wordt niet ingekort. Booleans gelden niet als gemeten voedingswaarden. +- De CI ontdekt nu alle JavaScript-testbestanden automatisch. Extra DOM-tests controleren de samenwerking van header, accountwidget, CAL-gids, Trustline en vertalingen. De echte decoder leest vier soorten synthetische barcodebeelden; extra bedieningstests controleren stoppen, taalwissels, weigeren en late antwoorden. + +Dit is één vervangend pakket; installatie of handmatig testen van tussenversies is niet nodig. Alle 1.4.8-functies hieronder zijn inbegrepen. De automatische controles vervangen geen bewijs van de werkelijk geïnstalleerde website of de camera op een echte telefoon. De appcorrecties vragen nog steeds de afzonderlijke frontend- en backenduitrol; deze ZIP installeert uitsluitend Site Style. + +## Inbegrepen: reviewronde 1.4.8 - Brizy- en usecase-headers delen nu dezelfde logobreedte, menuruimte, buitenmarges en mobiele afronding. Het usecase-menu opent in de pagina en bedekt de accountwidget niet meer. - De volledige Xaman-/CalorieApp-kaart kan meegroeien; lange tekst, adresregels, appknop en taalkeuze blijven binnen de beschikbare breedte. De oorspronkelijke inlogbediening blijft intact. @@ -16,7 +27,7 @@ WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorieto **Afzonderlijke app-uitrol:** de nieuwe barcodescanner, exacte Open Food Facts-opvraag, USDA-gewichtberekening en Nutri-Score-kleuren vragen uitrol van de bijbehorende appcode. De barcodefunctie vereist zowel de frontend als de backend uit deze PR. Deze ZIP installeert uitsluitend Site Style. De Identity Bridge en accountkoppeling zijn niet gewijzigd. -Test na upload Home, Whitepaper en Cafés op desktop en telefoon, met open/dicht menu en ingelogd/uitgelogd account; controleer ook footer, Richlist, Trustline, CAL & Crypto, Tokenomics, Roadmap, Blog en donaties. Controleer de app pas op de nieuwe functies nadat de afzonderlijke uitrol bevestigd is. +De uiteindelijke livecontrole betreft Home, Whitepaper, Cafés, Richlist, Trustline, CAL & Crypto, Tokenomics, Roadmap, Blog en donaties, plus de afzonderlijk uitgerolde app. De extra audit vraagt geen tussentijdse handmatige controles van de eigenaar. ## Historisch: auditronde 1.4.7 — basis behouden, review eenvoudiger diff --git a/wordpress-plugins/calorietoken-site-style/assets/content-language.js b/wordpress-plugins/calorietoken-site-style/assets/content-language.js index df3cf6b8..9f20e215 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/content-language.js +++ b/wordpress-plugins/calorietoken-site-style/assets/content-language.js @@ -28,17 +28,17 @@ if(record.changed){node.replaceChildren.apply(node,record.children);record.changed=false;} if(record.lang===null)node.removeAttribute('lang');else node.setAttribute('lang',record.lang); if(record.dir===null)node.removeAttribute('dir');else node.setAttribute('dir',record.dir); - record.last=norm(node.textContent);return; + record.last=norm(node.textContent);record.current=Array.from(node.childNodes);record.paintedLocale=null;return; } if(norm(node.textContent)!==value)node.replaceChildren(document.createTextNode(value)); - node.lang=locale;node.dir=['ar','ur'].includes(locale)?'rtl':'ltr';record.last=value;record.changed=true; + node.lang=locale;node.dir=['ar','ur'].includes(locale)?'rtl':'ltr';record.last=value;record.changed=true;record.current=Array.from(node.childNodes);record.paintedLocale=locale; } function block(n){ if(!safe(n)||n.querySelector(excluded)||records.has(n))return; var row=lookup.get(norm(n.textContent));if(!row)return; // Keep anchors, icons, labels and controls alive. Complex content uses text-node matches. if(Array.from(n.querySelectorAll('*')).some(function(x){return !['SPAN','STRONG','B','EM','I','BR'].includes(x.tagName);}))return; - var record={node:n,row:row,children:Array.from(n.childNodes),last:norm(n.textContent),kind:'block',lang:n.getAttribute('lang'),dir:n.getAttribute('dir'),changed:false}; + var record={node:n,row:row,children:Array.from(n.childNodes),current:Array.from(n.childNodes),last:norm(n.textContent),kind:'block',lang:n.getAttribute('lang'),dir:n.getAttribute('dir'),changed:false}; records.set(n,record);active.push(record);paint(record); } function fragments(root){ @@ -58,7 +58,19 @@ if(observer)observer.disconnect(); // Ask existing owners to translate their own mutable controls and notes. ['CalorieAppTokenomics'].forEach(function(key){if(window[key]&&typeof window[key].setLocale==='function')window[key].setLocale(locale);}); - active=active.filter(function(r){return r.node.isConnected;});active.forEach(paint); + active=active.filter(function(r){ + if(!r.node.isConnected)return false; + var changed=r.kind==='text'?r.node.data!==r.last: + norm(r.node.textContent)!==r.last || r.current.length!==r.node.childNodes.length || r.current.some(function(n,i){return n!==r.node.childNodes[i];}); + if(!changed)return true; + // The CMS owns this new content. Forget the old snapshot so a recognized + // replacement can translate and English restores the new, current text. + if(r.kind==='block' && r.paintedLocale && r.node.getAttribute('lang')===r.paintedLocale){ + if(r.lang===null)r.node.removeAttribute('lang');else r.node.setAttribute('lang',r.lang); + if(r.dir===null)r.node.removeAttribute('dir');else r.node.setAttribute('dir',r.dir); + } + records.delete(r.node);return false; + });active.forEach(paint); document.querySelectorAll(candidates).forEach(block); document.querySelectorAll('.ctstyle-legal,.ctstyle-market-card,.brz-rich-text,.entry-content,.woocommerce,.woocommerce-notices-wrapper,.woocommerce-info,.woocommerce-message,.woocommerce-error,.ctstyle-roadmap-preview,.calorieapp-context-note,.brz-posts').forEach(function(root){if(!records.has(root))fragments(root);}); if(observer)observer.observe(document.body,{subtree:true,childList:true,characterData:true}); diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.js b/wordpress-plugins/calorietoken-site-style/assets/discovery.js index 2a9f8eda..20da5cb7 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.js +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.js @@ -283,10 +283,17 @@ function watchAccount() { if (accountWatcher || typeof MutationObserver !== 'function') return; accountWatcher = new MutationObserver(function (records) { - if (records.some(function (record) {return Array.from(record.addedNodes).some(function (node) { - return node.nodeType===1 && !node.closest('#ctstyle-account-app') && - (node.matches('.xl-card,.ctstyle-header') || node.querySelector('.xl-card')); - });})) {accountWidget();} + if (records.some(function (record) { + // Native login widgets can replace just their footer/contents. Reattach + // our existing controls when removed, without rebuilding the login card. + if (accountApp && !accountApp.isConnected && Array.from(record.removedNodes).some(function (node) { + return node===accountApp || node.nodeType===1 && node.contains(accountApp); + })) return true; + return Array.from(record.addedNodes).some(function (node) { + return node.nodeType===1 && !node.closest('#ctstyle-account-app') && + (node.matches('.xl-card,.ctstyle-header') || node.querySelector('.xl-card')); + }); + })) {accountWidget();} }); accountWatcher.observe(document.body,{childList:true,subtree:true}); } diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index e0d52609..45000403 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.8 + * Version: 1.4.9 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.8'; + const VERSION = '1.4.9'; private static function json_asset($name) { // Request-local cache only: plugin updates never need a persistent cache purge. @@ -108,7 +108,7 @@ public static function body_class($classes) { public static function footer_only() { if (!(is_front_page() || is_page(1090)) || is_page(8001) || - is_admin() || is_feed() || is_embed() || + is_admin() || is_feed() || is_embed() || is_preview() || (defined('REST_REQUEST') && REST_REQUEST) || (function_exists('wp_doing_ajax') && wp_doing_ajax()) || (function_exists('is_customize_preview') && is_customize_preview())) { return false; } From 59d191d8ed808172908db596aef8dd63d9a3c278 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:25:33 +0200 Subject: [PATCH 014/368] Patch development tooling and identify Render builds automatically --- .github/workflows/ci.yml | 4 +- backend/app/main.py | 9 +- .../tests/test_release_version_contract.py | 15 +- docs/public/step123-review-2026-09-10.md | 14 +- frontend/app/layout.tsx | 5 +- frontend/package-lock.json | 762 +++--------------- frontend/package.json | 2 +- tools/tests/build_identity.test.mjs | 31 + 8 files changed, 171 insertions(+), 671 deletions(-) create mode 100644 tools/tests/build_identity.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e46dc471..03d6af33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -312,9 +312,9 @@ jobs: working-directory: frontend run: npm ci - - name: Audit frontend production dependencies + - name: Audit frontend runtime and development dependencies working-directory: frontend - run: npm audit --omit=dev --audit-level=critical + run: npm audit --audit-level=high - name: Lint frontend working-directory: frontend diff --git a/backend/app/main.py b/backend/app/main.py index 3e9933ee..7ca24a72 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -118,10 +118,11 @@ BRIDGE_STATE_VALIDATE_CONTEXT = "login_state_validate" -def _build_identifier(value: str | None) -> str: +def _build_identifier(value: str | None, *, render_commit: str | None = None) -> str: candidate = value.strip() if value else "" if not candidate: - return "development" + commit = render_commit.strip() if render_commit else "" + return commit if re.fullmatch(r"[A-Fa-f0-9]{40}", commit) else "development" if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}", candidate) is None: raise RuntimeError( "CALORIEAPP_BUILD_ID must be 1-64 letters, digits, dots, " @@ -151,7 +152,9 @@ def _build_identifier(value: str | None) -> str: _SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE", "lax").strip().lower() _CALORIEAPP_ENV_RAW = os.getenv("CALORIEAPP_ENV") _CALORIEAPP_ENV = _CALORIEAPP_ENV_RAW.strip().lower() if _CALORIEAPP_ENV_RAW and _CALORIEAPP_ENV_RAW.strip() else None -_CALORIEAPP_BUILD_ID = _build_identifier(os.getenv("CALORIEAPP_BUILD_ID")) +_CALORIEAPP_BUILD_ID = _build_identifier( + os.getenv("CALORIEAPP_BUILD_ID"), render_commit=os.getenv("RENDER_GIT_COMMIT") +) _BRIDGE_AUTH_MAX_AGE_SECONDS = int(os.getenv("BRIDGE_AUTH_MAX_AGE_SECONDS", "300")) _BRIDGE_AUTH_MAX_FUTURE_SECONDS = int(os.getenv("BRIDGE_AUTH_MAX_FUTURE_SECONDS", "30")) _BRIDGE_NONCE_RETENTION_SECONDS = int( diff --git a/backend/tests/test_release_version_contract.py b/backend/tests/test_release_version_contract.py index 70a11648..83f7f1d1 100644 --- a/backend/tests/test_release_version_contract.py +++ b/backend/tests/test_release_version_contract.py @@ -2,8 +2,9 @@ import json from pathlib import Path +import pytest -from app.main import app +from app.main import app, _build_identifier from app.services.open_food_facts import REQUEST_HEADERS @@ -107,6 +108,18 @@ def test_runtime_versions_are_aligned_to_v2() -> None: assert versioning["contract_directory_v1_means_contract_schema_version_not_product_v1"] is True +def test_render_commit_fills_only_an_unconfigured_runtime_build_identifier() -> None: + commit = "2ad5d87e74a0169dfa069fb77babe27d592c9f8b" + assert _build_identifier(None, render_commit=commit) == commit + assert _build_identifier(" ", render_commit=" " + commit + " ") == commit + assert _build_identifier("release-1", render_commit=commit) == "release-1" + assert _build_identifier("development", render_commit=commit) == "development" + assert _build_identifier(None, render_commit="invalid provider text") == "development" + assert _build_identifier(None) == "development" + with pytest.raises(RuntimeError): + _build_identifier("invalid explicit value", render_commit=commit) + + def test_data_safety_contract_points_to_v2_completion_boundary() -> None: data_safety = json.loads( (ROOT / "contracts" / "data-safety" / "v1" / "data-safety.json").read_text( diff --git a/docs/public/step123-review-2026-09-10.md b/docs/public/step123-review-2026-09-10.md index 05c59e9c..ae0b10f5 100644 --- a/docs/public/step123-review-2026-09-10.md +++ b/docs/public/step123-review-2026-09-10.md @@ -16,7 +16,7 @@ The older baseline and synthetic-provider evidence files describe dated observat The owner requested fewer manual test/debug cycles. This round reproduces and fixes additional integration failures while keeping the accepted identity -protocol, database schemas, artwork, CSS layout and existing dependency versions +protocol, database schemas, artwork, CSS layout and direct production dependency versions unchanged: - Reattach the existing app/language widget after native account-footer replacement. @@ -28,14 +28,24 @@ unchanged: - Isolate malformed food records and exclude results that would fail the existing diary schema. Boolean values are unknown nutrition, not measured zero/one. - Reset empty-query search flags, addressing the later automated review note. +- Use the valid Render commit as build metadata when no explicit release ID is + configured. The observed live app reported `development`; explicit operator + IDs retain priority. This uses Render's [documented public build variable](https://render.com/docs/environment-variables), + with no dashboard access or configuration change. CI now discovers every `tools/tests/*.test.mjs` file instead of relying on a manually maintained list. A development-only DOM dependency supports interaction tests of the actual WordPress scripts; it is not shipped in the plugin or app bundle. Additional scanner UI and actual ZXing raster-decoding tests cover all -four advertised formats. No new production package version, provider request, +four advertised formats. No new direct production package version, provider request, paid service, signing action or deployment is introduced. +The broader dependency audit also found eleven findings in pre-existing +development tools. ESLint's Next configuration is now aligned with Next 15.5.25 +and affected development dependencies receive compatible security fixes. The direct production packages remain unchanged; the shared caniuse-lite +browser data also receives a compatible update. The resulting complete npm audit reports +zero known vulnerabilities; this is a dependency check, not a security certification. + Live Home was rechecked and still loaded Site Style 1.4.5; the public app lacked the new scanner. The review requires no intermediate owner testing. Source/CI completion remains distinct from final installed layout, camera hardware, diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 3b71341d..39254c70 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -16,7 +16,10 @@ if ( "NEXT_PUBLIC_CALORIEAPP_BUILD_ID must be 1-64 letters, digits, dots, underscores or hyphens" ); } -const buildId = configuredBuildId || "development"; +// Render exposes the deployed Git SHA without additional dashboard setup. +// Explicit release identifiers keep priority; never expose arbitrary env text. +const renderCommit = process.env.RENDER_GIT_COMMIT?.trim(); +const buildId = configuredBuildId || (renderCommit && /^[a-f0-9]{40}$/i.test(renderCommit) ? renderCommit : "development"); export default function RootLayout({ children, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a379dba7..c6c60fa9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,7 +23,7 @@ "@typescript-eslint/parser": "^8.59.2", "autoprefixer": "10.4.19", "eslint": "8.57.0", - "eslint-config-next": "14.2.5", + "eslint-config-next": "15.5.25", "linkedom": "0.18.13", "postcss": "8.5.23", "tailwindcss": "3.4.7", @@ -678,53 +678,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -784,13 +737,43 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.5.tgz", - "integrity": "sha512-LY3btOpPh+OTIpviNojDpUdIbHW9j0JBYBjsIp8IxtDFfYFyORvw3yNq6N231FVqQA7n7lwaf7xHbVJlA1ED7g==", + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.25.tgz", + "integrity": "sha512-dAzOqZQCAOgIq5yQpsuMBZcwxK0AtzGqHMQpxNWYLQlvf79rsP3SDwdGPNQC4ySaMSihOQXMO/VXubQ3JWW+3A==", "dev": true, "license": "MIT", "dependencies": { - "glob": "10.3.10" + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/@next/swc-darwin-arm64": { @@ -969,17 +952,6 @@ "node": ">=12.4.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1258,16 +1230,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -1802,16 +1774,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -2031,9 +1993,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", - "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "version": "2.11.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz", + "integrity": "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2071,9 +2033,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -2095,9 +2057,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "funding": [ { @@ -2115,11 +2077,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" @@ -2199,9 +2161,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001790", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", - "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "funding": [ { "type": "opencollective", @@ -2532,19 +2494,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -2651,17 +2600,10 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { - "version": "1.5.344", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", - "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "version": "1.5.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", + "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==", "dev": true, "license": "ISC" }, @@ -2943,24 +2885,25 @@ } }, "node_modules/eslint-config-next": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.5.tgz", - "integrity": "sha512-zogs9zlOiZ7ka+wgUnmcM0KBEDjo4Jis7kxN1jvC0N4wynQ2MIx/KBkg4mVF63J5EK4W0QMCn7xO3vNisjaAoA==", + "version": "15.5.25", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.25.tgz", + "integrity": "sha512-hB1oClZzRO3vMAeMmBMch0FHNJ4irH74LvQWDEMHkJtG/Us9+SAsg1f5Pvcw9jz4tc8wSGnvbmF050BRAAIbBg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "14.2.5", - "@rushstack/eslint-patch": "^1.3.3", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", + "@next/eslint-plugin-next": "15.5.25", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0", + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -2969,153 +2912,6 @@ } } }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/parser": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", - "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "7.2.0", - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/typescript-estree": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/scope-manager": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", - "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", - "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", - "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "@typescript-eslint/visitor-keys": "7.2.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", - "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.2.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-config-next/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/eslint-config-next/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-config-next/node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, "node_modules/eslint-import-resolver-node": { "version": "0.3.10", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", @@ -3332,16 +3128,16 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.0.0-canary-7118f5dd7-20230705", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz", - "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { @@ -3603,23 +3399,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3777,30 +3556,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3814,32 +3569,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -3873,27 +3602,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4375,16 +4083,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4658,25 +4356,6 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -4694,10 +4373,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4877,13 +4566,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4941,16 +4623,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5093,11 +4765,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -5397,33 +5072,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5594,9 +5242,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6159,29 +5807,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6212,76 +5837,6 @@ "node": ">= 0.4" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6408,20 +5963,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6952,9 +6493,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -7114,107 +6655,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 55b41933..672d057a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,7 @@ "@typescript-eslint/parser": "^8.59.2", "autoprefixer": "10.4.19", "eslint": "8.57.0", - "eslint-config-next": "14.2.5", + "eslint-config-next": "15.5.25", "linkedom": "0.18.13", "postcss": "8.5.23", "tailwindcss": "3.4.7", diff --git a/tools/tests/build_identity.test.mjs b/tools/tests/build_identity.test.mjs new file mode 100644 index 00000000..e3d09918 --- /dev/null +++ b/tools/tests/build_identity.test.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import {readFileSync} from 'node:fs'; +import {createRequire} from 'node:module'; +import vm from 'node:vm'; + +const require = createRequire(new URL('../../frontend/package.json', import.meta.url)); +const ts = require('typescript'); +const compiled = ts.transpileModule(readFileSync(new URL('../../frontend/app/layout.tsx', import.meta.url), 'utf8'), { + compilerOptions: {jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022}, +}).outputText; +function buildId(env) { + const module = {exports: {}}; + vm.runInNewContext(compiled, {module, exports: module.exports, process: {env}, require(name) { + if (name === 'react/jsx-runtime') return {jsx: (type, props) => ({type, props})}; + if (name === './globals.css') return {}; + if (name === '@/components/DisplayLanguageProvider') return {DisplayLanguageProvider: 'DisplayLanguageProvider'}; + throw Error(`Unexpected import ${name}`); + }}); + return module.exports.default({children: null}).props['data-calorieapp-build-id']; +} +test('Rendered build metadata uses a valid Render commit only when no explicit release ID exists', () => { + const sha = '2ad5d87e74a0169dfa069fb77babe27d592c9f8b'; + assert.equal(buildId({RENDER_GIT_COMMIT: sha}), sha); + assert.equal(buildId({RENDER_GIT_COMMIT: ' ' + sha + ' ', NEXT_PUBLIC_CALORIEAPP_BUILD_ID: ' '}), sha); + assert.equal(buildId({RENDER_GIT_COMMIT: sha, NEXT_PUBLIC_CALORIEAPP_BUILD_ID: 'release-1'}), 'release-1'); + assert.equal(buildId({RENDER_GIT_COMMIT: sha, NEXT_PUBLIC_CALORIEAPP_BUILD_ID: 'development'}), 'development'); + assert.equal(buildId({RENDER_GIT_COMMIT: 'invalid provider text'}), 'development'); + assert.equal(buildId({}), 'development'); + assert.throws(() => buildId({NEXT_PUBLIC_CALORIEAPP_BUILD_ID: 'invalid explicit value', RENDER_GIT_COMMIT: sha})); +}); From d43c3a33953174a729c46cd1f72bc7b722e9a9b4 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:06:31 +0200 Subject: [PATCH 015/368] Fix issues observed in the installed Site Style review (1.4.10) --- docs/public/step123-review-2026-09-10.md | 34 ++++++++ tools/tests/wordpress_site_style_dom.test.mjs | 77 ++++++++++++++++++- .../calorietoken-site-style/LEESMIJ.md | 14 +++- .../assets/discovery.css | 5 ++ .../assets/menu-pages.js | 50 +++++++++++- .../assets/navigation.js | 44 +++++++++++ .../calorietoken-site-style.php | 4 +- 7 files changed, 218 insertions(+), 10 deletions(-) diff --git a/docs/public/step123-review-2026-09-10.md b/docs/public/step123-review-2026-09-10.md index ae0b10f5..59f84bd8 100644 --- a/docs/public/step123-review-2026-09-10.md +++ b/docs/public/step123-review-2026-09-10.md @@ -2,6 +2,40 @@ 10 September 2026. This is a source/evidence review, not a live deployment or production-readiness certificate. It preserves the owner's acceptance of Step 1 (foundation) and Step 2 (website/app identity integration). +## Latest: installed 1.4.9 review and bounded 1.4.10 corrections + +The owner installed 1.4.9 for this review. Chrome at 1363×936 confirmed the round +Home/Whitepaper/usecase headers, readable anonymous account/app controls, shared +footer, consolidation wallet and Roadmap card/button. Eight public pages were +inspected; Richlist inspection reached the header and table structure. The +browser then failed during navigation to Blog and did not recover. It exposes +no viewport/engine switching here, and the local Android preflight found neither +ADB nor an emulator. These observations do not establish mobile, rotated, +widescreen, Safari, Firefox or physical-device acceptance. + +Three concrete issues receive a narrow Site Style correction: + +- Above 1200 px, move the existing shortcut stack to the right edge: its previous + offset visibly covered CAL-guide and Trustline text. Smaller-screen geometry, + button design, account controls and the page layout are preserved. +- Reuse the exact legacy Trustline context note instead of inserting a second + copy with the same ID. Keep the existing link and handlers; restore only still + managed text on cleanup and preserve custom edits/markup. +- Follow late page-height growth for at most 1.8 seconds after an explicit + bottom-button click. Manual wheel/touch/pointer/keyboard input, page exit or + hiding cancels the adjustment. The original click handler is retained. + +The four added regression cases failed before these fixes and pass afterwards. +The existing isolated identity simulation passed 176 backend cases and 28 +interface/integration cases without a real XRPL account, network signature, +ledger submission or production data. It is mock identity-flow evidence, not +an Android/Xaman or ledger test. The old LiveCoinWatch loader also emitted +background console errors while XPMarket displayed data; its cause is recorded +without attributing the browser failure to it or changing Identity Bridge. + +No intermediate owner debugging round is required. Later entries below retain +the earlier implementation history; current live observations are those above. + ## What stays stable | Step | Existing foundation | Evidence boundary | diff --git a/tools/tests/wordpress_site_style_dom.test.mjs b/tools/tests/wordpress_site_style_dom.test.mjs index 8f6aea43..809b83dd 100644 --- a/tools/tests/wordpress_site_style_dom.test.mjs +++ b/tools/tests/wordpress_site_style_dom.test.mjs @@ -35,9 +35,10 @@ function fixture(html, {page = 1210, route = 'whitepaper', home = false} = {}) { getComputedStyle: node => ({display: node.style.display || 'block', visibility: node.style.visibility || 'visible', backgroundImage: node.style.backgroundImage || '', opacity: node.style.opacity || '1'}), requestAnimationFrame: fn => frames.push(fn), addEventListener(name, fn) { events.set(name, [...events.get(name) || [], fn]); }, + removeEventListener(name, fn) { events.set(name, (events.get(name) || []).filter(item => item !== fn)); }, dispatchEvent(event) { for (const fn of events.get(event.type) || []) fn(event); }, }; - const context = {window, document, URL, MutationObserver: Observer}; + const context = {window, document, URL, Event: dom.Event, MutationObserver: Observer, navigator: {}}; return {document, window, observers, frames, run(name) { vm.runInNewContext(source(name), context); document.dispatchEvent(new dom.Event('DOMContentLoaded')); }, emit(name) { for (const fn of events.get(name) || []) fn(); }, @@ -115,6 +116,80 @@ test('Trustline alternatives preserve native links/copy controls and do not dupl controls.forEach((node, index) => {assert.ok(node.isConnected); assert.equal(node.getAttribute('href'), links[index]);}); }); +const trustlineContext = ''; +function trustlineFixture(note = trustlineContext) { + return fixture('

    rNqGa93B8ewQP9mUwpwqA19SApbf62U7PY

    43616C6F72696500000000000000000000000000

    ' + note + '
    ', {page:1205,route:'trustline'}); +} + +test('The live legacy Trustline note is reused once through language changes and restored on cleanup', () => { + const h = trustlineFixture(), note = h.document.querySelector('aside'), link = note.querySelector('a'); + const original = note.outerHTML; let clicks = 0; link.addEventListener('click', () => clicks++); + h.run('menu-pages.js'); + assert.equal(h.document.querySelectorAll('#calorieapp-trustline-context').length, 1); + assert.equal(h.document.querySelectorAll('aside.calorieapp-context-note').length, 1); + for (const tag of Object.keys(menu.trustline.translations)) { + h.window.CalorieAppTrustline.setLocale(tag); + assert.equal(note.querySelector('p').textContent, menu.trustline.translations[tag].context); + assert.equal(note.querySelector('a'), link); + } + link.click(); assert.equal(clicks, 1); + note.parentElement.querySelector('p').textContent = 'Custom issuer text'; + h.window.CalorieAppTrustline.refresh(); + assert.equal(note.outerHTML, original, 'Retire only managed changes, preserving the original CMS node'); +}); + +test('Custom Trustline notes and edits stay intact without duplicate identifiers', () => { + const h = trustlineFixture(trustlineContext.replace('Before you continue', 'Custom instruction')); + const custom = h.document.querySelector('aside'), original = custom.outerHTML; + h.run('menu-pages.js'); + h.window.CalorieAppTrustline.setLocale('nl'); + assert.equal(custom.outerHTML, original); + const ids = [...h.document.querySelectorAll('[id]')].map(node => node.id); + assert.equal(new Set(ids).size, ids.length); + const h2 = trustlineFixture(), originalNote = h2.document.querySelector('aside'); + h2.run('menu-pages.js'); h2.window.CalorieAppTrustline.setLocale('nl'); + originalNote.querySelector('p').textContent = 'New custom CMS wording'; + h2.window.CalorieAppTrustline.refresh(); + assert.equal(originalNote.querySelector('p').textContent, 'New custom CMS wording'); + const h3=trustlineFixture(), note3=h3.document.querySelector('aside'); + h3.run('menu-pages.js'); + note3.querySelector('p').innerHTML='Custom editorial emphasis'; + h3.window.CalorieAppTrustline.setLocale('nl'); + assert.equal(note3.querySelector('p').innerHTML,'Custom editorial emphasis'); +}); + +function bottomFixture() { + const h = fixture('', {page:1090,home:true}); + h.window.CalorieAppPageNavigation = {}; + let next = 0; const timers = new Map(), scrolls = []; + h.window.setTimeout = (fn, delay) => {const id=++next;timers.set(id,{fn,delay});return id;}; + h.window.clearTimeout = id => timers.delete(id); + h.window.scrollTo = options => scrolls.push(options); + h.window.matchMedia = () => ({matches:false}); + h.document.documentElement.scrollHeight = 2000; h.document.body.scrollHeight = 2000; + h.run('navigation.js'); + return {...h,timers,scrolls,click:()=>h.document.querySelector('a').click(), + grow(value){h.document.documentElement.scrollHeight=value;h.document.body.scrollHeight=value;}, + advance(delay){for(const[id,t]of [...timers])if(t.delay<=delay){timers.delete(id);t.fn();}}, + }; +} + +test('Explicit bottom navigation follows late page growth for a bounded time only', () => { + const h=bottomFixture();h.click();h.advance(300);assert.equal(h.scrolls.length,0); + h.grow(2600);h.advance(800);assert.deepEqual(h.scrolls.map(s=>s.top),[2600]); + h.advance(2000);assert.equal(h.timers.size,0); + h.grow(3200);h.emit('pageshow');assert.equal(h.scrolls.length,1); +}); + +test('Manual interaction or leaving the page cancels late bottom adjustments', () => { + for(const event of ['wheel','touchstart','pointerdown','keydown','pagehide']) { + const h=bottomFixture();h.click();h.grow(2600);h.emit(event);h.advance(2000); + assert.equal(h.scrolls.length,0,event);assert.equal(h.timers.size,0,event); + } + const h=bottomFixture();h.click();h.click();h.grow(2600);h.advance(2000); + assert.equal(h.scrolls.length,1,'Repeated clicks leave one active request'); +}); + test('The full CAL guide/discovery/translation sequence retains exact routes and all eleven languages', () => { const ts = require('typescript'); const parsed = ts.createSourceFile('menu-pages.js', source('menu-pages.js'), ts.ScriptTarget.Latest, true); diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index f9c0a749..35a685c3 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,20 @@ -# CalorieToken Site Style 1.4.9 +# CalorieToken Site Style 1.4.10 Eén complete vervangende ZIP, 10 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.9.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.9** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.10.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.10** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## Extra regressie-audit 1.4.9 +## Gerichte correcties na livecontrole van 1.4.9 + +- Op brede desktops vanaf 1200 px staan de zwevende snelknoppen aan de rechterrand, zodat ze de CAL-gids en Trustline-uitleg niet bedekken. De bestaande knopvormen, kleuren, maten en de tablet-/mobiele layout blijven behouden. +- De exacte bestaande Trustline-toelichting wordt hergebruikt en vertaald; zij verschijnt niet meer dubbel. Eigen CMS-tekst, links en handlers blijven behouden, ook wanneer het beheerde onderdeel wordt verwijderd. +- Na een expliciete klik op ‘naar beneden’ wordt maximaal 1,8 seconde rekening gehouden met laat geladen pagina-inhoud. Handmatig scrollen, aanraken, toetsenbordgebruik of verlaten van de pagina stopt die aanvulling direct. De oorspronkelijke bediening blijft actief. + +Home, Whitepaper, Cafés, CAL & Crypto, Trustline, Tokenomics, Roadmap en de Richlist-header zijn live met 1.4.9 bekeken in Chrome op 1363×936. De correcties hierboven zijn vervolgens met gereproduceerde regressies getest. De cloudbrowser bood geen andere viewport/engine en verloor later de verbinding; er wordt geen mobiele, Safari-/Firefox- of na-installatiecontrole van 1.4.10 geclaimd. De appuitrol blijft afzonderlijk. Er is geen handmatige debugronde van tussenversies nodig. + +## Inbegrepen: extra regressie-audit 1.4.9 - De bestaande gecombineerde CalorieApp-/taalbediening wordt teruggeplaatst als de native accountwidget zijn footer vernieuwt. De inlogknop en zijn handlers blijven dezelfde nodes. - Een nieuwe bekende CMS- of donatiemelding wordt opnieuw vertaald, ook als de vorige tekst al vertaald was. Terug naar Engels herstelt de nieuwe tekst; onbekende eigen wijzigingen blijven behouden. diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.css b/wordpress-plugins/calorietoken-site-style/assets/discovery.css index a9334db4..5e3b3856 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.css +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.css @@ -214,6 +214,11 @@ body.ctstyle-enabled .ctstyle-testnet-help summary {cursor:pointer;min-height:44 body.ctstyle-enabled .ctstyle-faq-item {padding:8px 0;border-top:1px solid #d4d8e7;} :is(body.ctstyle-enabled,body.ctstyle-footer-only) :is(.ctstyle-help-widget,.ctstyle-faq-hub) :is(input,button,a,summary):focus-visible {outline:3px solid #008d36;outline-offset:3px;} /* S27: visible app/language controls inside the existing Xaman card. */ +/* Live desktop review: keep shortcuts out of the central reading column. + Existing tablet/mobile geometry and the buttons themselves stay unchanged. */ +@media(min-width:1200px){ + :is(body.ctstyle-enabled,body.ctstyle-footer-only) .calorieapp-page-tools {right:max(16px,env(safe-area-inset-right));} +} /* A single compact mobile row leaves forms and footer links usable. */ @media(max-width:767px){ :is(body.ctstyle-enabled,body.ctstyle-footer-only) {padding-bottom:calc(64px + env(safe-area-inset-bottom))!important;} diff --git a/wordpress-plugins/calorietoken-site-style/assets/menu-pages.js b/wordpress-plugins/calorietoken-site-style/assets/menu-pages.js index 4b834a1a..510ed196 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/menu-pages.js +++ b/wordpress-plugins/calorietoken-site-style/assets/menu-pages.js @@ -240,7 +240,11 @@ && candidate.rows.every(function (row, index) { return live.paragraphs[index] === row.paragraph && row.group.parentElement === row.paragraph.parentElement; - }) && candidate.note.parentElement === candidate.root; + }) && candidate.note.parentElement === candidate.root + && (!candidate.noteOriginal || candidate.noteOriginal.parts.every(function (part, index) { + return part.node.parentElement === candidate.note && part.node.childElementCount === 0 + && part.node.textContent === candidate.notePainted[index]; + })); } function copyForLocale() { var config = window.CalorieTokenSiteStyleMenu; @@ -266,6 +270,8 @@ text(view.note.querySelector("strong"), copy.before); text(view.note.querySelector("p"), copy.context); text(view.note.querySelector("a"), copy.toolkit); + view.notePainted = [copy.before, copy.context, copy.toolkit]; + view.noteLocale = { lang: selected.tag, dir: selected.direction }; if (view.link) { localize(view.link, selected); text(view.link, copy.signAction); @@ -278,7 +284,23 @@ row.group.remove(); if (row.addedClass) row.paragraph.classList.remove("calorieapp-copy-value"); }); - view.note.remove(); + if (view.noteOriginal) { + // The public CMS already contains this exact note. Restore only values + // still owned by this controller; never overwrite a later custom edit. + view.noteOriginal.parts.forEach(function (part, index) { + if (part.node.parentElement === view.note && part.node.childElementCount === 0 + && part.node.textContent === view.notePainted[index]) { + text(part.node, part.text); + } + }); + ["lang", "dir"].forEach(function (name) { + if (view.note.getAttribute(name) !== view.noteLocale[name]) return; + var original = view.noteOriginal[name]; + if (original === null) view.note.removeAttribute(name); + else view.note.setAttribute(name, original); + }); + view.note.removeAttribute("data-calorieapp-trustline-ui"); + } else view.note.remove(); if (view.link) view.link.remove(); if (view.addedClass) view.root.classList.remove("calorieapp-trustline-details"); view = null; @@ -323,9 +345,29 @@ } catch (_) { finish("manual"); } }); }); - candidate.note = contextNote("calorieapp-trustline-context", english.before, english.context, english.toolkit, "https://www.xrptoolkit.com/"); + var existingNotes = Array.from(live.root.children).filter(function (node) { + if (!node.matches('aside.calorieapp-context-note#calorieapp-trustline-context') + || node.matches(protectedSelector) || node.hasAttribute("data-calorieapp-trustline-ui")) return false; + var parts = Array.from(node.children); + return parts.length === 3 && parts.every(function (part) { return part.childElementCount === 0; }) + && parts[0].tagName === "STRONG" && plain(parts[0].textContent) === english.before + && parts[1].tagName === "P" && plain(parts[1].textContent) === english.context + && parts[2].tagName === "A" && plain(parts[2].textContent) === english.toolkit + && parts[2].getAttribute("href") === "https://www.xrptoolkit.com/"; + }); + if (existingNotes.length === 1) { + candidate.note = existingNotes[0]; + candidate.noteOriginal = { + lang: candidate.note.getAttribute("lang"), dir: candidate.note.getAttribute("dir"), + parts: Array.from(candidate.note.children).map(function (node) { return {node: node, text: node.textContent}; }) + }; + } else { + var noteId = "calorieapp-trustline-context", sequence = 2; + while (document.getElementById(noteId)) noteId = "calorieapp-trustline-context-" + sequence++; + candidate.note = contextNote(noteId, english.before, english.context, english.toolkit, "https://www.xrptoolkit.com/"); + live.root.appendChild(candidate.note); + } candidate.note.setAttribute("data-calorieapp-trustline-ui", ""); - live.root.appendChild(candidate.note); view = candidate; } function refresh() { diff --git a/wordpress-plugins/calorietoken-site-style/assets/navigation.js b/wordpress-plugins/calorietoken-site-style/assets/navigation.js index d65cab5f..817eb854 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/navigation.js +++ b/wordpress-plugins/calorietoken-site-style/assets/navigation.js @@ -14,6 +14,50 @@ } if (!allowed()) return; + // Brizy can reveal lazy images after the original bottom target was measured. + // Keep the existing click handler; follow only this explicit request, briefly, + // and immediately stop when the visitor takes control or leaves the page. + if (!document.documentElement.hasAttribute('data-ctstyle-bottom-follow')) { + document.documentElement.setAttribute('data-ctstyle-bottom-follow', '1'); + var stopBottomFollow = null; + document.addEventListener('click', function (event) { + var link = event.target && event.target.closest && event.target.closest('a.calorieapp-page-tool[data-calorieapp-scroll="bottom"]'); + if (!link || link.getAttribute('href') !== '#' || !allowed() + || !['https://calorietoken.net', 'https://www.calorietoken.net'].includes(window.location.origin) + || Array.from(new URL(window.location.href).searchParams.keys()).some(function (key) { return /^xl-/.test(key); }) + || link.closest('form,[contenteditable],[data-calorieapp-embed]')) return; + var stacks = document.querySelectorAll('[data-calorieapp-fallback-shortcuts]'); + if (stacks.length !== 1 || !stacks[0].contains(link)) return; + if (stopBottomFollow) stopBottomFollow(); + var active = true, height = pageHeight(), timers = []; + var cancelEvents = ['wheel', 'touchstart', 'pointerdown', 'keydown', 'pagehide']; + function stop() { + if (!active) return; + active = false; + timers.forEach(function (timer) { window.clearTimeout(timer); }); + cancelEvents.forEach(function (name) { window.removeEventListener(name, stop, true); }); + document.removeEventListener('visibilitychange', visibility); + if (stopBottomFollow === stop) stopBottomFollow = null; + } + function visibility() { if (document.visibilityState === 'hidden') stop(); } + stopBottomFollow = stop; + cancelEvents.forEach(function (name) { window.addEventListener(name, stop, {capture: true, passive: true}); }); + document.addEventListener('visibilitychange', visibility); + [300, 800, 1600].forEach(function (delay) { + timers.push(window.setTimeout(function () { + if (!active || !allowed() || !link.isConnected) { stop(); return; } + var nextHeight = pageHeight(); + if (nextHeight > height + 2) window.scrollTo({ + top: nextHeight, + behavior: window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' + }); + height = nextHeight; + }, delay)); + }); + timers.push(window.setTimeout(stop, 1800)); + }, true); + } + function extendNativeNavigation() { if (!allowed() || !window.CalorieAppPageNavigation) return; var stacks=document.querySelectorAll('[data-calorieapp-fallback-shortcuts]'); diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index 45000403..0d61ef1a 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.9 + * Version: 1.4.10 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.9'; + const VERSION = '1.4.10'; private static function json_asset($name) { // Request-local cache only: plugin updates never need a persistent cache purge. From 4c34430e507dd6d074a9ad8b64d1d46957f22095 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:35:24 +0200 Subject: [PATCH 016/368] Fix Blog X observer feedback and panel lifecycle in Site Style 1.4.11 --- docs/public/step123-review-2026-09-10.md | 35 ++++- tools/tests/wordpress_site_style_dom.test.mjs | 142 +++++++++++++++++- .../calorietoken-site-style/LEESMIJ.md | 18 ++- .../assets/blog-timeline.js | 54 +++++-- .../assets/menu-pages.js | 11 +- .../calorietoken-site-style.php | 4 +- 6 files changed, 235 insertions(+), 29 deletions(-) diff --git a/docs/public/step123-review-2026-09-10.md b/docs/public/step123-review-2026-09-10.md index 59f84bd8..134672fd 100644 --- a/docs/public/step123-review-2026-09-10.md +++ b/docs/public/step123-review-2026-09-10.md @@ -2,7 +2,40 @@ 10 September 2026. This is a source/evidence review, not a live deployment or production-readiness certificate. It preserves the owner's acceptance of Step 1 (foundation) and Step 2 (website/app identity integration). -## Latest: installed 1.4.9 review and bounded 1.4.10 corrections +## Latest: owner-installed 1.4.10 and Blog/X lifecycle audit 1.4.11 + +The owner reports installing 1.4.10. The cloud browser again timed out before +fresh page inspection, including after one bounded fresh-tab recovery. No new +installed-version, screenshot, mobile or cross-browser pass is claimed. The +eight-page live evidence below remains dated 1.4.9 evidence. + +Eight additional DOM scenarios exercise the Blog/X integration. Six reproduced +failures receive narrow corrections: a repeating observer feedback loop between +X readiness notifications and helper visibility; the missing clean `/blog/` route; +the old load state and observer surviving panel replacement; late results +disabling a newer observer; an existing CMS frame's state being watched on the +wrong panel; and a replacement anchor becoming undiscoverable after consent was +withdrawn and granted again with the shared SDK already loaded. + +Readiness notifications now emit only on a changed state, and the helper does +not rewrite unchanged visibility. A real DOM MutationObserver test detects and +bounds the previously repeating queue. This does not establish the cause of the +cloud browser connection failure. + +Each panel now owns its load generation and observer. Retiring a panel cancels +its local loading timer and removes only controller-owned output; CMS/CMP frames +remain intact. A render rejection ends the loading state while retaining cleanup +for late provider output. Existing profile links, eleven-language copy and +explicit CMP consent remain authoritative. No additional SDK, forced consent, +automatic retry loop, CSS/layout, login, frontend/backend or schema change is +introduced. Provider availability is not guaranteed by this repair. + +The final candidate is Site Style 1.4.11. The original 27 requirements remain in +the owner audit, including separate app rollout, real Xaman/device acceptance, +unreviewed live pages, historical translations and legacy LiveCoinWatch cleanup. +No merge, deployment, wallet or ledger operation accompanies this source audit. + +## Earlier: installed 1.4.9 review and bounded 1.4.10 corrections The owner installed 1.4.9 for this review. Chrome at 1363×936 confirmed the round Home/Whitepaper/usecase headers, readable anonymous account/app controls, shared diff --git a/tools/tests/wordpress_site_style_dom.test.mjs b/tools/tests/wordpress_site_style_dom.test.mjs index 809b83dd..abd0b887 100644 --- a/tools/tests/wordpress_site_style_dom.test.mjs +++ b/tools/tests/wordpress_site_style_dom.test.mjs @@ -12,13 +12,21 @@ const assets = new URL('../../wordpress-plugins/calorietoken-site-style/assets/' const source = name => readFileSync(new URL(name, assets), 'utf8'); const copy = JSON.parse(source('discovery-data.json')); const menu = JSON.parse(source('menu-data.json')); -function fixture(html, {page = 1210, route = 'whitepaper', home = false} = {}) { +function fixture(html, {page = 1210, route = 'whitepaper', home = false, nativeObservers = false} = {}) { const {document, window: dom} = parseHTML(`${html}`); - const observers = [], frames = [], events = new Map(); + const observers = [], frames = [], events = new Map(), observerOverflow = []; class Observer { - constructor(callback) { this.callback = callback; observers.push(this); } - observe(target, options) { this.target = target; this.options = options; this.active = true; } - disconnect() { this.active = false; } + constructor(callback) { + this.callback = callback; observers.push(this); this.deliveries = 0; + if (nativeObservers) this.native = new dom.MutationObserver(records => { + // Bound a broken feedback loop so the regression fails without hanging + // the test runner. Production must settle well before this limit. + if (++this.deliveries > 30) {observerOverflow.push(this);this.disconnect();return;} + callback(records); + }); + } + observe(target, options) { this.target = target; this.options = options; this.active = true; this.native?.observe(target,options); } + disconnect() { this.active = false; this.native?.disconnect(); } } // linkedom intentionally omits layout and writable select.value behavior. // Supply only the native control property; no CSS/layout results are invented. @@ -39,7 +47,7 @@ function fixture(html, {page = 1210, route = 'whitepaper', home = false} = {}) { dispatchEvent(event) { for (const fn of events.get(event.type) || []) fn(event); }, }; const context = {window, document, URL, Event: dom.Event, MutationObserver: Observer, navigator: {}}; - return {document, window, observers, frames, + return {document, window, observers, frames, observerOverflow, run(name) { vm.runInNewContext(source(name), context); document.dispatchEvent(new dom.Event('DOMContentLoaded')); }, emit(name) { for (const fn of events.get(name) || []) fn(); }, mutate(record) { for (const o of [...observers]) if (o.active && o.options.childList) o.callback([{type: 'childList', addedNodes: [], removedNodes: [], ...record}]); }, @@ -50,6 +58,128 @@ const account = '
    Public account const footer = ''; const native = `
    ${account}
    `; +const blogPanelMarkup = ''; +function blogFixture({permitted = true, cmsFrame = false, nativeObservers = false} = {}) { + const h = fixture(blogPanelMarkup, {page:1207,route:'blog',nativeObservers}); + h.window.CalorieTokenSiteStyleMenu = structuredClone(menu); + const calls = [], pending = [], timers = new Map(); let sequence = 0; + h.window.cmplz_has_service_consent = () => permitted; + h.window.setTimeout = (fn, delay) => {const id=++sequence;timers.set(id,{fn,delay});return id;}; + h.window.clearTimeout = id => timers.delete(id); + h.window.twttr = {widgets:{load(panel) { + calls.push(panel); return new Promise((resolve,reject) => pending.push({resolve,reject})); + }}}; + function frame(panel) { + const node = h.document.createElement('iframe'); + node.src = 'https://syndication.twitter.com/srv/timeline-profile/screen-name/CalorieToken'; + // These dimensions only exercise provider-state logic, never visual layout. + node.getBoundingClientRect = () => ({width:600,height:520}); + panel.append(node); return node; + } + const first = h.document.querySelector('.brz-wp-shortcode'); + if (cmsFrame) {first.replaceChildren();frame(first);} + return {...h,calls,pending,timers,first,frame, + consent(value) {permitted=value;h.document.dispatchEvent(new h.window.Event(value?'cmplz_status_change':'cmplz_revoke'));}, + inspect(panel) {for(const o of h.observers)if(o.active&&o.target===panel)o.callback([]);}, + replace() { + const holder=h.document.createElement('div');holder.innerHTML=blogPanelMarkup; + const next=holder.firstElementChild;h.document.querySelector('.brz-wp-shortcode').replaceWith(next);return next; + }, + }; +} + +test('Blog fallback and cookie controls work on both public URL forms in every locale', () => { + for (const route of ['/index.php/blog/','/blog/']) { + const h=blogFixture({permitted:false});h.window.location=new URL('https://calorietoken.net'+route); + h.run('menu-pages.js'); + assert.equal(h.document.querySelectorAll('.calorieapp-x-help').length,1,route); + for (const tag of Object.keys(menu.blog.copy)) { + h.window.CalorieAppBlogX.setLocale(tag); + assert.equal(h.document.querySelector('.calorieapp-x-fallback').textContent,menu.blog.copy[tag].action); + assert.equal(h.document.querySelector('.calorieapp-x-help').lang,tag); + } + } +}); + +test('X consent controls one render and removes managed output even when it arrives late', async () => { + const h=blogFixture({permitted:false});h.run('blog-timeline.js');assert.equal(h.calls.length,0); + h.consent(true);h.window.CalorieTokenBlogTimeline.refresh();assert.equal(h.calls.length,1); + const rendered=h.frame(h.first);h.inspect(h.first);assert.equal(h.first.classList.contains('ctstyle-x-ready'),true); + h.consent(false);assert.equal(rendered.isConnected,false);assert.equal(h.first.classList.contains('ctstyle-x-ready'),false); + const late=h.frame(h.first);h.inspect(h.first);assert.equal(late.isConnected,false); + h.pending[0].resolve();await Promise.resolve();assert.equal(h.calls.length,1); + h.consent(true);assert.equal(h.calls.length,2); +}); + +test('A replaced Blog panel receives one fresh timeline and retires the old observer', () => { + const h=blogFixture();h.run('blog-timeline.js');h.frame(h.first);h.inspect(h.first); + const replacement=h.replace();h.window.CalorieTokenBlogTimeline.refresh(); + assert.equal(h.calls.length,2);assert.equal(h.calls[0],h.first);assert.equal(h.calls[1],replacement); + assert.equal(h.observers.filter(o=>o.active&&o.target===h.first).length,0); + assert.equal(h.observers.filter(o=>o.active&&o.target===replacement).length,1); + h.window.CalorieTokenBlogTimeline.refresh();assert.equal(h.calls.length,2); +}); + +test('Old X completions cannot stop a newer panel or its consent cleanup', async () => { + const h=blogFixture();h.run('blog-timeline.js'); + const replacement=h.replace();h.window.CalorieTokenBlogTimeline.refresh(); + h.pending[0].reject(new Error('Old provider request failed'));await Promise.resolve(); + assert.equal(h.observers.filter(o=>o.active&&o.target===replacement).length,1); + const rendered=h.frame(replacement);h.inspect(replacement); + assert.equal(replacement.classList.contains('ctstyle-x-ready'),true); + h.consent(false);assert.equal(rendered.isConnected,false); +}); + +test('CMS-owned X frames survive panel changes while readiness follows the new panel', () => { + const h=blogFixture({cmsFrame:true}), original=h.first.querySelector('iframe');h.run('blog-timeline.js'); + const replacement=h.replace();replacement.replaceChildren();const current=h.frame(replacement); + h.window.CalorieTokenBlogTimeline.refresh(); + assert.equal(h.calls.length,0);assert.equal(original.parentElement,h.first); + assert.equal(h.observers.filter(o=>o.active&&o.target===replacement).length,1); + current.hidden=true;h.inspect(replacement);assert.equal(replacement.classList.contains('ctstyle-x-ready'),false); + h.consent(false);assert.equal(current.isConnected,true,'The CMP retains ownership of pre-existing frames'); +}); + +test('A replaced X anchor can be enabled again after the shared SDK loaded once', () => { + const h=blogFixture(), provider=h.window.twttr;delete h.window.twttr; + h.run('blog-timeline.js'); + const scripts=h.document.querySelectorAll('script[data-ctstyle-x-script]');assert.equal(scripts.length,1); + h.window.twttr=provider;scripts[0].dispatchEvent(new h.window.Event('load'));assert.equal(h.calls.length,1); + const replacement=h.replace();h.window.CalorieTokenBlogTimeline.refresh();assert.equal(h.calls.length,2); + h.consent(false);h.consent(true);assert.equal(h.calls.length,3); + assert.equal(h.calls[2],replacement);assert.equal(h.document.querySelectorAll('script[data-ctstyle-x-script]').length,1); +}); + +test('Rejected X rendering retains consent cleanup and invalid panels retire active work', async () => { + const h=blogFixture();h.run('blog-timeline.js'); + h.pending[0].reject(new Error('Provider render failed'));await Promise.resolve(); + assert.equal(h.first.classList.contains('ctstyle-x-loading'),false); + assert.equal(h.observers.filter(o=>o.active&&o.target===h.first).length,1); + h.consent(false);const late=h.frame(h.first);h.inspect(h.first);assert.equal(late.isConnected,false); + h.consent(true);h.first.remove();h.window.CalorieTokenBlogTimeline.refresh(); + assert.equal(h.observers.filter(o=>o.active).length,0); + assert.equal([...h.timers.values()].filter(t=>t.delay===12000).length,0); + h.pending[1].resolve();await Promise.resolve();assert.equal(h.calls.length,2); +}); + +test('X readiness and translated cookie controls settle without a DOM observer feedback loop', async () => { + const h=blogFixture({cmsFrame:true,nativeObservers:true}); + try { + h.run('menu-pages.js');h.run('blog-timeline.js'); + await new Promise(resolve=>setTimeout(resolve,0)); + assert.equal(h.observerOverflow.length,0,'Visible X state must not repeatedly rewrite the same hidden attributes'); + assert.equal(h.first.classList.contains('ctstyle-x-ready'),true); + assert.equal(h.first.querySelector('.calorieapp-x-description').hidden,true); + const deliveries=h.observers.reduce((sum,o)=>sum+o.deliveries,0); + await new Promise(resolve=>setTimeout(resolve,0)); + assert.equal(h.observers.reduce((sum,o)=>sum+o.deliveries,0),deliveries,'The real DOM mutation queue settles'); + h.consent(false);await new Promise(resolve=>setTimeout(resolve,0)); + assert.equal(h.first.classList.contains('ctstyle-x-ready'),false); + assert.equal(h.first.querySelector('.calorieapp-x-description').hidden,false); + assert.equal(h.observerOverflow.length,0); + } finally {h.observers.forEach(o=>o.disconnect());} +}); + test('Native headers keep menu/login nodes and handlers through eleven language changes', () => { for (const home of [false, true]) { const h = fixture(native + footer, {home, page: home ? 1090 : 1210}); diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index 35a685c3..7d917037 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,24 @@ -# CalorieToken Site Style 1.4.10 +# CalorieToken Site Style 1.4.11 Eén complete vervangende ZIP, 10 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.10.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.10** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.11.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.11** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## Gerichte correcties na livecontrole van 1.4.9 +## Gerichte Blog/X-controle 1.4.11 + +- Een aangetoonde herhalingslus tussen de X-laadstatus en de verborgen cookieknoppen is verwijderd. Statusberichten worden alleen bij een echte verandering verstuurd; ongewijzigde zichtbaarheid wordt niet opnieuw geschreven. +- De bestaande X-hulp en cookiebediening verschijnen op zowel `/index.php/blog/` als `/blog/`, met dezelfde elf talen. +- Een vervangen blogblok krijgt een eigen laadcyclus en controle; de oude controle en timer worden opgeruimd. Eerdere late antwoorden kunnen de nieuwe cyclus niet stilzetten. +- Na een laadfout blijft het intrekken van toestemming ook werken voor later aangeleverde inhoud. Reeds aanwezige CMS-/Complianz-iframes blijven onder hun bestaande beheer. +- Ook een vervangende X-link kan na het intrekken en opnieuw geven van toestemming weer worden gevonden, wanneer de gedeelde X-library al eerder is geladen. Er wordt geen tweede library toegevoegd. + +Acht nieuwe DOM-scenario's controleren deze samenwerking, waaronder zes gereproduceerde regressies. De lustest gebruikt echte DOM-mutatiewaarneming met een begrensde foutdetectie, zodat een defect de testrunner niet kan laten vastlopen. De reparaties wijzigen geen CSS, websitevormgeving, loginprotocol of appcode. Ze omzeilen geen cookietoestemming en garanderen niet dat X zelf altijd berichten levert. + +De eigenaar meldt dat 1.4.10 is geïnstalleerd. Deze nieuwe ronde kon geen verse livebeelden maken: de cloudbrowser gaf opnieuw verbindingsfouten, ook na een begrensde herstelpoging. Daarom is installatie van 1.4.10 niet onafhankelijk bevestigd en is er geen mobiele of crossbrowser-goedkeuring. De hieronder bewaarde livebeelden betreffen 1.4.9. De afzonderlijke appuitrol en resterende livecontroles blijven open. + +## Inbegrepen: correcties 1.4.10 na livecontrole van 1.4.9 - Op brede desktops vanaf 1200 px staan de zwevende snelknoppen aan de rechterrand, zodat ze de CAL-gids en Trustline-uitleg niet bedekken. De bestaande knopvormen, kleuren, maten en de tablet-/mobiele layout blijven behouden. - De exacte bestaande Trustline-toelichting wordt hergebruikt en vertaald; zij verschijnt niet meer dubbel. Eigen CMS-tekst, links en handlers blijven behouden, ook wanneer het beheerde onderdeel wordt verwijderd. diff --git a/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js b/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js index e572c8ab..91696597 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js +++ b/wordpress-plugins/calorietoken-site-style/assets/blog-timeline.js @@ -4,6 +4,7 @@ if (window.CalorieTokenBlogTimeline) return; var source = 'https://platform.twitter.com/widgets.js', panel = null, anchor = null; var attempted = false, managed = false, scriptRequested = false, watcher = null, timer = null; + var generation = 0; var originalFrames = new Set(), ownedFrames = new Set(), anchorStyle = null; var protectedRoot = 'form,[contenteditable],.xl-card,[data-calorieapp-account],[data-calorieapp-embed],.cmplz-blocked-content-container,[hidden],[inert]'; function allowed() { @@ -32,11 +33,14 @@ url.protocol === 'https:' && url.hostname === 'platform.twitter.com' && /^\/embed\//.test(url.pathname); } catch (_) { return false; } } - function stopWatching() { - if (watcher) { watcher.disconnect(); watcher = null; } + function finishLoading() { if (timer) { window.clearTimeout(timer); timer = null; } if (panel) panel.classList.remove('ctstyle-x-loading'); } + function stopWatching() { + if (watcher) { watcher.disconnect(); watcher = null; } + finishLoading(); + } function collectFrames() { if (!panel || !panel.isConnected) return; panel.querySelectorAll('iframe').forEach(function (frame) { @@ -51,6 +55,9 @@ return !frame.hidden && !frame.closest('.cmplz-blocked-content-container,[hidden],[inert]') && style.display!=='none' && style.visibility!=='hidden' && box.width>0 && box.height>0; }); + // The helper updates hidden attributes inside this observed panel. Emit + // only a changed state so those updates cannot feed back into themselves. + if (panel.classList.contains('ctstyle-x-ready') === ready) return; panel.classList.toggle('ctstyle-x-ready',ready); document.dispatchEvent(new window.Event('calorietoken:x-state')); } @@ -59,6 +66,7 @@ } function revoke() { if (!managed) return; + generation++; collectFrames(); ownedFrames.forEach(function (frame) { frame.remove(); }); ownedFrames.clear(); if (anchor && anchor.isConnected && anchor.style.display === 'none') { @@ -66,36 +74,58 @@ } if (anchor && scriptRequested) anchor.classList.remove('twitter-timeline'); attempted = false; - if (panel) panel.classList.remove('ctstyle-x-loading'); + finishLoading(); // Watch only this small panel, including late results of an already-started request. } + function usePanel(next) { + if (next === panel) return; + generation++; + stopWatching(); + // Retire only output created by this controller. Existing CMS/CMP frames + // retain their original owner, including in a detached former panel. + revoke(); + if (panel) panel.classList.remove('ctstyle-x-ready'); + panel = next; anchor = null; attempted = false; managed = false; + originalFrames = new Set(); ownedFrames = new Set(); anchorStyle = null; + } function inspect() { if (managed) collectFrames(); if (!consent()) { revoke(); status(); return; } - if (ownedFrames.size && panel) panel.classList.remove('ctstyle-x-loading'); + if (ownedFrames.size && panel) finishLoading(); status(); } function render() { if (!allowed() || !consent() || !panel || !panel.isConnected || panel.closest(protectedRoot) || attempted || !window.twttr || !window.twttr.widgets || typeof window.twttr.widgets.load !== 'function') return; if (frames().length) {status();return;} // The CMS already rendered this timeline. - attempted = true; managed = true; anchor.classList.add('twitter-timeline'); anchorStyle = anchor.getAttribute('style'); + attempted = true; managed = true; + // A CMS replacement may arrive after our one shared SDK request. Keep its + // anchor discoverable when consent withdrawal removes the provider hook. + anchor.setAttribute('data-ctstyle-x-anchor', ''); + anchor.classList.add('twitter-timeline'); anchorStyle = anchor.getAttribute('style'); originalFrames = new Set(panel.querySelectorAll('iframe')); panel.classList.add('ctstyle-x-loading'); stopWatching(); panel.classList.add('ctstyle-x-loading'); watch(); - timer = window.setTimeout(function () { inspect(); panel.classList.remove('ctstyle-x-loading'); timer = null; }, 12000); + var requestGeneration = ++generation, requestPanel = panel; + function current() { return requestGeneration === generation && requestPanel === panel; } + timer = window.setTimeout(function () { if (current()) { inspect(); finishLoading(); } }, 12000); try { var result = window.twttr.widgets.load(panel); - if (result && typeof result.then === 'function') result.then(inspect, stopWatching); - } catch (_) { stopWatching(); } + if (result && typeof result.then === 'function') result.then( + function () { if (current()) inspect(); }, + function () { if (current()) finishLoading(); } + ); + } catch (_) { if (current()) finishLoading(); } + // A failed render must not disable consent cleanup for late provider output. } function refresh() { - if (!allowed()) return; + if (!allowed()) { usePanel(null); return; } var matches = document.querySelectorAll('[data-brz-custom-id="amfuxnhsfmkknesyuldlbdorcvqsardaetus"].brz-wp-shortcode'); - if (matches.length !== 1 || matches[0].closest(protectedRoot) || - matches[0].querySelector('form,input,textarea,select,[contenteditable],.xl-card,[data-calorieapp-account],[data-calorieapp-embed]')) return; - panel = matches[0]; + var next = matches.length === 1 && !matches[0].closest(protectedRoot) && + !matches[0].querySelector('form,input,textarea,select,[contenteditable],.xl-card,[data-calorieapp-account],[data-calorieapp-embed]') ? matches[0] : null; + usePanel(next); + if (!panel) return; if (frames().length && !managed) {panel.classList.add('ctstyle-x-timeline');watch();status();return;} var anchors = Array.from(panel.querySelectorAll('a.twitter-timeline,a[data-ctstyle-x-anchor]')); if (anchors.length !== 1 || !profile(anchors[0])) return; diff --git a/wordpress-plugins/calorietoken-site-style/assets/menu-pages.js b/wordpress-plugins/calorietoken-site-style/assets/menu-pages.js index 510ed196..7b92bdf1 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/menu-pages.js +++ b/wordpress-plugins/calorietoken-site-style/assets/menu-pages.js @@ -80,7 +80,7 @@ function blogPanel() { if (window.CalorieTokenSiteStyleMenu?.blog?.publicPage !== true - || pageId() !== 1207 || window.location.pathname !== "/index.php/blog/" + || pageId() !== 1207 || !["/index.php/blog/", "/blog/"].includes(window.location.pathname) || ["https://calorietoken.net", "https://www.calorietoken.net"].indexOf(home) === -1 || document.querySelector(".brz-ed,#brz-ed-iframe") || /(?:^|[?&])(?:preview|brizy-edit|brizy-edit-iframe)(?:=|&|$)/.test(window.location.search || "")) return null; @@ -122,12 +122,13 @@ // Complianz's delegated native button opens settings. We do not grant or // revoke consent or reload the page. A separate Blog module initializes the widget only with service consent. var ready = panel.classList.contains('ctstyle-x-ready'); - blogView.description.hidden = ready; - blogView.settings.hidden = typeof window.cmplz_has_service_consent !== "function" - || !document.querySelector("#cmplz-cookiebanner-container .cmplz-cookiebanner"); + function setHidden(node, value) { if (node.hidden !== value) node.hidden = value; } + setHidden(blogView.description, ready); + setHidden(blogView.settings, typeof window.cmplz_has_service_consent !== "function" + || !document.querySelector("#cmplz-cookiebanner-container .cmplz-cookiebanner")); var permitted=false; try { permitted=typeof window.cmplz_has_service_consent === 'function' && window.cmplz_has_service_consent('twitter') === true; } catch (_) { /* Keep the CMP authoritative. */ } - blogView.load.hidden = ready || permitted || blogView.settings.hidden; + setHidden(blogView.load, ready || permitted || blogView.settings.hidden); } function refineBlogHelp() { var panel = blogPanel(); diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index 0d61ef1a..ff9cf13d 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.10 + * Version: 1.4.11 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.10'; + const VERSION = '1.4.11'; private static function json_asset($name) { // Request-local cache only: plugin updates never need a persistent cache purge. From 2dcc74b8f9529f985c7aec5d38e950b05c0bcfe8 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:44:27 +0200 Subject: [PATCH 017/368] Normalize cookie banner opacity and cover numeric zero variants --- docs/public/step123-review-2026-09-10.md | 5 +++++ tools/tests/wordpress_cookie_controls.test.mjs | 16 ++++++++++++++++ .../calorietoken-site-style/LEESMIJ.md | 2 ++ .../calorietoken-site-style/assets/discovery.js | 2 +- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/public/step123-review-2026-09-10.md b/docs/public/step123-review-2026-09-10.md index 134672fd..78564bb6 100644 --- a/docs/public/step123-review-2026-09-10.md +++ b/docs/public/step123-review-2026-09-10.md @@ -35,6 +35,11 @@ the owner audit, including separate app rollout, real Xaman/device acceptance, unreviewed live pages, historical translations and legacy LiveCoinWatch cleanup. No merge, deployment, wallet or ledger operation accompanies this source audit. +An additional automated-review correction compares cookie-banner opacity +numerically, including the containing CMP element. Its regression covers +equivalent zero representations and positive values. This is defensive handling +of numeric opacity, not evidence of a measured failure in a named browser. + ## Earlier: installed 1.4.9 review and bounded 1.4.10 corrections The owner installed 1.4.9 for this review. Chrome at 1363×936 confirmed the round diff --git a/tools/tests/wordpress_cookie_controls.test.mjs b/tools/tests/wordpress_cookie_controls.test.mjs index c696367d..5f7cfbc4 100644 --- a/tools/tests/wordpress_cookie_controls.test.mjs +++ b/tools/tests/wordpress_cookie_controls.test.mjs @@ -158,6 +158,22 @@ test('Hiding the Complianz container restores controls even when its banner has } }); +test('Numerically zero cookie opacity restores controls for the banner and its container', () => { + const h=discoveryFixture(),banner=h.banner(),container=h.banner(); + banner.closest=()=>container;h.banners.push(banner); + for (const target of [banner,container]) { + for (const value of ['0','0.0','0.00','0%','-0']) { + target.style.opacity=value;h.document.emit('cmplz_status_change'); + assert.equal(h.open(),false,value); + } + for (const value of ['0.001','0.1','1']) { + target.style.opacity=value;h.document.emit('cmplz_status_change'); + assert.equal(h.open(),true,value); + } + target.style.opacity='1'; + } +}); + test('An X frame is ready only with consent and visible nonzero dimensions, including browser restoration', () => { let permitted = true, blocked = false; const observers = [], box = {width: 0, height: 0}; diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index 7d917037..16f457fa 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -16,6 +16,8 @@ WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorieto Acht nieuwe DOM-scenario's controleren deze samenwerking, waaronder zes gereproduceerde regressies. De lustest gebruikt echte DOM-mutatiewaarneming met een begrensde foutdetectie, zodat een defect de testrunner niet kan laten vastlopen. De reparaties wijzigen geen CSS, websitevormgeving, loginprotocol of appcode. Ze omzeilen geen cookietoestemming en garanderen niet dat X zelf altijd berichten levert. +De automatische review leidde bovendien tot een numerieke controle van cookie-transparantie. Gelijkwaardige nulnotaties worden als volledig transparant herkend, zowel op de banner als op zijn container; positieve transparantiewaarden houden de bestaande bannercontrole actief. De aanvullende regressie is vóór de correctie gereproduceerd. Dit is defensieve compatibiliteitscontrole, geen gemeten fout in een specifieke mobiele browser. + De eigenaar meldt dat 1.4.10 is geïnstalleerd. Deze nieuwe ronde kon geen verse livebeelden maken: de cloudbrowser gaf opnieuw verbindingsfouten, ook na een begrensde herstelpoging. Daarom is installatie van 1.4.10 niet onafhankelijk bevestigd en is er geen mobiele of crossbrowser-goedkeuring. De hieronder bewaarde livebeelden betreffen 1.4.9. De afzonderlijke appuitrol en resterende livecontroles blijven open. ## Inbegrepen: correcties 1.4.10 na livecontrole van 1.4.9 diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.js b/wordpress-plugins/calorietoken-site-style/assets/discovery.js index 20da5cb7..817d1955 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.js +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.js @@ -184,7 +184,7 @@ function visible(node) { if (node.hidden || node.classList.contains('cmplz-dismissed')) return false; var style = window.getComputedStyle(node); - return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; + return style.display !== 'none' && style.visibility !== 'hidden' && Number.parseFloat(style.opacity) !== 0; } return Array.from(document.querySelectorAll('.cmplz-cookiebanner')).some(function (node) { var container = node.closest('#cmplz-cookiebanner-container'); From 53013d75627c7a40e7c252386dda6c85e92d72e1 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:34:26 +0200 Subject: [PATCH 018/368] Fix confirmed live footer and language controls; retire replaced market loader --- .../components/DisplayLanguageProvider.tsx | 36 +++++-- .../display_language_controller.test.mjs | 97 +++++++++++++++++++ tools/tests/wordpress_site_style_dom.test.mjs | 59 +++++++++++ .../wordpress_site_style_review.test.php | 47 +++++++++ .../calorietoken-site-style/LEESMIJ.md | 16 ++- .../calorietoken-site-style/assets/style.js | 17 +++- .../calorietoken-site-style.php | 34 ++++++- 7 files changed, 294 insertions(+), 12 deletions(-) create mode 100644 tools/tests/display_language_controller.test.mjs diff --git a/frontend/components/DisplayLanguageProvider.tsx b/frontend/components/DisplayLanguageProvider.tsx index f01d8154..00f1a27a 100644 --- a/frontend/components/DisplayLanguageProvider.tsx +++ b/frontend/components/DisplayLanguageProvider.tsx @@ -5,15 +5,17 @@ import { createStore, connectGuest, type DisplayStore } from "@/lib/displayLangu import { localeDirection, resolveLocale, supportedLocales } from "@/lib/locales"; import translations from "@/config/display-language-copy.json"; -type DisplayContext = { enabled: boolean; locale: string; select(locale: string): void }; -const Context = createContext({ enabled: false, locale: "en", select: () => {} }); +type DisplayContext = { enabled: boolean; locale: string; parentManaged: boolean; select(locale: string): void }; +const Context = createContext({ enabled: false, locale: "en", parentManaged: false, select: () => {} }); export function DisplayLanguageProvider({ children }: { children: React.ReactNode }) { const enabled = process.env.NEXT_PUBLIC_CALORIEAPP_DISPLAY_LANGUAGE !== "0"; const store = useRef(null); const [locale, setLocale] = useState("en"); + const [parentManaged, setParentManaged] = useState(false); useEffect(() => { if (!enabled) return; + setParentManaged(false); // This public display choice never changes the login's locale query/state. const initial = new URLSearchParams(window.location.search).get("ui_lang") || new URLSearchParams(window.location.search).get("locale") || @@ -31,21 +33,43 @@ export function DisplayLanguageProvider({ children }: { children: React.ReactNod const unsubscribe = current.subscribe(state => setLocale(state.locale)); const disconnect = window.parent !== window && typeof window.crypto?.randomUUID === "function" ? connectGuest({ - window, parent: window.parent, store: current, channel: window.crypto.randomUUID(), + window, parent: window.parent, channel: window.crypto.randomUUID(), + store: { + ...current, + apply(value, explicit) { + // connectGuest invokes apply only after validating the parent, + // origin and protocol state, even when the locale is unchanged. + setParentManaged(true); + return current.apply(value, explicit); + }, + }, origins: ["https://calorietoken.net", "https://www.calorietoken.net"], }) : () => {}; return () => { disconnect(); unsubscribe(); store.current = null; }; }, [enabled]); - return store.current?.select(value) }}>{children}; + useEffect(() => { + if (!enabled) return; + const root = document.documentElement; + const priorLanguage = root.getAttribute("lang"); + const priorDirection = root.getAttribute("dir"); + root.lang = locale; + root.dir = localeDirection(locale); + return () => { + if (priorLanguage === null) root.removeAttribute("lang"); else root.setAttribute("lang", priorLanguage); + if (priorDirection === null) root.removeAttribute("dir"); else root.setAttribute("dir", priorDirection); + }; + }, [enabled, locale]); + + return store.current?.select(value) }}>{children}; } export function useDisplayLanguage() { return useContext(Context); } export function DisplayLanguagePicker() { - const { enabled, locale, select } = useDisplayLanguage(); - if (!enabled) return null; + const { enabled, locale, parentManaged, select } = useDisplayLanguage(); + if (!enabled || parentManaged) return null; const copy = translations[locale as keyof typeof translations] ?? translations.en; return (
    diff --git a/tools/tests/display_language_controller.test.mjs b/tools/tests/display_language_controller.test.mjs new file mode 100644 index 00000000..a2ce5df4 --- /dev/null +++ b/tools/tests/display_language_controller.test.mjs @@ -0,0 +1,97 @@ +import assert from 'node:assert/strict'; +import {createRequire} from 'node:module'; +import {readFileSync} from 'node:fs'; +import test from 'node:test'; +import vm from 'node:vm'; + +const require = createRequire(new URL('../../frontend/package.json',import.meta.url)); +const React = require('react'); +const {createRoot} = require('react-dom/client'); +const {parseHTML} = require('linkedom'); +const ts = require('typescript'); +const runtime = require('./lib/displayLanguageRuntime.js'); +const registry = JSON.parse(readFileSync(new URL('../../frontend/config/locales.json',import.meta.url))); +const copy = JSON.parse(readFileSync(new URL('../../frontend/config/display-language-copy.json',import.meta.url))); +function load(path, imports, globals = {}) { + const source = readFileSync(new URL('../../frontend/' + path,import.meta.url),'utf8'); + const code = ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS, + target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText; + const module = {exports:{}}; + vm.runInNewContext(code,{...globals,module,exports:module.exports,require(name){ + if(name==='react'||name==='react/jsx-runtime')return require(name); + if(Object.hasOwn(imports,name))return imports[name]; + throw new Error('Unexpected language dependency: '+name); + }}); + return module.exports; +} +const locales = load('lib/locales.ts',{'@/config/locales.json':{default:registry}}); + +async function fixture({embedded = true, enabled = undefined, saved = null} = {}) { + const {window,document} = parseHTML('
    '); + const previous = new Map(['window','document','IS_REACT_ACT_ENVIRONMENT'].map(key=>[key,Object.getOwnPropertyDescriptor(globalThis,key)])); + globalThis.window=window;globalThis.document=document;globalThis.IS_REACT_ACT_ENVIRONMENT=true; + const sent=[]; + const parent={postMessage:(data,origin)=>sent.push({data,origin})}; + window.location=new URL('https://app.calorietoken.net/'); + window.parent=embedded?parent:window; + window.localStorage={getItem:()=>saved,setItem(){},removeItem(){}}; + const module=load('components/DisplayLanguageProvider.tsx',{ + '@/lib/displayLanguageRuntime':runtime,'@/lib/locales':locales, + '@/config/display-language-copy.json':{default:copy}, + },{window,document,URLSearchParams,navigator:{language:'en'},process:{env:{NEXT_PUBLIC_CALORIEAPP_DISPLAY_LANGUAGE:enabled}}}); + const root=createRoot(document.querySelector('#root')); + await React.act(async()=>root.render(React.createElement(module.DisplayLanguageProvider,null, + React.createElement(module.DisplayLanguagePicker),React.createElement('p',null,'Existing app')))); + return {document,sent,parent,window, + async message(data,{origin='https://calorietoken.net',source=parent}={}) { + const event=new window.Event('message');Object.assign(event,{data,origin,source}); + await React.act(async()=>window.dispatchEvent(event)); + }, + async close(){ + await React.act(async()=>root.unmount()); + for(const [key,descriptor]of previous){if(descriptor)Object.defineProperty(globalThis,key,descriptor);else delete globalThis[key];} + }, + }; +} +function state(channel, overrides = {}) { + return {type:'calorieapp:display-language:state',version:1,channel,epoch:'trusted-host-epoch-1234',revision:0,ack:0,locale:'en',explicit:false,...overrides}; +} + +test('The embedded picker remains usable until the trusted parent confirms even an unchanged language',async()=>{ + const h=await fixture(); + try{ + assert.equal(h.document.querySelectorAll('select').length,1); + const channel=h.sent.find(x=>x.data.type==='calorieapp:display-language:ready').data.channel; + await h.message(state(channel),{origin:'https://untrusted.example'}); + await h.message(state(channel),{source:{}}); + await h.message(state('wrong-channel-1234')); + assert.equal(h.document.querySelectorAll('select').length,1,'Invalid messages cannot remove the fallback control'); + await h.message(state(channel)); + assert.equal(h.document.querySelectorAll('select').length,0,'The parent owns the one visible language control'); + await h.message(state(channel,{locale:'ur',explicit:true,revision:1})); + assert.equal(h.document.documentElement.lang,'ur'); + assert.equal(h.document.documentElement.dir,'rtl'); + assert.ok(h.document.body.textContent.includes('Existing app')); + }finally{await h.close();} +}); + +test('The standalone picker keeps eleven choices and follows a saved language without a parent',async()=>{ + const h=await fixture({embedded:false,saved:JSON.stringify({locale:'nl',savedAt:Date.now()})}); + try{ + assert.equal(h.document.querySelectorAll('option').length,11); + assert.equal(h.sent.length,0); + assert.equal(h.document.documentElement.lang,'nl'); + assert.equal(h.document.documentElement.dir,'ltr'); + }finally{await h.close();} +}); + +test('Disabling display language preserves the document and does not open a host channel',async()=>{ + const h=await fixture({enabled:'0'}); + try{ + assert.equal(h.document.querySelectorAll('select').length,0); + assert.equal(h.sent.length,0); + assert.equal(h.document.documentElement.lang,'en'); + assert.equal(h.document.documentElement.hasAttribute('dir'),false); + assert.ok(h.document.body.textContent.includes('Existing app')); + }finally{await h.close();} +}); diff --git a/tools/tests/wordpress_site_style_dom.test.mjs b/tools/tests/wordpress_site_style_dom.test.mjs index abd0b887..32b76182 100644 --- a/tools/tests/wordpress_site_style_dom.test.mjs +++ b/tools/tests/wordpress_site_style_dom.test.mjs @@ -58,6 +58,65 @@ const account = '
    Public account const footer = ''; const native = `
    ${account}
    `; +function sharedFooterFixture({hub = true, custom = false} = {}) { + const base = 'https://calorietoken.net/index.php/'; + const urls = ['https://t.me/+7YxaKdQYWNA0NDA0','https://github.com/CalorieToken','https://x.com/CalorieToken', + 'https://www.facebook.com/CalorieToken-100422882407878','https://www.youtube.com/channel/UCV_87rxST-cQOVu4W8nFZkA', + 'https://www.linkedin.com/company/calorie-token/','https://www.instagram.com/calorietoken/']; + const social = urls.map((url,i)=>`Channel ${i}`).join(''); + const privacy = `Privacy Policy`; + const terms = `Terms & Conditions`; + const hubLink = hub ? `Community Voting Hub` : ''; + const shared = `
    `; + const fallback = ``; + const h = fixture(shared + fallback, {page:7880,route:'calorieapp'}); + h.window.CalorieTokenSiteStyle = {}; + return h; +} + +test('The installed bridge footer gains the public hub and cookie controls without replacing native handlers', () => { + const h = sharedFooterFixture(); + const original = h.document.querySelector('footer'); + const privacy = h.document.querySelector('#original-privacy'), carousel = h.document.querySelector('#original-carousel'); + let clicks = 0; carousel.addEventListener('click',()=>clicks++); + h.run('style.js'); h.run('refinements.js'); + assert.equal(h.document.querySelector('footer'),original); + assert.equal(h.document.querySelector('#original-privacy'),privacy); + const links = original.querySelector('.ctstyle-legal-links'); + assert.ok(links, 'The adopted bridge legal row participates in Site Style footer controls'); + assert.equal(links.querySelectorAll('a').length,3); + assert.equal(links.querySelectorAll('a[href$="community-voting-hub-info/"]').length,1); + assert.equal(links.querySelectorAll('button.cmplz-manage-consent').length,1); + h.run('style.js'); + for(const locale of Object.keys(copy))h.window.CalorieTokenRefinements.refresh(locale); + assert.equal(links.querySelectorAll('a').length,3); + assert.equal(links.querySelectorAll('button').length,1); + assert.equal(h.document.querySelectorAll('.calorieapp-shared-social').length,7); + carousel.dispatchEvent(new h.window.Event('click')); + assert.equal(clicks,1,'The bridge retains ownership of the existing carousel'); +}); + +test('An adopted footer does not invent a hub URL when WordPress has no published hub', () => { + const h = sharedFooterFixture({hub:false}); + h.run('style.js'); h.run('refinements.js'); + const footer = h.document.querySelector('footer'); + assert.equal(footer.querySelectorAll('.calorieapp-shared-legal-links a').length,2); + assert.equal(footer.querySelectorAll('button.cmplz-manage-consent').length,1); +}); + +test('An unfamiliar customized bridge footer is retained without adding or hiding controls', () => { + const h = sharedFooterFixture({custom:true}); + const footer = h.document.querySelector('footer'), before = footer.outerHTML; + h.run('style.js'); h.run('refinements.js'); + assert.equal(h.document.querySelector('footer'),footer); + assert.equal(footer.outerHTML,before); +}); + const blogPanelMarkup = ''; function blogFixture({permitted = true, cmsFrame = false, nativeObservers = false} = {}) { const h = fixture(blogPanelMarkup, {page:1207,route:'blog',nativeObservers}); diff --git a/tools/tests/wordpress_site_style_review.test.php b/tools/tests/wordpress_site_style_review.test.php index 6dea648c..84af38b6 100644 --- a/tools/tests/wordpress_site_style_review.test.php +++ b/tools/tests/wordpress_site_style_review.test.php @@ -58,3 +58,50 @@ function post_fixture($id, $status, $content) { $report = Review::snapshot(); check(count($report['pages']) === 500 && $report['truncated'], 'Partial inventory must be explicit'); echo "Site Style review: access, nonce, redaction, preserved data and bounded inventory passed.\n"; + +// The WordPress parser is a core dependency, not reimplemented here. These +// fixtures model its SCRIPT/attribute API and verify our scope and URL policy. +$site_url = 'https://calorietoken.net/'; $market_renderer = true; +function home_url($path = '/') { global $site_url; return $site_url; } +function wp_script_is($handle, $status = 'enqueued') { + global $market_renderer; + check(in_array($handle, array('calorieapp-identity-bridge-layout','calorieapp-identity-bridge-site-polish'), true), 'Unexpected renderer dependency'); + return $market_renderer; +} +class WP_HTML_Tag_Processor { + public static $scripts = array(); + public static $last = null; + public $nodes; private $index = -1; + public function __construct($html) { $this->nodes = self::$scripts; self::$last = $this; } + public function next_tag($tag) { check($tag === 'SCRIPT', 'Only scripts should be visited'); return ++$this->index < count($this->nodes); } + public function get_attribute($name) { return isset($this->nodes[$this->index][$name]) ? $this->nodes[$this->index][$name] : null; } + public function remove_attribute($name) { unset($this->nodes[$this->index][$name]); } + public function set_attribute($name, $value) { $this->nodes[$this->index][$name] = $value; } + public function get_updated_html() { return 'updated-public-markup'; } +} +$html = '
    Public widget
    '; +$targets = array('https://www.livecoinwatch.com/static/lcw-widget.js', 'https://livecoinwatch.com/static/lcw-widget.js?ver=1', '//www.livecoinwatch.com/static/lcw-widget.js'); +$others = array('https://platform.twitter.com/widgets.js', 'https://livecoinwatch.com.example/static/lcw-widget.js', + 'https://www.livecoinwatch.com/static/other.js', 'https://other.example/lcw-widget.js', + 'https://user@www.livecoinwatch.com/static/lcw-widget.js', '/static/lcw-widget.js', null, true); +WP_HTML_Tag_Processor::$scripts = array_map(static function ($src) { return array('src'=>$src, 'type'=>'text/javascript', 'keep'=>'original'); }, array_merge($targets, $others)); +$result = \CalorieToken\SiteStyle\Plugin::retire_market_loader($html); +check($result === 'updated-public-markup', 'Return the core parser output'); +foreach (WP_HTML_Tag_Processor::$last->nodes as $index => $node) { + if ($index < count($targets)) { + check(!isset($node['src']) && $node['type'] === 'application/x-calorietoken-retired', 'The exact retired loader must not fetch or execute inline content'); + check($node['keep'] === 'original', 'Unrelated attributes remain intact'); + } else { check($node === WP_HTML_Tag_Processor::$scripts[$index], 'Unrelated scripts must stay unchanged'); } +} +$market_renderer = false; +check(\CalorieToken\SiteStyle\Plugin::retire_market_loader($html) === $html, 'Retain legacy loading when its replacement is unavailable'); +$market_renderer = true; $preview = true; +check(\CalorieToken\SiteStyle\Plugin::retire_market_loader($html) === $html, 'Never filter editor previews'); +$preview = false; $_GET['xl-return'] = '1'; +check(\CalorieToken\SiteStyle\Plugin::retire_market_loader($html) === $html, 'Never filter Xaman return/action responses'); +unset($_GET['xl-return']); $site_url = 'https://unrelated.example/'; +check(\CalorieToken\SiteStyle\Plugin::retire_market_loader($html) === $html, 'Only the supported public site is filtered'); +$site_url = 'https://calorietoken.net/'; $front_page = true; +check(\CalorieToken\SiteStyle\Plugin::retire_market_loader($html) === 'updated-public-markup', 'Home also retires the replaced loader'); +$front_page = false; +echo "Market loader: exact URLs, inert output, renderer dependency and preview/auth/origin boundaries passed.\n"; diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index 16f457fa..69fb57be 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,10 +1,20 @@ -# CalorieToken Site Style 1.4.11 +# CalorieToken Site Style 1.4.12 -Eén complete vervangende ZIP, 10 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. +Eén complete vervangende ZIP, 11 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.11.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.11** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.12.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.12** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. + +## Correcties op basis van livecontrole van 1.4.11 + +- De bestaande Identity Bridge-footer krijgt de ontbrekende Community Voting Hub-link uit het publieke WordPress-template. De bestaande rij krijgt ook de gedeelde cookieknop en vertalingen. Eigen links, social-carousel en handlers blijven dezelfde nodes; een onbekende aangepaste footer wordt behouden. Zonder gepubliceerde Hub wordt geen bestemming verzonnen. +- De exacte oude LiveCoinWatch-loader wordt in de publieke HTML onwerkzaam gemaakt voordat de browser hem kan laden. Dit gebeurt alleen op calorietoken.net als beide bestaande bridge-assets voor de sitebrede XPMarket-weergave worden geleverd. De CMS-bron en andere scripts blijven behouden; editor-, preview- en Xaman-actieverzoeken worden overgeslagen. Zonder de vervangende renderer blijft de bestaande loader staan. +- De aparte frontendcorrectie toont één taalkeuze zodra de vertrouwde website de bestaande taalsynchronisatie bevestigt. Zelfstandig of vóór die bevestiging blijft de appselector beschikbaar. De appdocumenttaal en leesrichting volgen de gekozen taal. Het taalprotocol en het Xaman-protocol veranderen niet. + +De footerfout en dubbele taalkeuze zijn vóór de correctie automatisch gereproduceerd. De taal- en footercontroles testen tevens behoud van bestaande bediening. De loadercontrole gebruikt de WordPress-parserinterface met nagebootste script-attributen; de echte Brizy-/WordPress-uitvoer moet na installatie worden bevestigd. De filterpunten volgen [Brizy's publieke uitvoer](https://github.com/ThemeFuse/Brizy/blob/master/public/main.php); de gerichte HTML-aanpassing gebruikt [WordPress HTML Tag Processor](https://developer.wordpress.org/reference/classes/wp_html_tag_processor/). + +De livecontrole vooraf omvatte 25 publieke URL's in desktop-Chrome met Site Style-assets 1.4.11 en Identity Bridge-assets 0.3.29. Dit pakket verandert de bridge niet en mag de live bridge niet vervangen door bronversie 0.3.18. Er is nog geen liveacceptatie van 1.4.12, fysieke camera- of nieuwe Xaman-ondertekenproef. De frontendcorrectie, scanner, USDA-gewichtskeuze en Nutri-Score-aanpassingen vragen een afzonderlijke appuitrol; deze ZIP installeert alleen Site Style. Onderstaande eerdere audit- en browserstatus is historisch. ## Gerichte Blog/X-controle 1.4.11 diff --git a/wordpress-plugins/calorietoken-site-style/assets/style.js b/wordpress-plugins/calorietoken-site-style/assets/style.js index 38c7c00c..375aafb0 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/style.js +++ b/wordpress-plugins/calorietoken-site-style/assets/style.js @@ -162,7 +162,22 @@ known.forEach(function (node) { if (node !== footer && !node.contains(footer)) node.classList.add('ctstyle-retired-footer'); }); - if (canonical) return; // The installed bridge already owns its carousel. + if (canonical) { + // Adopt the installed bridge's existing row so shared translations and + // cookie controls also apply. Copy only missing public template links. + var legal = footer.querySelector('.calorieapp-shared-legal-links'); + if (legal) { + legal.classList.add('ctstyle-legal-links'); + var present = new Set(Array.from(legal.querySelectorAll('a[href]')).map(function (a) { + return canonicalURL(a.getAttribute('href')); + })); + fallback.querySelectorAll('.ctstyle-legal-links a[href]').forEach(function (a) { + var url = canonicalURL(a.getAttribute('href')); + if (url && !present.has(url)) { legal.append(a.cloneNode(true)); present.add(url); } + }); + } + return; // The installed bridge still owns its carousel and handlers. + } var track = footer.querySelector('.ctstyle-social-track'); footer.querySelectorAll('[data-ctstyle-direction]').forEach(function (button) { button.addEventListener('click', function () { diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index ff9cf13d..3df87782 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.11 + * Version: 1.4.12 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,34 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.11'; + const VERSION = '1.4.12'; + + public static function retire_market_loader($html) { + if (!is_string($html) || stripos($html, 'livecoinwatch.com') === false || + (!self::enabled() && !self::footer_only()) || !class_exists('WP_HTML_Tag_Processor') || + !in_array(strtolower((string) parse_url(home_url('/'), PHP_URL_HOST)), array('calorietoken.net', 'www.calorietoken.net'), true)) { return $html; } + foreach (array_keys($_GET) as $key) { if (strpos($key, 'xl-') === 0) { return $html; } } + // These installed bridge handles provide the site-wide XPMarket view. + // Keep the legacy loader if that replacement is not being delivered. + foreach (array('calorieapp-identity-bridge-layout', 'calorieapp-identity-bridge-site-polish') as $handle) { + if (!wp_script_is($handle, 'enqueued') && !wp_script_is($handle, 'done')) { return $html; } + } + $tags = new \WP_HTML_Tag_Processor($html); + while ($tags->next_tag('SCRIPT')) { + $src = $tags->get_attribute('src'); + if (!is_string($src)) { continue; } + $url = parse_url(strpos($src, '//') === 0 ? 'https:' . $src : $src); + if (!is_array($url) || !isset($url['scheme'], $url['host'], $url['path']) || + !in_array(strtolower($url['scheme']), array('http', 'https'), true) || isset($url['user']) || isset($url['pass']) || + !in_array(strtolower($url['host']), array('livecoinwatch.com', 'www.livecoinwatch.com'), true) || + $url['path'] !== '/static/lcw-widget.js') { continue; } + // Make the script inert before HTML reaches the browser. A distinct + // data type also prevents consent tools from reactivating its body. + $tags->remove_attribute('src'); + $tags->set_attribute('type', 'application/x-calorietoken-retired'); + } + return $tags->get_updated_html(); + } private static function json_asset($name) { // Request-local cache only: plugin updates never need a persistent cache purge. @@ -172,5 +199,8 @@ public static function templates() { add_filter('body_class', array(Plugin::class, 'body_class')); add_action('wp_enqueue_scripts', array(Plugin::class, 'enqueue'), 99); add_action('wp_footer', array(Plugin::class, 'templates'), 19); +add_filter('brizy_content', array(Plugin::class, 'retire_market_loader'), 9999); +add_filter('the_content', array(Plugin::class, 'retire_market_loader'), 9999); +add_filter('do_shortcode_tag', array(Plugin::class, 'retire_market_loader'), 9999); require_once __DIR__ . '/public-pages.php'; require_once __DIR__ . '/review.php'; From c98bcce258ab81ba0ca266beae1a801df3afabc4 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:53:34 +0200 Subject: [PATCH 019/368] Restore a clear diary grade indicator and guide camera permission recovery --- frontend/components/FoodBarcodeScanner.tsx | 9 +- frontend/components/FoodSearchPlaceholder.tsx | 41 +++-- frontend/config/barcode-copy.json | 66 +++++--- frontend/config/food-ui-copy.json | 154 +++++++++++------- frontend/lib/foodBarcode.ts | 13 ++ frontend/lib/foodUi.ts | 15 +- tools/tests/food_barcode_ui.test.mjs | 35 ++++ tools/tests/food_logging_ui.test.mjs | 31 +++- 8 files changed, 271 insertions(+), 93 deletions(-) diff --git a/frontend/components/FoodBarcodeScanner.tsx b/frontend/components/FoodBarcodeScanner.tsx index e0ce4ae2..fdd9e582 100644 --- a/frontend/components/FoodBarcodeScanner.tsx +++ b/frontend/components/FoodBarcodeScanner.tsx @@ -2,9 +2,9 @@ import { FormEvent, useCallback, useEffect, useRef, useState } from "react"; import translations from "@/config/barcode-copy.json"; -import { startBarcodeCamera, validFoodBarcode } from "@/lib/foodBarcode"; +import { cameraBlockedByPolicy, startBarcodeCamera, validFoodBarcode } from "@/lib/foodBarcode"; -type Status = "opening" | "scanning" | "denied" | "unavailable" | "invalid" | "found" | "paused" | "cameraError"; +type Status = "opening" | "scanning" | "denied" | "policyBlocked" | "unavailable" | "invalid" | "found" | "paused" | "cameraError"; export function FoodBarcodeScanner({ locale, disabled, onLookup }: { locale: string; disabled: boolean; onLookup: (code: string) => void; @@ -42,6 +42,7 @@ export function FoodBarcodeScanner({ locale, disabled, onLookup }: { async function scan() { if (disabled || camera.current || !video.current) return; + if (cameraBlockedByPolicy(document)) { setStatus("policyBlocked"); return; } if (!window.isSecureContext || !navigator.mediaDevices?.getUserMedia) { setStatus("unavailable"); return; } const controller = new AbortController(); camera.current = controller; setActive(true); setStatus("opening"); @@ -54,7 +55,8 @@ export function FoodBarcodeScanner({ locale, disabled, onLookup }: { } catch (error) { if (camera.current !== controller) return; const name = error && typeof error === "object" && "name" in error ? error.name : ""; - stop(name === "NotAllowedError" || name === "SecurityError" ? "denied" : "cameraError"); + stop(cameraBlockedByPolicy(document) ? "policyBlocked" + : name === "NotAllowedError" || name === "SecurityError" ? "denied" : "cameraError"); } } @@ -81,6 +83,7 @@ export function FoodBarcodeScanner({ locale, disabled, onLookup }: { {status ?

    {copy[status]}

    : null} + {status === "denied" ?

    {copy.permissionHelp}

    : null}
    diff --git a/frontend/components/FoodSearchPlaceholder.tsx b/frontend/components/FoodSearchPlaceholder.tsx index 4e7c23ce..a037c61e 100644 --- a/frontend/components/FoodSearchPlaceholder.tsx +++ b/frontend/components/FoodSearchPlaceholder.tsx @@ -15,7 +15,7 @@ import { SearchBar } from "@/components/SearchBar"; import { FoodSearchItem, FoodSearchResponse } from "@/components/foodTypes"; import Image from "next/image"; import { useDisplayLanguage } from "@/components/DisplayLanguageProvider"; -import { countRecordedGrades, formatFoodUi, getFoodUi, recordedGradeStyle, translateFoodStatus } from "@/lib/foodUi"; +import { countRecordedGrades, formatFoodUi, getFoodUi, recordedGradePosition, recordedGradeStyle, translateFoodStatus } from "@/lib/foodUi"; import { foodSearchRetryAt } from "@/lib/foodSearchAvailability"; import { AUTH_STATE_CHANGED_EVENT, @@ -267,6 +267,11 @@ export function FoodSearchPlaceholder() { [logs, selectedLogId] ); const recordedGrades = useMemo(() => countRecordedGrades(logs), [logs]); + const gradePosition = recordedGradePosition(recordedGrades); + const gradePositionLabel = gradePosition + ? formatFoodUi(gradePosition.lower === gradePosition.upper ? copy.scoreAt : copy.scoreBetween, + { grade: gradePosition.lower, lower: gradePosition.lower, upper: gradePosition.upper }) + : copy.scoreEmpty; const selectedPortionPercentage = useMemo( () => getPortionPercentage(portionOption, customPortion), [portionOption, customPortion] @@ -910,19 +915,35 @@ export function FoodSearchPlaceholder() {

    {copy.scoreTitle}

    -

    {copy.scoreDescription}

    +

    {gradePositionLabel}

    +
    +
    + {gradePosition ?

    {formatFoodUi(copy.scoreCoverage, { known: displayInteger(recordedGrades.known), total: displayInteger(recordedGrades.total) })}

    -
    - {recordedGrades.grades.map(({ grade, count }) => ( -
    -
    {grade}
    -
    {displayInteger(count)}
    -
    - ))} -

    {copy.scoreMissing}: {displayInteger(recordedGrades.missing)}

    +
    + {copy.scoreDetails} +

    {copy.scoreDescription}

    +
    + {recordedGrades.grades.map(({ grade, count }) => ( +
    +
    {grade}
    +
    {displayInteger(count)}
    +
    + ))} +
    +
    ) : null} diff --git a/frontend/config/barcode-copy.json b/frontend/config/barcode-copy.json index 91730eca..3fa46137 100644 --- a/frontend/config/barcode-copy.json +++ b/frontend/config/barcode-copy.json @@ -8,14 +8,16 @@ "stop": "Stop camera", "opening": "Opening camera…", "scanning": "Centre the barcode in the frame. Use good light and hold steady.", - "denied": "Camera access was not allowed. Enter the barcode below.", + "denied": "Camera access is blocked. Check the camera permission for this website in your browser settings.", "unavailable": "Camera unavailable. Enter the barcode below.", "invalid": "Enter a valid barcode with 8, 12, 13 or 14 digits.", "found": "Barcode read. Check the product below before logging it.", "paused": "Camera stopped. Try again or enter the barcode.", "cameraError": "The camera could not start. Try again or enter the barcode.", "privacy": "Camera images stay on your device. Only the barcode is used to look up the food in Open Food Facts.", - "notFound": "No product with complete nutrition was found for this barcode. Try searching by food name." + "notFound": "No product with complete nutrition was found for this barcode. Try searching by food name.", + "permissionHelp": "On a phone, also check that your browser may use the camera in the phone settings. Then return here and tap Start camera. You do not need to sign in.", + "policyBlocked": "This page does not allow camera use. Enter the barcode below; signing in is not required." }, "nl": { "title": "Scan een voedselbarcode", @@ -26,14 +28,16 @@ "stop": "Stop camera", "opening": "Camera openen…", "scanning": "Houd de barcode in het kader. Zorg voor voldoende licht en houd de camera stil.", - "denied": "Cameratoegang is niet toegestaan. Vul de barcode hieronder in.", + "denied": "Cameratoegang is geblokkeerd. Controleer in je browserinstellingen de cameratoestemming voor deze website.", "unavailable": "Camera niet beschikbaar. Vul de barcode hieronder in.", "invalid": "Vul een geldige barcode met 8, 12, 13 of 14 cijfers in.", "found": "Barcode gelezen. Controleer het product hieronder voordat je het toevoegt.", "paused": "Camera gestopt. Probeer opnieuw of vul de barcode in.", "cameraError": "De camera kon niet starten. Probeer opnieuw of vul de barcode in.", "privacy": "Camerabeelden blijven op je apparaat. Alleen de barcode wordt gebruikt om het voedsel in Open Food Facts op te zoeken.", - "notFound": "Voor deze barcode is geen product met volledige voedingswaarden gevonden. Zoek op de naam van het voedsel." + "notFound": "Voor deze barcode is geen product met volledige voedingswaarden gevonden. Zoek op de naam van het voedsel.", + "permissionHelp": "Controleer op je telefoon ook of je browser de camera mag gebruiken in de telefooninstellingen. Kom daarna terug en tik op Start camera. Inloggen is niet nodig.", + "policyBlocked": "Deze pagina staat cameragebruik niet toe. Vul de barcode hieronder in; inloggen is niet nodig." }, "zh-Hans": { "title": "扫描食品条码", @@ -44,14 +48,16 @@ "stop": "停止相机", "opening": "正在打开相机…", "scanning": "将条码置于框内,保持光线充足和相机稳定。", - "denied": "相机访问未获允许。请在下方输入条码。", + "denied": "相机访问被阻止。请在浏览器设置中检查此网站的相机权限。", "unavailable": "相机不可用。请在下方输入条码。", "invalid": "请输入有效的 8、12、13 或 14 位条码。", "found": "已读取条码。记录前请核对下方产品。", "paused": "相机已停止。请重试或输入条码。", "cameraError": "相机无法启动。请重试或输入条码。", "privacy": "相机图像保留在您的设备上。仅使用条码在 Open Food Facts 中查询食品。", - "notFound": "未找到具有完整营养数据的对应产品。请尝试按食品名称搜索。" + "notFound": "未找到具有完整营养数据的对应产品。请尝试按食品名称搜索。", + "permissionHelp": "如果使用手机,请同时检查手机设置是否允许浏览器使用相机。然后返回此页并点击启动相机。无需登录。", + "policyBlocked": "此页面不允许使用相机。请在下方输入条码,无需登录。" }, "hi": { "title": "भोजन का बारकोड स्कैन करें", @@ -62,14 +68,16 @@ "stop": "कैमरा रोकें", "opening": "कैमरा खुल रहा है…", "scanning": "बारकोड को फ्रेम के बीच रखें। अच्छी रोशनी में कैमरा स्थिर रखें।", - "denied": "कैमरे की अनुमति नहीं मिली। नीचे बारकोड दर्ज करें।", + "denied": "कैमरे का उपयोग अवरुद्ध है। ब्राउज़र सेटिंग में इस वेबसाइट की कैमरा अनुमति जाँचें।", "unavailable": "कैमरा उपलब्ध नहीं है। नीचे बारकोड दर्ज करें।", "invalid": "8, 12, 13 या 14 अंकों का मान्य बारकोड दर्ज करें।", "found": "बारकोड पढ़ लिया गया। दर्ज करने से पहले नीचे उत्पाद जाँचें।", "paused": "कैमरा बंद है। फिर कोशिश करें या बारकोड दर्ज करें।", "cameraError": "कैमरा शुरू नहीं हो सका। फिर कोशिश करें या बारकोड दर्ज करें।", "privacy": "कैमरे की छवियाँ आपके डिवाइस पर रहती हैं। Open Food Facts में भोजन खोजने के लिए केवल बारकोड का उपयोग होता है।", - "notFound": "इस बारकोड का पूरा पोषण डेटा वाला उत्पाद नहीं मिला। भोजन के नाम से खोजें।" + "notFound": "इस बारकोड का पूरा पोषण डेटा वाला उत्पाद नहीं मिला। भोजन के नाम से खोजें।", + "permissionHelp": "फ़ोन पर, फ़ोन की सेटिंग में भी जाँचें कि ब्राउज़र को कैमरे की अनुमति है। फिर यहाँ लौटकर कैमरा शुरू करें दबाएँ। लॉग इन करना ज़रूरी नहीं है।", + "policyBlocked": "यह पेज कैमरे का उपयोग नहीं करने देता। नीचे बारकोड दर्ज करें; लॉग इन करना ज़रूरी नहीं है।" }, "es": { "title": "Escanea un código de alimentos", @@ -80,14 +88,16 @@ "stop": "Detener cámara", "opening": "Abriendo la cámara…", "scanning": "Centra el código en el recuadro. Usa buena luz y mantén la cámara estable.", - "denied": "No se permitió el acceso a la cámara. Introduce el código abajo.", + "denied": "El acceso a la cámara está bloqueado. Revisa el permiso de cámara de este sitio en los ajustes del navegador.", "unavailable": "Cámara no disponible. Introduce el código abajo.", "invalid": "Introduce un código válido de 8, 12, 13 o 14 dígitos.", "found": "Código leído. Revisa el producto abajo antes de registrarlo.", "paused": "Cámara detenida. Vuelve a intentarlo o introduce el código.", "cameraError": "La cámara no pudo iniciarse. Reinténtalo o introduce el código.", "privacy": "Las imágenes permanecen en tu dispositivo. Solo se usa el código para buscar el alimento en Open Food Facts.", - "notFound": "No se encontró un producto con nutrición completa para este código. Busca por el nombre del alimento." + "notFound": "No se encontró un producto con nutrición completa para este código. Busca por el nombre del alimento.", + "permissionHelp": "En el móvil, comprueba también que el navegador pueda usar la cámara en los ajustes del teléfono. Después vuelve aquí y pulsa Iniciar cámara. No necesitas iniciar sesión.", + "policyBlocked": "Esta página no permite usar la cámara. Introduce el código abajo; no necesitas iniciar sesión." }, "ar": { "title": "امسح رمز الطعام الشريطي", @@ -98,14 +108,16 @@ "stop": "إيقاف الكاميرا", "opening": "جارٍ فتح الكاميرا…", "scanning": "ضع الرمز في منتصف الإطار مع إضاءة جيدة وثبّت الكاميرا.", - "denied": "لم يُسمح بالوصول إلى الكاميرا. أدخل الرمز أدناه.", + "denied": "الوصول إلى الكاميرا محظور. تحقق من إذن الكاميرا لهذا الموقع في إعدادات المتصفح.", "unavailable": "الكاميرا غير متاحة. أدخل الرمز أدناه.", "invalid": "أدخل رمزًا صالحًا من 8 أو 12 أو 13 أو 14 رقمًا.", "found": "تمت قراءة الرمز. تحقق من المنتج أدناه قبل تسجيله.", "paused": "توقفت الكاميرا. حاول مجددًا أو أدخل الرمز.", "cameraError": "تعذر تشغيل الكاميرا. حاول مجددًا أو أدخل الرمز.", "privacy": "تبقى صور الكاميرا على جهازك. يُستخدم الرمز فقط للبحث عن الطعام في Open Food Facts.", - "notFound": "لم يُعثر على منتج ببيانات غذائية مكتملة لهذا الرمز. جرّب البحث باسم الطعام." + "notFound": "لم يُعثر على منتج ببيانات غذائية مكتملة لهذا الرمز. جرّب البحث باسم الطعام.", + "permissionHelp": "على الهاتف، تحقق أيضًا من السماح للمتصفح باستخدام الكاميرا في إعدادات الهاتف. ثم عُد إلى هنا واضغط تشغيل الكاميرا. لا تحتاج إلى تسجيل الدخول.", + "policyBlocked": "هذه الصفحة لا تسمح باستخدام الكاميرا. أدخل الرمز أدناه؛ لا يلزم تسجيل الدخول." }, "fr": { "title": "Scanner un code-barres alimentaire", @@ -116,14 +128,16 @@ "stop": "Arrêter la caméra", "opening": "Ouverture de la caméra…", "scanning": "Centrez le code dans le cadre, avec un bon éclairage et une caméra stable.", - "denied": "L’accès à la caméra n’a pas été autorisé. Saisissez le code ci-dessous.", + "denied": "L’accès à la caméra est bloqué. Vérifiez l’autorisation de ce site dans les paramètres du navigateur.", "unavailable": "Caméra indisponible. Saisissez le code ci-dessous.", "invalid": "Saisissez un code valide de 8, 12, 13 ou 14 chiffres.", "found": "Code lu. Vérifiez le produit ci-dessous avant de l’enregistrer.", "paused": "Caméra arrêtée. Réessayez ou saisissez le code.", "cameraError": "La caméra n’a pas pu démarrer. Réessayez ou saisissez le code.", "privacy": "Les images restent sur votre appareil. Seul le code sert à rechercher l’aliment dans Open Food Facts.", - "notFound": "Aucun produit avec des données nutritionnelles complètes n’a été trouvé pour ce code. Recherchez par nom d’aliment." + "notFound": "Aucun produit avec des données nutritionnelles complètes n’a été trouvé pour ce code. Recherchez par nom d’aliment.", + "permissionHelp": "Sur téléphone, vérifiez aussi que le navigateur peut utiliser la caméra dans les paramètres du téléphone. Revenez ensuite ici et appuyez sur Démarrer la caméra. Aucune connexion n’est nécessaire.", + "policyBlocked": "Cette page n’autorise pas la caméra. Saisissez le code ci-dessous ; aucune connexion n’est nécessaire." }, "bn": { "title": "খাবারের বারকোড স্ক্যান করুন", @@ -134,14 +148,16 @@ "stop": "ক্যামেরা বন্ধ করুন", "opening": "ক্যামেরা খুলছে…", "scanning": "বারকোড ফ্রেমের মাঝখানে রাখুন। পর্যাপ্ত আলোতে ক্যামেরা স্থির রাখুন।", - "denied": "ক্যামেরার অনুমতি মেলেনি। নিচে বারকোড লিখুন।", + "denied": "ক্যামেরার প্রবেশাধিকার অবরুদ্ধ। ব্রাউজারের সেটিংসে এই ওয়েবসাইটের ক্যামেরার অনুমতি দেখুন।", "unavailable": "ক্যামেরা নেই। নিচে বারকোড লিখুন।", "invalid": "৮, ১২, ১৩ বা ১৪ সংখ্যার বৈধ বারকোড লিখুন।", "found": "বারকোড পড়া হয়েছে। লগ করার আগে নিচের পণ্যটি যাচাই করুন।", "paused": "ক্যামেরা বন্ধ। আবার চেষ্টা করুন অথবা বারকোড লিখুন।", "cameraError": "ক্যামেরা চালু হয়নি। আবার চেষ্টা করুন অথবা বারকোড লিখুন।", "privacy": "ক্যামেরার ছবি আপনার ডিভাইসেই থাকে। শুধু বারকোড দিয়ে Open Food Facts-এ খাবার খোঁজা হয়।", - "notFound": "এই বারকোডে সম্পূর্ণ পুষ্টিতথ্যসহ পণ্য পাওয়া যায়নি। খাবারের নাম দিয়ে খুঁজুন।" + "notFound": "এই বারকোডে সম্পূর্ণ পুষ্টিতথ্যসহ পণ্য পাওয়া যায়নি। খাবারের নাম দিয়ে খুঁজুন।", + "permissionHelp": "ফোনে, ফোনের সেটিংসেও দেখুন ব্রাউজারকে ক্যামেরা ব্যবহার করতে দেওয়া হয়েছে কি না। তারপর এখানে ফিরে ক্যামেরা চালু করুন চাপুন। সাইন ইন করার দরকার নেই।", + "policyBlocked": "এই পৃষ্ঠায় ক্যামেরা ব্যবহার অনুমোদিত নয়। নিচে বারকোড লিখুন; সাইন ইন করার দরকার নেই।" }, "pt": { "title": "Digitalizar um código alimentar", @@ -152,14 +168,16 @@ "stop": "Parar câmara", "opening": "A abrir a câmara…", "scanning": "Centre o código na moldura. Use boa iluminação e mantenha a câmara estável.", - "denied": "O acesso à câmara não foi permitido. Introduza o código abaixo.", + "denied": "O acesso à câmera está bloqueado. Confira a permissão de câmera deste site nas configurações do navegador.", "unavailable": "Câmara indisponível. Introduza o código abaixo.", "invalid": "Introduza um código válido com 8, 12, 13 ou 14 dígitos.", "found": "Código lido. Verifique o produto abaixo antes de o registar.", "paused": "Câmara parada. Tente novamente ou introduza o código.", "cameraError": "Não foi possível iniciar a câmara. Tente novamente ou introduza o código.", "privacy": "As imagens ficam no seu dispositivo. Só o código é usado para procurar o alimento no Open Food Facts.", - "notFound": "Não foi encontrado um produto com dados nutricionais completos para este código. Pesquise pelo nome do alimento." + "notFound": "Não foi encontrado um produto com dados nutricionais completos para este código. Pesquise pelo nome do alimento.", + "permissionHelp": "No celular, confira também se o navegador pode usar a câmera nas configurações do aparelho. Depois volte aqui e toque em Iniciar câmera. Não é necessário entrar na conta.", + "policyBlocked": "Esta página não permite usar a câmera. Digite o código abaixo; não é necessário entrar na conta." }, "id": { "title": "Pindai kode batang makanan", @@ -170,14 +188,16 @@ "stop": "Hentikan kamera", "opening": "Membuka kamera…", "scanning": "Posisikan kode di tengah bingkai. Gunakan cahaya yang cukup dan pegang dengan stabil.", - "denied": "Akses kamera tidak diizinkan. Masukkan kode di bawah.", + "denied": "Akses kamera diblokir. Periksa izin kamera untuk situs ini di pengaturan browser.", "unavailable": "Kamera tidak tersedia. Masukkan kode di bawah.", "invalid": "Masukkan kode yang valid dengan 8, 12, 13, atau 14 digit.", "found": "Kode terbaca. Periksa produk di bawah sebelum mencatatnya.", "paused": "Kamera berhenti. Coba lagi atau masukkan kode.", "cameraError": "Kamera tidak dapat dimulai. Coba lagi atau masukkan kode.", "privacy": "Gambar kamera tetap di perangkat Anda. Hanya kode yang digunakan untuk mencari makanan di Open Food Facts.", - "notFound": "Tidak ditemukan produk dengan gizi lengkap untuk kode ini. Coba cari berdasarkan nama makanan." + "notFound": "Tidak ditemukan produk dengan gizi lengkap untuk kode ini. Coba cari berdasarkan nama makanan.", + "permissionHelp": "Di ponsel, periksa juga apakah browser boleh memakai kamera di pengaturan ponsel. Lalu kembali ke sini dan ketuk Mulai kamera. Anda tidak perlu masuk.", + "policyBlocked": "Halaman ini tidak mengizinkan penggunaan kamera. Masukkan kode di bawah; Anda tidak perlu masuk." }, "ur": { "title": "کھانے کا بارکوڈ اسکین کریں", @@ -188,13 +208,15 @@ "stop": "کیمرہ بند کریں", "opening": "کیمرہ کھل رہا ہے…", "scanning": "بارکوڈ فریم کے بیچ میں رکھیں۔ مناسب روشنی میں کیمرہ ساکن رکھیں۔", - "denied": "کیمرے کی اجازت نہیں ملی۔ نیچے بارکوڈ درج کریں۔", + "denied": "کیمرے تک رسائی مسدود ہے۔ براؤزر کی ترتیبات میں اس ویب سائٹ کے کیمرے کی اجازت دیکھیں۔", "unavailable": "کیمرہ دستیاب نہیں۔ نیچے بارکوڈ درج کریں۔", "invalid": "8، 12، 13 یا 14 ہندسوں کا درست بارکوڈ درج کریں۔", "found": "بارکوڈ پڑھ لیا گیا۔ لاگ میں شامل کرنے سے پہلے نیچے پروڈکٹ چیک کریں۔", "paused": "کیمرہ بند ہے۔ دوبارہ کوشش کریں یا بارکوڈ درج کریں۔", "cameraError": "کیمرہ شروع نہیں ہو سکا۔ دوبارہ کوشش کریں یا بارکوڈ درج کریں۔", "privacy": "کیمرے کی تصاویر آپ کے آلے پر رہتی ہیں۔ Open Food Facts میں کھانا تلاش کرنے کے لیے صرف بارکوڈ استعمال ہوتا ہے۔", - "notFound": "اس بارکوڈ کے لیے مکمل غذائی معلومات والی پروڈکٹ نہیں ملی۔ کھانے کے نام سے تلاش کریں۔" + "notFound": "اس بارکوڈ کے لیے مکمل غذائی معلومات والی پروڈکٹ نہیں ملی۔ کھانے کے نام سے تلاش کریں۔", + "permissionHelp": "فون پر، فون کی ترتیبات میں بھی دیکھیں کہ براؤزر کو کیمرہ استعمال کرنے کی اجازت ہے۔ پھر یہاں واپس آ کر کیمرہ شروع کریں دبائیں۔ سائن ان ضروری نہیں ہے۔", + "policyBlocked": "یہ صفحہ کیمرہ استعمال کرنے کی اجازت نہیں دیتا۔ نیچے بارکوڈ درج کریں؛ سائن ان ضروری نہیں ہے۔" } } diff --git a/frontend/config/food-ui-copy.json b/frontend/config/food-ui-copy.json index 3a2dd58e..4b4747b1 100644 --- a/frontend/config/food-ui-copy.json +++ b/frontend/config/food-ui-copy.json @@ -51,10 +51,10 @@ "totalFat": "Total Fat", "totalCarbohydrates": "Total Carbohydrates", "foodsLogged": "Foods Logged", - "scoreTitle": "Recorded product Nutri-Score grades", - "scoreDescription": "Counts of the product letters in your loaded entries. This is not a Nutri-Score for your diet; portion sizes are not taken into account.", - "scoreCoverage": "Entries with an A–E grade: {known} of {total}.", - "scoreMissing": "Without an A–E grade", + "scoreTitle": "Your logged product overview", + "scoreDescription": "Each logged product with an A–E letter counts once. The arrow averages those letters; portion sizes do not count. This is not a Nutri-Score for your whole diet.", + "scoreCoverage": "Based on {known} of {total} entries.", + "scoreMissing": "Not included: no A–E letter", "detailsTitle": "Logged Food Details", "backToList": "Back to list", "portionEaten": "Portion eaten", @@ -92,7 +92,11 @@ "addedFeedback": "Added {product} ({percentage}%) to your food log.", "searchWait": "Wait {seconds}s", "searchCooldownNotice": "The search button becomes available after this pause. Your search text stays here; you do not need to refresh.", - "searchStartupHint": "The first search after inactivity can take a few minutes. Please leave this page open; one search is already in progress." + "searchStartupHint": "The first search after inactivity can take a few minutes. Please leave this page open; one search is already in progress.", + "scoreDetails": "How does this work?", + "scoreAt": "Average: {grade}", + "scoreBetween": "Average: between {lower} and {upper}", + "scoreEmpty": "No product letters available yet" }, "nl": { "searchTitle": "Voedingsmiddelen zoeken", @@ -146,10 +150,10 @@ "totalFat": "Totaal vet", "totalCarbohydrates": "Totaal koolhydraten", "foodsLogged": "Vastgelegde voedingsmiddelen", - "scoreTitle": "Vastgelegde Nutri-Score-productletters", - "scoreDescription": "Aantallen productletters in je geladen vermeldingen. Dit is geen Nutri-Score voor je voedingspatroon; portiegroottes tellen niet mee.", - "scoreCoverage": "Vermeldingen met een letter A–E: {known} van {total}.", - "scoreMissing": "Zonder letter A–E", + "scoreTitle": "Overzicht van je gelogde producten", + "scoreDescription": "Elk gelogd product met een letter A–E telt één keer mee. De pijl toont het gemiddelde van die letters; hoeveel je at telt niet mee. Dit is geen Nutri-Score voor je hele voeding.", + "scoreCoverage": "Gebaseerd op {known} van {total} registraties.", + "scoreMissing": "Niet meegeteld: geen letter A–E", "detailsTitle": "Details van vastgelegd eten", "backToList": "Terug naar lijst", "portionEaten": "Gegeten portie", @@ -187,7 +191,11 @@ "addedFeedback": "{product} ({percentage}%) toegevoegd aan je eetdagboek.", "searchWait": "Wacht {seconds}s", "searchCooldownNotice": "Na deze pauze kun je weer zoeken. Je zoektekst blijft staan; verversen is niet nodig.", - "searchStartupHint": "De eerste zoekopdracht na inactiviteit kan enkele minuten duren. Laat deze pagina open; je zoekopdracht wordt al uitgevoerd." + "searchStartupHint": "De eerste zoekopdracht na inactiviteit kan enkele minuten duren. Laat deze pagina open; je zoekopdracht wordt al uitgevoerd.", + "scoreDetails": "Hoe werkt dit?", + "scoreAt": "Gemiddelde: {grade}", + "scoreBetween": "Gemiddelde: tussen {lower} en {upper}", + "scoreEmpty": "Nog geen productletters beschikbaar" }, "es": { "searchTitle": "Buscar alimentos", @@ -241,10 +249,10 @@ "totalFat": "Grasas totales", "totalCarbohydrates": "Carbohidratos totales", "foodsLogged": "Alimentos registrados", - "scoreTitle": "Letras Nutri-Score registradas de los productos", - "scoreDescription": "Recuento de las letras de producto en los registros cargados. No es un Nutri-Score de tu dieta; no tiene en cuenta el tamaño de las raciones.", - "scoreCoverage": "Registros con una letra A–E: {known} de {total}.", - "scoreMissing": "Sin letra A–E", + "scoreTitle": "Resumen de tus productos registrados", + "scoreDescription": "Cada registro con una letra A–E cuenta una vez. La flecha indica el promedio de esas letras; no tiene en cuenta las porciones. No es un Nutri-Score de toda tu alimentación.", + "scoreCoverage": "Basado en {known} de {total} registros.", + "scoreMissing": "No incluidos: sin letra A–E", "detailsTitle": "Detalles del alimento registrado", "backToList": "Volver a la lista", "portionEaten": "Ración consumida", @@ -282,7 +290,11 @@ "addedFeedback": "Se añadió {product} ({percentage}%) a tu diario.", "searchWait": "Espera {seconds}s", "searchCooldownNotice": "Podrás volver a buscar después de esta pausa. El texto se conserva; no hace falta actualizar la página.", - "searchStartupHint": "La primera búsqueda tras un periodo de inactividad puede tardar unos minutos. Deja esta página abierta; tu búsqueda ya está en curso." + "searchStartupHint": "La primera búsqueda tras un periodo de inactividad puede tardar unos minutos. Deja esta página abierta; tu búsqueda ya está en curso.", + "scoreDetails": "¿Cómo funciona?", + "scoreAt": "Promedio: {grade}", + "scoreBetween": "Promedio: entre {lower} y {upper}", + "scoreEmpty": "Aún no hay letras de productos disponibles" }, "fr": { "searchTitle": "Rechercher des aliments", @@ -336,10 +348,10 @@ "totalFat": "Lipides totaux", "totalCarbohydrates": "Glucides totaux", "foodsLogged": "Aliments enregistrés", - "scoreTitle": "Lettres Nutri-Score enregistrées des produits", - "scoreDescription": "Nombre de lettres de produit dans les entrées chargées. Ce n’est pas un Nutri-Score de votre alimentation ; les quantités consommées ne sont pas prises en compte.", - "scoreCoverage": "Entrées avec une lettre A–E : {known} sur {total}.", - "scoreMissing": "Sans lettre A–E", + "scoreTitle": "Aperçu de vos produits enregistrés", + "scoreDescription": "Chaque entrée avec une lettre A–E compte une fois. La flèche indique la moyenne de ces lettres, sans tenir compte des portions. Ce n’est pas un Nutri-Score de votre alimentation globale.", + "scoreCoverage": "Basé sur {known} entrées sur {total}.", + "scoreMissing": "Non incluses : sans lettre A–E", "detailsTitle": "Détails de l’aliment enregistré", "backToList": "Retour à la liste", "portionEaten": "Portion consommée", @@ -377,7 +389,11 @@ "addedFeedback": "{product} ({percentage}%) a été ajouté à votre journal alimentaire.", "searchWait": "Attendre {seconds}s", "searchCooldownNotice": "Le bouton de recherche sera disponible après cette pause. Le texte est conservé ; inutile de recharger la page.", - "searchStartupHint": "La première recherche après une période d’inactivité peut prendre quelques minutes. Laissez cette page ouverte ; votre recherche est déjà en cours." + "searchStartupHint": "La première recherche après une période d’inactivité peut prendre quelques minutes. Laissez cette page ouverte ; votre recherche est déjà en cours.", + "scoreDetails": "Comment cela fonctionne-t-il ?", + "scoreAt": "Moyenne : {grade}", + "scoreBetween": "Moyenne : entre {lower} et {upper}", + "scoreEmpty": "Aucune lettre de produit disponible pour le moment" }, "pt": { "searchTitle": "Pesquisar alimentos", @@ -431,10 +447,10 @@ "totalFat": "Total de gorduras", "totalCarbohydrates": "Total de hidratos de carbono", "foodsLogged": "Alimentos registados", - "scoreTitle": "Letras Nutri-Score registadas dos produtos", - "scoreDescription": "Contagem das letras dos produtos nos registos carregados. Não é um Nutri-Score da sua alimentação; o tamanho das porções não é considerado.", - "scoreCoverage": "Registos com uma letra A–E: {known} de {total}.", - "scoreMissing": "Sem letra A–E", + "scoreTitle": "Resumo dos seus produtos registrados", + "scoreDescription": "Cada registro com uma letra A–E conta uma vez. A seta mostra a média dessas letras; o tamanho das porções não é considerado. Não é um Nutri-Score de toda a sua alimentação.", + "scoreCoverage": "Com base em {known} de {total} registros.", + "scoreMissing": "Não incluídos: sem letra A–E", "detailsTitle": "Detalhes do alimento registado", "backToList": "Voltar à lista", "portionEaten": "Porção consumida", @@ -472,7 +488,11 @@ "addedFeedback": "{product} ({percentage}%) foi adicionado ao seu diário alimentar.", "searchWait": "Aguarde {seconds}s", "searchCooldownNotice": "O botão de pesquisa ficará disponível após esta pausa. O texto é mantido; não é preciso atualizar a página.", - "searchStartupHint": "A primeira pesquisa após um período de inatividade pode demorar alguns minutos. Deixe esta página aberta; a pesquisa já está em andamento." + "searchStartupHint": "A primeira pesquisa após um período de inatividade pode demorar alguns minutos. Deixe esta página aberta; a pesquisa já está em andamento.", + "scoreDetails": "Como funciona?", + "scoreAt": "Média: {grade}", + "scoreBetween": "Média: entre {lower} e {upper}", + "scoreEmpty": "Ainda não há letras de produtos disponíveis" }, "zh-Hans": { "searchTitle": "搜索食品", @@ -526,10 +546,10 @@ "totalFat": "总脂肪", "totalCarbohydrates": "总碳水化合物", "foodsLogged": "已记录食品数量", - "scoreTitle": "记录中的产品 Nutri-Score 等级", - "scoreDescription": "统计已加载记录中的产品等级字母。这不是对你的整体饮食的 Nutri-Score 评分,也未计入食用份量。", - "scoreCoverage": "带有 A–E 等级的记录:{known} 条,共 {total} 条。", - "scoreMissing": "无 A–E 等级", + "scoreTitle": "已记录产品概览", + "scoreDescription": "每条带有 A–E 等级的食品记录计为一次。箭头显示这些等级的平均位置,不考虑食用份量。这不是您整体饮食的 Nutri-Score。", + "scoreCoverage": "基于 {total} 条记录中的 {known} 条。", + "scoreMissing": "未计入:没有 A–E 等级", "detailsTitle": "已记录食品的详情", "backToList": "返回列表", "portionEaten": "食用比例", @@ -567,7 +587,11 @@ "addedFeedback": "已将 {product}({percentage}%)添加到你的饮食记录。", "searchWait": "请等待 {seconds} 秒", "searchCooldownNotice": "暂停结束后即可再次搜索。搜索文字会保留,无需刷新页面。", - "searchStartupHint": "闲置后的首次搜索可能需要几分钟。请保持页面打开,搜索已经在进行中。" + "searchStartupHint": "闲置后的首次搜索可能需要几分钟。请保持页面打开,搜索已经在进行中。", + "scoreDetails": "这是如何计算的?", + "scoreAt": "平均位置:{grade}", + "scoreBetween": "平均位置:{lower} 与 {upper} 之间", + "scoreEmpty": "暂无产品等级" }, "id": { "searchTitle": "Cari makanan", @@ -621,10 +645,10 @@ "totalFat": "Total lemak", "totalCarbohydrates": "Total karbohidrat", "foodsLogged": "Makanan tercatat", - "scoreTitle": "Huruf Nutri-Score produk yang tercatat", - "scoreDescription": "Jumlah huruf produk dalam catatan yang dimuat. Ini bukan Nutri-Score untuk pola makan Anda; ukuran porsi tidak diperhitungkan.", - "scoreCoverage": "Catatan dengan huruf A–E: {known} dari {total}.", - "scoreMissing": "Tanpa huruf A–E", + "scoreTitle": "Ringkasan produk yang Anda catat", + "scoreDescription": "Setiap catatan produk dengan huruf A–E dihitung sekali. Panah menunjukkan rata-rata huruf tersebut; ukuran porsi tidak dihitung. Ini bukan Nutri-Score untuk seluruh pola makan Anda.", + "scoreCoverage": "Berdasarkan {known} dari {total} catatan.", + "scoreMissing": "Tidak dihitung: tanpa huruf A–E", "detailsTitle": "Detail makanan tercatat", "backToList": "Kembali ke daftar", "portionEaten": "Porsi yang dimakan", @@ -662,7 +686,11 @@ "addedFeedback": "{product} ({percentage}%) ditambahkan ke catatan makanan Anda.", "searchWait": "Tunggu {seconds} detik", "searchCooldownNotice": "Tombol pencarian tersedia lagi setelah jeda ini. Teks pencarian tetap tersimpan di sini; tidak perlu memuat ulang halaman.", - "searchStartupHint": "Pencarian pertama setelah tidak aktif dapat memerlukan beberapa menit. Biarkan halaman ini terbuka; pencarian Anda sudah berjalan." + "searchStartupHint": "Pencarian pertama setelah tidak aktif dapat memerlukan beberapa menit. Biarkan halaman ini terbuka; pencarian Anda sudah berjalan.", + "scoreDetails": "Bagaimana cara kerjanya?", + "scoreAt": "Rata-rata: {grade}", + "scoreBetween": "Rata-rata: antara {lower} dan {upper}", + "scoreEmpty": "Belum ada huruf produk yang tersedia" }, "hi": { "searchTitle": "खाद्य पदार्थ खोजें", @@ -716,10 +744,10 @@ "totalFat": "कुल वसा", "totalCarbohydrates": "कुल कार्बोहाइड्रेट", "foodsLogged": "दर्ज खाद्य पदार्थ", - "scoreTitle": "दर्ज उत्पादों के Nutri-Score अक्षर", - "scoreDescription": "लोड किए गए रिकॉर्ड में उत्पादों के अक्षरों की गिनती। यह आपके आहार का Nutri-Score नहीं है; खाई गई मात्रा को शामिल नहीं किया गया है।", - "scoreCoverage": "A–E अक्षर वाले रिकॉर्ड: {total} में से {known}।", - "scoreMissing": "A–E अक्षर के बिना", + "scoreTitle": "दर्ज किए गए उत्पादों का सारांश", + "scoreDescription": "A–E अक्षर वाला हर दर्ज उत्पाद एक बार गिना जाता है। तीर इन अक्षरों का औसत दिखाता है; खाई गई मात्रा नहीं गिनी जाती। यह पूरे आहार का Nutri-Score नहीं है।", + "scoreCoverage": "{total} में से {known} प्रविष्टियों पर आधारित।", + "scoreMissing": "शामिल नहीं: A–E अक्षर नहीं है", "detailsTitle": "दर्ज भोजन का विवरण", "backToList": "सूची पर वापस जाएँ", "portionEaten": "खाई गई मात्रा", @@ -757,7 +785,11 @@ "addedFeedback": "{product} ({percentage}%) आपके भोजन रिकॉर्ड में जोड़ा गया।", "searchWait": "{seconds} सेकंड रुकें", "searchCooldownNotice": "इस विराम के बाद खोज बटन फिर उपलब्ध होगा। आपका खोज टेक्स्ट यहीं रहेगा; पेज रीफ़्रेश करने की ज़रूरत नहीं है।", - "searchStartupHint": "निष्क्रियता के बाद पहली खोज में कुछ मिनट लग सकते हैं। यह पेज खुला रखें; आपकी खोज पहले से जारी है।" + "searchStartupHint": "निष्क्रियता के बाद पहली खोज में कुछ मिनट लग सकते हैं। यह पेज खुला रखें; आपकी खोज पहले से जारी है।", + "scoreDetails": "यह कैसे काम करता है?", + "scoreAt": "औसत: {grade}", + "scoreBetween": "औसत: {lower} और {upper} के बीच", + "scoreEmpty": "अभी उत्पादों के अक्षर उपलब्ध नहीं हैं" }, "bn": { "searchTitle": "খাদ্য খুঁজুন", @@ -811,10 +843,10 @@ "totalFat": "মোট চর্বি", "totalCarbohydrates": "মোট কার্বোহাইড্রেট", "foodsLogged": "নথিভুক্ত খাবার", - "scoreTitle": "নথিভুক্ত পণ্যের Nutri-Score অক্ষর", - "scoreDescription": "লোড করা রেকর্ডে থাকা পণ্যের অক্ষরগুলোর সংখ্যা। এটি আপনার খাদ্যাভ্যাসের Nutri-Score নয়; খাওয়া পরিমাণ এতে বিবেচনা করা হয় না।", - "scoreCoverage": "A–E অক্ষরসহ রেকর্ড: {total}-এর মধ্যে {known}।", - "scoreMissing": "A–E অক্ষর ছাড়া", + "scoreTitle": "আপনার নথিভুক্ত পণ্যের সারসংক্ষেপ", + "scoreDescription": "A–E অক্ষরযুক্ত প্রতিটি নথিভুক্ত পণ্য একবার গণনা করা হয়। তীরটি অক্ষরগুলোর গড় দেখায়; খাওয়া পরিমাণ গণনা করা হয় না। এটি আপনার পুরো খাদ্যাভ্যাসের Nutri-Score নয়।", + "scoreCoverage": "{total}টি নথির মধ্যে {known}টির ভিত্তিতে।", + "scoreMissing": "গণনা করা হয়নি: A–E অক্ষর নেই", "detailsTitle": "নথিভুক্ত খাবারের বিস্তারিত", "backToList": "তালিকায় ফিরে যান", "portionEaten": "খাওয়া পরিমাণ", @@ -852,7 +884,11 @@ "addedFeedback": "{product} ({percentage}%) আপনার খাবারের ডায়েরিতে যোগ করা হয়েছে।", "searchWait": "{seconds} সেকেন্ড অপেক্ষা করুন", "searchCooldownNotice": "এই বিরতির পরে আবার খোঁজা যাবে। আপনার অনুসন্ধানের লেখা এখানেই থাকবে; পেজ রিফ্রেশ করার দরকার নেই।", - "searchStartupHint": "কিছুক্ষণ ব্যবহার না করার পর প্রথম অনুসন্ধানে কয়েক মিনিট লাগতে পারে। এই পেজটি খোলা রাখুন; আপনার অনুসন্ধান ইতিমধ্যেই চলছে।" + "searchStartupHint": "কিছুক্ষণ ব্যবহার না করার পর প্রথম অনুসন্ধানে কয়েক মিনিট লাগতে পারে। এই পেজটি খোলা রাখুন; আপনার অনুসন্ধান ইতিমধ্যেই চলছে।", + "scoreDetails": "এটি কীভাবে কাজ করে?", + "scoreAt": "গড়: {grade}", + "scoreBetween": "গড়: {lower} এবং {upper}-এর মধ্যে", + "scoreEmpty": "এখনও পণ্যের অক্ষর পাওয়া যায়নি" }, "ar": { "searchTitle": "البحث عن الأغذية", @@ -906,10 +942,10 @@ "totalFat": "إجمالي الدهون", "totalCarbohydrates": "إجمالي الكربوهيدرات", "foodsLogged": "الأغذية المسجّلة", - "scoreTitle": "حروف Nutri-Score المسجّلة للمنتجات", - "scoreDescription": "عدد مرات ظهور حروف المنتجات في السجلات المحمّلة. هذا ليس Nutri-Score لنظامك الغذائي؛ لا تؤخذ أحجام الحصص في الحسبان.", - "scoreCoverage": "السجلات ذات الحروف A–E: {known} من {total}.", - "scoreMissing": "دون حرف من A–E", + "scoreTitle": "نظرة عامة على منتجاتك المسجلة", + "scoreDescription": "يُحتسب كل منتج مسجل يحمل حرفًا من A إلى E مرة واحدة. يشير السهم إلى متوسط هذه الحروف دون احتساب حجم الحصة. هذا ليس Nutri-Score لنظامك الغذائي الكامل.", + "scoreCoverage": "استنادًا إلى {known} من أصل {total} سجلًا.", + "scoreMissing": "غير محسوب: لا يوجد حرف A–E", "detailsTitle": "تفاصيل الغذاء المسجّل", "backToList": "العودة إلى القائمة", "portionEaten": "الكمية المتناولة", @@ -947,7 +983,11 @@ "addedFeedback": "أُضيف {product} ({percentage}%) إلى سجلّك الغذائي.", "searchWait": "انتظر {seconds} ث", "searchCooldownNotice": "سيصبح زر البحث متاحًا بعد هذه الاستراحة. سيبقى نص البحث هنا؛ لا حاجة لتحديث الصفحة.", - "searchStartupHint": "قد يستغرق البحث الأول بعد فترة من عدم النشاط بضع دقائق. اترك هذه الصفحة مفتوحة؛ بحثك قيد التنفيذ بالفعل." + "searchStartupHint": "قد يستغرق البحث الأول بعد فترة من عدم النشاط بضع دقائق. اترك هذه الصفحة مفتوحة؛ بحثك قيد التنفيذ بالفعل.", + "scoreDetails": "كيف يعمل هذا؟", + "scoreAt": "المتوسط: {grade}", + "scoreBetween": "المتوسط: بين {lower} و{upper}", + "scoreEmpty": "لا توجد حروف للمنتجات بعد" }, "ur": { "searchTitle": "غذائیں تلاش کریں", @@ -1001,10 +1041,10 @@ "totalFat": "کل چکنائی", "totalCarbohydrates": "کل کاربوہائیڈریٹس", "foodsLogged": "درج شدہ غذائیں", - "scoreTitle": "مصنوعات کے درج شدہ Nutri-Score حروف", - "scoreDescription": "لوڈ شدہ ریکارڈ میں مصنوعات کے حروف کی گنتی۔ یہ آپ کی خوراک کا Nutri-Score نہیں ہے؛ کھائی ہوئی مقداروں کو شمار نہیں کیا جاتا۔", - "scoreCoverage": "A–E حرف والے ریکارڈ: {total} میں سے {known}۔", - "scoreMissing": "A–E حرف کے بغیر", + "scoreTitle": "آپ کی درج کردہ مصنوعات کا خلاصہ", + "scoreDescription": "A–E حرف والی ہر درج کردہ مصنوعہ ایک بار شمار ہوتی ہے۔ تیر ان حروف کی اوسط دکھاتا ہے؛ کھائی گئی مقدار شمار نہیں ہوتی۔ یہ آپ کی پوری غذا کا Nutri-Score نہیں ہے۔", + "scoreCoverage": "{total} میں سے {known} اندراجات پر مبنی۔", + "scoreMissing": "شامل نہیں: A–E حرف موجود نہیں", "detailsTitle": "درج شدہ کھانے کی تفصیلات", "backToList": "فہرست پر واپس جائیں", "portionEaten": "کھائی ہوئی مقدار", @@ -1042,6 +1082,10 @@ "addedFeedback": "{product} ({percentage}%) آپ کے کھانے کے ریکارڈ میں شامل کر دیا گیا۔", "searchWait": "{seconds} سیکنڈ رکیں", "searchCooldownNotice": "اس وقفے کے بعد تلاش کا بٹن دوبارہ دستیاب ہوگا۔ آپ کی تلاش کا متن یہیں رہے گا؛ صفحہ ریفریش کرنے کی ضرورت نہیں۔", - "searchStartupHint": "کچھ وقت استعمال نہ ہونے کے بعد پہلی تلاش میں چند منٹ لگ سکتے ہیں۔ یہ صفحہ کھلا رکھیں؛ آپ کی تلاش پہلے سے جاری ہے۔" + "searchStartupHint": "کچھ وقت استعمال نہ ہونے کے بعد پہلی تلاش میں چند منٹ لگ سکتے ہیں۔ یہ صفحہ کھلا رکھیں؛ آپ کی تلاش پہلے سے جاری ہے۔", + "scoreDetails": "یہ کیسے کام کرتا ہے؟", + "scoreAt": "اوسط: {grade}", + "scoreBetween": "اوسط: {lower} اور {upper} کے درمیان", + "scoreEmpty": "ابھی مصنوعات کے حروف دستیاب نہیں ہیں" } } diff --git a/frontend/lib/foodBarcode.ts b/frontend/lib/foodBarcode.ts index 52214633..95b99652 100644 --- a/frontend/lib/foodBarcode.ts +++ b/frontend/lib/foodBarcode.ts @@ -9,6 +9,19 @@ export function validFoodBarcode(value: string): string | null { return (10 - total % 10) % 10 === Number(code.at(-1)) ? code : null; } +/** Read the browser's policy without requesting or changing any permission. */ +export function cameraBlockedByPolicy(page: Document): boolean { + type Policy = { allowsFeature: (feature: string) => boolean }; + const policyPage = page as Document & { permissionsPolicy?: Policy; featurePolicy?: Policy }; + try { + const policy = policyPage.permissionsPolicy ?? policyPage.featurePolicy; + return policy?.allowsFeature("camera") === false; + } catch { + // Unsupported introspection is inconclusive; let the normal camera request decide. + return false; + } +} + type Reader = { decode: (video: HTMLVideoElement) => string | null }; type CameraDependencies = { openStream: () => Promise; diff --git a/frontend/lib/foodUi.ts b/frontend/lib/foodUi.ts index 9e36a94e..8fa96e49 100644 --- a/frontend/lib/foodUi.ts +++ b/frontend/lib/foodUi.ts @@ -34,12 +34,25 @@ export function countRecordedGrades(items: ReadonlyArray<{ nutri_score?: string const grades = ["A", "B", "C", "D", "E"].map(grade => ({ grade, count: 0 })); let known = 0; for (const item of items) { - const grade = grades.find(entry => entry.grade === item.nutri_score?.toUpperCase()); + const grade = grades.find(entry => entry.grade === item.nutri_score?.trim().toUpperCase()); if (grade) { grade.count += 1; known += 1; } } return { grades, known, total: items.length, missing: items.length - known }; } +/** A visual mean of recorded product letters, not a new nutritional grade. + * Each known entry counts once; missing grades and portion size add no weight. + */ +export function recordedGradePosition(summary: ReturnType) { + if (summary.known === 0) return null; + const average = summary.grades.reduce((sum, entry, index) => sum + index * entry.count, 0) / summary.known; + return { + percent: average * 25, + lower: summary.grades[Math.floor(average)].grade, + upper: summary.grades[Math.ceil(average)].grade, + }; +} + const recordedGradeColors: Record = { A: { backgroundColor: "#038141", color: "#ffffff" }, B: { backgroundColor: "#85bb2f", color: "#17211a" }, diff --git a/tools/tests/food_barcode_ui.test.mjs b/tools/tests/food_barcode_ui.test.mjs index 942e2291..e347e4b3 100644 --- a/tools/tests/food_barcode_ui.test.mjs +++ b/tools/tests/food_barcode_ui.test.mjs @@ -40,6 +40,7 @@ function harness() { if (name === 'react/jsx-runtime') return {jsx, jsxs: jsx}; if (name === '@/config/barcode-copy.json') return {default: copy}; if (name === '@/lib/foodBarcode') return {validFoodBarcode: library.exports.validFoodBarcode, + cameraBlockedByPolicy: library.exports.cameraBlockedByPolicy, startBarcodeCamera(video, signal, onCode, onReady) {return new Promise((resolve, reject) => sessions.push({video, signal, onCode, onReady, resolve, reject}));}}; throw Error(`Unexpected import ${name}`); }, @@ -110,3 +111,37 @@ test('Camera refusal and unavailable contexts retain a working manual fallback; h.window.isSecureContext = false; await h.click(); h.render(); assert.ok(text(h.tree).includes(copy.en.unavailable)); h.render({disabled: true}); h.submit(); await h.click(); assert.equal(h.sessions.length, 1); assert.equal(h.results.length, 1); }); + + +test('A page policy block is distinguished from visitor refusal without opening a camera or requiring sign-in', async () => { + for (const property of ['permissionsPolicy', 'featurePolicy']) { + const h = harness(); + h.document[property] = {allowsFeature(name) {assert.equal(name, 'camera'); return false;}}; + await h.click(); h.render(); + assert.equal(h.sessions.length, 0); assert.equal(h.timers.size, 0); + for (const locale of Object.keys(copy)) { + h.render({locale}); + assert.ok(text(h.tree).includes(copy[locale].policyBlocked)); + assert.ok(!text(h.tree).includes(copy[locale].permissionHelp)); + } + h.type('0034000470693'); h.submit(); + assert.deepEqual(h.results, ['0034000470693']); + } +}); + +test('Visitor refusal provides localized recovery guidance; unavailable policy introspection is not treated as refusal', async () => { + const h = harness(); + h.document.featurePolicy = {allowsFeature() {throw Error('Introspection unsupported');}}; + const pending = h.click(); + assert.equal(h.sessions.length, 1); + h.sessions[0].reject(Object.assign(new Error('Blocked'), {name: 'NotAllowedError'})); + await pending; + for (const locale of Object.keys(copy)) { + h.render({locale}); + assert.ok(text(h.tree).includes(copy[locale].denied)); + assert.ok(text(h.tree).includes(copy[locale].permissionHelp)); + } + h.document.permissionsPolicy = {allowsFeature() {return true;}}; + const retry = h.click(); assert.equal(h.sessions.length, 2); + h.sessions[1].resolve(); await retry; h.unmount(); +}); diff --git a/tools/tests/food_logging_ui.test.mjs b/tools/tests/food_logging_ui.test.mjs index 2ca6003f..4c4b8a0e 100644 --- a/tools/tests/food_logging_ui.test.mjs +++ b/tools/tests/food_logging_ui.test.mjs @@ -893,7 +893,7 @@ test("bulk deletion keeps its explicit confirmation and uses the current display assert.ok(nodes(h.tree, node => node.type === "EmptyState").some(node => node.props.title === foodUiCopy.ar.emptyLogsTitle)); }); -test("recorded product grades count entries and missing values without deriving an average or changing records", async () => { +test("product overview places one average marker above the detailed counts without changing food records", async () => { const entries = ["A", "a", "B", "E", null, "", "unknown"].map((grade, index) => Object.freeze({ ...foods[index], id: index + 1, nutri_score: grade, portion_percentage: index + 1 })); Object.freeze(entries); @@ -907,7 +907,7 @@ test("recorded product grades count entries and missing values without deriving h.login(); await h.flush(); for (const { tag } of localeRegistry.locales) { h.setDisplayLanguage(tag); - const distribution = nodes(h.tree, node => node.type === "dl" && node.props["aria-label"] === foodUiCopy[tag].scoreTitle)[0]; + const distribution = nodes(h.tree, node => node.type === "dl" && node.props["aria-label"] === foodUiCopy[tag].scoreDetails)[0]; assert.ok(distribution, tag); assert.equal(distribution.props.dir, "ltr"); const colors = nodes(distribution, node => node.props.style?.backgroundColor).map(node => node.props.style.backgroundColor); @@ -916,6 +916,15 @@ test("recorded product grades count entries and missing values without deriving assert.deepEqual(nodes(distribution, node => node.type === "dd").map(node => text(node)), [2, 1, 0, 0, 1].map(count => new Intl.NumberFormat(tag).format(count))); assert.ok(text(h.tree).includes(foodUiCopy[tag].scoreDescription)); + const marker = nodes(h.tree, node => node.props["data-grade-pointer"] === "true"); + assert.equal(marker.length, 1); + assert.equal(marker[0].props.style.left, "31.25%"); + const label = foodUi.formatFoodUi(foodUiCopy[tag].scoreBetween, {lower: "B", upper: "C"}); + const scale = nodes(h.tree, node => node.props.role === "img" && node.props["aria-label"] === label)[0]; + assert.ok(scale, `${tag}: the position is also available as text`); + assert.equal(scale.props.dir, "ltr", "The green-to-red scale remains in the same order in RTL languages"); + const details = nodes(h.tree, node => node.type === "details" && text(node).includes(foodUiCopy[tag].scoreDescription))[0]; + assert.ok(details); assert.ok(!details.props.open, "Counts and explanation remain available without crowding the overview"); assert.equal(h.requests.length, 1); } assert.equal(JSON.stringify(entries), before); @@ -982,3 +991,21 @@ test('Barcode absence has useful translated fallback text and name search restor await h.search('oats'); assert.ok(nodes(h.tree, n => n.type === 'EmptyState').some(n => n.props.description === foodUiCopy.ar.noResultsDescription)); }); + + +test("the overview marker follows added and removed grades, excludes missing scores and stays absent without known scores", async () => { + const summarize = grades => foodUi.recordedGradePosition(foodUi.countRecordedGrades(grades.map(nutri_score => ({nutri_score})))); + assert.equal(summarize(["A"]).percent, 0); + assert.equal(summarize(["E"]).percent, 100); + assert.equal(summarize(["A", "E"]).percent, 50); + assert.ok(Math.abs(summarize(["A", "E", "E"]).percent - 200 / 3) < 1e-10); + assert.equal(summarize(["A", "E", "", null, "unknown"]).percent, 50); + assert.equal(summarize([" a ", "b"]).percent, 12.5); + assert.equal(summarize([]), null); + assert.equal(summarize([null, "unknown"]), null); + const h = await harness("FoodSearchPlaceholder", undefined, () => ({ok:true, json:async () => [{...foods[0],id:1,nutri_score:null}]})); + h.login(); await h.flush(); + assert.equal(nodes(h.tree, node => node.props["data-grade-pointer"] === "true").length, 0); + assert.ok(text(h.tree).includes(foodUiCopy.en.scoreEmpty)); + assert.equal(h.requests.length, 1); +}); From 29def148b9a70fe11f3f5beaabdbce269748a53a Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:26:36 +0200 Subject: [PATCH 020/368] Fix camera delegation before iframe navigation and distinguish preview errors --- frontend/lib/foodBarcode.ts | 8 ++- tools/tests/food_barcode.test.mjs | 16 +++++ tools/tests/food_barcode_ui.test.mjs | 14 +++++ .../wordpress_site_style_review.test.php | 62 ++++++++++++++++++- .../calorietoken-site-style/LEESMIJ.md | 16 ++++- .../calorietoken-site-style.php | 45 +++++++++++++- 6 files changed, 152 insertions(+), 9 deletions(-) diff --git a/frontend/lib/foodBarcode.ts b/frontend/lib/foodBarcode.ts index 95b99652..0ed3b689 100644 --- a/frontend/lib/foodBarcode.ts +++ b/frontend/lib/foodBarcode.ts @@ -71,8 +71,14 @@ export async function startBarcodeCamera( if (stopped) return; stream = await dependencies.openStream(); if (stopped) { stop(); return; } + video.muted = true; video.defaultMuted = true; video.playsInline = true; video.srcObject = stream; - await video.play(); + try { await video.play(); } + catch (cause) { + // Playback can also throw NotAllowedError after capture was granted. + // Do not present that as a refusal of camera access. + throw Object.assign(new Error("The camera preview could not play."), { name: "CameraPlaybackError", cause }); + } if (stopped) { stop(); return; } onReady(); await new Promise((resolve, reject) => { diff --git a/tools/tests/food_barcode.test.mjs b/tools/tests/food_barcode.test.mjs index ed8baa6b..06be19ad 100644 --- a/tools/tests/food_barcode.test.mjs +++ b/tools/tests/food_barcode.test.mjs @@ -72,6 +72,22 @@ test('Denied camera and decoder failures remain recoverable and release owned re assert.ok(c.counts().stopped > 0); assert.equal(c.video.srcObject, null); }); +test('Camera preview starts muted and inline; playback refusal is distinct from capture refusal and releases tracks', async () => { + const c = camera(), controller = new AbortController(); + const refusal = Object.assign(new Error('Playback blocked'), {name: 'NotAllowedError'}); + c.video.play = async () => { + assert.equal(c.video.muted, true); assert.equal(c.video.defaultMuted, true); assert.equal(c.video.playsInline, true); + assert.equal(c.video.srcObject, c.stream); + throw refusal; + }; + await assert.rejects(startBarcodeCamera(c.video, controller.signal, () => assert.fail('No result'), + () => assert.fail('No ready signal before playback'), c.dependencies), error => { + assert.equal(error.name, 'CameraPlaybackError'); assert.equal(error.cause, refusal); return true; + }); + assert.equal(c.counts().opened, 1); assert.equal(c.counts().decoded, 0); + assert.ok(c.counts().stopped > 0); assert.equal(c.video.srcObject, null); +}); + test('Invalid detections keep scanning locally and abort clears the loop', async () => { const c = camera(), controller = new AbortController(); let attempted = 0; c.dependencies.createReader = async () => ({ decode: () => { attempted++; return 'https://example.com'; } }); diff --git a/tools/tests/food_barcode_ui.test.mjs b/tools/tests/food_barcode_ui.test.mjs index e347e4b3..9d5215dc 100644 --- a/tools/tests/food_barcode_ui.test.mjs +++ b/tools/tests/food_barcode_ui.test.mjs @@ -112,6 +112,20 @@ test('Camera refusal and unavailable contexts retain a working manual fallback; h.render({disabled: true}); h.submit(); await h.click(); assert.equal(h.sessions.length, 1); assert.equal(h.results.length, 1); }); +test('A failed preview after capture is granted does not tell the visitor camera permission was refused', async () => { + const h = harness(), pending = h.click(); + h.sessions[0].reject(Object.assign(new Error('Preview did not play'), {name: 'CameraPlaybackError'})); + await pending; + for (const locale of Object.keys(copy)) { + h.render({locale}); + assert.ok(text(h.tree).includes(copy[locale].cameraError)); + assert.ok(!text(h.tree).includes(copy[locale].denied)); + assert.ok(!text(h.tree).includes(copy[locale].permissionHelp)); + } + assert.equal(h.timers.size, 0); + h.type('034000470693'); h.submit(); assert.deepEqual(h.results, ['034000470693']); +}); + test('A page policy block is distinguished from visitor refusal without opening a camera or requiring sign-in', async () => { for (const property of ['permissionsPolicy', 'featurePolicy']) { diff --git a/tools/tests/wordpress_site_style_review.test.php b/tools/tests/wordpress_site_style_review.test.php index 84af38b6..1f194874 100644 --- a/tools/tests/wordpress_site_style_review.test.php +++ b/tools/tests/wordpress_site_style_review.test.php @@ -3,12 +3,12 @@ define('ABSPATH', __DIR__); function add_action(...$args) {} function add_filter(...$args) {} -$allowed = true; $fixture = array(); $queries = 0; $preview = false; $front_page = false; +$allowed = true; $fixture = array(); $queries = 0; $preview = false; $front_page = false; $page_id = 0; function is_admin() { return false; } function is_feed() { return false; } function is_embed() { return false; } function is_front_page() { global $front_page; return $front_page; } -function is_page($ids) { return false; } +function is_page($ids) { global $page_id; return in_array($page_id, (array) $ids, true); } function is_preview() { global $preview; return $preview; } function current_user_can($capability) { global $allowed; return $allowed && $capability === 'manage_options'; } function get_posts($args) { @@ -70,10 +70,15 @@ function wp_script_is($handle, $status = 'enqueued') { } class WP_HTML_Tag_Processor { public static $scripts = array(); + public static $iframes = array(); public static $last = null; public $nodes; private $index = -1; public function __construct($html) { $this->nodes = self::$scripts; self::$last = $this; } - public function next_tag($tag) { check($tag === 'SCRIPT', 'Only scripts should be visited'); return ++$this->index < count($this->nodes); } + public function next_tag($tag) { + check(in_array($tag, array('SCRIPT', 'IFRAME'), true), 'Only scripts or app frames should be visited'); + if ($this->index === -1) { $this->nodes = $tag === 'SCRIPT' ? self::$scripts : self::$iframes; } + return ++$this->index < count($this->nodes); + } public function get_attribute($name) { return isset($this->nodes[$this->index][$name]) ? $this->nodes[$this->index][$name] : null; } public function remove_attribute($name) { unset($this->nodes[$this->index][$name]); } public function set_attribute($name, $value) { $this->nodes[$this->index][$name] = $value; } @@ -105,3 +110,54 @@ public function get_updated_html() { return 'updated-public-markup'; } check(\CalorieToken\SiteStyle\Plugin::retire_market_loader($html) === 'updated-public-markup', 'Home also retires the replaced loader'); $front_page = false; echo "Market loader: exact URLs, inert output, renderer dependency and preview/auth/origin boundaries passed.\n"; + +// Camera delegation must be present before the browser navigates the frame. +// The fixture checks our selection/preservation rules; core parses the HTML. +$page_id = 7880; $_SERVER['REQUEST_URI'] = '/index.php/calorieapp/'; +$app_html = '
    '; +$app_frame = array('class'=>'calorieapp-embed-frame', 'title'=>'CalorieApp', 'src'=>'https://app.calorietoken.net/?ui_lang=nl', 'loading'=>'eager', 'data-keep'=>'original'); +foreach (array('app.calorietoken.net', 'calorieapp-frontend.onrender.com') as $host) { + $frame = $app_frame; $frame['src'] = 'https://' . $host . '/?ui_lang=nl'; $frame['allow'] = "fullscreen 'self'"; + WP_HTML_Tag_Processor::$iframes = array($frame); + check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html, 'calorieapp_embed') === 'updated-public-markup', 'Render the permission before iframe navigation'); + $expected = $frame; $expected['allow'] .= '; camera https://' . $host; + check(WP_HTML_Tag_Processor::$last->nodes[0] === $expected, 'Only append the exact camera origin; preserve source, query and all other attributes'); + WP_HTML_Tag_Processor::$iframes = array($expected); + check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Repeated content filtering must be idempotent'); +} +foreach (array("camera 'none'", "fullscreen; CAMERA 'self'", 'camera', "camera https://other.example") as $permission) { + $frame = $app_frame; $frame['allow'] = $permission; WP_HTML_Tag_Processor::$iframes = array($frame); + check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Preserve every explicit camera policy'); +} +foreach (array('srcdoc'=>'

    Custom

    ', 'sandbox'=>'allow-scripts', 'hidden'=>true, 'inert'=>true, 'title'=>'Other app', 'allow'=>true) as $attribute=>$value) { + $frame = $app_frame; $frame[$attribute] = $value; WP_HTML_Tag_Processor::$iframes = array($frame); + check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Do not alter restricted or unrecognized frames'); +} +foreach (array('http://app.calorietoken.net/', 'https://app.calorietoken.net.example/', 'https://user@app.calorietoken.net/', 'https://app.calorietoken.net:8443/', 'https://app.calorietoken.net/other', '/app', null, true) as $src) { + $frame = $app_frame; $frame['src'] = $src; WP_HTML_Tag_Processor::$iframes = array($frame); + check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Reject noncanonical app destinations'); +} +WP_HTML_Tag_Processor::$iframes = array($app_frame, $app_frame); +check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Ambiguous duplicate app frames must not be changed'); +WP_HTML_Tag_Processor::$iframes = array(array('class'=>'video-embed', 'src'=>'https://video.example/'), $app_frame); +check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === 'updated-public-markup', 'Unrelated frames can coexist'); +check(WP_HTML_Tag_Processor::$last->nodes[0] === WP_HTML_Tag_Processor::$iframes[0], 'Unrelated iframe is unchanged'); +WP_HTML_Tag_Processor::$iframes = array($app_frame); +check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html, 'other_shortcode') === $app_html, 'Do not modify another shortcode'); +foreach (array('preview', 'brizy-edit', 'xl-return', 'xl-login', 'unknown') as $query) { + $_GET[$query] = '1'; + check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Editor and auth requests are excluded'); + unset($_GET[$query]); +} +$_GET['ui_lang'] = 'nl'; $_SERVER['REQUEST_URI'] = '/calorieapp/?ui_lang=nl'; +check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === 'updated-public-markup', 'The short public app URL supports display-language selection'); +unset($_GET['ui_lang']); +$page_id = 1207; +check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Only the app page is eligible'); +$page_id = 7880; $preview = true; +check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Preview is excluded'); +$preview = false; $site_url = 'https://other.example/'; +check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Only supported website origins are eligible'); +$site_url = 'https://calorietoken.net/'; $_SERVER['REQUEST_URI'] = '/index.php/login/'; +check(\CalorieToken\SiteStyle\Plugin::delegate_app_camera($app_html) === $app_html, 'Unexpected public routes remain unchanged'); +echo "Camera delegation: early rendered policy, exact origins, existing restrictions, duplicate frames and preview/auth/page boundaries passed.\n"; diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index 69fb57be..badf55fd 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,22 @@ -# CalorieToken Site Style 1.4.12 +# CalorieToken Site Style 1.4.13 Eén complete vervangende ZIP, 11 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.12.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.12** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.13.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.13** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## Correcties op basis van livecontrole van 1.4.11 +## Cameracorrectie na de telefoonproef + +De camera gaf direct een toestemmingsfout zonder vraag om toegang. Een herhaalde telefoonproef gaf dezelfde melding en een kort opflitsend vlak. Dat vlak bewijst geen werkende camerastream; de camera is nog niet mobiel geaccepteerd. De eigenaar bevestigt wel soepel in- en uitloggen. + +Deze versie zet de cameradelegatie al in de geleverde publieke HTML, vóór de eerste navigatie van de ingesloten app. Dat corrigeert de bestaande timingfout: een later toegevoegd `allow`-attribuut verandert volgens de [HTML-standaard](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-allow) geen al geladen document. Zowel nieuwe shortcode-uitvoer als publieke Brizy-/inhouduitvoer wordt verwerkt. Alleen de herkende CalorieApp-iframe op de twee bestaande publieke app-routes krijgt de eigen gecontroleerde camera-oorsprong. Bestaande expliciete beperkingen en overige attributen blijven behouden. De camera start uitsluitend na een klik en de bezoeker blijft over toegang beslissen. + +De aparte frontendcorrectie start de preview expliciet gedempt en inline en houdt een geweigerde videostart gescheiden van geweigerde cameratoegang. Zij bevat ook de eerder voorbereide doorlopende groen-rode productbalk met één aanwijzer. Deze ZIP installeert alleen Site Style; de frontend moet afzonderlijk worden uitgerold. Backend, opgeslagen voeding en de geïnstalleerde Identity Bridge 0.3.29 worden hierbij niet aangepast. + +De exacte foutfase op de telefoon is nog onbekend. De oorspronkelijke live paginabron kon niet worden ingezien door een browserbeveiligingsblokkade. De late delegatie is vastgesteld in de bron; dit pakket neemt die fout weg, maar bewijst nog geen werkende telefooncamera. Na installatie en de afzonderlijke frontenduitrol volgt één gerichte scanproef. De eerdere loginproef hoeft niet opnieuw. + +## Historisch: correcties op basis van livecontrole van 1.4.11 - De bestaande Identity Bridge-footer krijgt de ontbrekende Community Voting Hub-link uit het publieke WordPress-template. De bestaande rij krijgt ook de gedeelde cookieknop en vertalingen. Eigen links, social-carousel en handlers blijven dezelfde nodes; een onbekende aangepaste footer wordt behouden. Zonder gepubliceerde Hub wordt geen bestemming verzonnen. - De exacte oude LiveCoinWatch-loader wordt in de publieke HTML onwerkzaam gemaakt voordat de browser hem kan laden. Dit gebeurt alleen op calorietoken.net als beide bestaande bridge-assets voor de sitebrede XPMarket-weergave worden geleverd. De CMS-bron en andere scripts blijven behouden; editor-, preview- en Xaman-actieverzoeken worden overgeslagen. Zonder de vervangende renderer blijft de bestaande loader staan. diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index 3df87782..5c50adde 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.12 + * Version: 1.4.13 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,45 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.12'; + const VERSION = '1.4.13'; + + public static function delegate_app_camera($html, $shortcode_tag = null) { + if (!is_string($html) || ($shortcode_tag !== null && $shortcode_tag !== 'calorieapp_embed') || + !self::enabled() || !is_page(7880) || !class_exists('WP_HTML_Tag_Processor') || + !in_array(rtrim(home_url('/'), '/'), array('https://calorietoken.net', 'https://www.calorietoken.net'), true)) { return $html; } + foreach (array_keys($_GET) as $key) { if ($key !== 'ui_lang') { return $html; } } + $path = parse_url(isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', PHP_URL_PATH); + if (!in_array($path, array('/index.php/calorieapp/', '/calorieapp/'), true)) { return $html; } + + // Set the permission in rendered HTML, before the iframe's first + // navigation. A late DOM change cannot update an already-loaded policy. + // Filter both fresh shortcode output and cached public Brizy content. + $tags = new \WP_HTML_Tag_Processor($html); + $found = false; + while ($tags->next_tag('IFRAME')) { + $classes = $tags->get_attribute('class'); + if (!is_string($classes) || !in_array('calorieapp-embed-frame', preg_split('/\s+/', trim($classes)), true)) { continue; } + if ($found || $tags->get_attribute('title') !== 'CalorieApp') { return $html; } + $found = true; + foreach (array('srcdoc', 'sandbox', 'hidden', 'inert') as $attribute) { + if ($tags->get_attribute($attribute) !== null) { return $html; } + } + $src = $tags->get_attribute('src'); + $url = is_string($src) ? parse_url($src) : false; + if (!is_array($url) || !isset($url['scheme'], $url['host']) || $url['scheme'] !== 'https' || + isset($url['user']) || isset($url['pass']) || isset($url['port']) || + !in_array($url['host'], array('app.calorietoken.net', 'calorieapp-frontend.onrender.com'), true) || + !in_array(isset($url['path']) ? $url['path'] : '/', array('', '/'), true)) { return $html; } + $permission = $tags->get_attribute('allow'); + if ($permission !== null && !is_string($permission)) { return $html; } + $permission = $permission === null ? '' : $permission; + // Preserve an operator's explicit camera restriction and every + // other directive. Never grant the microphone or a wildcard origin. + if (preg_match('/(?:^|;)\s*camera(?:\s|;|$)/i', $permission)) { return $html; } + $tags->set_attribute('allow', $permission . (trim($permission) !== '' ? '; ' : '') . 'camera https://' . $url['host']); + } + return $found ? $tags->get_updated_html() : $html; + } public static function retire_market_loader($html) { if (!is_string($html) || stripos($html, 'livecoinwatch.com') === false || @@ -199,6 +237,9 @@ public static function templates() { add_filter('body_class', array(Plugin::class, 'body_class')); add_action('wp_enqueue_scripts', array(Plugin::class, 'enqueue'), 99); add_action('wp_footer', array(Plugin::class, 'templates'), 19); +add_filter('brizy_content', array(Plugin::class, 'delegate_app_camera'), 9998); +add_filter('the_content', array(Plugin::class, 'delegate_app_camera'), 9998); +add_filter('do_shortcode_tag', array(Plugin::class, 'delegate_app_camera'), 9998, 2); add_filter('brizy_content', array(Plugin::class, 'retire_market_loader'), 9999); add_filter('the_content', array(Plugin::class, 'retire_market_loader'), 9999); add_filter('do_shortcode_tag', array(Plugin::class, 'retire_market_loader'), 9999); From d013e77b881a41d1a57f9050d6b07fbfb6b88d78 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:10:15 +0200 Subject: [PATCH 021/368] Simplify Testnet faucet requests and distinguish onboarding failures --- .../wordpress_testnet_onboarding.test.mjs | 236 ++++++++++++++++++ .../calorietoken-site-style/LEESMIJ.md | 18 +- .../assets/testnet-data.json | 33 +++ .../calorietoken-site-style/assets/testnet.js | 21 +- .../calorietoken-site-style.php | 4 +- 5 files changed, 300 insertions(+), 12 deletions(-) create mode 100644 tools/tests/wordpress_testnet_onboarding.test.mjs diff --git a/tools/tests/wordpress_testnet_onboarding.test.mjs b/tools/tests/wordpress_testnet_onboarding.test.mjs new file mode 100644 index 00000000..c3b5ac6d --- /dev/null +++ b/tools/tests/wordpress_testnet_onboarding.test.mjs @@ -0,0 +1,236 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import {createRequire} from 'node:module'; +import vm from 'node:vm'; + +const require = createRequire(new URL('../../frontend/package.json', import.meta.url)); +const {parseHTML} = require('linkedom'); +const assets = new URL('../../wordpress-plugins/calorietoken-site-style/assets/', import.meta.url); +const source = readFileSync(new URL('testnet.js', assets), 'utf8'); +const copy = JSON.parse(readFileSync(new URL('testnet-data.json', assets), 'utf8')); +// Shape-only placeholders with invalid checksums: no real wallet or network is used. +const address = 'r' + '1'.repeat(25), secret = 's' + '2'.repeat(24); +const accountReply = () => ({account: {classicAddress: address, secret}}); +const response = (data = accountReply(), {status = 200, type = 'application/json', json} = {}) => ({ + ok: status >= 200 && status < 300, status, + headers: {get: name => name === 'content-type' ? type : null}, + json: json || (async () => data), +}); +const deferred = () => {let resolve, reject; const promise = new Promise((yes, no) => {resolve = yes; reject = no;}); return {promise, resolve, reject};}; +const settle = async () => {await new Promise(setImmediate);}; + +function fixture({fetch = async () => response(), locale = 'nl', url = 'https://calorietoken.net/index.php/calorieapp/', bodyClass = 'ctstyle-enabled page-id-7880'} = {}) { + const {document, window: dom} = parseHTML(`
    `); + const requests = [], sockets = [], timers = new Map(), events = new Map(), clipboard = []; + let sequence = 0; + const forbidden = () => {throw new Error('Unexpected storage, logging or message transport');}; + class Socket { + constructor(url) {this.url = url; this.sent = []; this.closed = false; sockets.push(this);} + send(text) {this.sent.push(JSON.parse(text));} + close() {this.closed = true;} + open() {this.onopen?.();} + message(data) {this.onmessage?.({data: JSON.stringify(data)});} + fail() {this.onerror?.();} + } + const window = { + location: new URL(url), + CalorieTokenDiscovery: {page: 7880, testCopy: copy}, + fetch: (url, options) => {requests.push({url, options}); return fetch(url, options);}, + AbortController, WebSocket: Socket, + setTimeout(fn, delay) {const id = ++sequence; timers.set(id, {fn, delay}); return id;}, + clearTimeout: id => timers.delete(id), + addEventListener(name, fn) {events.set(name, [...events.get(name) || [], fn]);}, + postMessage: forbidden, + }; + Object.defineProperties(window, {localStorage: {get: forbidden}, sessionStorage: {get: forbidden}}); + vm.runInNewContext(source, {window, document, URL, + console: {log: forbidden, error: forbidden, warn: forbidden, debug: forbidden}, + navigator: {clipboard: {writeText: async text => {clipboard.push(text);}}, sendBeacon: forbidden}, + }); + document.dispatchEvent(new dom.Event('DOMContentLoaded')); + const query = selector => document.querySelector(selector); + return {document, window, requests, sockets, timers, clipboard, query, + click(selector) {const node = query(selector); assert.ok(node, selector); node.dispatchEvent(new dom.Event('click'));}, + clickCopy(key) {const node = [...document.querySelectorAll('button')].find(node => node.textContent === copy[locale][key]); assert.ok(node, key); node.dispatchEvent(new dom.Event('click'));}, + status: () => query('.ctstyle-discovery-status')?.textContent, + emit(name) {for (const fn of events.get(name) || []) fn();}, + expire(delay) {for (const [id, timer] of [...timers]) if (timer.delay === delay) {timers.delete(id); timer.fn();}}, + funded(socket = sockets.at(-1)) {socket.open(); socket.message({id: 1, status: 'success', result: {validated: true, account_data: {Account: address, Balance: '10000000'}}});}, + }; +} + +test('Creating an account requires a click and sends one minimal, credential-free Testnet POST', async () => { + const pending = deferred(), h = fixture({fetch: () => pending.promise}); + assert.equal(h.requests.length, 0); + h.window.CalorieTokenTestnet.refresh('nl'); h.emit('load'); + assert.equal(h.requests.length, 0); + h.click('#ctstyle-testnet-create-button'); h.click('#ctstyle-testnet-create-button'); + assert.equal(h.requests.length, 1, 'A second click while waiting cannot create another account'); + const {url, options} = h.requests[0]; + assert.equal(url, 'https://faucet.altnet.rippletest.net/accounts'); + assert.equal(options.method, 'POST'); assert.equal(options.mode, 'cors'); + assert.equal(options.credentials, 'omit'); assert.equal(options.redirect, 'error'); + assert.equal(options.referrerPolicy, 'no-referrer'); assert.equal(options.cache, 'no-store'); + assert.equal(options.body, undefined, 'Creating a fresh account needs no destination or metadata'); + assert.equal(options.headers, undefined, 'Do not force an unnecessary JSON preflight'); + pending.resolve(response()); await settle(); + h.funded(); await settle(); + assert.equal(h.status(), copy.nl.funded); + assert.equal(h.timers.size, 0); +}); + +test('Both documented address fields are accepted and funding is independently checked on Testnet', async () => { + for (const field of ['classicAddress', 'address']) { + const h = fixture({fetch: async () => response({account: {[field]: address, secret}})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(), copy.nl.checking); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); + assert.equal(h.clipboard.length, 0); + const socket = h.sockets[0]; assert.equal(socket.url, 'wss://s.altnet.rippletest.net:51233/'); + h.funded(); await settle(); + assert.deepEqual(socket.sent, [{id: 1, command: 'account_info', account: address, ledger_index: 'validated', strict: true}]); + assert.equal(socket.closed, true); assert.equal(h.status(), copy.nl.funded); + h.click('#ctstyle-testnet-create-button'); assert.equal(h.requests.length, 1); + h.clickCopy('next'); h.clickCopy('show'); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, secret); + h.clickCopy('next'); assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); + assert.equal([...h.document.querySelectorAll('a')].some(node => node.href.includes(secret) || node.href.includes(address)), false); + } +}); + +test('An unconfirmed balance retains the account and Check test XRP never calls the faucet again', async () => { + const h = fixture(); h.click('#ctstyle-testnet-create-button'); await settle(); + h.sockets[0].fail(); await settle(); + assert.equal(h.status(), copy.nl.pending); + assert.equal(h.query('#ctstyle-testnet-address').textContent, address); + h.clickCopy('check'); await settle(); h.funded(); await settle(); + assert.equal(h.requests.length, 1); assert.equal(h.sockets.length, 2); + assert.equal(h.status(), copy.nl.funded); +}); + +test('A rejected browser request reports a connection problem without claiming the faucet returned an error', async () => { + const h = fixture({fetch: async () => {throw new TypeError('Failed to fetch');}}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(), copy.nl.connectionFailed); + assert.notEqual(h.status(), copy.nl.failed); + assert.equal(h.query('#ctstyle-testnet-create-button').disabled, false); + assert.equal(h.query('.ctstyle-testnet-result').hidden, true); + assert.equal(h.query('a[href="https://xrpl.org/resources/dev-tools/xrp-faucets"]').hidden, false); + assert.equal(h.requests.length, 1); assert.equal(h.sockets.length, 0); assert.equal(h.timers.size, 0); +}); + +test('The request timeout aborts once, reports an unknown outcome and never retries automatically', async () => { + const h = fixture({fetch: (_url, options) => new Promise((_resolve, reject) => { + options.signal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), {once: true}); + })}); + h.click('#ctstyle-testnet-create-button'); h.expire(25000); await settle(); + assert.equal(h.requests[0].options.signal.aborted, true); + assert.equal(h.status(), copy.nl.timedOut); assert.equal(h.requests.length, 1); + assert.equal(h.sockets.length, 0); assert.equal(h.timers.size, 0); +}); + +test('An account body delivered after the deadline is not exposed or verified', async () => { + const body = deferred(), h = fixture({fetch: async () => response(null, {json: () => body.promise})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + h.expire(25000); body.resolve(accountReply()); await settle(); + assert.equal(h.status(), copy.nl.timedOut); + assert.equal(h.query('#ctstyle-testnet-address').textContent, ''); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); + assert.equal(h.sockets.length, 0); +}); + +test('HTML, malformed JSON and incomplete account data produce an unusable-response message without showing partial credentials', async () => { + const cases = [ + response(null, {type: 'text/html'}), + response(null, {json: async () => {throw new SyntaxError('Malformed JSON');}}), + response(null), response({account: {classicAddress: address}}), + response({account: {classicAddress: '', secret}}), + response({account: {classicAddress: address, secret: ''}}), + ]; + for (const reply of cases) { + const h = fixture({fetch: async () => reply}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(), copy.nl.invalidResponse); + assert.equal(h.query('#ctstyle-testnet-address').textContent, ''); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); + assert.equal(h.sockets.length, 0); assert.equal(h.timers.size, 0); + } +}); + +test('Rate limiting remains distinct from other HTTP failures and response bodies are not displayed', async () => { + for (const [code, key] of [[429, 'limited'], [403, 'failed'], [503, 'failed']]) { + const h = fixture({fetch: async () => response(null, {status: code, json: async () => {throw new Error('An error body must not be read');}})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(), copy.nl[key]); assert.equal(h.requests.length, 1); + assert.equal(h.query('.ctstyle-testnet-result').hidden, true); + } +}); + +test('Leaving the page aborts creation and an old response cannot overwrite a new visit', async () => { + const old = deferred(), latest = deferred(); let calls = 0; + const h = fixture({fetch: () => (++calls === 1 ? old : latest).promise}); + h.click('#ctstyle-testnet-create-button'); h.emit('pagehide'); + assert.equal(h.requests[0].options.signal.aborted, true); + h.emit('pageshow'); h.click('#ctstyle-testnet-create-button'); + old.resolve(response()); await settle(); + assert.equal(h.status(), copy.nl.creating); assert.equal(h.query('#ctstyle-testnet-address').textContent, ''); + latest.reject(new TypeError('Failed to fetch')); await settle(); + assert.equal(h.status(), copy.nl.connectionFailed); assert.equal(h.sockets.length, 0); +}); + +test('Leaving the page clears revealed credentials and cancels the independent balance check', async () => { + const h = fixture(); h.click('#ctstyle-testnet-create-button'); await settle(); + h.clickCopy('next'); h.clickCopy('show'); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, secret); + h.emit('pagehide'); await settle(); + assert.equal(h.sockets[0].closed, true); assert.equal(h.timers.size, 0); + h.emit('pageshow'); + assert.equal(h.query('#ctstyle-testnet-address').textContent, ''); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); + assert.equal(h.query('.ctstyle-testnet-result').hidden, true); + assert.equal(h.status(), copy.nl.ready); +}); + +test('Only a validated positive balance for the returned account can be reported as funded', async () => { + for (const result of [ + {validated: false, account_data: {Account: address, Balance: '10000000'}}, + {validated: true, account_data: {Account: 'r' + '3'.repeat(25), Balance: '10000000'}}, + {validated: true, account_data: {Account: address, Balance: '0'}}, + {validated: true, account_data: {Account: address, Balance: 10000000}}, + ]) { + const h = fixture(); h.click('#ctstyle-testnet-create-button'); await settle(); + h.sockets[0].message({id: 1, status: 'success', result}); await settle(); + assert.equal(h.status(), copy.nl.pending); assert.equal(h.requests.length, 1); + } + const h = fixture(); h.click('#ctstyle-testnet-create-button'); await settle(); + h.expire(15000); await settle(); + assert.equal(h.status(), copy.nl.pending); assert.equal(h.sockets[0].closed, true); +}); + +test('Account creation stays absent outside the permitted public app page', () => { + for (const options of [ + {url: 'https://calorietoken.net/index.php/calorieapp/?preview=1'}, + {url: 'https://calorietoken.net/index.php/blog/'}, + {url: 'https://example.org/index.php/calorieapp/'}, + {bodyClass: 'ctstyle-enabled page-id-7880 brz-ed'}, + {bodyClass: 'page-id-7880'}, + ]) { + const h = fixture(options); assert.equal(h.query('#ctstyle-testnet-create-button'), null); + assert.equal(h.requests.length, 0); + } +}); + +test('All eleven languages explain each failure state and a language change retains that state', async () => { + const h = fixture({fetch: async () => {throw new TypeError('Failed to fetch');}}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(Object.keys(copy).length, 11); + for (const [locale, words] of Object.entries(copy)) { + for (const key of ['connectionFailed', 'timedOut', 'invalidResponse', 'failed', 'limited']) { + assert.equal(typeof words[key], 'string', `${locale}.${key}`); assert.ok(words[key].trim()); + } + h.window.CalorieTokenTestnet.refresh(locale); + assert.equal(h.status(), words.connectionFailed); + } + assert.equal(h.requests.length, 1); +}); diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index badf55fd..a65e5c5e 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,24 @@ -# CalorieToken Site Style 1.4.13 +# CalorieToken Site Style 1.4.14 Eén complete vervangende ZIP, 11 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.13.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.13** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.14.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.14** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## Cameracorrectie na de telefoonproef +## Testaccountmelding na de telefoonproef + +De eigenaar kreeg na ‘Maak mijn gratis testaccount’ een algemene faucetfout. Die melding werd ook getoond als de browser geen antwoord kon lezen, de wachttijd verstreek of accountgegevens ontbraken. De screenshot bepaalt niet welke oorzaak optrad. Een rechtstreekse, alleen-lezen verbindingscontrole vanuit de werkomgeving kon niet worden uitgevoerd; er is geen actuele foutstatus van de faucet vastgesteld. + +Deze versie gebruikt de [door XRPLF gedocumenteerde lege POST-aanvraag](https://github.com/XRPLF/xrpl.js/wiki/Using-the-Testnet-and-Devnet-faucets-programmatically) voor een nieuw Testnet-account. De onnodige JSON-metadata vervalt. Zo veroorzaakt de aanvraag zelf geen aanvullende JSON-preflight; de browser blijft de [CORS-toestemming voor het antwoord](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) controleren. Dit is een compatibiliteitsverbetering, geen bewijs dat preflight de telefoonfout veroorzaakte. + +Alle elf talen onderscheiden nu een ontbrekend antwoord, een time-out en onbruikbare accountgegevens. De bestaande meldingen voor HTTP-fouten en begrenzing door de faucet blijven beschikbaar. Na een time-out wordt niet beweerd dat er zeker geen account is gemaakt; late accountgegevens worden niet alsnog gebruikt. De officiële faucet blijft bereikbaar via de bestaande knop. Er komen geen automatische herhaalverzoeken. Een aangemaakt account blijft behouden als alleen de afzonderlijke saldoverificatie mislukt. + +Dertien gerichte DOM-controles gebruiken uitsluitend synthetische antwoorden en ongeldige voorbeeldsleutels. Zij testen ook dubbelklikken, de twee ondersteunde adresvelden, positieve gevalideerde Testnet-saldi, opruimen bij vertrek en behoud van de foutstatus bij taalwisselen. Zij bewijzen geen werkende verbinding op een echte telefoon. Er is tijdens deze voorbereiding geen echte faucetaanvraag verstuurd en geen testaccount gemaakt. + +De cameracorrectie uit 1.4.13 blijft inbegrepen. De aparte frontendcorrectie voor de camerapreview en de doorlopende productbalk wacht nog op toestemming voor Render-uitrol. Deze ZIP wijzigt die appuitrol niet. Plan de nog benodigde telefoonproeven samen na installatie en de afzonderlijk goedgekeurde frontenduitrol. + +## Inbegrepen: cameracorrectie uit 1.4.13 De camera gaf direct een toestemmingsfout zonder vraag om toegang. Een herhaalde telefoonproef gaf dezelfde melding en een kort opflitsend vlak. Dat vlak bewijst geen werkende camerastream; de camera is nog niet mobiel geaccepteerd. De eigenaar bevestigt wel soepel in- en uitloggen. diff --git a/wordpress-plugins/calorietoken-site-style/assets/testnet-data.json b/wordpress-plugins/calorietoken-site-style/assets/testnet-data.json index 467f99ac..2d0c122c 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/testnet-data.json +++ b/wordpress-plugins/calorietoken-site-style/assets/testnet-data.json @@ -9,6 +9,9 @@ "funded": "Your account has test XRP. Now add it to Xaman below.", "pending": "Account created; funding could not yet be confirmed. Keep this recovery code and use Check test XRP again.", "failed": "The faucet could not complete this request. Wait a moment and try again, or open the official faucet below.", + "connectionFailed": "Your browser could not receive a reply from the test account service. Open the official faucet below, using your usual browser if needed.", + "timedOut": "The test account service did not reply in time. We do not know whether an account was created. Try later or open the official faucet below.", + "invalidResponse": "The test account service did not return usable account details. Open the official faucet below.", "limited": "The faucet is busy or has reached its limit. Please try later; no payment is required.", "unsupported": "This browser cannot create and check the account here. Use the official faucet below.", "account": "Your test account", @@ -46,6 +49,9 @@ "funded": "Je account heeft test-XRP. Voeg het nu hieronder toe aan Xaman.", "pending": "Account aangemaakt; het testtegoed is nog niet bevestigd. Bewaar deze herstelcode en gebruik Test-XRP controleren opnieuw.", "failed": "De faucet kon dit verzoek niet afronden. Wacht even en probeer opnieuw, of open hieronder de officiële faucet.", + "connectionFailed": "Je browser kon geen antwoord van de testaccountdienst ontvangen. Open de officiële faucet hieronder, zo nodig in je gewone browser.", + "timedOut": "De testaccountdienst antwoordde niet op tijd. Het is onbekend of er een account is gemaakt. Probeer het later of open de officiële faucet hieronder.", + "invalidResponse": "De testaccountdienst gaf geen bruikbare accountgegevens terug. Open de officiële faucet hieronder.", "limited": "De faucet is druk of heeft zijn limiet bereikt. Probeer het later; je hoeft niet te betalen.", "unsupported": "Deze browser kan het account hier niet aanmaken en controleren. Gebruik hieronder de officiële faucet.", "account": "Jouw testaccount", @@ -83,6 +89,9 @@ "funded": "账户已有测试 XRP。请按下方步骤添加到 Xaman。", "pending": "账户已创建,但尚未确认测试资金。请保留恢复代码并再次检查测试 XRP。", "failed": "水龙头未能完成请求。请稍后重试,或打开下方官方水龙头。", + "connectionFailed": "浏览器未能收到测试账户服务的回复。请打开下方的官方水龙头,必要时使用你平时的浏览器。", + "timedOut": "测试账户服务未能及时回复。我们无法确定账户是否已创建。请稍后重试,或打开下方的官方水龙头。", + "invalidResponse": "测试账户服务未返回可用的账户信息。请打开下方的官方水龙头。", "limited": "水龙头繁忙或已达限额。请稍后再试,无需付款。", "unsupported": "此浏览器无法在这里创建并检查账户。请使用下方官方水龙头。", "account": "您的测试账户", @@ -120,6 +129,9 @@ "funded": "खाते में टेस्ट XRP हैं। अब नीचे दिए चरणों से Xaman में जोड़ें।", "pending": "खाता बन गया; निधि की पुष्टि अभी नहीं हुई। रिकवरी कोड रखें और टेस्ट XRP फिर जाँचें।", "failed": "फ़ॉसेट अनुरोध पूरा नहीं कर पाया। थोड़ी देर बाद फिर कोशिश करें या नीचे आधिकारिक फ़ॉसेट खोलें।", + "connectionFailed": "आपके ब्राउज़र को टेस्ट खाते की सेवा से जवाब नहीं मिला। नीचे आधिकारिक फ़ॉसेट खोलें; ज़रूरत हो तो अपना सामान्य ब्राउज़र इस्तेमाल करें।", + "timedOut": "टेस्ट खाते की सेवा ने समय पर जवाब नहीं दिया। हमें नहीं पता कि खाता बना या नहीं। बाद में कोशिश करें या नीचे आधिकारिक फ़ॉसेट खोलें।", + "invalidResponse": "टेस्ट खाते की सेवा से इस्तेमाल करने योग्य खाते की जानकारी नहीं मिली। नीचे आधिकारिक फ़ॉसेट खोलें।", "limited": "फ़ॉसेट व्यस्त है या उसकी सीमा पूरी हो गई है। बाद में फिर कोशिश करें; भुगतान नहीं चाहिए।", "unsupported": "यह ब्राउज़र यहाँ खाता बनाकर जाँच नहीं सकता। नीचे आधिकारिक फ़ॉसेट उपयोग करें।", "account": "आपका टेस्ट खाता", @@ -157,6 +169,9 @@ "funded": "Tu cuenta tiene XRP de prueba. Añádela a Xaman siguiendo los pasos de abajo.", "pending": "Cuenta creada; aún no se pudo confirmar el saldo. Conserva el código y vuelve a comprobar los XRP de prueba.", "failed": "El faucet no pudo completar la solicitud. Espera y reinténtalo o abre el faucet oficial de abajo.", + "connectionFailed": "Tu navegador no pudo recibir una respuesta del servicio de cuentas de prueba. Abre el faucet oficial de abajo, usando tu navegador habitual si hace falta.", + "timedOut": "El servicio de cuentas de prueba no respondió a tiempo. No sabemos si se creó una cuenta. Inténtalo más tarde o abre el faucet oficial de abajo.", + "invalidResponse": "El servicio de cuentas de prueba no devolvió datos de cuenta utilizables. Abre el faucet oficial de abajo.", "limited": "El faucet está ocupado o alcanzó su límite. Inténtalo más tarde; no necesitas pagar.", "unsupported": "Este navegador no puede crear y comprobar la cuenta aquí. Usa el faucet oficial de abajo.", "account": "Tu cuenta de prueba", @@ -194,6 +209,9 @@ "funded": "يحتوي حسابك على XRP تجريبي. أضفه الآن إلى Xaman بالخطوات أدناه.", "pending": "تم إنشاء الحساب؛ لم يتأكد الرصيد بعد. احتفظ برمز الاسترداد وأعد التحقق من XRP التجريبي.", "failed": "تعذر إكمال الطلب. انتظر قليلًا وحاول مجددًا أو افتح الصنبور الرسمي أدناه.", + "connectionFailed": "لم يتمكن متصفحك من تلقي رد من خدمة الحسابات التجريبية. افتح الصنبور الرسمي أدناه، واستخدم متصفحك المعتاد إذا لزم الأمر.", + "timedOut": "لم ترد خدمة الحسابات التجريبية في الوقت المحدد. لا نعرف ما إذا تم إنشاء حساب. حاول لاحقًا أو افتح الصنبور الرسمي أدناه.", + "invalidResponse": "لم تُرجع خدمة الحسابات التجريبية بيانات حساب قابلة للاستخدام. افتح الصنبور الرسمي أدناه.", "limited": "الصنبور مشغول أو بلغ الحد. حاول لاحقًا؛ لا حاجة إلى الدفع.", "unsupported": "لا يستطيع هذا المتصفح إنشاء الحساب والتحقق منه هنا. استخدم الصنبور الرسمي أدناه.", "account": "حساب الاختبار الخاص بك", @@ -231,6 +249,9 @@ "funded": "Votre compte dispose de XRP de test. Ajoutez-le maintenant à Xaman ci-dessous.", "pending": "Compte créé ; les fonds ne sont pas encore confirmés. Gardez ce code et vérifiez à nouveau les XRP de test.", "failed": "Le faucet n’a pas pu terminer la demande. Réessayez dans un moment ou ouvrez le faucet officiel ci-dessous.", + "connectionFailed": "Votre navigateur n’a pas pu recevoir de réponse du service de comptes de test. Ouvrez le faucet officiel ci-dessous, avec votre navigateur habituel si nécessaire.", + "timedOut": "Le service de comptes de test n’a pas répondu à temps. Nous ne savons pas si un compte a été créé. Réessayez plus tard ou ouvrez le faucet officiel ci-dessous.", + "invalidResponse": "Le service de comptes de test n’a pas renvoyé de données de compte utilisables. Ouvrez le faucet officiel ci-dessous.", "limited": "Le faucet est occupé ou a atteint sa limite. Réessayez plus tard ; aucun paiement nécessaire.", "unsupported": "Ce navigateur ne peut pas créer et vérifier le compte ici. Utilisez le faucet officiel ci-dessous.", "account": "Votre compte de test", @@ -268,6 +289,9 @@ "funded": "আপনার অ্যাকাউন্টে টেস্ট XRP আছে। নিচের ধাপ অনুসরণ করে Xaman-এ যোগ করুন।", "pending": "অ্যাকাউন্ট তৈরি হয়েছে; তহবিল এখনও নিশ্চিত হয়নি। রিকভারি কোড রাখুন এবং আবার টেস্ট XRP যাচাই করুন।", "failed": "ফসেট অনুরোধ সম্পূর্ণ করতে পারেনি। কিছুক্ষণ পর চেষ্টা করুন বা নিচে অফিসিয়াল ফসেট খুলুন।", + "connectionFailed": "আপনার ব্রাউজার পরীক্ষামূলক অ্যাকাউন্টের সেবা থেকে উত্তর পায়নি। নিচের অফিসিয়াল ফসেট খুলুন, প্রয়োজনে আপনার নিয়মিত ব্রাউজার ব্যবহার করুন।", + "timedOut": "পরীক্ষামূলক অ্যাকাউন্টের সেবা সময়মতো উত্তর দেয়নি। অ্যাকাউন্ট তৈরি হয়েছে কি না জানা যায়নি। পরে চেষ্টা করুন অথবা নিচের অফিসিয়াল ফসেট খুলুন।", + "invalidResponse": "পরীক্ষামূলক অ্যাকাউন্টের সেবা ব্যবহারযোগ্য অ্যাকাউন্টের তথ্য দেয়নি। নিচের অফিসিয়াল ফসেট খুলুন।", "limited": "ফসেট ব্যস্ত বা সীমা পূর্ণ হয়েছে। পরে চেষ্টা করুন; পেমেন্ট লাগবে না।", "unsupported": "এই ব্রাউজার এখানে অ্যাকাউন্ট তৈরি ও যাচাই করতে পারে না। নিচের অফিসিয়াল ফসেট ব্যবহার করুন।", "account": "আপনার টেস্ট অ্যাকাউন্ট", @@ -305,6 +329,9 @@ "funded": "Sua conta tem XRP de teste. Agora adicione-a à Xaman abaixo.", "pending": "Conta criada; os fundos ainda não foram confirmados. Guarde o código e verifique o XRP de teste novamente.", "failed": "O faucet não concluiu a solicitação. Espere um momento e tente novamente ou abra o faucet oficial abaixo.", + "connectionFailed": "O navegador não conseguiu receber uma resposta do serviço de contas de teste. Abra o faucet oficial abaixo, usando seu navegador habitual se necessário.", + "timedOut": "O serviço de contas de teste não respondeu a tempo. Não sabemos se uma conta foi criada. Tente mais tarde ou abra o faucet oficial abaixo.", + "invalidResponse": "O serviço de contas de teste não retornou dados de conta utilizáveis. Abra o faucet oficial abaixo.", "limited": "O faucet está ocupado ou atingiu o limite. Tente mais tarde; não é preciso pagar.", "unsupported": "Este navegador não consegue criar e verificar a conta aqui. Use o faucet oficial abaixo.", "account": "Sua conta de teste", @@ -342,6 +369,9 @@ "funded": "Akun Anda memiliki XRP uji. Tambahkan ke Xaman dengan langkah di bawah.", "pending": "Akun dibuat; dana belum terkonfirmasi. Simpan kode pemulihan dan periksa XRP uji lagi.", "failed": "Faucet tidak dapat menyelesaikan permintaan. Tunggu lalu coba lagi, atau buka faucet resmi di bawah.", + "connectionFailed": "Browser Anda tidak dapat menerima balasan dari layanan akun uji. Buka faucet resmi di bawah, menggunakan browser yang biasa Anda pakai jika perlu.", + "timedOut": "Layanan akun uji tidak membalas tepat waktu. Kami belum tahu apakah akun sudah dibuat. Coba lagi nanti atau buka faucet resmi di bawah.", + "invalidResponse": "Layanan akun uji tidak mengembalikan data akun yang dapat digunakan. Buka faucet resmi di bawah.", "limited": "Faucet sibuk atau mencapai batas. Coba nanti; tidak perlu membayar.", "unsupported": "Browser ini tidak dapat membuat dan memeriksa akun di sini. Gunakan faucet resmi di bawah.", "account": "Akun uji Anda", @@ -379,6 +409,9 @@ "funded": "آپ کے اکاؤنٹ میں ٹیسٹ XRP ہیں۔ نیچے کے مراحل سے اسے Xaman میں شامل کریں۔", "pending": "اکاؤنٹ بن گیا؛ رقم کی تصدیق ابھی نہیں ہوئی۔ بازیابی کوڈ رکھیں اور ٹیسٹ XRP دوبارہ چیک کریں۔", "failed": "فاسٹ درخواست مکمل نہیں کر سکا۔ کچھ دیر بعد کوشش کریں یا نیچے سرکاری فاسٹ کھولیں۔", + "connectionFailed": "آپ کے براؤزر کو ٹیسٹ اکاؤنٹ کی سروس سے جواب نہیں ملا۔ نیچے آفیشل فاسٹ کھولیں، ضرورت ہو تو اپنا معمول کا براؤزر استعمال کریں۔", + "timedOut": "ٹیسٹ اکاؤنٹ کی سروس نے وقت پر جواب نہیں دیا۔ ہمیں معلوم نہیں کہ اکاؤنٹ بنا ہے یا نہیں۔ بعد میں کوشش کریں یا نیچے آفیشل فاسٹ کھولیں۔", + "invalidResponse": "ٹیسٹ اکاؤنٹ کی سروس نے قابل استعمال اکاؤنٹ کی معلومات واپس نہیں دیں۔ نیچے آفیشل فاسٹ کھولیں۔", "limited": "فاسٹ مصروف ہے یا حد پوری ہو گئی ہے۔ بعد میں کوشش کریں؛ ادائیگی کی ضرورت نہیں۔", "unsupported": "یہ براؤزر یہاں اکاؤنٹ بنا کر چیک نہیں کر سکتا۔ نیچے سرکاری فاسٹ استعمال کریں۔", "account": "آپ کا ٹیسٹ اکاؤنٹ", diff --git a/wordpress-plugins/calorietoken-site-style/assets/testnet.js b/wordpress-plugins/calorietoken-site-style/assets/testnet.js index be8b722e..08343c0f 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/testnet.js +++ b/wordpress-plugins/calorietoken-site-style/assets/testnet.js @@ -82,29 +82,36 @@ if (typeof window.fetch !== 'function' || typeof window.AbortController !== 'function' || typeof window.WebSocket !== 'function') { status = 'unsupported'; render(); return; } - var current = generation, timeout, response, request = new window.AbortController(); + var current = generation, timeout, response, timedOut = false, request = new window.AbortController(); busy = true; status = 'creating'; render(); controller = request; try { - timeout = window.setTimeout(function () { request.abort(); },25000); + timeout = window.setTimeout(function () { timedOut = true; request.abort(); },25000); // No destination supplied: the official faucet creates a fresh TESTNET wallet. // Its secret is returned to this browser, never to WordPress or the CalorieApp. + // The documented empty POST needs no JSON metadata or additional CORS preflight. + // The browser still enforces the faucet's cross-origin response permission. response = await window.fetch(faucet,{method:'POST',mode:'cors',credentials:'omit',cache:'no-store', - redirect:'error',referrerPolicy:'no-referrer',signal:request.signal, - headers:{'Content-Type':'application/json'},body:JSON.stringify({userAgent:'CalorieToken-Testnet',usageContext:'CalorieApp test account'})}); + redirect:'error',referrerPolicy:'no-referrer',signal:request.signal}); if (current !== generation || !allowed()) return; + if (timedOut) { status = 'timedOut'; return; } if (!response.ok) { status = response.status === 429 ? 'limited' : 'failed'; return; } - if (!(response.headers.get('content-type') || '').toLowerCase().startsWith('application/json')) { status = 'failed'; return; } + if (!(response.headers.get('content-type') || '').toLowerCase().startsWith('application/json')) { status = 'invalidResponse'; return; } var data = await response.json(), wallet = data && data.account; if (current !== generation || !allowed()) return; + if (timedOut) { status = 'timedOut'; return; } var address = wallet && (wallet.classicAddress || wallet.address), secret = wallet && wallet.secret; // Schema checks only; the official Testnet ledger independently verifies funding below. if (typeof address !== 'string' || !/^r[1-9A-HJ-NP-Za-km-z]{24,34}$/.test(address) || - typeof secret !== 'string' || !/^s[1-9A-HJ-NP-Za-km-z]{20,34}$/.test(secret)) { status = 'failed'; return; } + typeof secret !== 'string' || !/^s[1-9A-HJ-NP-Za-km-z]{20,34}$/.test(secret)) { status = 'invalidResponse'; return; } if (current !== generation || !allowed()) return; account = {address:address,secret:secret}; step = 1; ui.address.textContent = address; status = 'created'; - } catch (_) { if (current === generation && allowed()) status = 'failed'; } + } catch (error) { + if (current === generation && allowed()) { + status = timedOut ? 'timedOut' : error && error.name === 'SyntaxError' ? 'invalidResponse' : 'connectionFailed'; + } + } finally { window.clearTimeout(timeout); if (controller === request) controller = null; if (current === generation && allowed()) { busy = false; render(); } diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index 5c50adde..64434aca 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.13 + * Version: 1.4.14 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.13'; + const VERSION = '1.4.14'; public static function delegate_app_camera($html, $shortcode_tag = null) { if (!is_string($html) || ($shortcode_tag !== null && $shortcode_tag !== 'calorieapp_embed') || From 4cbc31e850d98c4e58df547ea38248e033274d23 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:36:40 +0200 Subject: [PATCH 022/368] Bound Testnet UI retries and show translated waiting times --- .../wordpress_testnet_onboarding.test.mjs | 122 ++++++++++++++++-- .../calorietoken-site-style/LEESMIJ.md | 18 ++- .../assets/testnet-data.json | 44 +++++-- .../calorietoken-site-style/assets/testnet.js | 57 +++++++- .../calorietoken-site-style.php | 4 +- 5 files changed, 212 insertions(+), 33 deletions(-) diff --git a/tools/tests/wordpress_testnet_onboarding.test.mjs b/tools/tests/wordpress_testnet_onboarding.test.mjs index c3b5ac6d..a33d4f91 100644 --- a/tools/tests/wordpress_testnet_onboarding.test.mjs +++ b/tools/tests/wordpress_testnet_onboarding.test.mjs @@ -12,18 +12,20 @@ const copy = JSON.parse(readFileSync(new URL('testnet-data.json', assets), 'utf8 // Shape-only placeholders with invalid checksums: no real wallet or network is used. const address = 'r' + '1'.repeat(25), secret = 's' + '2'.repeat(24); const accountReply = () => ({account: {classicAddress: address, secret}}); -const response = (data = accountReply(), {status = 200, type = 'application/json', json} = {}) => ({ +const response = (data = accountReply(), {status = 200, type = 'application/json', json, retryAfter = null} = {}) => ({ ok: status >= 200 && status < 300, status, - headers: {get: name => name === 'content-type' ? type : null}, + headers: {get: name => name === 'content-type' ? type : name === 'retry-after' ? retryAfter : null}, json: json || (async () => data), }); const deferred = () => {let resolve, reject; const promise = new Promise((yes, no) => {resolve = yes; reject = no;}); return {promise, resolve, reject};}; const settle = async () => {await new Promise(setImmediate);}; -function fixture({fetch = async () => response(), locale = 'nl', url = 'https://calorietoken.net/index.php/calorieapp/', bodyClass = 'ctstyle-enabled page-id-7880'} = {}) { +function fixture({fetch = async () => response(), locale = 'nl', url = 'https://calorietoken.net/index.php/calorieapp/', bodyClass = 'ctstyle-enabled page-id-7880', storage = new Map(), storageBlocked = false} = {}) { const {document, window: dom} = parseHTML(`
    `); const requests = [], sockets = [], timers = new Map(), events = new Map(), clipboard = []; let sequence = 0; + let now = Date.UTC(2026,8,11,12,0,0); + class Clock extends Date {static now() {return now;}} const forbidden = () => {throw new Error('Unexpected storage, logging or message transport');}; class Socket { constructor(url) {this.url = url; this.sent = []; this.closed = false; sockets.push(this);} @@ -43,14 +45,19 @@ function fixture({fetch = async () => response(), locale = 'nl', url = 'https:// addEventListener(name, fn) {events.set(name, [...events.get(name) || [], fn]);}, postMessage: forbidden, }; - Object.defineProperties(window, {localStorage: {get: forbidden}, sessionStorage: {get: forbidden}}); - vm.runInNewContext(source, {window, document, URL, + Object.defineProperties(window, {localStorage: {get: forbidden}, sessionStorage: {get() { + if (storageBlocked) throw new Error('Storage unavailable'); + return {getItem: key => storage.get(key) ?? null, setItem: (key, value) => storage.set(key,value)}; + }}}); + vm.runInNewContext(source, {window, document, URL, Date: Clock, console: {log: forbidden, error: forbidden, warn: forbidden, debug: forbidden}, navigator: {clipboard: {writeText: async text => {clipboard.push(text);}}, sendBeacon: forbidden}, }); document.dispatchEvent(new dom.Event('DOMContentLoaded')); const query = selector => document.querySelector(selector); - return {document, window, requests, sockets, timers, clipboard, query, + return {document, window, requests, sockets, timers, clipboard, query, storage, + advance(milliseconds) {now += milliseconds; window.CalorieTokenTestnet.refresh();}, + elapse(milliseconds) {now += milliseconds;}, click(selector) {const node = query(selector); assert.ok(node, selector); node.dispatchEvent(new dom.Event('click'));}, clickCopy(key) {const node = [...document.querySelectorAll('button')].find(node => node.textContent === copy[locale][key]); assert.ok(node, key); node.dispatchEvent(new dom.Event('click'));}, status: () => query('.ctstyle-discovery-status')?.textContent, @@ -104,7 +111,7 @@ test('An unconfirmed balance retains the account and Check test XRP never calls h.sockets[0].fail(); await settle(); assert.equal(h.status(), copy.nl.pending); assert.equal(h.query('#ctstyle-testnet-address').textContent, address); - h.clickCopy('check'); await settle(); h.funded(); await settle(); + h.advance(15000); h.clickCopy('check'); await settle(); h.funded(); await settle(); assert.equal(h.requests.length, 1); assert.equal(h.sockets.length, 2); assert.equal(h.status(), copy.nl.funded); }); @@ -114,10 +121,10 @@ test('A rejected browser request reports a connection problem without claiming t h.click('#ctstyle-testnet-create-button'); await settle(); assert.equal(h.status(), copy.nl.connectionFailed); assert.notEqual(h.status(), copy.nl.failed); - assert.equal(h.query('#ctstyle-testnet-create-button').disabled, false); + assert.equal(h.query('#ctstyle-testnet-create-button').disabled, true); assert.equal(h.query('.ctstyle-testnet-result').hidden, true); assert.equal(h.query('a[href="https://xrpl.org/resources/dev-tools/xrp-faucets"]').hidden, false); - assert.equal(h.requests.length, 1); assert.equal(h.sockets.length, 0); assert.equal(h.timers.size, 0); + assert.equal(h.requests.length, 1); assert.equal(h.sockets.length, 0); assert.equal(h.timers.size, 1); }); test('The request timeout aborts once, reports an unknown outcome and never retries automatically', async () => { @@ -127,7 +134,7 @@ test('The request timeout aborts once, reports an unknown outcome and never retr h.click('#ctstyle-testnet-create-button'); h.expire(25000); await settle(); assert.equal(h.requests[0].options.signal.aborted, true); assert.equal(h.status(), copy.nl.timedOut); assert.equal(h.requests.length, 1); - assert.equal(h.sockets.length, 0); assert.equal(h.timers.size, 0); + assert.equal(h.sockets.length, 0); assert.equal(h.timers.size, 1); }); test('An account body delivered after the deadline is not exposed or verified', async () => { @@ -154,7 +161,7 @@ test('HTML, malformed JSON and incomplete account data produce an unusable-respo assert.equal(h.status(), copy.nl.invalidResponse); assert.equal(h.query('#ctstyle-testnet-address').textContent, ''); assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); - assert.equal(h.sockets.length, 0); assert.equal(h.timers.size, 0); + assert.equal(h.sockets.length, 0); assert.equal(h.timers.size, 1); } }); @@ -172,7 +179,7 @@ test('Leaving the page aborts creation and an old response cannot overwrite a ne const h = fixture({fetch: () => (++calls === 1 ? old : latest).promise}); h.click('#ctstyle-testnet-create-button'); h.emit('pagehide'); assert.equal(h.requests[0].options.signal.aborted, true); - h.emit('pageshow'); h.click('#ctstyle-testnet-create-button'); + h.advance(60000); h.emit('pageshow'); h.click('#ctstyle-testnet-create-button'); old.resolve(response()); await settle(); assert.equal(h.status(), copy.nl.creating); assert.equal(h.query('#ctstyle-testnet-address').textContent, ''); latest.reject(new TypeError('Failed to fetch')); await settle(); @@ -226,11 +233,100 @@ test('All eleven languages explain each failure state and a language change reta h.click('#ctstyle-testnet-create-button'); await settle(); assert.equal(Object.keys(copy).length, 11); for (const [locale, words] of Object.entries(copy)) { - for (const key of ['connectionFailed', 'timedOut', 'invalidResponse', 'failed', 'limited']) { + for (const key of ['connectionFailed', 'timedOut', 'invalidResponse', 'failed', 'limited', 'waitCreate', 'waitCheck']) { assert.equal(typeof words[key], 'string', `${locale}.${key}`); assert.ok(words[key].trim()); } h.window.CalorieTokenTestnet.refresh(locale); assert.equal(h.status(), words.connectionFailed); + assert.equal(h.query('#ctstyle-testnet-create-button').textContent, + words.waitCreate.replace('{seconds}',new Intl.NumberFormat(locale).format(60))); } assert.equal(h.requests.length, 1); }); + +test('Courtesy delay blocks repeated clicks after a completed error and expires without another automatic request', async () => { + const h = fixture({fetch: async () => response(null,{status:503})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + for (let i=0; i<20; i++) h.click('#ctstyle-testnet-create-button'); + assert.equal(h.requests.length,1); + assert.equal(h.query('#ctstyle-testnet-create-button').disabled,true); + h.elapse(59000); h.expire(1000); h.click('#ctstyle-testnet-create-button'); assert.equal(h.requests.length,1); + h.elapse(1000); h.expire(1000); + assert.equal(h.query('#ctstyle-testnet-create-button').disabled,false); + assert.equal(h.query('#ctstyle-testnet-create-button').textContent,copy.nl.create); + assert.equal(h.requests.length,1); assert.equal(h.timers.size,0); + h.click('#ctstyle-testnet-create-button'); await settle(); assert.equal(h.requests.length,2); +}); + +test('Courtesy delay survives a tab reload while storage contains only two numeric deadlines', async () => { + const storage = new Map(), h = fixture({storage,fetch:async()=>response(null,{status:503})}); + h.click('#ctstyle-testnet-create-button'); await settle(); h.emit('pagehide'); + const reloaded=fixture({storage}); reloaded.click('#ctstyle-testnet-create-button'); + assert.equal(reloaded.requests.length,0); + const saved=JSON.parse(storage.get('ctstyle-testnet-delays-v1')); + assert.deepEqual(Object.keys(saved).sort(),['check','create']); + assert.ok(Object.values(saved).every(Number.isSafeInteger)); + assert.equal(storage.size,1); + assert.equal([...storage.values()].some(value=>value.includes(secret)||value.includes(address)),false); + reloaded.advance(60000); reloaded.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(reloaded.requests.length,1); +}); + +test('Courtesy delay respects long Retry-After seconds and HTTP dates on 429 and 503', async () => { + for(const status of [429,503]) for(const retryAfter of ['7200',new Date(Date.UTC(2026,8,11,14,0,0)).toUTCString()]) { + const h=fixture({fetch:async()=>response(null,{status,retryAfter})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(),copy.nl[status===429?'limited':'failed']); + h.advance(7199000); h.click('#ctstyle-testnet-create-button'); assert.equal(h.requests.length,1); + h.advance(1000); assert.equal(h.query('#ctstyle-testnet-create-button').disabled,false); + assert.equal(h.requests.length,1,'Expiry enables the button, never submits a new request'); + } +}); + +test('Courtesy delay remains at least a minute with absent, invalid or shorter provider advice', async () => { + for(const retryAfter of [null,'garbage','1','0','-1']) { + const h=fixture({fetch:async()=>response(null,{status:429,retryAfter})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + h.advance(59000); h.click('#ctstyle-testnet-create-button'); assert.equal(h.requests.length,1); + h.advance(1000); assert.equal(h.query('#ctstyle-testnet-create-button').disabled,false); + } +}); + +test('Courtesy delay survives page return, cancels its timer on departure, and tolerates blocked storage', async () => { + const h=fixture({storageBlocked:true,fetch:async()=>{throw new TypeError('Failed to fetch');}}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(),copy.nl.connectionFailed); + h.emit('pagehide'); assert.equal(h.timers.size,0); + h.emit('pageshow'); h.click('#ctstyle-testnet-create-button'); assert.equal(h.requests.length,1); + assert.equal(h.timers.size,1); + h.advance(60000); h.click('#ctstyle-testnet-create-button'); await settle(); assert.equal(h.requests.length,2); +}); + +test('Courtesy delay bounds balance checks and slows further after an XRPL load warning or policy close', async () => { + for(const mode of ['failure','warning','close']) { + const h=fixture(); h.click('#ctstyle-testnet-create-button'); await settle(); + const socket=h.sockets[0]; + if(mode==='warning')socket.message({id:1,status:'error',warning:'load'}); + else if(mode==='close')socket.onclose({code:1008}); + else socket.fail(); + await settle(); + const button=[...h.document.querySelectorAll('button')].find(n=>n.textContent.startsWith('Opnieuw controleren')); + assert.ok(button); assert.equal(button.disabled,true); + for(let i=0;i<20;i++)button.click(); + assert.equal(h.sockets.length,1); + h.advance(mode==='failure'?14000:59000); button.click(); assert.equal(h.sockets.length,1); + h.advance(1000); h.clickCopy('check'); await settle(); assert.equal(h.sockets.length,2); + assert.equal(h.requests.length,1,'Balance checks do not create or fund another account'); + } +}); + +test('Courtesy delay ignores corrupt saved state and never stores an account returned by the faucet', async () => { + for(const value of ['{bad','null','{"create":"infinity","check":-1}']) { + const storage=new Map([['ctstyle-testnet-delays-v1',value]]),h=fixture({storage}); + h.click('#ctstyle-testnet-create-button'); await settle(); h.funded(); await settle(); + assert.equal(h.status(),copy.nl.funded); + const state=JSON.parse(storage.get('ctstyle-testnet-delays-v1')); + assert.ok(Object.values(state).every(Number.isSafeInteger)); + assert.equal(JSON.stringify(state).includes(secret),false); assert.equal(JSON.stringify(state).includes(address),false); + } +}); diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index a65e5c5e..ff4384e1 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,24 @@ -# CalorieToken Site Style 1.4.14 +# CalorieToken Site Style 1.4.15 Eén complete vervangende ZIP, 11 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.14.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.14** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.15.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.15** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## Testaccountmelding na de telefoonproef +## Begrensde Testnet-bediening na de vraag over misbruik + +Site Style 1.4.14 is live bevestigd; de app-build-ID en Render wijzen nog op 53013d75. Eén nieuwe camerastart in de cloudbrowser eindigde met de algemene startfout. De werkelijke telefoonwerking is daarmee niet vastgesteld. De afzonderlijke frontendcorrectie voor videostart en productbalk blijft voorbereid en vraagt eigen uitrolakkoord. + +Deze versie beperkt gewone herhaalde bediening van de Testnet-hulp: ten minste zestig seconden tussen accountaanvragen en vijftien seconden tussen saldocontroles. HTTP 429/503 met een leesbare `Retry-After` kan de wachttijd verlengen, ook wanneer de dienst uren vraagt. Een XRPL-belastingswaarschuwing of WebSocket-beleidssluiting verlengt de saldowachttijd tot ten minste zestig seconden. Er volgen geen automatische herhaalverzoeken. De knop toont in alle elf talen hoe lang de bezoeker moet wachten en wordt vanzelf weer beschikbaar. + +Alleen twee tijdstippen worden in tijdelijke tabopslag bewaard, zodat gewoon verversen de wachttijd niet wist. Herstelcodes, accountadressen en gebruikersidentificatoren worden niet opgeslagen. Als tabopslag is geblokkeerd blijft de wachttijd binnen de huidige pagina werken. De enige afteltimer stopt bij vertrek; terugkeer behoudt de wachttijd. Deze browsermaatregel vermindert onbedoelde herhaling en is **geen afdwingbare bescherming tegen bots of meerdere tabbladen**. De directe faucet en Testnet-server moeten op hun eigen toegangspunten misbruik begrenzen. Hun exacte actuele faucetlimieten zijn niet bevestigd; de [XRPL-limietdocumentatie](https://xrpl.org/docs/references/http-websocket-apis/api-conventions/rate-limiting) beschrijft ledger-API's, niet de faucet. + +De bestaande CalorieApp-backend heeft een afzonderlijk gedeeld budget van acht daadwerkelijke Open Food Facts-pogingen per zestig seconden, gecoördineerd via PostgreSQL; naamzoeken en barcodes gebruiken ditzelfde budget. Er zijn daarnaast routebudgetten, een begrensde wachtrij, samenvoegen van gelijke lopende verzoeken, een kleine cache en respect voor bronwachttijden. Deze controles zijn al onderdeel van de laatst bevestigde backendbron. Ze begrenzen verkeer uit deze app, niet het verkeer van andere projecten achter dezelfde provider-IP. Zoekverkeer gaat alleen na expliciet zoeken naar OFF; de app schrijft niet naar de OFF-database. De USDA-pilot leest een gebundeld referentiebestand en doet geen live USDA-API-aanvragen. Een latere live USDA-adapter vraagt eigen serverlimieten. Deze versie verandert geen backend, login, databron of serviceconfiguratie. + +Twintig gerichte Testnet-DOM-tests slagen, inclusief zeven nieuwe scenario's die de ontbrekende wachttijden in 1.4.14 reproduceren. Alles gebruikt synthetische antwoorden; er is geen belastingstest tegen een echte provider of echte accountaanmaak gedaan. Dit is een gerichte aanvulling op de nieuwe gebruikersvraag en geen heropening van de geaccepteerde basis van stap 1/2. De volledige publieke website wordt hiermee niet als DDoS-bestendig verklaard. + +## Inbegrepen: testaccountmelding uit 1.4.14 De eigenaar kreeg na ‘Maak mijn gratis testaccount’ een algemene faucetfout. Die melding werd ook getoond als de browser geen antwoord kon lezen, de wachttijd verstreek of accountgegevens ontbraken. De screenshot bepaalt niet welke oorzaak optrad. Een rechtstreekse, alleen-lezen verbindingscontrole vanuit de werkomgeving kon niet worden uitgevoerd; er is geen actuele foutstatus van de faucet vastgesteld. diff --git a/wordpress-plugins/calorietoken-site-style/assets/testnet-data.json b/wordpress-plugins/calorietoken-site-style/assets/testnet-data.json index 2d0c122c..025b296b 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/testnet-data.json +++ b/wordpress-plugins/calorietoken-site-style/assets/testnet-data.json @@ -37,7 +37,9 @@ "next": "Next step", "networkHelp": "Show the Testnet instructions", "aboutAccount": "How is this account created?", - "pilotScope": "This is a test account, not pilot registration or retailer access." + "pilotScope": "This is a test account, not pilot registration or retailer access.", + "waitCreate": "Try again in {seconds} s", + "waitCheck": "Check again in {seconds} s" }, "nl": { "create": "Maak mijn gratis testaccount", @@ -77,7 +79,9 @@ "next": "Volgende stap", "networkHelp": "Toon de Testnet-uitleg", "aboutAccount": "Hoe wordt dit account gemaakt?", - "pilotScope": "Dit is een testaccount, geen pilotinschrijving of winkelierstoegang." + "pilotScope": "Dit is een testaccount, geen pilotinschrijving of winkelierstoegang.", + "waitCreate": "Opnieuw proberen over {seconds} s", + "waitCheck": "Opnieuw controleren over {seconds} s" }, "zh-Hans": { "create": "创建免费测试账户", @@ -117,7 +121,9 @@ "next": "下一步", "networkHelp": "查看 Testnet 说明", "aboutAccount": "这个账户如何创建?", - "pilotScope": "这是测试账户,不会注册试点计划或授予商家权限。" + "pilotScope": "这是测试账户,不会注册试点计划或授予商家权限。", + "waitCreate": "{seconds} 秒后重试", + "waitCheck": "{seconds} 秒后重新检查" }, "hi": { "create": "मेरा मुफ्त टेस्ट खाता बनाएँ", @@ -157,7 +163,9 @@ "next": "अगला चरण", "networkHelp": "Testnet के निर्देश दिखाएँ", "aboutAccount": "यह खाता कैसे बनता है?", - "pilotScope": "यह टेस्ट खाता है, पायलट पंजीकरण या विक्रेता की पहुँच नहीं।" + "pilotScope": "यह टेस्ट खाता है, पायलट पंजीकरण या विक्रेता की पहुँच नहीं।", + "waitCreate": "{seconds} सेकंड बाद फिर कोशिश करें", + "waitCheck": "{seconds} सेकंड बाद फिर जाँचें" }, "es": { "create": "Crear mi cuenta de prueba gratis", @@ -197,7 +205,9 @@ "next": "Siguiente paso", "networkHelp": "Ver las instrucciones de Testnet", "aboutAccount": "¿Cómo se crea esta cuenta?", - "pilotScope": "Es una cuenta de prueba, no una inscripción a un piloto ni acceso de comercio." + "pilotScope": "Es una cuenta de prueba, no una inscripción a un piloto ni acceso de comercio.", + "waitCreate": "Reintentar en {seconds} s", + "waitCheck": "Comprobar de nuevo en {seconds} s" }, "ar": { "create": "إنشاء حساب الاختبار المجاني", @@ -237,7 +247,9 @@ "next": "الخطوة التالية", "networkHelp": "عرض تعليمات Testnet", "aboutAccount": "كيف يُنشأ هذا الحساب؟", - "pilotScope": "هذا حساب اختبار، وليس تسجيلًا في برنامج تجريبي أو صلاحية تاجر." + "pilotScope": "هذا حساب اختبار، وليس تسجيلًا في برنامج تجريبي أو صلاحية تاجر.", + "waitCreate": "أعد المحاولة بعد {seconds} ث", + "waitCheck": "تحقق مجددًا بعد {seconds} ث" }, "fr": { "create": "Créer mon compte de test gratuit", @@ -277,7 +289,9 @@ "next": "Étape suivante", "networkHelp": "Voir les instructions Testnet", "aboutAccount": "Comment ce compte est-il créé ?", - "pilotScope": "Il s’agit d’un compte de test, pas d’une inscription à un pilote ni d’un accès commerçant." + "pilotScope": "Il s’agit d’un compte de test, pas d’une inscription à un pilote ni d’un accès commerçant.", + "waitCreate": "Réessayer dans {seconds} s", + "waitCheck": "Vérifier à nouveau dans {seconds} s" }, "bn": { "create": "আমার বিনামূল্যের টেস্ট অ্যাকাউন্ট তৈরি করুন", @@ -317,7 +331,9 @@ "next": "পরের ধাপ", "networkHelp": "Testnet-এর নির্দেশ দেখুন", "aboutAccount": "এই অ্যাকাউন্ট কীভাবে তৈরি হয়?", - "pilotScope": "এটি পরীক্ষার অ্যাকাউন্ট, পাইলটে নিবন্ধন বা বিক্রেতার প্রবেশাধিকার নয়।" + "pilotScope": "এটি পরীক্ষার অ্যাকাউন্ট, পাইলটে নিবন্ধন বা বিক্রেতার প্রবেশাধিকার নয়।", + "waitCreate": "{seconds} সেকেন্ড পরে আবার চেষ্টা করুন", + "waitCheck": "{seconds} সেকেন্ড পরে আবার যাচাই করুন" }, "pt": { "create": "Criar minha conta de teste grátis", @@ -357,7 +373,9 @@ "next": "Próximo passo", "networkHelp": "Ver as instruções de Testnet", "aboutAccount": "Como é criada esta conta?", - "pilotScope": "É uma conta de teste, não uma inscrição num piloto nem acesso de comerciante." + "pilotScope": "É uma conta de teste, não uma inscrição num piloto nem acesso de comerciante.", + "waitCreate": "Tentar novamente em {seconds} s", + "waitCheck": "Verificar novamente em {seconds} s" }, "id": { "create": "Buat akun uji gratis saya", @@ -397,7 +415,9 @@ "next": "Langkah berikutnya", "networkHelp": "Lihat petunjuk Testnet", "aboutAccount": "Bagaimana akun ini dibuat?", - "pilotScope": "Ini akun uji, bukan pendaftaran pilot atau akses pedagang." + "pilotScope": "Ini akun uji, bukan pendaftaran pilot atau akses pedagang.", + "waitCreate": "Coba lagi dalam {seconds} dtk", + "waitCheck": "Periksa lagi dalam {seconds} dtk" }, "ur": { "create": "میرا مفت ٹیسٹ اکاؤنٹ بنائیں", @@ -437,6 +457,8 @@ "next": "اگلا مرحلہ", "networkHelp": "Testnet کی ہدایات دیکھیں", "aboutAccount": "یہ اکاؤنٹ کیسے بنتا ہے؟", - "pilotScope": "یہ ٹیسٹ اکاؤنٹ ہے، پائلٹ میں اندراج یا دکاندار کی رسائی نہیں۔" + "pilotScope": "یہ ٹیسٹ اکاؤنٹ ہے، پائلٹ میں اندراج یا دکاندار کی رسائی نہیں۔", + "waitCreate": "{seconds} سیکنڈ بعد دوبارہ کوشش کریں", + "waitCheck": "{seconds} سیکنڈ بعد دوبارہ چیک کریں" } } diff --git a/wordpress-plugins/calorietoken-site-style/assets/testnet.js b/wordpress-plugins/calorietoken-site-style/assets/testnet.js index 08343c0f..cb85d0fb 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/testnet.js +++ b/wordpress-plugins/calorietoken-site-style/assets/testnet.js @@ -7,6 +7,42 @@ var ledger = 'wss://s.altnet.rippletest.net:51233/'; var panel = null, fields = [], ui = {}, locale = 'en', status = 'ready', step = 0; var account = null, busy = false, controller = null, cancelLedger = null, generation = 0, departed = false; + // Courtesy limits for ordinary UI use, not a server-side anti-bot boundary. + // Only two deadlines survive a tab reload; account data is never stored. + var delayKey = 'ctstyle-testnet-delays-v1', retryAt = {create:0,check:0}, delayTimer = null; + function readDelays() { + try { + var saved = JSON.parse(window.sessionStorage.getItem(delayKey) || 'null'); + ['create','check'].forEach(function (key) { + var value = saved && saved[key]; + if (Number.isSafeInteger(value) && value > 0) retryAt[key] = Math.max(retryAt[key],value); + }); + } catch (_) { /* Storage may be blocked; the current page still keeps its delay. */ } + } + function delayRequest(key, milliseconds) { + retryAt[key] = Math.max(retryAt[key],Date.now()+milliseconds); + try { window.sessionStorage.setItem(delayKey,JSON.stringify(retryAt)); } catch (_) {} + } + function remaining(key) { return Math.max(0,Math.ceil((retryAt[key]-Date.now())/1000)); } + function providerDelay(value) { + if (!value) return 60000; + value = value.trim(); + var delay = /^\d{1,10}$/.test(value) ? Number(value)*1000 : Date.parse(value)-Date.now(); + return Number.isFinite(delay) ? Math.max(60000,delay) : 60000; + } + function requestControls() { + window.clearTimeout(delayTimer); delayTimer = null; + var createWait = remaining('create'), checkWait = remaining('check'), words = cfg.testCopy[locale]; + ui.create.disabled = busy || !!account || status === 'unsupported' || createWait > 0; + ui.check.disabled = busy || checkWait > 0; + var createText = !busy && !account && createWait ? words.waitCreate.replace('{seconds}',new Intl.NumberFormat(locale).format(createWait)) : words.create; + var checkText = !busy && account && status !== 'funded' && checkWait ? words.waitCheck.replace('{seconds}',new Intl.NumberFormat(locale).format(checkWait)) : words.check; + if (ui.create.textContent !== createText) ui.create.textContent = createText; + if (ui.check.textContent !== checkText) ui.check.textContent = checkText; + if (!departed && !busy && ((!account && createWait) || (account && status !== 'funded' && checkWait))) { + delayTimer = window.setTimeout(requestControls,1000); + } + } function allowed() { return !departed && document.body.matches('.ctstyle-enabled.page-id-7880') && !document.body.matches('.brz-ed') && !document.querySelector('.brz-ed,#brz-ed-iframe') && @@ -25,9 +61,8 @@ fields.forEach(function (entry) { entry.node.textContent = cfg.testCopy[locale][entry.key]; }); ui.status.textContent = cfg.testCopy[locale][status]; ui.status.hidden = status === 'ready'; - ui.create.disabled = busy || !!account || status === 'unsupported'; ui.create.hidden = !!account; - ui.check.disabled = busy; ui.check.hidden = !account || status === 'funded'; + ui.check.hidden = !account || status === 'funded'; ui.result.hidden = !account; panel.setAttribute('aria-busy',busy ? 'true' : 'false'); ui.copySeed.disabled = !account; ui.show.disabled = !account; ui.progress.textContent = (step + 1) + ' / 4'; @@ -35,6 +70,7 @@ ui.steps.forEach(function (node,index) { node.hidden = step !== index + 1; }); ui.previous.hidden = step <= 1; ui.next.hidden = step < 1 || step >= 3; + requestControls(); } function go(next) { if (!allowed() || !account || next < 1 || next > 3) return; @@ -61,17 +97,24 @@ socket.onmessage = function (event) { var data; try { data = JSON.parse(event.data); } catch (_) { finish(false); return; } if (data.id !== 1) return; + if (data.warning === 'load') delayRequest('check',60000); var result = data.result, info = result && result.account_data; finish(data.status === 'success' && result.validated === true && info && info.Account === address && typeof info.Balance === 'string' && /^[0-9]{1,18}$/.test(info.Balance) && Number(info.Balance) > 0); }; - socket.onerror = socket.onclose = function () { finish(false); }; + socket.onerror = function () { finish(false); }; + socket.onclose = function (event) { + if (event && event.code === 1008) delayRequest('check',60000); + finish(false); + }; } catch (_) { finish(false); } }); } async function check() { if (!allowed() || busy || !account) return; + readDelays(); if (remaining('check')) { requestControls(); return; } var current = generation, address = account.address; + delayRequest('check',15000); busy = true; status = 'checking'; render(); try { await verify(address); if (current === generation && allowed()) status = 'funded'; } catch (_) { if (current === generation && allowed()) status = 'pending'; } @@ -79,10 +122,12 @@ } async function create() { if (!allowed() || busy || account || !panel.isConnected) return; + readDelays(); if (remaining('create')) { requestControls(); return; } if (typeof window.fetch !== 'function' || typeof window.AbortController !== 'function' || typeof window.WebSocket !== 'function') { status = 'unsupported'; render(); return; } var current = generation, timeout, response, timedOut = false, request = new window.AbortController(); + delayRequest('create',60000); busy = true; status = 'creating'; render(); controller = request; try { timeout = window.setTimeout(function () { timedOut = true; request.abort(); },25000); @@ -94,7 +139,10 @@ redirect:'error',referrerPolicy:'no-referrer',signal:request.signal}); if (current !== generation || !allowed()) return; if (timedOut) { status = 'timedOut'; return; } - if (!response.ok) { status = response.status === 429 ? 'limited' : 'failed'; return; } + if (!response.ok) { + if (response.status === 429 || response.status === 503) delayRequest('create',providerDelay(response.headers.get('retry-after'))); + status = response.status === 429 ? 'limited' : 'failed'; return; + } if (!(response.headers.get('content-type') || '').toLowerCase().startsWith('application/json')) { status = 'invalidResponse'; return; } var data = await response.json(), wallet = data && data.account; if (current !== generation || !allowed()) return; @@ -185,6 +233,7 @@ } function refresh(tag) { if (!allowed()) return; + readDelays(); if (cfg.testCopy[tag]) locale = tag; else if (!panel) { var root = document.getElementById('ctstyle-testnet'), initial = (root && root.lang) || document.documentElement.lang; diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index 64434aca..124785b1 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.14 + * Version: 1.4.15 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.14'; + const VERSION = '1.4.15'; public static function delegate_app_camera($html, $shortcode_tag = null) { if (!is_string($html) || ($shortcode_tag !== null && $shortcode_tag !== 'calorieapp_embed') || From 19077f78ecb5b280d7ed6be004619ead44246fe2 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:08:02 +0200 Subject: [PATCH 023/368] fix: restore CAL logo shortcut and clarify crypto page choices --- tools/tests/wordpress_site_style_dom.test.mjs | 69 +++++++++++++ .../calorietoken-site-style/LEESMIJ.md | 16 ++- .../assets/discovery-data.json | 99 ++++++++++++++++--- .../assets/discovery.js | 9 +- .../assets/navigation.js | 36 ++++--- .../assets/refinements.css | 13 +++ .../calorietoken-site-style.php | 5 +- 7 files changed, 217 insertions(+), 30 deletions(-) diff --git a/tools/tests/wordpress_site_style_dom.test.mjs b/tools/tests/wordpress_site_style_dom.test.mjs index 32b76182..7cdbeff5 100644 --- a/tools/tests/wordpress_site_style_dom.test.mjs +++ b/tools/tests/wordpress_site_style_dom.test.mjs @@ -39,6 +39,7 @@ function fixture(html, {page = 1210, route = 'whitepaper', home = false, nativeO location: new URL(`https://calorietoken.net/${home ? '' : 'index.php/' + route + '/'}`), CalorieTokenDiscovery: {page, copy, appURL: 'https://calorietoken.net/index.php/calorieapp/', appLogo: '/logo.svg'}, CalorieTokenSiteStyleMenu: menu, + CalorieTokenSiteStyle: {calLogo:'https://calorietoken.net/wp-content/uploads/2021/12/C-Logotranspa-1024x936.png'}, localStorage: {getItem: () => null, setItem() {}}, getComputedStyle: node => ({display: node.style.display || 'block', visibility: node.style.visibility || 'visible', backgroundImage: node.style.backgroundImage || '', opacity: node.style.opacity || '1'}), requestAnimationFrame: fn => frames.push(fn), @@ -379,6 +380,65 @@ test('Manual interaction or leaving the page cancels late bottom adjustments', ( assert.equal(h.scrolls.length,1,'Repeated clicks leave one active request'); }); +function nativeShortcuts() { + return ''; +} + +test('CAL shortcut recognizes the installed bridge DOM without relying on its controller name', () => { + for (const home of [false,true]) { + const h=fixture(nativeShortcuts(),{home}); + const originals=[...h.document.querySelectorAll('[data-calorieapp-shortcut]')]; + let clicks=0; originals[2].querySelector('a').addEventListener('click',()=>clicks++); + h.run('navigation.js'); h.emit('load'); h.emit('pageshow'); + h.document.dispatchEvent(new h.window.CustomEvent('calorietoken:display-language',{detail:{locale:'nl'}})); + const added=h.document.querySelector('[data-ctstyle-exchange-shortcut]'); + assert.ok(added); assert.equal(added.hidden,false); + assert.equal(h.document.querySelectorAll('[data-ctstyle-exchange-shortcut]').length,1); + assert.equal(h.document.querySelectorAll('[data-calorieapp-shortcut]').length,4); + assert.equal(added.querySelector('a').href,'https://calorietoken.net/index.php/how-to-buy-calorie/'); + assert.equal(added.querySelector('a').getAttribute('aria-label'),'CAL & Crypto'); + assert.equal(added.querySelector('img').src,h.window.CalorieTokenSiteStyle.calLogo); + assert.equal(added.querySelector('img').getAttribute('aria-hidden'),'true'); + assert.equal(h.window.CalorieAppPageNavigation,undefined,'No replacement bridge controller'); + originals.forEach(n=>assert.ok(n.isConnected)); + // Native button event handlers are still installed on the same nodes. + originals[2].querySelector('a').dispatchEvent(new h.window.Event('click')); + assert.equal(clicks,1); + } +}); + +test('CAL shortcut handles delayed native initialization and hides on its own page', () => { + const h=fixture('',{page:4205,route:'how-to-buy-calorie'}); + h.window.CalorieAppPageNavigation={}; h.run('navigation.js'); + h.document.body.insertAdjacentHTML('beforeend',nativeShortcuts());h.emit('load'); + const added=h.document.querySelector('[data-ctstyle-exchange-shortcut]'); + assert.ok(added);assert.equal(added.hidden,true); + h.emit('pageshow');assert.equal(h.document.querySelectorAll('[data-ctstyle-exchange-shortcut]').length,1); +}); + +test('CAL shortcut leaves incomplete, edited, duplicate and embedded native stacks alone', () => { + const broken=[nativeShortcuts().replace('data-calorieapp-shortcut="top"','data-calorieapp-shortcut="unknown"'), + nativeShortcuts().replace('https://calorietoken.net/index.php/calorieapp/','https://example.com/'), + nativeShortcuts()+nativeShortcuts(),''+nativeShortcuts()+'', + '
    '+nativeShortcuts()+'
    ']; + for(const html of broken){const h=fixture(html);const before=h.document.body.innerHTML;h.run('navigation.js');h.emit('load'); + assert.equal(h.document.querySelectorAll('[data-ctstyle-exchange-shortcut]').length,0); + assert.equal(h.document.body.innerHTML,before);} +}); + +test('CAL shortcut uses the same logo in the standalone fallback without duplicating native slots', () => { + const config=''; + const h=fixture(config+nativeShortcuts()); + h.window.innerHeight=800;h.document.documentElement.scrollHeight=h.document.body.scrollHeight=2000; + h.run('navigation.js');h.emit('load');h.flush(); + const added=h.document.querySelector('[data-calorieapp-shortcut="exchange"]'); + assert.ok(added);assert.equal(added.querySelector('img').src,h.window.CalorieTokenSiteStyle.calLogo); + assert.equal(h.document.querySelectorAll('[data-calorieapp-shortcut]').length,5); + assert.equal(h.document.querySelectorAll('[data-ctstyle-exchange-shortcut]').length,0); +}); + test('The full CAL guide/discovery/translation sequence retains exact routes and all eleven languages', () => { const ts = require('typescript'); const parsed = ts.createSourceFile('menu-pages.js', source('menu-pages.js'), ts.ScriptTarget.Latest, true); @@ -403,5 +463,14 @@ test('The full CAL guide/discovery/translation sequence retains exact routes and assert.equal(h.document.querySelectorAll('#ctstyle-own-dex').length, 1); assert.equal(h.document.querySelectorAll('#ctstyle-external-exchange').length, 1); assert.equal(h.document.querySelectorAll('iframe').length, 0, 'An unopened external exchange must not load a provider'); + const routes=[...h.document.querySelectorAll('.ctstyle-discovery-tabs a')]; + assert.deepEqual(routes.map(a=>a.getAttribute('href')),['#ctstyle-own-dex','#'+guide.id,'#ctstyle-external-exchange']); + routes.forEach((a,index)=>{ + const key=['routeTrade','routeLearn','routeOther'][index]; + assert.equal(a.querySelector('strong').textContent,copy[locale][key]); + assert.equal(a.querySelector('.ctstyle-crypto-route-description').textContent,copy[locale][key+'Text']); + assert.ok(h.document.querySelector(a.getAttribute('href'))); + }); + assert.ok(h.document.querySelector('.ctstyle-crypto-intro').textContent.includes(copy[locale].appWithoutCAL)); } }); diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index ff4384e1..f8f6aee4 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,22 @@ -# CalorieToken Site Style 1.4.15 +# CalorieToken Site Style 1.4.16 Eén complete vervangende ZIP, 11 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.15.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.15** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.16.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.16** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## Begrensde Testnet-bediening na de vraag over misbruik +## CAL & Crypto: herkenbare snelkoppeling en duidelijke keuzes + +Op verzoek van de eigenaar gebruikt de zwevende CAL & Crypto-knop het bestaande transparante CAL-logo uit WordPress. De knop sluit aan op de gecontroleerde vier knoppen van de geïnstalleerde Identity Bridge, ook wanneer die versie een andere controllernaam gebruikt. De originele knoppen en hun afhandeling blijven behouden. De eigen terugvalnavigatie krijgt hetzelfde CAL-logo. De CAL-knop heeft dezelfde afmetingen, kleuren, toetsenbordfocus, mobiele rij, cookieverberging en afdrukregels; op CAL & Crypto zelf verdwijnt hij zonder lege plek. + +De pagina benoemt drie herkenbare keuzes: CAL verhandelen, beginnen met de uitleg en andere crypto bekijken. De ankerlinks blijven op de pagina en markeren het gekozen onderdeel. De ingang naar CalorieApp staat duidelijker bij het praktische nut, met de uitleg dat voeding zoeken en het eetdagboek geen aankoop van CAL vereisen. Alle nieuwe tekst is beschikbaar in de bestaande elf talen. Bestaande marktlinks, risico-uitleg, SWFT-toestemming en financiële uitvoering blijven bij hun bestaande diensten. + +Site Style 1.4.15 en Identity Bridge 0.3.29 zijn live bevestigd. De eigenaar heeft de afzonderlijke frontenduitrol op 11 september goedgekeurd, op voorwaarde dat deze CAL-aanvulling wordt meegenomen. Dit ZIP-bestand installeert uitsluitend Site Style; de exacte Render-uitrol en terugrolpunten staan in het uitroldossier. Er wordt geen Identity Bridge- of backendwijziging meegeleverd. + +De onderstaande notities bewaren de historie van de inbegrepen eerdere correcties; oude vermeldingen van nog ontbrekend frontendakkoord beschrijven die eerdere momenten. + +## Inbegrepen uit 1.4.15: begrensde Testnet-bediening na de vraag over misbruik Site Style 1.4.14 is live bevestigd; de app-build-ID en Render wijzen nog op 53013d75. Eén nieuwe camerastart in de cloudbrowser eindigde met de algemene startfout. De werkelijke telefoonwerking is daarmee niet vastgesteld. De afzonderlijke frontendcorrectie voor videostart en productbalk blijft voorbereid en vraagt eigen uitrolakkoord. diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery-data.json b/wordpress-plugins/calorietoken-site-style/assets/discovery-data.json index 8a5553bf..9e6536af 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery-data.json +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery-data.json @@ -49,7 +49,14 @@ "journeyRoute": "Choose your route", "journeyRouteText": "Follow the setup guide or open the external CAL/XRP order book or swap.", "journeyReview": "Review in your wallet", - "journeyReviewText": "Check the asset, amount, fees and destination before approving a request." + "journeyReviewText": "Check the asset, amount, fees and destination before approving a request.", + "routeTrade": "Trade CAL", + "routeTradeText": "Already have a wallet? Explore CAL/XRP at XPMarket.", + "routeLearn": "Start with the guide", + "routeLearnText": "New to CAL? Follow the wallet and trustline steps.", + "routeOther": "Explore other crypto", + "routeOtherText": "Check supported coins and networks at SWFT.", + "appWithoutCAL": "Food search and your food diary do not require buying CAL." }, "nl": { "intro": "Ontdek CAL op de XRP Ledger. Volg de uitleg, open de CAL/XRP-markten of bekijk andere cryptoroutes.", @@ -101,7 +108,14 @@ "journeyRoute": "Kies je route", "journeyRouteText": "Volg de installatiegids of open het externe CAL/XRP-orderboek of de swap.", "journeyReview": "Controleer in je wallet", - "journeyReviewText": "Controleer de munt, het bedrag, de kosten en de bestemming voordat je een verzoek goedkeurt." + "journeyReviewText": "Controleer de munt, het bedrag, de kosten en de bestemming voordat je een verzoek goedkeurt.", + "routeTrade": "CAL verhandelen", + "routeTradeText": "Heb je al een wallet? Bekijk CAL/XRP bij XPMarket.", + "routeLearn": "Begin met de uitleg", + "routeLearnText": "Nieuw met CAL? Volg de stappen voor je wallet en trustline.", + "routeOther": "Andere crypto bekijken", + "routeOtherText": "Bekijk de ondersteunde munten en netwerken bij SWFT.", + "appWithoutCAL": "Je hoeft geen CAL te kopen om voeding te zoeken en je eetdagboek te gebruiken." }, "zh-Hans": { "intro": "探索 XRP Ledger 上的 CAL。阅读指南、打开 CAL/XRP 市场,或了解其他加密资产兑换途径。", @@ -153,7 +167,14 @@ "journeyRoute": "选择方式", "journeyRouteText": "按照设置指南操作,或打开外部 CAL/XRP 订单簿或兑换页面。", "journeyReview": "在钱包中核对", - "journeyReviewText": "批准请求前,请核对资产、金额、费用和目标地址。" + "journeyReviewText": "批准请求前,请核对资产、金额、费用和目标地址。", + "routeTrade": "交易 CAL", + "routeTradeText": "已有钱包?在 XPMarket 查看 CAL/XRP。", + "routeLearn": "从指南开始", + "routeLearnText": "刚接触 CAL?按步骤设置钱包和信任线。", + "routeOther": "了解其他加密货币", + "routeOtherText": "在 SWFT 查看支持的币种和网络。", + "appWithoutCAL": "搜索食品和使用饮食日记无需购买 CAL。" }, "hi": { "intro": "XRP Ledger पर CAL के बारे में जानें। गाइड पढ़ें, CAL/XRP बाज़ार खोलें या अन्य क्रिप्टो विकल्प देखें।", @@ -205,7 +226,14 @@ "journeyRoute": "अपना तरीका चुनें", "journeyRouteText": "सेटअप गाइड अपनाएँ या बाहरी CAL/XRP ऑर्डर बुक या स्वैप खोलें।", "journeyReview": "वॉलेट में जाँचें", - "journeyReviewText": "अनुरोध स्वीकार करने से पहले संपत्ति, राशि, शुल्क और गंतव्य जाँचें।" + "journeyReviewText": "अनुरोध स्वीकार करने से पहले संपत्ति, राशि, शुल्क और गंतव्य जाँचें।", + "routeTrade": "CAL का व्यापार करें", + "routeTradeText": "वॉलेट पहले से है? XPMarket पर CAL/XRP देखें।", + "routeLearn": "गाइड से शुरू करें", + "routeLearnText": "CAL के लिए नए हैं? वॉलेट और ट्रस्टलाइन के चरण अपनाएँ।", + "routeOther": "अन्य क्रिप्टो देखें", + "routeOtherText": "SWFT पर समर्थित कॉइन और नेटवर्क जाँचें।", + "appWithoutCAL": "खाद्य खोज और भोजन डायरी के लिए CAL खरीदना ज़रूरी नहीं है।" }, "es": { "intro": "Descubre CAL en XRP Ledger. Sigue la guía, abre los mercados CAL/XRP o explora otras opciones cripto.", @@ -257,7 +285,14 @@ "journeyRoute": "Elige tu ruta", "journeyRouteText": "Sigue la guía de configuración o abre el libro de órdenes o el intercambio externo CAL/XRP.", "journeyReview": "Revisa en tu cartera", - "journeyReviewText": "Comprueba el activo, el importe, las comisiones y el destino antes de aprobar una solicitud." + "journeyReviewText": "Comprueba el activo, el importe, las comisiones y el destino antes de aprobar una solicitud.", + "routeTrade": "Operar con CAL", + "routeTradeText": "¿Ya tienes una cartera? Explora CAL/XRP en XPMarket.", + "routeLearn": "Empezar con la guía", + "routeLearnText": "¿Es tu primera vez con CAL? Sigue los pasos de cartera y línea de confianza.", + "routeOther": "Explorar otras criptomonedas", + "routeOtherText": "Consulta las monedas y redes compatibles en SWFT.", + "appWithoutCAL": "No necesitas comprar CAL para buscar alimentos y usar tu diario alimentario." }, "ar": { "intro": "اكتشف CAL على XRP Ledger. اتبع الدليل، أو افتح أسواق CAL/XRP، أو استكشف مسارات أخرى للعملات المشفرة.", @@ -309,7 +344,14 @@ "journeyRoute": "اختر المسار", "journeyRouteText": "اتبع دليل الإعداد أو افتح دفتر أوامر CAL/XRP أو المبادلة لدى الخدمة الخارجية.", "journeyReview": "راجع في محفظتك", - "journeyReviewText": "تحقق من الأصل والمبلغ والرسوم والوجهة قبل الموافقة على الطلب." + "journeyReviewText": "تحقق من الأصل والمبلغ والرسوم والوجهة قبل الموافقة على الطلب.", + "routeTrade": "تداول CAL", + "routeTradeText": "لديك محفظة بالفعل؟ استكشف CAL/XRP على XPMarket.", + "routeLearn": "ابدأ بالدليل", + "routeLearnText": "جديد على CAL؟ اتبع خطوات المحفظة وخط الثقة.", + "routeOther": "استكشف عملات رقمية أخرى", + "routeOtherText": "تحقق من العملات والشبكات المدعومة لدى SWFT.", + "appWithoutCAL": "لا تحتاج إلى شراء CAL للبحث عن الأطعمة واستخدام سجل طعامك." }, "fr": { "intro": "Découvrez CAL sur XRP Ledger. Suivez le guide, ouvrez les marchés CAL/XRP ou explorez d’autres options crypto.", @@ -361,7 +403,14 @@ "journeyRoute": "Choisissez votre parcours", "journeyRouteText": "Suivez le guide de configuration ou ouvrez le carnet d’ordres ou l’échange externe CAL/XRP.", "journeyReview": "Vérifiez dans votre portefeuille", - "journeyReviewText": "Vérifiez l’actif, le montant, les frais et la destination avant d’approuver une demande." + "journeyReviewText": "Vérifiez l’actif, le montant, les frais et la destination avant d’approuver une demande.", + "routeTrade": "Échanger du CAL", + "routeTradeText": "Vous avez déjà un portefeuille ? Découvrez CAL/XRP sur XPMarket.", + "routeLearn": "Commencer par le guide", + "routeLearnText": "Vous découvrez CAL ? Suivez les étapes du portefeuille et de la ligne de confiance.", + "routeOther": "Découvrir d’autres cryptos", + "routeOtherText": "Consultez les monnaies et réseaux pris en charge par SWFT.", + "appWithoutCAL": "La recherche d’aliments et votre journal alimentaire ne nécessitent pas d’acheter du CAL." }, "bn": { "intro": "XRP Ledger-এ CAL সম্পর্কে জানুন। নির্দেশিকা পড়ুন, CAL/XRP বাজার খুলুন অথবা অন্য ক্রিপ্টো বিকল্প দেখুন।", @@ -413,7 +462,14 @@ "journeyRoute": "পদ্ধতি বেছে নিন", "journeyRouteText": "সেটআপ নির্দেশিকা অনুসরণ করুন অথবা বাহ্যিক CAL/XRP অর্ডার বুক বা সোয়াপ খুলুন।", "journeyReview": "ওয়ালেটে যাচাই করুন", - "journeyReviewText": "অনুরোধ অনুমোদনের আগে সম্পদ, পরিমাণ, ফি ও গন্তব্য যাচাই করুন।" + "journeyReviewText": "অনুরোধ অনুমোদনের আগে সম্পদ, পরিমাণ, ফি ও গন্তব্য যাচাই করুন।", + "routeTrade": "CAL লেনদেন করুন", + "routeTradeText": "আগেই ওয়ালেট আছে? XPMarket-এ CAL/XRP দেখুন।", + "routeLearn": "নির্দেশিকা দিয়ে শুরু করুন", + "routeLearnText": "CAL নতুন ব্যবহার করছেন? ওয়ালেট ও ট্রাস্টলাইনের ধাপ অনুসরণ করুন।", + "routeOther": "অন্যান্য ক্রিপ্টো দেখুন", + "routeOtherText": "SWFT-এ সমর্থিত কয়েন ও নেটওয়ার্ক যাচাই করুন।", + "appWithoutCAL": "খাবার খোঁজা এবং খাবারের ডায়েরি ব্যবহার করতে CAL কিনতে হয় না।" }, "pt": { "intro": "Explore CAL no XRP Ledger. Siga o guia, abra os mercados CAL/XRP ou veja outras opções cripto.", @@ -465,7 +521,14 @@ "journeyRoute": "Escolha o seu caminho", "journeyRouteText": "Siga o guia de configuração ou abra o livro de ordens ou a troca externa CAL/XRP.", "journeyReview": "Reveja na carteira", - "journeyReviewText": "Verifique o ativo, o montante, as taxas e o destino antes de aprovar um pedido." + "journeyReviewText": "Verifique o ativo, o montante, as taxas e o destino antes de aprovar um pedido.", + "routeTrade": "Negociar CAL", + "routeTradeText": "Já tem uma carteira? Explore CAL/XRP na XPMarket.", + "routeLearn": "Começar pelo guia", + "routeLearnText": "Primeira vez com CAL? Siga os passos da carteira e da linha de confiança.", + "routeOther": "Explorar outras criptomoedas", + "routeOtherText": "Confira as moedas e redes compatíveis na SWFT.", + "appWithoutCAL": "Não é preciso comprar CAL para buscar alimentos e usar seu diário alimentar." }, "id": { "intro": "Jelajahi CAL di XRP Ledger. Ikuti panduan, buka pasar CAL/XRP, atau lihat pilihan kripto lainnya.", @@ -517,7 +580,14 @@ "journeyRoute": "Pilih cara Anda", "journeyRouteText": "Ikuti panduan penyiapan atau buka buku pesanan atau swap CAL/XRP di layanan eksternal.", "journeyReview": "Periksa di dompet", - "journeyReviewText": "Periksa aset, jumlah, biaya, dan tujuan sebelum menyetujui permintaan." + "journeyReviewText": "Periksa aset, jumlah, biaya, dan tujuan sebelum menyetujui permintaan.", + "routeTrade": "Perdagangkan CAL", + "routeTradeText": "Sudah punya dompet? Jelajahi CAL/XRP di XPMarket.", + "routeLearn": "Mulai dari panduan", + "routeLearnText": "Baru mengenal CAL? Ikuti langkah dompet dan trustline.", + "routeOther": "Jelajahi kripto lain", + "routeOtherText": "Periksa koin dan jaringan yang didukung di SWFT.", + "appWithoutCAL": "Mencari makanan dan menggunakan catatan makanan tidak mengharuskan Anda membeli CAL." }, "ur": { "intro": "XRP Ledger پر CAL کے بارے میں جانیں۔ رہنما پڑھیں، CAL/XRP مارکیٹ کھولیں یا دوسرے کرپٹو راستے دیکھیں۔", @@ -569,6 +639,13 @@ "journeyRoute": "اپنا طریقہ منتخب کریں", "journeyRouteText": "سیٹ اپ گائیڈ پر عمل کریں یا بیرونی CAL/XRP آرڈر بک یا سویپ کھولیں۔", "journeyReview": "والیٹ میں جائزہ لیں", - "journeyReviewText": "درخواست منظور کرنے سے پہلے اثاثہ، رقم، فیس اور منزل چیک کریں۔" + "journeyReviewText": "درخواست منظور کرنے سے پہلے اثاثہ، رقم، فیس اور منزل چیک کریں۔", + "routeTrade": "CAL کی تجارت کریں", + "routeTradeText": "والٹ پہلے سے ہے؟ XPMarket پر CAL/XRP دیکھیں۔", + "routeLearn": "رہنمائی سے شروع کریں", + "routeLearnText": "CAL پر نئے ہیں؟ والٹ اور ٹرسٹ لائن کے مراحل پر عمل کریں۔", + "routeOther": "دیگر کرپٹو دیکھیں", + "routeOtherText": "SWFT پر معاون سکوں اور نیٹ ورکس کی جانچ کریں۔", + "appWithoutCAL": "خوراک تلاش کرنے اور خوراک کی ڈائری استعمال کرنے کے لیے CAL خریدنا ضروری نہیں۔" } } diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.js b/wordpress-plugins/calorietoken-site-style/assets/discovery.js index 817d1955..85c2e844 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.js +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.js @@ -120,13 +120,18 @@ intro.append(element('p','ctstyle-crypto-kicker','CalorieToken · XRP Ledger'),label('h2','exchangeLabel'),label('p','intro','ctstyle-discovery-intro')); var actions=element('div','ctstyle-discovery-actions'); actions.append(link('exploreApp',window.location.origin+'/index.php/calorieapp/',false),link('readWhitepaper',window.location.origin+'/index.php/whitepaper/',false)); - intro.append(actions); + intro.append(label('p','appPitch','ctstyle-crypto-app-pitch'),actions,label('p','appWithoutCAL','ctstyle-discovery-small')); var journey=element('ol','ctstyle-crypto-journey'); ['Wallet','Route','Review'].forEach(function(key){var step=element('li');step.append(label('h3','journey'+key),label('p','journey'+key+'Text'));journey.append(step);}); intro.append(journey);hub.append(intro); var nav = element('nav','ctstyle-discovery-tabs'); nav.setAttribute('aria-label','CAL & Crypto'); if (!guide.id) guide.id = 'ctstyle-cal-options'; - nav.append(link('dexTitle','#ctstyle-own-dex'),link('guideTitle','#'+guide.id),link('bridgeTitle','#ctstyle-external-exchange')); + [['routeTrade','#ctstyle-own-dex'],['routeLearn','#'+guide.id],['routeOther','#ctstyle-external-exchange']].forEach(function (route) { + var action=element('a','ctstyle-discovery-action'); action.href=route[1]; + var copy=element('span','ctstyle-crypto-route-copy'); + copy.append(label('strong',route[0]),label('span',route[0]+'Text','ctstyle-crypto-route-description')); + action.append(copy); nav.append(action); + }); hub.append(nav); var dex = section('ctstyle-own-dex','dexTitle'); dex.append(label('p','dexStatus','ctstyle-discovery-badge'),label('p','dexText'),link('openDex',dexURL,true),link('openSwap','https://xpmarket.com/swap/Calorie-rNqGa93B8ewQP9mUwpwqA19SApbf62U7PY/XRP/market',true),trustlineCard('ctstyle-buy-trustline')); diff --git a/wordpress-plugins/calorietoken-site-style/assets/navigation.js b/wordpress-plugins/calorietoken-site-style/assets/navigation.js index 817eb854..cc27059e 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/navigation.js +++ b/wordpress-plugins/calorietoken-site-style/assets/navigation.js @@ -58,8 +58,24 @@ }, true); } + function calIcon() { + var logo = window.CalorieTokenSiteStyle && window.CalorieTokenSiteStyle.calLogo; + try { + var url = new URL(logo, window.location.href); + if (!logo || url.origin !== window.location.origin || url.protocol !== 'https:' || url.username || url.password) throw new Error('Invalid logo'); + var image = document.createElement('img'); + image.className = 'calorieapp-page-tool-icon ctstyle-cal-logo'; + image.src = url.href; image.alt = ''; image.width = image.height = 48; + image.setAttribute('aria-hidden','true'); + return image; + } catch (_) { + var text = document.createElement('span'); text.textContent = 'CAL'; text.setAttribute('aria-hidden','true'); return text; + } + } function extendNativeNavigation() { - if (!allowed() || !window.CalorieAppPageNavigation) return; + // Installed bridge versions expose different controller names. Match only + // the complete, known public DOM; the bridge retains its four native slots. + if (!allowed() || document.querySelector('[data-ctstyle-site-integration]')) return false; var stacks=document.querySelectorAll('[data-calorieapp-fallback-shortcuts]'); if (stacks.length!==1 || stacks[0].closest('form,[contenteditable],[data-calorieapp-embed]')) return; var stack=stacks[0], slots=Array.from(stack.querySelectorAll('[data-calorieapp-shortcut]')); @@ -79,18 +95,20 @@ // Separate ownership: the installed bridge continues to see its original four slots. slot.setAttribute('data-ctstyle-exchange-shortcut',''); var a=document.createElement('a');a.className='calorieapp-page-tool';a.href=origin+'/index.php/how-to-buy-calorie/'; - a.innerHTML=''; + a.append(calIcon()); slot.append(a);stack.append(slot); } slot.hidden=path(new URL(window.location.href))==='/how-to-buy-calorie'; var a=slot.querySelector('a'), locale=(window.CalorieTokenDiscoveryUI && window.CalorieTokenDiscoveryUI.getLocale()) || document.documentElement.lang || 'en'; var copy=window.CalorieTokenSiteStyleMenu?.navigation?.translations?.[locale]; - a.setAttribute('aria-label',copy?.exchange || 'CAL & Crypto');a.title=copy?.exchange || 'CAL & Crypto'; + var title=copy?.exchange || 'CAL & Crypto'; + if (a.getAttribute('aria-label')!==title) a.setAttribute('aria-label',title); + if (a.title!==title) a.title=title; + return true; } document.addEventListener('calorietoken:display-language',extendNativeNavigation); window.addEventListener('calorieapp:page-tools-ready',extendNativeNavigation); window.addEventListener('pageshow',extendNativeNavigation); - if (window.CalorieAppPageNavigation) {extendNativeNavigation();return;} function path(url) { return url.pathname.replace(/\/index\.php(?=\/|$)/, "").replace(/\/+$/, "") || "/"; } @@ -106,7 +124,7 @@ function init() { if (!allowed()) return; - if (window.CalorieAppPageNavigation) {extendNativeNavigation();return;} + if (extendNativeNavigation() || window.CalorieAppPageNavigation) return; var template = document.getElementById('ctstyle-tools-template'); if (!document.querySelector('[data-calorieapp-fallback-shortcuts]') && template) document.body.appendChild(template.content.cloneNode(true)); var config = document.querySelector("[data-ctstyle-site-integration]"); @@ -160,13 +178,7 @@ slot.className = 'calorieapp-page-tool-position calorieapp-page-tool-position-exchange'; slot.setAttribute('data-calorieapp-shortcut','exchange'); link.className = 'calorieapp-page-tool'; link.href = exchangePage.href; - var icon = document.createElementNS('http://www.w3.org/2000/svg','svg'); - icon.setAttribute('viewBox','0 0 24 24'); icon.setAttribute('aria-hidden','true'); icon.setAttribute('focusable','false'); - icon.setAttribute('class','calorieapp-page-tool-icon'); - var drawing = document.createElementNS('http://www.w3.org/2000/svg','path'); - drawing.setAttribute('d','M4 7h15m-4-4 4 4-4 4M20 17H5m4-4-4 4 4 4'); - drawing.setAttribute('fill','none'); drawing.setAttribute('stroke','currentColor'); drawing.setAttribute('stroke-width','2'); drawing.setAttribute('stroke-linecap','round'); drawing.setAttribute('stroke-linejoin','round'); - icon.append(drawing); link.append(icon); slot.append(link); shortcuts.append(slot); found.exchange = link; + link.append(calIcon()); slot.append(link); shortcuts.append(slot); found.exchange = link; } return found; } diff --git a/wordpress-plugins/calorietoken-site-style/assets/refinements.css b/wordpress-plugins/calorietoken-site-style/assets/refinements.css index 050067ea..f32ad4d3 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/refinements.css +++ b/wordpress-plugins/calorietoken-site-style/assets/refinements.css @@ -275,3 +275,16 @@ body.ctstyle-enabled .ctstyle-crypto-journey h3 {margin:0 0 8px;color:#008d36;fo body.ctstyle-enabled .ctstyle-crypto-journey p {margin:0;font:400 19px/1.55 Amiri,Georgia,serif;} body.ctstyle-enabled .ctstyle-crypto-intro .ctstyle-discovery-actions {margin-top:22px;} @media(max-width:767px){body.ctstyle-enabled .ctstyle-crypto-journey {grid-template-columns:minmax(0,1fr);}} +/* CAL keeps the same shortcut geometry, palette, cookie and print behavior. */ +:is(body.ctstyle-enabled,body.ctstyle-footer-only) [data-ctstyle-exchange-shortcut][hidden] {display:none!important;} +:is(body.ctstyle-enabled,body.ctstyle-footer-only) .calorieapp-page-tool .ctstyle-cal-logo {object-fit:contain;background:transparent;border-radius:0;padding:0;} +body.ctstyle-enabled .ctstyle-crypto-intro .ctstyle-discovery-actions {justify-content:center;} +body.ctstyle-enabled .ctstyle-crypto-intro .ctstyle-discovery-actions a:first-child {background:#008d36;border-color:#008d36;color:#fff!important;} +body.ctstyle-enabled .ctstyle-crypto-intro .ctstyle-discovery-actions a:first-child:is(:hover,:focus-visible) {background:#505ba9;border-color:#505ba9;} +body.ctstyle-enabled .ctstyle-crypto-intro .ctstyle-crypto-app-pitch {margin:20px auto 8px!important;color:#008d36;font-weight:700!important;} +body.ctstyle-enabled .ctstyle-exchange-layout .ctstyle-discovery-tabs a {align-items:flex-start;text-align:start;} +body.ctstyle-enabled .ctstyle-crypto-route-copy {display:block;min-width:0;} +body.ctstyle-enabled .ctstyle-crypto-route-copy strong {display:block;font:700 23px/1.35 Amiri,Georgia,serif;} +body.ctstyle-enabled .ctstyle-crypto-route-description {display:block;margin-top:8px;color:#414d94;font:400 18px/1.5 Amiri,Georgia,serif;} +body.ctstyle-enabled :is(#ctstyle-own-dex,#ctstyle-cal-options,#ctstyle-external-exchange) {scroll-margin-top:24px;} +body.ctstyle-enabled :is(#ctstyle-own-dex,#ctstyle-cal-options,#ctstyle-external-exchange):target {outline:3px solid #008d36;outline-offset:5px;} diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index 124785b1..f7188260 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.15 + * Version: 1.4.16 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.15'; + const VERSION = '1.4.16'; public static function delegate_app_camera($html, $shortcode_tag = null) { if (!is_string($html) || ($shortcode_tag !== null && $shortcode_tag !== 'calorieapp_embed') || @@ -193,6 +193,7 @@ public static function enqueue() { 'headerImage' => content_url('/uploads/2024/01/achtergrondbannersitea1.png'), 'titleImage' => content_url('/uploads/2022/10/kopje-banner3.png'), 'paperImage' => content_url('/uploads/2021/12/Websiteachtergrond.png'), + 'calLogo' => content_url('/uploads/2021/12/C-Logotranspa-1024x936.png'), 'footerOnly' => self::footer_only(), )); $copy = self::json_asset('menu-data'); From 0dfe591b9a76db8c049362036dead5601658413d Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:18:37 +0200 Subject: [PATCH 024/368] Keep public headers aligned with the published Brizy menu --- .../calorietoken-site-style/LEESMIJ.md | 14 ++++- .../assets/discovery.js | 2 +- .../assets/menu-data.json | 55 +++++++++++++++---- .../calorietoken-site-style.php | 25 ++++++++- .../calorietoken-site-style/templates.php | 3 +- 5 files changed, 81 insertions(+), 18 deletions(-) diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index f8f6aee4..014538d0 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,20 @@ -# CalorieToken Site Style 1.4.16 +# CalorieToken Site Style 1.4.17 Eén complete vervangende ZIP, 11 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.16.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.16** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.17.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.17** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## CAL & Crypto: herkenbare snelkoppeling en duidelijke keuzes +## Gedeeld menu na publicatie van Showcases + +Op verzoek van de eigenaar zijn de bestaande Showcases-introductie (pagina 7945) en de drie ingangen CalorieApp, Showcases en Voting Hub in het bestaande WordPress/Brizy-menu Hoofdmenu3 bijgewerkt. CalorieApp en de informatieve Community Voting Hub (8020) waren al gepubliceerd. De oude Voting Hub-werkversie blijft een concept; er wordt geen stemming geactiveerd. De verdere showcases blijven werk voor stap 4. + +De terugvalheader leest nu hetzelfde bestaande Hoofdmenu3, zodat de documentpagina’s de actuele paginalinks en volgorde gebruiken. Er wordt geen willekeurig ander menu gekozen. Desktop en mobiel krijgen dezelfde links zonder dubbele menu-item-ID’s. De nieuwe menuteksten zijn opgenomen in de bestaande elf talen; ook de bestaande Showcases-header volgt deze menutaal. De inhoud en Xaman-aanmelding worden niet herschreven. + +De nette URL-structuur is op 11 september via WordPress ingesteld met behoud van de bestaande datumstructuur voor blogberichten. Oude index.php-links naar Whitepaper en CalorieApp zijn daarna werkend doorgestuurd naar hun nette URL. Dit ZIP-bestand verandert geen permalinks, Apache-instellingen, Brizy-inhoud of publicatiestatus; het definitieve livebewijs en de overige linkcontroles staan in het menu-/URL-dossier. Een terugkeer naar Site Style 1.4.16 zet menu- of URL-instellingen daarom niet terug. + +## Inbegrepen uit 1.4.16: CAL & Crypto Op verzoek van de eigenaar gebruikt de zwevende CAL & Crypto-knop het bestaande transparante CAL-logo uit WordPress. De knop sluit aan op de gecontroleerde vier knoppen van de geïnstalleerde Identity Bridge, ook wanneer die versie een andere controllernaam gebruikt. De originele knoppen en hun afhandeling blijven behouden. De eigen terugvalnavigatie krijgt hetzelfde CAL-logo. De CAL-knop heeft dezelfde afmetingen, kleuren, toetsenbordfocus, mobiele rij, cookieverberging en afdrukregels; op CAL & Crypto zelf verdwijnt hij zonder lege plek. diff --git a/wordpress-plugins/calorietoken-site-style/assets/discovery.js b/wordpress-plugins/calorietoken-site-style/assets/discovery.js index 85c2e844..aa49c05e 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/discovery.js +++ b/wordpress-plugins/calorietoken-site-style/assets/discovery.js @@ -333,7 +333,7 @@ function sharedLanguage(tag) { var all=window.CalorieTokenSiteStyleMenu && window.CalorieTokenSiteStyleMenu.sharedLabels; if (!all || !all[tag]) return; - document.querySelectorAll('.brz-menu-simple a[href],.ctstyle-header-nav a[href],.ctstyle-footer .ctstyle-legal-links a[href]').forEach(function (a) { + document.querySelectorAll('.brz-menu-simple a[href],.ctstyle-header-nav a[href],.showcase-header-menu a[href],.showcase-mobile-menu a[href],.ctstyle-footer .ctstyle-legal-links a[href]').forEach(function (a) { if (a.closest('form,[contenteditable],.xl-card,[hidden],[inert]')) return; var url;try {url=new URL(a.getAttribute('href'),window.location.href);} catch (_) {return;} var route=url.pathname.replace(/^\/index\.php(?=\/|$)/,'').replace(/\/+$/,'') || '/'; diff --git a/wordpress-plugins/calorietoken-site-style/assets/menu-data.json b/wordpress-plugins/calorietoken-site-style/assets/menu-data.json index b4675757..8a5541de 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/menu-data.json +++ b/wordpress-plugins/calorietoken-site-style/assets/menu-data.json @@ -1737,7 +1737,10 @@ "/donate": "Donate", "/merchnfts": "Merch&NFTs", "/privacy-policy": "Privacy Policy", - "/terms-conditions": "Terms & Conditions" + "/terms-conditions": "Terms & Conditions", + "/calorieapp": "CalorieApp", + "/showcases": "Showcases", + "/community-voting-hub-info": "Voting Hub" }, "nl": { "/": "Home", @@ -1753,7 +1756,10 @@ "/donate": "Doneren", "/merchnfts": "Merch & NFT’s", "/privacy-policy": "Privacybeleid", - "/terms-conditions": "Algemene voorwaarden" + "/terms-conditions": "Algemene voorwaarden", + "/calorieapp": "CalorieApp", + "/showcases": "Showcases", + "/community-voting-hub-info": "Stemhub" }, "zh-Hans": { "/": "首页", @@ -1769,7 +1775,10 @@ "/donate": "捐赠", "/merchnfts": "商品与 NFT", "/privacy-policy": "隐私政策", - "/terms-conditions": "条款与条件" + "/terms-conditions": "条款与条件", + "/calorieapp": "CalorieApp", + "/showcases": "项目展示", + "/community-voting-hub-info": "投票中心" }, "hi": { "/": "होम", @@ -1785,7 +1794,10 @@ "/donate": "दान करें", "/merchnfts": "उत्पाद और NFT", "/privacy-policy": "गोपनीयता नीति", - "/terms-conditions": "नियम और शर्तें" + "/terms-conditions": "नियम और शर्तें", + "/calorieapp": "CalorieApp", + "/showcases": "झलकियाँ", + "/community-voting-hub-info": "मतदान केंद्र" }, "es": { "/": "Inicio", @@ -1801,7 +1813,10 @@ "/donate": "Donar", "/merchnfts": "Productos y NFT", "/privacy-policy": "Política de privacidad", - "/terms-conditions": "Términos y condiciones" + "/terms-conditions": "Términos y condiciones", + "/calorieapp": "CalorieApp", + "/showcases": "Presentaciones", + "/community-voting-hub-info": "Centro de votación" }, "ar": { "/": "الرئيسية", @@ -1817,7 +1832,10 @@ "/donate": "تبرع", "/merchnfts": "المنتجات وNFT", "/privacy-policy": "سياسة الخصوصية", - "/terms-conditions": "الشروط والأحكام" + "/terms-conditions": "الشروط والأحكام", + "/calorieapp": "CalorieApp", + "/showcases": "العروض", + "/community-voting-hub-info": "مركز التصويت" }, "fr": { "/": "Accueil", @@ -1833,7 +1851,10 @@ "/donate": "Faire un don", "/merchnfts": "Produits et NFT", "/privacy-policy": "Politique de confidentialité", - "/terms-conditions": "Conditions générales" + "/terms-conditions": "Conditions générales", + "/calorieapp": "CalorieApp", + "/showcases": "Présentations", + "/community-voting-hub-info": "Espace de vote" }, "bn": { "/": "হোম", @@ -1849,7 +1870,10 @@ "/donate": "অনুদান দিন", "/merchnfts": "পণ্য ও NFT", "/privacy-policy": "গোপনীয়তা নীতি", - "/terms-conditions": "শর্তাবলি" + "/terms-conditions": "শর্তাবলি", + "/calorieapp": "CalorieApp", + "/showcases": "প্রদর্শনী", + "/community-voting-hub-info": "ভোট কেন্দ্র" }, "pt": { "/": "Início", @@ -1865,7 +1889,10 @@ "/donate": "Doar", "/merchnfts": "Produtos e NFT", "/privacy-policy": "Política de privacidade", - "/terms-conditions": "Termos e condições" + "/terms-conditions": "Termos e condições", + "/calorieapp": "CalorieApp", + "/showcases": "Apresentações", + "/community-voting-hub-info": "Central de votação" }, "id": { "/": "Beranda", @@ -1881,7 +1908,10 @@ "/donate": "Donasi", "/merchnfts": "Produk & NFT", "/privacy-policy": "Kebijakan privasi", - "/terms-conditions": "Syarat dan ketentuan" + "/terms-conditions": "Syarat dan ketentuan", + "/calorieapp": "CalorieApp", + "/showcases": "Showcase", + "/community-voting-hub-info": "Pusat voting" }, "ur": { "/": "ہوم", @@ -1897,7 +1927,10 @@ "/donate": "عطیہ دیں", "/merchnfts": "مصنوعات اور NFT", "/privacy-policy": "رازداری کی پالیسی", - "/terms-conditions": "شرائط و ضوابط" + "/terms-conditions": "شرائط و ضوابط", + "/calorieapp": "CalorieApp", + "/showcases": "جھلکیاں", + "/community-voting-hub-info": "ووٹنگ مرکز" } } } diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index f7188260..0a29a274 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.16 + * Version: 1.4.17 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,28 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.16'; + const VERSION = '1.4.17'; + + public static function header_menu() { + // The menu verified in the live Brizy header. Never fall through to an + // unrelated menu or WordPress's automatic list of all public pages. + $menu = wp_get_nav_menu_object('Hoofdmenu3'); + if (!$menu || is_wp_error($menu)) { return ''; } + $html = wp_nav_menu(array( + 'menu' => $menu, 'container' => false, 'echo' => false, + 'fallback_cb' => false, 'items_wrap' => '
      %3$s
    ', + )); + if (!is_string($html) || trim($html) === '') { return ''; } + // Desktop and mobile share these links, without duplicate item IDs. + $tags = new \WP_HTML_Tag_Processor($html); + while ($tags->next_tag('LI')) { + $id = $tags->get_attribute('id'); + if (is_string($id) && preg_match('/^menu-item-\d+$/', $id)) { + $tags->remove_attribute('id'); + } + } + return $tags->get_updated_html(); + } public static function delegate_app_camera($html, $shortcode_tag = null) { if (!is_string($html) || ($shortcode_tag !== null && $shortcode_tag !== 'calorieapp_embed') || diff --git a/wordpress-plugins/calorietoken-site-style/templates.php b/wordpress-plugins/calorietoken-site-style/templates.php index 4b110cc4..5c2da5af 100644 --- a/wordpress-plugins/calorietoken-site-style/templates.php +++ b/wordpress-plugins/calorietoken-site-style/templates.php @@ -78,7 +78,8 @@ - + + From f9dfe41366494f1ed14c93364da99e3bff22b27c Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:02:28 +0200 Subject: [PATCH 025/368] Improve food barcode recognition in mobile camera frames --- docs/BARCODE_SCANNING.md | 39 ++++++ frontend/config/barcode-copy.json | 22 +-- frontend/lib/foodBarcode.ts | 110 ++++++++++++--- tools/tests/food_barcode.test.mjs | 6 + tools/tests/food_barcode_recognition.test.mjs | 132 ++++++++++++++++++ 5 files changed, 276 insertions(+), 33 deletions(-) create mode 100644 tools/tests/food_barcode_recognition.test.mjs diff --git a/docs/BARCODE_SCANNING.md b/docs/BARCODE_SCANNING.md index ad0e03e8..9d190d2a 100644 --- a/docs/BARCODE_SCANNING.md +++ b/docs/BARCODE_SCANNING.md @@ -1,5 +1,44 @@ # Food barcode scanning +## Recognition follow-up — 11 September 2026 + +The owner confirmed a working camera preview in Android Brave, but no recognition +of two packages (Becel Light and Douwe Egberts coffee). Clearing browser history +preceded this report; a causal connection has not been established. Neither +compressed screenshot yielded a reliable barcode in local decoder attempts, so +they do not establish the exact optical or device-specific failure. + +The recognition follow-up adds optional on-device `BarcodeDetector` support for +the same permitted food formats. Capability discovery and at most one outstanding +native detection run independently of the bundled fallback. Missing support, +rejection or a slow native operation cannot prevent ZXing from scanning. Native +results must pass the same format and GTIN checksum checks, and late results from +a disposed session are ignored. + +ZXing now examines more scan lines inside the visible guide, retaining quiet-zone +margins and correctly mapping an object-cover preview to portrait source pixels. +Every fourth unsuccessful frame also receives the previous wider scan. The rear +camera requests an ideal 1920 × 1080 image; these dimensions are preferences, not +requirements. Continuous focus is requested only when the selected track reports +support, and tuning failure leaves the working preview alone. Localized guidance +asks visitors to fill most of the frame while preserving the white margins and +waiting for sharp bars. No new dependency, image upload or backend call is added. + +Targeted regression tests demonstrate that the old reader misses a synthetic +barcode between its sampled rows and the updated reader finds it. They cover +portrait framing, native API availability/failure/late results and focus rejection, +alongside the existing supported-format and camera lifecycle tests. Canvas +capture is substituted with local raster pixels; these tests do not establish +real-phone focus, lighting or successful recognition of the owner's packages. +The frontend follow-up requires its own verified publication and a short real +package scan before scanner acceptance can be marked complete. + +References: [BarcodeDetector](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector) +and [camera constraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints). +The native API is an optional enhancement because browser support varies. + +## Original implementation record + Prepared 10 September 2026 for the owner's Step 1–3 review. This is implemented source awaiting the normal app release and real-device acceptance. diff --git a/frontend/config/barcode-copy.json b/frontend/config/barcode-copy.json index 3fa46137..b51f7a8e 100644 --- a/frontend/config/barcode-copy.json +++ b/frontend/config/barcode-copy.json @@ -7,7 +7,7 @@ "scan": "Start camera", "stop": "Stop camera", "opening": "Opening camera…", - "scanning": "Centre the barcode in the frame. Use good light and hold steady.", + "scanning": "Move closer until the barcode fills most of the frame, keeping the white space on both sides visible. Hold still until the bars are sharp.", "denied": "Camera access is blocked. Check the camera permission for this website in your browser settings.", "unavailable": "Camera unavailable. Enter the barcode below.", "invalid": "Enter a valid barcode with 8, 12, 13 or 14 digits.", @@ -27,7 +27,7 @@ "scan": "Start camera", "stop": "Stop camera", "opening": "Camera openen…", - "scanning": "Houd de barcode in het kader. Zorg voor voldoende licht en houd de camera stil.", + "scanning": "Kom dichterbij tot de barcode bijna het hele kader vult. Houd de witte ruimte aan beide kanten zichtbaar en wacht even tot de streepjes scherp zijn.", "denied": "Cameratoegang is geblokkeerd. Controleer in je browserinstellingen de cameratoestemming voor deze website.", "unavailable": "Camera niet beschikbaar. Vul de barcode hieronder in.", "invalid": "Vul een geldige barcode met 8, 12, 13 of 14 cijfers in.", @@ -47,7 +47,7 @@ "scan": "启动相机", "stop": "停止相机", "opening": "正在打开相机…", - "scanning": "将条码置于框内,保持光线充足和相机稳定。", + "scanning": "靠近一些,让条码占满框内大部分区域,同时保留两侧的空白。保持稳定,等条纹清晰。", "denied": "相机访问被阻止。请在浏览器设置中检查此网站的相机权限。", "unavailable": "相机不可用。请在下方输入条码。", "invalid": "请输入有效的 8、12、13 或 14 位条码。", @@ -67,7 +67,7 @@ "scan": "कैमरा शुरू करें", "stop": "कैमरा रोकें", "opening": "कैमरा खुल रहा है…", - "scanning": "बारकोड को फ्रेम के बीच रखें। अच्छी रोशनी में कैमरा स्थिर रखें।", + "scanning": "पास लाएँ ताकि बारकोड फ्रेम का अधिकतर हिस्सा भर दे और दोनों ओर की सफेद जगह दिखे। धारियाँ साफ होने तक स्थिर रखें।", "denied": "कैमरे का उपयोग अवरुद्ध है। ब्राउज़र सेटिंग में इस वेबसाइट की कैमरा अनुमति जाँचें।", "unavailable": "कैमरा उपलब्ध नहीं है। नीचे बारकोड दर्ज करें।", "invalid": "8, 12, 13 या 14 अंकों का मान्य बारकोड दर्ज करें।", @@ -87,7 +87,7 @@ "scan": "Iniciar cámara", "stop": "Detener cámara", "opening": "Abriendo la cámara…", - "scanning": "Centra el código en el recuadro. Usa buena luz y mantén la cámara estable.", + "scanning": "Acércate hasta que el código ocupe casi todo el recuadro, dejando visible el espacio blanco a ambos lados. Mantén la cámara quieta hasta que las barras estén nítidas.", "denied": "El acceso a la cámara está bloqueado. Revisa el permiso de cámara de este sitio en los ajustes del navegador.", "unavailable": "Cámara no disponible. Introduce el código abajo.", "invalid": "Introduce un código válido de 8, 12, 13 o 14 dígitos.", @@ -107,7 +107,7 @@ "scan": "تشغيل الكاميرا", "stop": "إيقاف الكاميرا", "opening": "جارٍ فتح الكاميرا…", - "scanning": "ضع الرمز في منتصف الإطار مع إضاءة جيدة وثبّت الكاميرا.", + "scanning": "اقترب حتى يملأ الرمز معظم الإطار، مع إبقاء المساحة البيضاء على الجانبين ظاهرة. ثبّت الكاميرا حتى تتضح الخطوط.", "denied": "الوصول إلى الكاميرا محظور. تحقق من إذن الكاميرا لهذا الموقع في إعدادات المتصفح.", "unavailable": "الكاميرا غير متاحة. أدخل الرمز أدناه.", "invalid": "أدخل رمزًا صالحًا من 8 أو 12 أو 13 أو 14 رقمًا.", @@ -127,7 +127,7 @@ "scan": "Démarrer la caméra", "stop": "Arrêter la caméra", "opening": "Ouverture de la caméra…", - "scanning": "Centrez le code dans le cadre, avec un bon éclairage et une caméra stable.", + "scanning": "Rapprochez le code pour qu’il remplisse presque le cadre, en gardant les marges blanches visibles des deux côtés. Restez immobile jusqu’à ce que les barres soient nettes.", "denied": "L’accès à la caméra est bloqué. Vérifiez l’autorisation de ce site dans les paramètres du navigateur.", "unavailable": "Caméra indisponible. Saisissez le code ci-dessous.", "invalid": "Saisissez un code valide de 8, 12, 13 ou 14 chiffres.", @@ -147,7 +147,7 @@ "scan": "ক্যামেরা চালু করুন", "stop": "ক্যামেরা বন্ধ করুন", "opening": "ক্যামেরা খুলছে…", - "scanning": "বারকোড ফ্রেমের মাঝখানে রাখুন। পর্যাপ্ত আলোতে ক্যামেরা স্থির রাখুন।", + "scanning": "কাছে আনুন যাতে বারকোড ফ্রেমের বেশির ভাগ জুড়ে থাকে এবং দুই পাশের সাদা ফাঁকা জায়গা দেখা যায়। দাগগুলো স্পষ্ট হওয়া পর্যন্ত স্থির রাখুন।", "denied": "ক্যামেরার প্রবেশাধিকার অবরুদ্ধ। ব্রাউজারের সেটিংসে এই ওয়েবসাইটের ক্যামেরার অনুমতি দেখুন।", "unavailable": "ক্যামেরা নেই। নিচে বারকোড লিখুন।", "invalid": "৮, ১২, ১৩ বা ১৪ সংখ্যার বৈধ বারকোড লিখুন।", @@ -167,7 +167,7 @@ "scan": "Iniciar câmara", "stop": "Parar câmara", "opening": "A abrir a câmara…", - "scanning": "Centre o código na moldura. Use boa iluminação e mantenha a câmara estável.", + "scanning": "Aproxime até o código ocupar quase toda a moldura, mantendo visível o espaço branco dos dois lados. Segure firme até as barras ficarem nítidas.", "denied": "O acesso à câmera está bloqueado. Confira a permissão de câmera deste site nas configurações do navegador.", "unavailable": "Câmara indisponível. Introduza o código abaixo.", "invalid": "Introduza um código válido com 8, 12, 13 ou 14 dígitos.", @@ -187,7 +187,7 @@ "scan": "Mulai kamera", "stop": "Hentikan kamera", "opening": "Membuka kamera…", - "scanning": "Posisikan kode di tengah bingkai. Gunakan cahaya yang cukup dan pegang dengan stabil.", + "scanning": "Dekatkan hingga kode memenuhi sebagian besar bingkai, dengan ruang putih di kedua sisi tetap terlihat. Pegang dengan stabil sampai garis terlihat tajam.", "denied": "Akses kamera diblokir. Periksa izin kamera untuk situs ini di pengaturan browser.", "unavailable": "Kamera tidak tersedia. Masukkan kode di bawah.", "invalid": "Masukkan kode yang valid dengan 8, 12, 13, atau 14 digit.", @@ -207,7 +207,7 @@ "scan": "کیمرہ شروع کریں", "stop": "کیمرہ بند کریں", "opening": "کیمرہ کھل رہا ہے…", - "scanning": "بارکوڈ فریم کے بیچ میں رکھیں۔ مناسب روشنی میں کیمرہ ساکن رکھیں۔", + "scanning": "قریب لائیں تاکہ بارکوڈ فریم کا زیادہ تر حصہ بھر دے اور دونوں طرف کی سفید جگہ نظر آئے۔ لکیریں واضح ہونے تک کیمرہ ساکن رکھیں۔", "denied": "کیمرے تک رسائی مسدود ہے۔ براؤزر کی ترتیبات میں اس ویب سائٹ کے کیمرے کی اجازت دیکھیں۔", "unavailable": "کیمرہ دستیاب نہیں۔ نیچے بارکوڈ درج کریں۔", "invalid": "8، 12، 13 یا 14 ہندسوں کا درست بارکوڈ درج کریں۔", diff --git a/frontend/lib/foodBarcode.ts b/frontend/lib/foodBarcode.ts index 0ed3b689..d346088f 100644 --- a/frontend/lib/foodBarcode.ts +++ b/frontend/lib/foodBarcode.ts @@ -22,31 +22,95 @@ export function cameraBlockedByPolicy(page: Document): boolean { } } -type Reader = { decode: (video: HTMLVideoElement) => string | null }; +type Reader = { decode: (video: HTMLVideoElement) => string | null; dispose?: () => void }; +type NativeReader = { detect: (video: HTMLVideoElement) => Promise<{ rawValue: string; format: string }[]> }; +type NativeReaderConstructor = { + new(options: { formats: string[] }): NativeReader; + getSupportedFormats: () => Promise; +}; + +/** Use the phone's detector when available, with a bundled, independent fallback. */ +export async function createFoodBarcodeReader(): Promise { + const [{ BrowserMultiFormatReader }, { BarcodeFormat, DecodeHintType, NotFoundException, ChecksumException, FormatException }] = await Promise.all([ + import("@zxing/browser"), import("@zxing/library"), + ]); + const hints = new Map(); + hints.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.EAN_13, BarcodeFormat.EAN_8, BarcodeFormat.UPC_A, BarcodeFormat.ITF]); + const wideReader = new BrowserMultiFormatReader(hints); + const focusedReader = new BrowserMultiFormatReader(new Map([...hints, [DecodeHintType.TRY_HARDER, true]])); + let native: NativeReader | null = null, busy = false, closed = false, detected: string | null = null; + let canvas: HTMLCanvasElement | undefined, frames = 0; + const formats = ["ean_13", "ean_8", "upc_a", "itf"]; + const Native = typeof window === "undefined" ? undefined + : (window as Window & { BarcodeDetector?: NativeReaderConstructor }).BarcodeDetector; + // Capability discovery must never delay opening the camera or the fallback. + if (Native) void Promise.resolve().then(() => Native.getSupportedFormats()).then(supported => { + const available = formats.filter(format => supported.includes(format)); + if (!closed && available.length) native = new Native({ formats: available }); + }).catch(() => { /* Optional browser API; ZXing remains available. */ }); + + function attempt(decode: () => string): string | null { + try { return validFoodBarcode(decode()); } + catch (error) { + if (error instanceof NotFoundException || error instanceof ChecksumException || error instanceof FormatException) return null; + throw error; + } + } + return { + decode(video) { + if (closed) return null; + if (detected) return detected; + if (native && !busy) { + busy = true; + // At most one native request. A slow or broken detector cannot block ZXing. + const detector = native; + void Promise.resolve().then(() => closed ? [] : detector.detect(video)).then(results => { + if (!closed) detected = results.filter(result => formats.includes(result.format)) + .map(result => validFoodBarcode(result.rawValue)).find(code => code !== null) ?? null; + }).catch(() => { native = null; }).finally(() => { busy = false; }); + } + const width = video.videoWidth, height = video.videoHeight; + if (!width || !height) return null; + canvas ??= document.createElement("canvas"); + // Map the visible object-cover preview to source pixels, including portrait video. + // Keep a margin around the guide so barcode quiet zones are not cut off. + const scale = Math.max((video.clientWidth || width) / width, (video.clientHeight || height) / height); + const visibleWidth = (video.clientWidth || width) / scale; + const visibleHeight = (video.clientHeight || height) / scale; + const sw = visibleWidth * 0.9, sh = visibleHeight * 0.6; + const sx = (width - sw) / 2, sy = (height - sh) / 2; + canvas.width = Math.max(1, Math.round(sw)); canvas.height = Math.max(1, Math.round(sh)); + const context = canvas.getContext("2d", { willReadFrequently: true }); + if (!context) throw new Error("Camera frame could not be read."); + context.drawImage(video, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height); + const code = attempt(() => focusedReader.decodeFromCanvas(canvas!).getText()); + // Periodically retain a wider search for a code just outside the guide. + return code ?? (++frames % 4 === 0 ? attempt(() => wideReader.decode(video).getText()) : null); + }, + dispose() { closed = true; native = null; detected = null; if (canvas) { canvas.width = 0; canvas.height = 0; } canvas = undefined; }, + }; +} + +/** Focus is optional: unsupported/rejected camera tuning must not break a preview. */ +export async function focusBarcodeCamera(stream: MediaStream): Promise { + try { + const track = stream.getVideoTracks?.()[0]; + const capabilities = track?.getCapabilities?.() as (MediaTrackCapabilities & { focusMode?: string[] }) | undefined; + if (capabilities?.focusMode?.includes("continuous")) { + await track!.applyConstraints({ advanced: [{ focusMode: "continuous" } as MediaTrackConstraintSet] }); + } + } catch { /* Keep the working camera and its existing focus mode. */ } +} + type CameraDependencies = { openStream: () => Promise; createReader: () => Promise; }; const cameraDependencies: CameraDependencies = { openStream: () => navigator.mediaDevices.getUserMedia({ - audio: false, video: { facingMode: { ideal: "environment" }, width: { ideal: 1280 }, height: { ideal: 720 } }, + audio: false, video: { facingMode: { ideal: "environment" }, width: { ideal: 1920 }, height: { ideal: 1080 } }, }), - async createReader() { - // Bundled by Next; no CDN, remote decoding or camera image upload. - const [{ BrowserMultiFormatReader }, { BarcodeFormat, DecodeHintType, NotFoundException, ChecksumException, FormatException }] = await Promise.all([ - import("@zxing/browser"), import("@zxing/library"), - ]); - const hints = new Map(); - hints.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.EAN_13, BarcodeFormat.EAN_8, BarcodeFormat.UPC_A, BarcodeFormat.ITF]); - const reader = new BrowserMultiFormatReader(hints); - return { decode(video) { - try { return reader.decode(video).getText(); } - catch (error) { - if (error instanceof NotFoundException || error instanceof ChecksumException || error instanceof FormatException) return null; - throw error; - } - } }; - }, + createReader: createFoodBarcodeReader, }; /** Own and release this session's tracks, including a permission grant after cancellation. */ @@ -54,11 +118,12 @@ export async function startBarcodeCamera( video: HTMLVideoElement, signal: AbortSignal, onCode: (code: string) => void, onReady: () => void, dependencies: CameraDependencies = cameraDependencies, ): Promise { - let stream: MediaStream | undefined, stopped = false; + let stream: MediaStream | undefined, reader: Reader | undefined, stopped = false; let timer: ReturnType | undefined, finish: (() => void) | undefined; function stop() { stopped = true; if (timer !== undefined) clearTimeout(timer); + reader?.dispose?.(); stream?.getTracks().forEach(track => track.stop()); if (stream && video.srcObject === stream) { video.pause(); video.srcObject = null; } signal.removeEventListener("abort", stop); @@ -67,10 +132,11 @@ export async function startBarcodeCamera( if (signal.aborted) return; signal.addEventListener("abort", stop, { once: true }); try { - const reader = await dependencies.createReader(); - if (stopped) return; + reader = await dependencies.createReader(); + if (stopped) { reader.dispose?.(); return; } stream = await dependencies.openStream(); if (stopped) { stop(); return; } + void focusBarcodeCamera(stream); video.muted = true; video.defaultMuted = true; video.playsInline = true; video.srcObject = stream; try { await video.play(); } @@ -86,7 +152,7 @@ export async function startBarcodeCamera( const tick = () => { if (stopped) { resolve(); return; } try { - const raw = video.readyState >= 2 ? reader.decode(video) : null; + const raw = video.readyState >= 2 ? reader!.decode(video) : null; const code = raw === null ? null : validFoodBarcode(raw); if (code) { stop(); onCode(code); return; } timer = setTimeout(tick, 250); diff --git a/tools/tests/food_barcode.test.mjs b/tools/tests/food_barcode.test.mjs index 06be19ad..4099b7e1 100644 --- a/tools/tests/food_barcode.test.mjs +++ b/tools/tests/food_barcode.test.mjs @@ -114,11 +114,16 @@ test('The actual bundled decoder reads EAN-8, EAN-13, UPC-A and ITF-14 raster fi // binarization, real ZXing decoding, validation and session cleanup run as shipped. class RasterReader extends BrowserMultiFormatReader { decode(video) { return this.decodeBitmap(video.bitmap); } + decodeFromCanvas(canvas) { return this.decodeBitmap(canvas.bitmap); } } const rasterModule = {exports: {}}; let nextStream; vm.runInNewContext(ts.transpileModule(source, {compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022}}).outputText, { module: rasterModule, exports: rasterModule.exports, setTimeout, clearTimeout, + document: {createElement() { + const canvas = {getContext: () => ({drawImage(video) {canvas.bitmap = video.bitmap;}})}; + return canvas; + }}, navigator: {mediaDevices: {getUserMedia: async () => nextStream}}, require(name) { return name === '@zxing/browser' ? {BrowserMultiFormatReader: RasterReader} : require(name); }, }); @@ -142,6 +147,7 @@ test('The actual bundled decoder reads EAN-8, EAN-13, UPC-A and ITF-14 raster fi const width = bars.length * 3, height = 100, pixels = new Uint8ClampedArray(width * height); for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) pixels[y * width + x] = bars[Math.floor(x / 3)] === '1' ? 0 : 255; const c = camera(); nextStream = c.stream; + c.video.videoWidth = width; c.video.videoHeight = height; c.video.bitmap = new zxing.BinaryBitmap(new zxing.HybridBinarizer(new zxing.RGBLuminanceSource(pixels, width, height))); const controller = new AbortController(), found = []; const timeout = setTimeout(() => controller.abort(), 2_000); diff --git a/tools/tests/food_barcode_recognition.test.mjs b/tools/tests/food_barcode_recognition.test.mjs new file mode 100644 index 00000000..5f453111 --- /dev/null +++ b/tools/tests/food_barcode_recognition.test.mjs @@ -0,0 +1,132 @@ +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import {createRequire} from 'node:module'; +import vm from 'node:vm'; +import test from 'node:test'; + +const require = createRequire(new URL('../../frontend/package.json', import.meta.url)); +const ts = require('typescript'), z = require('@zxing/library'); +const {BrowserMultiFormatReader} = require('@zxing/browser'); +const source = ts.transpileModule(readFileSync(new URL('../../frontend/lib/foodBarcode.ts', import.meta.url), 'utf8'), + {compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022}}).outputText; +const code = '3017620422003'; +const settle = () => new Promise(setImmediate); + +// The canvas capture is a pixel-copy double; the bundled luminance, binarizer +// and barcode readers are real. No browser, camera or provider is contacted. +function bitmap(image) { + return new z.BinaryBitmap(new z.HybridBinarizer(new z.RGBLuminanceSource(image.pixels, image.width, image.height))); +} +class RasterReader extends BrowserMultiFormatReader { + decode(video) { return this.decodeBitmap(bitmap({...video, width: video.videoWidth, height: video.videoHeight})); } + decodeFromCanvas(canvas) { return this.decodeBitmap(bitmap(canvas)); } +} +function load(Native, Reader = RasterReader) { + const captures = [], canvases = [], module = {exports: {}}; + const document = {createElement(name) { + assert.equal(name, 'canvas'); + const canvas = {getContext() {return {drawImage(video, sx, sy, sw, sh) { + captures.push({sx, sy, sw, sh}); + canvas.pixels = new Uint8ClampedArray(canvas.width * canvas.height); + for (let y = 0; y < canvas.height; y++) for (let x = 0; x < canvas.width; x++) { + canvas.pixels[y * canvas.width + x] = video.pixels[Math.floor(sy + y * sh / canvas.height) * video.videoWidth + Math.floor(sx + x * sw / canvas.width)]; + } + }};}}; + canvases.push(canvas); return canvas; + }}; + vm.runInNewContext(source, {module, exports: module.exports, document, window: {BarcodeDetector: Native}, setTimeout, clearTimeout, + require(name) {return name === '@zxing/browser' ? {BrowserMultiFormatReader: Reader} : require(name);}}); + return {...module.exports, captures, canvases}; +} +function frame(width = 1280, height = 720) { + return {videoWidth: width, videoHeight: height, clientWidth: 400, clientHeight: 225, + pixels: new Uint8ClampedArray(width * height).fill(255)}; +} +function barcode(video, x, y, height = 8) { + const left = ['0001101','0011001','0010011','0111101','0100011','0110001','0101111','0111011','0110111','0001011']; + const invert = bits => bits.replace(/[01]/g, bit => bit === '1' ? '0' : '1'); + const parity = 'LLGGGL'; // EAN-13 leading digit 3. + const bars = '101' + [...code.slice(1, 7)].map((d, i) => parity[i] === 'L' ? left[d] : invert([...left[d]].reverse().join(''))).join('') + + '01010' + [...code.slice(7)].map(d => invert(left[d])).join('') + '101'; + for (let row = y; row < y + height; row++) for (let col = 0; col < bars.length * 2; col++) { + video.pixels[row * video.videoWidth + x + col] = bars[Math.floor(col / 2)] === '1' ? 0 : 255; + } + return video; +} + +test('A small barcode between the old scan rows is found by the focused decoder', async () => { + const video = barcode(frame(), 540, 365); + const old = new RasterReader(new Map([[z.DecodeHintType.POSSIBLE_FORMATS, [z.BarcodeFormat.EAN_13]]])); + assert.throws(() => old.decode(video), z.NotFoundException); + const h = load(), reader = await h.createFoodBarcodeReader(); + assert.equal(reader.decode(video), code); + reader.dispose(); + assert.equal(reader.decode(video), null); + assert.equal(h.canvases[0].width, 0); +}); + +test('The scan region follows the visible guide in a portrait camera stream', async () => { + const h = load(), reader = await h.createFoodBarcodeReader(); + assert.equal(reader.decode(barcode(frame(720, 1280), 260, 645)), code); + assert.deepEqual(h.captures[0], {sx: 36, sy: 518.5, sw: 648, sh: 243}); + reader.dispose(); +}); + +class EmptyReader { decode() {throw new z.NotFoundException();} decodeFromCanvas() {throw new z.NotFoundException();} } +test('An unsupported native API leaves the bundled decoder available', async () => { + for (const Native of [undefined, class {static getSupportedFormats() {return Promise.reject(new Error('unsupported'));}}, + class {static async getSupportedFormats() {return ['qr_code'];} constructor() {assert.fail('Do not enable QR');}}]) { + const h = load(Native), reader = await h.createFoodBarcodeReader(); await settle(); + assert.equal(reader.decode(barcode(frame(), 540, 365)), code); reader.dispose(); + } +}); + +test('A slow native detection never blocks fallback or creates overlapping requests', async () => { + let resolve, calls = 0; + class Native { + static async getSupportedFormats() {return ['ean_13', 'qr_code'];} + constructor(options) {assert.deepEqual([...options.formats], ['ean_13']);} + detect() {calls++; return new Promise(r => {resolve = r;});} + } + const h = load(Native), reader = await h.createFoodBarcodeReader(); await settle(); + const video = barcode(frame(), 540, 365); + assert.equal(reader.decode(video), code); await settle(); + assert.equal(reader.decode(video), code); await settle(); assert.equal(calls, 1); + reader.dispose(); resolve([{format: 'ean_13', rawValue: code}]); await settle(); + assert.equal(reader.decode(video), null); +}); + +test('Native detections must be supported food formats with valid checksums', async () => { + let results = [{format: 'qr_code', rawValue: code}, {format: 'ean_13', rawValue: '3017620422004'}]; + class Native {static async getSupportedFormats() {return ['ean_13'];} async detect() {return results;}} + const h = load(Native, EmptyReader), reader = await h.createFoodBarcodeReader(), video = frame(320, 180); + await settle(); assert.equal(reader.decode(video), null); await settle(); assert.equal(reader.decode(video), null); await settle(); + results = [{format: 'ean_13', rawValue: '0034000470693'}]; + assert.equal(reader.decode(video), null); await settle(); + assert.equal(reader.decode(video), '0034000470693'); reader.dispose(); +}); + +test('Native detection rejection falls back without stopping the working camera', async () => { + let calls = 0; + class Native {static async getSupportedFormats() {return ['ean_13'];} async detect() {calls++; throw new Error('device API failed');}} + const h = load(Native), reader = await h.createFoodBarcodeReader(), video = barcode(frame(), 540, 365); + await settle(); assert.equal(reader.decode(video), code); await settle(); + assert.equal(reader.decode(video), code); await settle(); assert.equal(calls, 1); reader.dispose(); +}); + +test('Stopping during native capability discovery prevents a late detector from starting', async () => { + let resolve; + class Native {static getSupportedFormats() {return new Promise(r => {resolve = r;});} constructor() {assert.fail('Cancelled');}} + const h = load(Native), reader = await h.createFoodBarcodeReader(); await settle(); + reader.dispose(); resolve(['ean_13']); await settle(); assert.equal(reader.decode(frame()), null); +}); + +test('Continuous focus is requested only if supported; a rejection is harmless', async () => { + const {focusBarcodeCamera} = load(); let attempts = 0; + await focusBarcodeCamera({getVideoTracks: () => []}); + await focusBarcodeCamera({getVideoTracks: () => [{getCapabilities: () => ({focusMode: ['manual']}), applyConstraints() {assert.fail();}}]}); + const track = {getCapabilities: () => ({focusMode: ['continuous']}), async applyConstraints(options) { + attempts++; assert.equal(options.advanced[0].focusMode, 'continuous'); throw Error('Camera does not accept tuning'); + }}; + await focusBarcodeCamera({getVideoTracks: () => [track]}); assert.equal(attempts, 1); +}); From d2b127c40f5ea197c11ad45a73bbd0ca27935d7e Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 20:25:54 +0200 Subject: [PATCH 026/368] Fix Testnet faucet seed parsing for current response format --- .../wordpress_testnet_onboarding.test.mjs | 54 ++++++++++++++++++- .../calorietoken-site-style/LEESMIJ.md | 16 ++++-- .../calorietoken-site-style/assets/testnet.js | 9 +++- .../calorietoken-site-style.php | 4 +- 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/tools/tests/wordpress_testnet_onboarding.test.mjs b/tools/tests/wordpress_testnet_onboarding.test.mjs index a33d4f91..0634a38b 100644 --- a/tools/tests/wordpress_testnet_onboarding.test.mjs +++ b/tools/tests/wordpress_testnet_onboarding.test.mjs @@ -11,7 +11,9 @@ const source = readFileSync(new URL('testnet.js', assets), 'utf8'); const copy = JSON.parse(readFileSync(new URL('testnet-data.json', assets), 'utf8')); // Shape-only placeholders with invalid checksums: no real wallet or network is used. const address = 'r' + '1'.repeat(25), secret = 's' + '2'.repeat(24); -const accountReply = () => ({account: {classicAddress: address, secret}}); +// Matches the field types observed in one approved faucet response on 2026-09-11. +// All values remain synthetic; no returned account or recovery data is retained. +const accountReply = () => ({account: {address, classicAddress: address, xAddress: 'T' + '3'.repeat(40)}, amount: 10, seed: secret}); const response = (data = accountReply(), {status = 200, type = 'application/json', json, retryAfter = null} = {}) => ({ ok: status >= 200 && status < 300, status, headers: {get: name => name === 'content-type' ? type : name === 'retry-after' ? retryAfter : null}, @@ -87,7 +89,55 @@ test('Creating an account requires a click and sends one minimal, credential-fre assert.equal(h.timers.size, 0); }); -test('Both documented address fields are accepted and funding is independently checked on Testnet', async () => { +test('The current faucet top-level seed reaches independent verification and remains hidden until requested', async () => { + const h = fixture(); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(), copy.nl.checking); + assert.equal(h.query('#ctstyle-testnet-address').textContent, address); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); + assert.equal(h.requests.length, 1); assert.equal(h.sockets.length, 1); + assert.equal(h.clipboard.length, 0); + h.funded(); await settle(); + assert.equal(h.status(), copy.nl.funded); + h.clickCopy('next'); h.clickCopy('show'); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, secret); + h.clickCopy('next'); assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); + assert.equal([...h.storage.values()].some(value => value.includes(secret) || value.includes(address)), false); + assert.equal([...h.document.querySelectorAll('a')].some(node => node.href.includes(secret)), false); +}); + +test('The current faucet seed accepts both supported family-seed shapes without accepting missing or malformed seeds', async () => { + for (const seed of [secret, 'sEd' + '3'.repeat(28)]) { + const h = fixture({fetch: async () => response({...accountReply(), seed})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(), copy.nl.checking); + h.clickCopy('next'); h.clickCopy('show'); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, seed); + } + for (const seed of [null, '', 42, {}, [], 'not-a-family-seed', 's' + '0'.repeat(24)]) { + const h = fixture({fetch: async () => response({...accountReply(), seed})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(), copy.nl.invalidResponse); + assert.equal(h.sockets.length, 0); + assert.equal(h.query('#ctstyle-testnet-address').textContent, ''); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); + } +}); + +test('A malformed or conflicting current seed never silently falls back to a legacy secret', async () => { + for (const seed of [null, '', 42, 's' + '4'.repeat(24)]) { + const h = fixture({fetch: async () => response({account: {classicAddress: address, secret}, seed})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(), copy.nl.invalidResponse); + assert.equal(h.sockets.length, 0); + assert.equal(h.query('#ctstyle-testnet-secret').textContent, ''); + } + const h = fixture({fetch: async () => response({account: {classicAddress: address, secret}, seed: secret})}); + h.click('#ctstyle-testnet-create-button'); await settle(); + assert.equal(h.status(), copy.nl.checking); +}); + +test('Both legacy address fields and account.secret remain accepted with independent Testnet verification', async () => { for (const field of ['classicAddress', 'address']) { const h = fixture({fetch: async () => response({account: {[field]: address, secret}})}); h.click('#ctstyle-testnet-create-button'); await settle(); diff --git a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md index 014538d0..70cf12e0 100644 --- a/wordpress-plugins/calorietoken-site-style/LEESMIJ.md +++ b/wordpress-plugins/calorietoken-site-style/LEESMIJ.md @@ -1,12 +1,22 @@ -# CalorieToken Site Style 1.4.17 +# CalorieToken Site Style 1.4.18 Eén complete vervangende ZIP, 11 september 2026. Alle eerdere Site Style-correcties zijn inbegrepen; tussenversies zijn niet nodig. ## Installeren -WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.17.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.17** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. +WordPress → Plugins → Nieuwe plugin → Plugin uploaden → kies **calorietoken-site-style-1.4.18.zip** → Nu installeren → **Huidige vervangen door geüploade**. Controleer dat **CalorieToken Site Style 1.4.18** actief is. Laat XummLogin, Identity Bridge en Content Workbench staan. -## Gedeeld menu na publicatie van Showcases +## Testnet-antwoordcorrectie + +Na expliciet akkoord van de eigenaar gaf één officiële Testnet-faucetaanvraag HTTP 200 met JSON terug. De herstelcode staat daarin als `seed` naast `account`; de vorige module las alleen `account.secret`. Daardoor wees zij dit antwoord af als onbruikbare accountgegevens. Alleen status en bekende veldtypen zijn vastgelegd, geen echte herstelcode, adres of volledig antwoord. Er is geen tweede aanvraag gedaan. + +Deze versie leest het actuele `seed`-veld en behoudt ondersteuning voor het oudere `account.secret`-formaat. De adres- en codevormcontrole blijft verplicht. Een ongeldige aanwezige `seed` wordt niet vervangen door een legacywaarde; tegenstrijdige velden worden afgewezen. De herstelcode blijft verborgen tot de bezoeker haar bewust toont of kopieert, staat uitsluitend tijdelijk in browsergeheugen en wordt bij vertrek uit de module gewist. De aparte gevalideerde Testnet-saldocontrole, wachttellers, provider-backoff en één-aanvraagbewaking blijven behouden. + +Gerichte tests gebruiken de waargenomen antwoordstructuur met uitsluitend fictieve waarden. De eerdere code reproduceert daarbij de screenshotmelding. Een geslaagde HTTP-reactie bewijst nog geen walletimport of geverifieerd saldo in de geïnstalleerde pagina. De al geaccepteerde telefoonscan, productopvraag en gedeelde Xaman-login/logout hoeven niet opnieuw getest te worden. + +Dit pakket verandert uitsluitend de Testnet-uitlezing, versie en installatie-uitleg. Het bevat geen nieuwe afhankelijkheid, frontend-/backenduitrol, menu-/permalinkwijziging of wijziging van de geïnstalleerde Identity Bridge. Directe terugval is het bewaarde pakket **1.4.17**. De onderstaande notities bewaren eerdere tussenstanden; oude verzoeken om camera- of loginproeven gelden niet opnieuw. + +## Inbegrepen uit 1.4.17: gedeeld menu na publicatie van Showcases Op verzoek van de eigenaar zijn de bestaande Showcases-introductie (pagina 7945) en de drie ingangen CalorieApp, Showcases en Voting Hub in het bestaande WordPress/Brizy-menu Hoofdmenu3 bijgewerkt. CalorieApp en de informatieve Community Voting Hub (8020) waren al gepubliceerd. De oude Voting Hub-werkversie blijft een concept; er wordt geen stemming geactiveerd. De verdere showcases blijven werk voor stap 4. diff --git a/wordpress-plugins/calorietoken-site-style/assets/testnet.js b/wordpress-plugins/calorietoken-site-style/assets/testnet.js index cb85d0fb..6eb7fcdb 100644 --- a/wordpress-plugins/calorietoken-site-style/assets/testnet.js +++ b/wordpress-plugins/calorietoken-site-style/assets/testnet.js @@ -147,7 +147,14 @@ var data = await response.json(), wallet = data && data.account; if (current !== generation || !allowed()) return; if (timedOut) { status = 'timedOut'; return; } - var address = wallet && (wallet.classicAddress || wallet.address), secret = wallet && wallet.secret; + var address = wallet && (wallet.classicAddress || wallet.address); + // The current faucet returns seed beside account; older replies used account.secret. + // An explicitly supplied current seed must be valid, never replaced by a legacy value. + var hasSeed = !!data && Object.prototype.hasOwnProperty.call(data,'seed'); + var secret = hasSeed ? data.seed : wallet && wallet.secret; + if (hasSeed && wallet && Object.prototype.hasOwnProperty.call(wallet,'secret') && wallet.secret !== secret) { + status = 'invalidResponse'; return; + } // Schema checks only; the official Testnet ledger independently verifies funding below. if (typeof address !== 'string' || !/^r[1-9A-HJ-NP-Za-km-z]{24,34}$/.test(address) || typeof secret !== 'string' || !/^s[1-9A-HJ-NP-Za-km-z]{20,34}$/.test(secret)) { status = 'invalidResponse'; return; } diff --git a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php index 0a29a274..c671e9df 100644 --- a/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php +++ b/wordpress-plugins/calorietoken-site-style/calorietoken-site-style.php @@ -2,7 +2,7 @@ /** * Plugin Name: CalorieToken Site Style * Description: CalorieApp-huisstijl en gebundelde stap 3-verfijningen. Gedeelde huisstijl, appinformatie en paginakoppelingen; geaccepteerde Home-inhoud behouden. - * Version: 1.4.17 + * Version: 1.4.18 * Requires at least: 6.2 * Requires PHP: 7.4 * Author: ICTHendrikse @@ -15,7 +15,7 @@ if (!defined('ABSPATH')) { exit; } final class Plugin { - const VERSION = '1.4.17'; + const VERSION = '1.4.18'; public static function header_menu() { // The menu verified in the live Brizy header. Never fall through to an From 6d9641f57a9c1fd2f7863f2fc0e9750cc2e0f1c4 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:50:39 +0200 Subject: [PATCH 027/368] Fix embedded logout and add period diaries with consistent site navigation --- backend/app/food_log_view.py | 39 +++ backend/app/main.py | 16 + backend/app/route_rate_limiter.py | 1 + backend/app/schemas.py | 11 + backend/tests/test_food_log_view.py | 63 ++++ backend/tests/test_route_rate_limiter.py | 1 + frontend/app/api/backend/[...path]/route.ts | 1 + frontend/components/FoodDiaryPeriod.tsx | 34 +++ frontend/components/FoodLogList.tsx | 21 +- frontend/components/FoodSearchPlaceholder.tsx | 134 +++++--- frontend/components/XamanLoginPanel.tsx | 96 ++++-- frontend/config/auth-ui-copy.json | 44 ++- frontend/config/diary-copy.json | 288 ++++++++++++++++++ frontend/lib/authUi.ts | 2 +- frontend/lib/foodDiary.ts | 48 +++ tools/build_site_style_release.py | 3 + tools/tests/embedded_logout_ui.test.mjs | 76 +++++ tools/tests/food_diary_dates.test.mjs | 30 ++ tools/tests/food_logging_ui.test.mjs | 69 ++++- tools/tests/wordpress_presentation.test.mjs | 43 +++ .../calorietoken-site-style/LEESMIJ.md | 6 +- .../assets/content-language.js | 5 +- .../assets/presentation-data.json | 79 +++++ .../assets/presentation.css | 64 ++++ .../assets/presentation.js | 111 +++++++ .../calorietoken-site-style/assets/style.js | 2 +- .../calorietoken-site-style.php | 40 ++- 27 files changed, 1231 insertions(+), 96 deletions(-) create mode 100644 backend/app/food_log_view.py create mode 100644 backend/tests/test_food_log_view.py create mode 100644 frontend/components/FoodDiaryPeriod.tsx create mode 100644 frontend/config/diary-copy.json create mode 100644 frontend/lib/foodDiary.ts create mode 100644 tools/tests/embedded_logout_ui.test.mjs create mode 100644 tools/tests/food_diary_dates.test.mjs create mode 100644 tools/tests/wordpress_presentation.test.mjs create mode 100644 wordpress-plugins/calorietoken-site-style/assets/presentation-data.json create mode 100644 wordpress-plugins/calorietoken-site-style/assets/presentation.css create mode 100644 wordpress-plugins/calorietoken-site-style/assets/presentation.js diff --git a/backend/app/food_log_view.py b/backend/app/food_log_view.py new file mode 100644 index 00000000..0115b2da --- /dev/null +++ b/backend/app/food_log_view.py @@ -0,0 +1,39 @@ +"""Owner-scoped diary pages and whole-period totals, without changing stored logs.""" +from datetime import UTC, datetime + +from fastapi import HTTPException +from sqlalchemy import case, func +from sqlmodel import Session, select + +from .models import FoodLogDB +from .schemas import FoodLog, FoodLogOverview + + +def food_log_overview(session: Session, owner_id: int, start: datetime | None, + end: datetime | None, before: int | None, limit: int) -> FoodLogOverview: + if (start is None) != (end is None): + raise HTTPException(422, "Supply both start and end, or neither") + conditions = [FoodLogDB.owner_id == owner_id] + if start is not None and end is not None: + if start.utcoffset() is None or end.utcoffset() is None: + raise HTTPException(422, "Diary boundaries must include a time zone") + if end <= start or (end - start).total_seconds() > 32 * 86400: + raise HTTPException(422, "Diary range must be positive and at most 32 days") + # The existing table stores UTC in timezone-naive SQL DateTime columns. + conditions += [FoodLogDB.created_at >= start.astimezone(UTC).replace(tzinfo=None), + FoodLogDB.created_at < end.astimezone(UTC).replace(tzinfo=None)] + columns = [func.count(FoodLogDB.id)] + [ + func.coalesce(func.sum(getattr(FoodLogDB, key)), 0) + for key in ("calories", "protein", "fat", "carbohydrates") + ] + [func.coalesce(func.sum(case((func.upper(func.trim(FoodLogDB.nutri_score)) == grade, 1), else_=0)), 0) + for grade in "ABCDE"] + totals = session.exec(select(*columns).where(*conditions)).one() + page_conditions = conditions + ([FoodLogDB.id < before] if before is not None else []) + entries = session.exec(select(FoodLogDB).where(*page_conditions) + .order_by(FoodLogDB.id.desc()).limit(limit + 1)).all() + return FoodLogOverview( + entries=[FoodLog.model_validate(row.model_dump()) for row in entries[:limit]], + next_before=entries[limit - 1].id if len(entries) > limit else None, + count=totals[0], calories=totals[1], protein=totals[2], fat=totals[3], carbohydrates=totals[4], + grades=dict(zip("ABCDE", totals[5:])), + ) diff --git a/backend/app/main.py b/backend/app/main.py index 7ca24a72..215e05c2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -42,6 +42,8 @@ validate_capacity_configuration, ) from .database import database_readiness, get_session, init_db +from .food_log_view import food_log_overview +from .schemas import FoodLogOverview from .data_growth import ( DataGrowthAdmissionRejected, create_food_log_with_subject_budget, @@ -1643,6 +1645,20 @@ def get_logs( return [FoodLog.model_validate(e.model_dump()) for e in entries] +@app.get("/logs/overview", response_model=FoodLogOverview) +def get_log_overview( + session: DbSession, + current_user: CurrentUser, + response: Response, + start: datetime | None = None, + end: datetime | None = None, + before: int | None = Query(default=None, ge=1), + limit: int = Query(default=100, ge=1, le=200), +) -> FoodLogOverview: + response.headers["Cache-Control"] = "private, no-store" + return food_log_overview(session, current_user.id, start, end, before, limit) + + @app.delete("/logs/{log_id}") def delete_log( log_id: int, diff --git a/backend/app/route_rate_limiter.py b/backend/app/route_rate_limiter.py index b9e1391e..22e2ca31 100644 --- a/backend/app/route_rate_limiter.py +++ b/backend/app/route_rate_limiter.py @@ -66,6 +66,7 @@ def __post_init__(self) -> None: ("POST", "/api/identity/logout"): RouteRatePolicy("identity_logout", 120), ("POST", "/log-food"): RouteRatePolicy("food_log_create", 120), ("GET", "/logs"): RouteRatePolicy("food_log_list", 240), + ("GET", "/logs/overview"): RouteRatePolicy("food_log_list", 240), ("DELETE", "/logs"): RouteRatePolicy("food_log_delete_all", 30), ("GET", "/search-food"): RouteRatePolicy("food_search", 60), } diff --git a/backend/app/schemas.py b/backend/app/schemas.py index f4a147a3..ecb6da1a 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -56,6 +56,17 @@ def serialize_created_at_as_utc(cls, value: datetime) -> datetime: return _ensure_utc(value) +class FoodLogOverview(BaseModel): + entries: list[FoodLog] + next_before: int | None + count: int + calories: float + protein: float + fat: float + carbohydrates: float + grades: dict[str, int] + + class FoodSearchResult(BaseModel): model_config = ConfigDict(allow_inf_nan=False) diff --git a/backend/tests/test_food_log_view.py b/backend/tests/test_food_log_view.py new file mode 100644 index 00000000..62f375a0 --- /dev/null +++ b/backend/tests/test_food_log_view.py @@ -0,0 +1,63 @@ +from datetime import datetime + +from sqlmodel import Session, select + +import app.database as db +from app.models import CalorieAppUserDB, FoodLogDB + + +def seed_entries(): + with Session(db.engine) as session: + owner = session.exec(select(CalorieAppUserDB)).first() + other = CalorieAppUserDB(status="active") + session.add(other) + session.flush() + for index in range(105): + session.add(FoodLogDB(owner_id=owner.id, product_name=f"Test food {index}", + calories=10, protein=2, fat=1, carbohydrates=3, nutri_score="A", + created_at=datetime(2026, 9, 10, 22))) + for who, when in [(owner.id, datetime(2026, 9, 11, 22)), + (owner.id, datetime(2026, 9, 10, 21, 59)), + (other.id, datetime(2026, 9, 11)), + (None, datetime(2026, 9, 11))]: + session.add(FoodLogDB(owner_id=who, product_name="Outside selected period or owner", + calories=900, created_at=when)) + session.commit() + + +def test_period_totals_include_all_pages_and_isolate_owner(authenticated_client): + seed_entries() + params = {"start": "2026-09-11T00:00:00+02:00", "end": "2026-09-12T00:00:00+02:00"} + response = authenticated_client.get("/logs/overview", params=params) + assert response.status_code == 200 + assert "no-store" in response.headers["cache-control"] + first = response.json() + assert len(first["entries"]) == 100 + assert first["count"] == 105 + assert first["calories"] == 1050 + assert first["protein"] == 210 + assert first["grades"] == {"A": 105, "B": 0, "C": 0, "D": 0, "E": 0} + second = authenticated_client.get("/logs/overview", params={**params, "before": first["next_before"]}).json() + assert len(second["entries"]) == 5 + assert second["count"] == 105 + assert second["next_before"] is None + assert not {x["id"] for x in first["entries"]} & {x["id"] for x in second["entries"]} + # An ordinary old client still receives the original list contract. + assert isinstance(authenticated_client.get("/logs").json(), list) + assert authenticated_client.get("/logs/overview").json()["count"] == 107 + + +def test_period_empty_and_invalid_boundaries(authenticated_client): + valid = {"start": "2026-03-28T23:00:00Z", "end": "2026-03-29T22:00:00Z"} + response = authenticated_client.get("/logs/overview", params=valid) + assert response.status_code == 200 + assert response.json()["count"] == 0 + assert response.json()["calories"] == 0 + for params in [{"start": valid["start"]}, {**valid, "end": valid["start"]}, + {**valid, "end": "2027-01-01T00:00:00Z"}, + {"start": "2026-09-11", "end": "2026-09-12"}, {"limit": 201}, {"before": 0}]: + assert authenticated_client.get("/logs/overview", params=params).status_code == 422 + + +def test_diary_overview_requires_authentication(client): + assert client.get("/logs/overview").status_code == 401 diff --git a/backend/tests/test_route_rate_limiter.py b/backend/tests/test_route_rate_limiter.py index c93a2535..7d7ebde6 100644 --- a/backend/tests/test_route_rate_limiter.py +++ b/backend/tests/test_route_rate_limiter.py @@ -120,6 +120,7 @@ def test_current_public_route_budgets_are_explicit_and_reviewable() -> None: ("POST", "/api/identity/logout"): ("identity_logout", 120, 60), ("POST", "/log-food"): ("food_log_create", 120, 60), ("GET", "/logs"): ("food_log_list", 240, 60), + ("GET", "/logs/overview"): ("food_log_list", 240, 60), ("DELETE", "/logs"): ("food_log_delete_all", 30, 60), ("GET", "/search-food"): ("food_search", 60, 60), } diff --git a/frontend/app/api/backend/[...path]/route.ts b/frontend/app/api/backend/[...path]/route.ts index ea3317f9..bb68c9e8 100644 --- a/frontend/app/api/backend/[...path]/route.ts +++ b/frontend/app/api/backend/[...path]/route.ts @@ -25,6 +25,7 @@ const ROUTE_METHODS: Array<{ pattern: RegExp; methods: Set }> = [ { pattern: /^search-food$/, methods: new Set(["GET"]) }, { pattern: /^log-food$/, methods: new Set(["POST"]) }, { pattern: /^logs$/, methods: new Set(["GET", "DELETE"]) }, + { pattern: /^logs\/overview$/, methods: new Set(["GET"]) }, { pattern: /^logs\/[^/]+$/, methods: new Set(["DELETE"]) }, { pattern: /^api\/identity\/(login\/(start|status)|callback|logout)$/, diff --git a/frontend/components/FoodDiaryPeriod.tsx b/frontend/components/FoodDiaryPeriod.tsx new file mode 100644 index 00000000..3f493440 --- /dev/null +++ b/frontend/components/FoodDiaryPeriod.tsx @@ -0,0 +1,34 @@ +"use client"; +import { useId } from "react"; +import { DiaryPeriod, diaryCopy, diaryRange, localDiaryDate, moveDiaryDate } from "@/lib/foodDiary"; + +export function FoodDiaryPeriod({period, date, locale, disabled, onChange}: { + period: DiaryPeriod; date: string; locale: string; disabled: boolean; + onChange: (period: DiaryPeriod, date: string) => void; +}) { + const copy = diaryCopy(locale), dateId = useId(); + const range = diaryRange(period, date); + const format = (value: Date) => new Intl.DateTimeFormat(locale, {day: "numeric", month: "long", year: "numeric"}).format(value); + const label = range ? (period === "day" ? format(range.start) + : `${format(range.start)} – ${format(new Date(range.end.getTime() - 1))}`) : copy.all; + const button = "min-h-11 rounded-full border-2 border-brand-secondary px-4 py-2 text-sm font-semibold text-brand-secondary hover:bg-brand-secondary/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-brand-primary disabled:opacity-50"; + return
    +

    {copy.title}

    +
    + {(["day", "week", "month", "all"] as DiaryPeriod[]).map(value => )} +
    + {period !== "all" ?
    + +
    + { if (e.target.value && e.target.validity.valid) onChange(period, e.target.value); }} + className="min-h-11 w-full min-w-0 rounded-lg border border-brand-secondary/30 bg-white px-3 text-brand-secondary" />
    + + +
    : null} +

    {label}

    +

    {copy.scope}

    +
    ; +} diff --git a/frontend/components/FoodLogList.tsx b/frontend/components/FoodLogList.tsx index 1bd7632a..e9220496 100644 --- a/frontend/components/FoodLogList.tsx +++ b/frontend/components/FoodLogList.tsx @@ -4,10 +4,15 @@ import { useEffect, useId, useMemo, useRef, useState } from "react"; import { FoodSearchItem } from "@/components/foodTypes"; import { filterLoggedFoods, getFoodLogFilterCopy } from "@/lib/foodLogFilter"; import { useDisplayLanguage } from "@/components/DisplayLanguageProvider"; +import { diaryCopy } from "@/lib/foodDiary"; import { formatFoodUi, getFoodUi } from "@/lib/foodUi"; type FoodLogListProps = { logs: FoodSearchItem[]; + periodFiltered?: boolean; + total?: number; + hasMore?: boolean; + onLoadMore?: () => void; onRefresh: () => void; onSelectLog: (log: FoodSearchItem) => void; onDeleteLog: (logId: number) => void; @@ -20,6 +25,7 @@ type FoodLogListProps = { export function FoodLogList({ logs, + periodFiltered = false, total = logs.length, hasMore = false, onLoadMore, onRefresh, onSelectLog, onDeleteLog, @@ -33,6 +39,7 @@ export function FoodLogList({ const [initialLocale, setLocale] = useState("en"); const display = useDisplayLanguage(); const locale = display.enabled ? display.locale : initialLocale; + const diaryUi = diaryCopy(locale); const ui = getFoodUi(display.enabled ? locale : "en"); const filterId = useId(); const filterInput = useRef(null); @@ -72,10 +79,10 @@ export function FoodLogList({ className="rounded-full border-2 border-red-300 bg-transparent px-4 py-2 text-xs font-semibold text-red-600 transition hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-50" onClick={onDeleteAllLogs} disabled={ - isLoading || isClearingAll || deletingLogId !== null || logs.length === 0 || filtering + isLoading || isClearingAll || deletingLogId !== null || logs.length === 0 || filtering || periodFiltered } aria-label={ui.copy.deleteAllLabel} - aria-describedby={filtering ? `${filterId}-delete-hint` : undefined} + aria-describedby={filtering || periodFiltered ? `${filterId}-delete-hint` : undefined} > {isClearingAll ? ui.copy.deleting : ui.copy.deleteAll} @@ -111,14 +118,14 @@ export function FoodLogList({ ) : null}
    -

    {copy.scope}

    +

    {diaryUi.listScope}

    {copy.count .replace("{shown}", new Intl.NumberFormat(locale).format(visibleLogs.length)) .replace("{total}", new Intl.NumberFormat(locale).format(logs.length))}

    - {filtering ? ( -

    {copy.delete_all_hint}

    + {filtering || periodFiltered ? ( +

    {diaryUi.deleteHint}

    ) : null} {visibleLogs.length === 0 ? (

    {copy.empty}

    @@ -139,6 +146,7 @@ export function FoodLogList({ aria-label={formatFoodUi(ui.copy.viewDetails, { product: item.product_name })} >

    {item.product_name}

    + {item.created_at && Number.isFinite(Date.parse(item.created_at)) ?

    : null}
    {ui.copy.calories} @@ -173,6 +181,9 @@ export function FoodLogList({
  • ))}
+

{formatFoodUi(diaryUi.loaded, {shown: String(logs.length), total: String(total)})}

+ {hasMore ? : null}
); } diff --git a/frontend/components/FoodSearchPlaceholder.tsx b/frontend/components/FoodSearchPlaceholder.tsx index a037c61e..06f6e496 100644 --- a/frontend/components/FoodSearchPlaceholder.tsx +++ b/frontend/components/FoodSearchPlaceholder.tsx @@ -8,6 +8,8 @@ import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "re import { EmptyState } from "@/components/EmptyState"; import { ErrorBanner } from "@/components/ErrorBanner"; import { FoodCard } from "@/components/FoodCard"; +import { FoodDiaryPeriod } from "@/components/FoodDiaryPeriod"; +import { DiaryPeriod, DiaryOverview, diaryCopy, diaryParams, emptyDiary, localDiaryDate } from "@/lib/foodDiary"; import { FoodLogList } from "@/components/FoodLogList"; import { NutriScoreBar } from "@/components/NutriScoreBar"; import { LoadingState } from "@/components/LoadingState"; @@ -15,7 +17,7 @@ import { SearchBar } from "@/components/SearchBar"; import { FoodSearchItem, FoodSearchResponse } from "@/components/foodTypes"; import Image from "next/image"; import { useDisplayLanguage } from "@/components/DisplayLanguageProvider"; -import { countRecordedGrades, formatFoodUi, getFoodUi, recordedGradePosition, recordedGradeStyle, translateFoodStatus } from "@/lib/foodUi"; +import { formatFoodUi, getFoodUi, recordedGradePosition, recordedGradeStyle, translateFoodStatus } from "@/lib/foodUi"; import { foodSearchRetryAt } from "@/lib/foodSearchAvailability"; import { AUTH_STATE_CHANGED_EVENT, @@ -191,6 +193,8 @@ export function FoodSearchPlaceholder() { const searchInFlightRef = useRef(false); const searchRetryAtRef = useRef(0); const logsRequestIdRef = useRef(0); + const logsAbortRef = useRef(null); + const diaryAuthenticatedRef = useRef(false); const privateStateVersionRef = useRef(0); const logMutationInFlightRef = useRef(false); const logSelectionIdRef = useRef(0); @@ -198,6 +202,13 @@ export function FoodSearchPlaceholder() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [logs, setLogs] = useState([]); + const [diaryOverview, setDiaryOverview] = useState(emptyDiary); + const [diaryPeriod, setDiaryPeriod] = useState("day"); + const [diaryDate, setDiaryDate] = useState(() => localDiaryDate()); + const diaryUi = diaryCopy(locale); + const [lastSearch, setLastSearch] = useState<{query: string; barcode: boolean} | null>(null); + const [resultsQuery, setResultsQuery] = useState(""); + const [searchFailure, setSearchFailure] = useState<"startFailed" | "offline" | "busy" | "slow" | "unavailable" | null>(null); const [isLoading, setIsLoading] = useState(false); const [isLogsLoading, setIsLogsLoading] = useState(false); const [isLogging, setIsLogging] = useState(null); @@ -243,30 +254,16 @@ export function FoodSearchPlaceholder() { const hasResults = useMemo(() => results.length > 0, [results]); const hasLogs = useMemo(() => logs.length > 0, [logs]); - const summary = useMemo(() => { - return logs.reduce( - (totals, item) => { - totals.calories += item.calories; - totals.protein += item.protein; - totals.fat += item.fat; - totals.carbohydrates += item.carbohydrates; - totals.count += 1; - return totals; - }, - { - calories: 0, - protein: 0, - fat: 0, - carbohydrates: 0, - count: 0, - } - ); - }, [logs]); + const summary = diaryOverview; const selectedLog = useMemo( () => logs.find((item) => item.id === selectedLogId) ?? null, [logs, selectedLogId] ); - const recordedGrades = useMemo(() => countRecordedGrades(logs), [logs]); + const recordedGrades = useMemo(() => { + const grades = ["A", "B", "C", "D", "E"].map(grade => ({grade, count: diaryOverview.grades[grade] ?? 0})); + const known = grades.reduce((sum, item) => sum + item.count, 0); + return {grades, known, total: diaryOverview.count, missing: diaryOverview.count - known}; + }, [diaryOverview]); const gradePosition = recordedGradePosition(recordedGrades); const gradePositionLabel = gradePosition ? formatFoodUi(gradePosition.lower === gradePosition.upper ? copy.scoreAt : copy.scoreBetween, @@ -287,6 +284,9 @@ export function FoodSearchPlaceholder() { // Invalidate any request that began under the previous authentication // state so a late response cannot repopulate another session's logs. logsRequestIdRef.current += 1; + logsAbortRef.current?.abort(); + diaryAuthenticatedRef.current = false; + setDiaryOverview(emptyDiary); privateStateVersionRef.current += 1; logSelectionIdRef.current += 1; setLogs([]); @@ -309,13 +309,16 @@ export function FoodSearchPlaceholder() { } }, [logs, selectedLogId]); - const fetchLogs = useCallback(async () => { + const fetchLogs = useCallback(async (before?: number) => { + logsAbortRef.current?.abort(); + const controller = new AbortController(); + logsAbortRef.current = controller; const requestId = ++logsRequestIdRef.current; setIsLogsLoading(true); setLogError(null); try { - const response = await backendRequest(`${BACKEND_BASE_URL}/logs`); + const response = await backendRequest(`${BACKEND_BASE_URL}/logs/overview?${diaryParams(diaryPeriod, diaryDate, before)}`, {signal: controller.signal, cache: "no-store"}); if (requestId !== logsRequestIdRef.current) { return; } @@ -326,11 +329,14 @@ export function FoodSearchPlaceholder() { if (!response.ok) { throw new Error("Logs request failed."); } - const data = (await response.json()) as unknown[]; + const data = (await response.json()) as DiaryOverview; + if (!Array.isArray(data.entries) || !Number.isFinite(data.count) || !data.grades) throw new Error("Invalid diary response"); if (requestId !== logsRequestIdRef.current) { return; } - setLogs(normalizeFoodItems(data ?? [])); + const entries = normalizeFoodItems(data.entries); + setLogs(current => before ? [...current, ...entries.filter(item => !current.some(old => old.id === item.id))] : entries); + setDiaryOverview(data); setLogError(null); } catch (requestError) { if (requestId === logsRequestIdRef.current) { @@ -346,11 +352,28 @@ export function FoodSearchPlaceholder() { setIsLogsLoading(false); } } - }, [clearPrivateLogState]); + }, [clearPrivateLogState, diaryDate, diaryPeriod]); + + useEffect(() => { + if (diaryAuthenticatedRef.current) void fetchLogs(); + }, [fetchLogs]); + + function changeDiaryPeriod(period: DiaryPeriod, date: string) { + if (period === diaryPeriod && date === diaryDate) return; + logsRequestIdRef.current += 1; + logsAbortRef.current?.abort(); + setLogs([]); + setDiaryOverview(emptyDiary); + setSelectedLogId(null); + setIsLogsLoading(true); + setDiaryPeriod(period); + setDiaryDate(date); + } useEffect(() => { return () => { searchAbortControllerRef.current?.abort(); + logsAbortRef.current?.abort(); logsRequestIdRef.current += 1; privateStateVersionRef.current += 1; }; @@ -360,6 +383,8 @@ export function FoodSearchPlaceholder() { function handleAuthStateChanged(event: Event) { const authEvent = event as CustomEvent; if (authEvent.detail?.authenticated) { + clearPrivateLogState(); + diaryAuthenticatedRef.current = true; void fetchLogs(); return; } @@ -408,6 +433,8 @@ export function FoodSearchPlaceholder() { setIsLoading(true); setError(null); + setSearchFailure(null); + setLastSearch({query: trimmedQuery, barcode}); setBarcodeSearch(barcode); if (barcode) setQuery(trimmedQuery); setDidSearch(true); @@ -415,8 +442,12 @@ export function FoodSearchPlaceholder() { "Preparing food search. This can take a moment after inactivity." ); + let lookupStarted = false; try { + if (typeof navigator !== "undefined" && navigator.onLine === false) throw new Error("Offline"); await (searchReadinessRef.current?.prepare(controller.signal) ?? waitForBackendReady(BACKEND_WAKE_BASE_URL, controller.signal)); + if (controller.signal.aborted || requestId !== searchRequestIdRef.current) return; + lookupStarted = true; setSearchStatus("Searching foods. This can take up to 45 seconds."); const response = await backendRequest( @@ -431,7 +462,7 @@ export function FoodSearchPlaceholder() { if (!response.ok) { pauseSearch(foodSearchRetryAt(response.status, response.headers?.get("retry-after") ?? null)); - setResults([]); + setSearchFailure(response.status === 429 ? "busy" : response.status === 504 ? "slow" : "unavailable"); setError(response.status === 429 ? "Food search is busy. Please wait before searching again." : response.status === 504 @@ -444,7 +475,9 @@ export function FoodSearchPlaceholder() { if (requestId !== searchRequestIdRef.current) { return; } - const items = normalizeFoodItems(data.results ?? []); + if (!Array.isArray(data.results)) throw new Error("Invalid food response"); + const items = normalizeFoodItems(data.results); + setResultsQuery(trimmedQuery); setResults(barcode ? items.filter(item => { const code = typeof item.barcode === "string" ? validFoodBarcode(item.barcode) : null; return code !== null && code.padStart(14, "0") === trimmedQuery.padStart(14, "0"); @@ -452,7 +485,7 @@ export function FoodSearchPlaceholder() { } catch (requestError) { if (!controller.signal.aborted && requestId === searchRequestIdRef.current) { pauseSearch(foodSearchRetryAt(503, null)); - setResults([]); + setSearchFailure(typeof navigator !== "undefined" && navigator.onLine === false ? "offline" : lookupStarted ? (requestError instanceof Error && requestError.name === "BackendRequestTimeoutError" ? "slow" : "unavailable") : "startFailed"); setError( backendUnavailableMessage( requestError, @@ -470,6 +503,17 @@ export function FoodSearchPlaceholder() { } } + function cancelSearch() { + searchAbortControllerRef.current?.abort(); + searchRequestIdRef.current += 1; + searchInFlightRef.current = false; + setIsLoading(false); + setSearchStatus(null); + setDidSearch(false); + // Cancellation does not authorize a burst of replacement provider requests. + pauseSearch(foodSearchRetryAt(503, null)); + } + function onLogFood(item: FoodSearchItem, index: number) { if (logMutationInFlightRef.current || isLoading) return; logSelectionIdRef.current += 1; @@ -609,7 +653,7 @@ export function FoodSearchPlaceholder() { } async function onDeleteAllLogs() { - if (deleteMutationInFlightRef.current) { + if (deleteMutationInFlightRef.current || diaryPeriod !== "all") { return; } @@ -801,7 +845,13 @@ export function FoodSearchPlaceholder() { 0 || isLogging !== null} onLookup={code => { void runSearch(code, true); }} /> - {error ?
: null} + {error ?
+ {lastSearch ? : null} +
: null} {searchWaitSeconds > 0 ?

{copy.searchCooldownNotice}

: null} {!error && !hasResults && !isLoading && !didSearch ? ( @@ -817,6 +867,7 @@ export function FoodSearchPlaceholder() {

{copy.searchStartupHint}

+
) : null} @@ -829,6 +880,7 @@ export function FoodSearchPlaceholder() {
) : null} + {hasResults ?

{formatFoodUi(diaryUi.resultsFor, {query: resultsQuery})}

: null} {hasResults ? (
- + ''' FAMILIES = { 'legal-and-whitepaper': '

Privacy Policy

Document text with terms.

', @@ -107,10 +107,14 @@ def protected_snapshot(page): page.locator('#faq summary').click();ok('FAQ disclosure still opens',page.locator('#faq').evaluate('n=>n.open')) ok('Disabled action and input value survive styling',page.locator('#disabled-button').is_disabled() and page.locator('#synthetic-form input').input_value()=='synthetic@example.test') ok('CalorieHelp adopts the same typography outside the main page flow',page.locator('.ctstyle-help-widget h2').evaluate("n=>getComputedStyle(n).fontFamily.includes('Segoe UI') && getComputedStyle(n).color==='rgb(0, 141, 54)'")) + page.locator('#help-toggle').click() + ok('CalorieHelp opens through its original disclosure',page.locator('.ctstyle-app-launcher-panel').is_visible()) page.locator('#help-topic').click() ok('Help topics still reveal their answer',page.locator('.ctstyle-help-reply').is_visible()) ok('Selected help topic stays visually distinct',page.locator('#help-topic').evaluate("n=>getComputedStyle(n).backgroundColor==='rgb(80, 91, 169)'")) page.locator('.ctstyle-app-launcher-panel').screenshot(path=str(OUT/'caloriehelp-360.png')) + page.locator('#help-toggle').click() + ok('CalorieHelp closes before reviewing the page content',not page.locator('.ctstyle-app-launcher-panel').is_visible()) for width in [360,412,1440]: page.set_viewport_size({'width':width,'height':900}) ok(f'{width}px content has no horizontal overflow',page.evaluate('document.documentElement.scrollWidth<=document.documentElement.clientWidth')) From 30eb8ebf9fd75d3180c33534c766569b012dd49e Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:51:45 +0200 Subject: [PATCH 072/368] Match login widget and Brizy menu controls to CalorieApp styling --- .../wordpress-content-style-2026-09-17.md | 9 +++ tools/tests/browser_wordpress_content.py | 48 ++++++++++--- tools/tests/wordpress_content_style.test.mjs | 33 ++++++++- tools/tests/wordpress_heading_repair.test.mjs | 4 +- .../calorietoken-heading-repair/README.txt | 8 ++- .../assets/content-style.css | 68 +++++++++++++++++++ .../assets/content-style.js | 17 +++++ .../calorietoken-heading-repair.php | 4 +- 8 files changed, 177 insertions(+), 14 deletions(-) diff --git a/docs/release-review/wordpress-content-style-2026-09-17.md b/docs/release-review/wordpress-content-style-2026-09-17.md index ad1b9410..689b1d42 100644 --- a/docs/release-review/wordpress-content-style-2026-09-17.md +++ b/docs/release-review/wordpress-content-style-2026-09-17.md @@ -83,3 +83,12 @@ No publication is claimed. PR 146 remains draft/unmerged. Install the tested ZIP - Approved commit `d7223878a075186271c26ec3efcd2a31c244f525`, tree `440c46c971d5fefe938e15eaa2bf90742e60afd2`, was uploaded with a non-forced branch update. [Dedicated browser run 35239975029](https://github.com/CalorieToken/CalorieApp/actions/runs/35239975029) passed 89 checks, PHP syntax and five package checks. Its tested merge tree matches the source tree exactly. [Full regression run 35239975096](https://github.com/CalorieToken/CalorieApp/actions/runs/35239975096) also passed all stages, including the production build and existing browser regressions. - The first screenshot review found that the synthetic help fixture was always open and covered the content-card examples. The follow-up changes only the test fixture to use the actual native details/summary structure: verify opening the bot, capture its answer, close it, then capture unobscured market/app/FAQ/RTL examples. No product change or release-package change was needed. Final follow-up CI and visual results are recorded in PR 146. - Prepared package: `calorietoken-heading-repair-1.6.7.zip`, SHA-256 `f10d8cd8e9390ce0f2c4156cd8f2d5a359e55b048d515bdd8fd7395607d4892d`. The first CI build is byte-identical. No live WordPress installation, Render deployment, wallet action or PR merge has been performed for this styling release. + +## Follow-up requested after installing 1.6.7 +The owner explicitly extended the scope to the login widget and **only the Brizy menu buttons**. Historical header/layout, title banners, footer and page backgrounds remain protected. The owner reiterated coverage of every page and subpage. + +Candidate 1.6.8 adds narrowly scoped visual markers for the original login card and menu controls. It does not replace account fields, nicknames, avatars, destinations, language selection, authentication handlers, menu wrappers or disclosure/visibility state. The other chat's nickname/account-settings work must not be overwritten. Browser checks include signed-in/guest synthetic widget states, existing handlers, menu opening/closing, historical background preservation and 360/412/1440px captures. + +The owner asked to use another browser or Tinyfish after the primary cloud browser returned 502. Tinyfish run `f4cdb6c5-0e21-4b8f-aac8-9ef08ccf407a` successfully inspected the live FAQ on desktop and reported working FAQ/CalorieHelp disclosures, app-style cards, historical title banner and blue footer. It returned no usable screenshot artifact, did not reach CalorieApp, and could not resize to mobile. This is a provider-reported FAQ check, not visual acceptance of all routes. Its initial strict/custom-step options were rejected before any run started; run listings confirmed zero matching jobs before the standard run was submitted. + +Local follow-up validation: 388 Node checks and five deterministic package checks pass; browser-test Python compiles. Current authenticated inventory remains 30 public pages plus 11 articles. Rendered widget/menu evidence and the per-route live audit will be recorded with their actual limits in PR 146. diff --git a/tools/tests/browser_wordpress_content.py b/tools/tests/browser_wordpress_content.py index a40e5731..3a10e744 100644 --- a/tools/tests/browser_wordpress_content.py +++ b/tools/tests/browser_wordpress_content.py @@ -14,7 +14,7 @@ OUT.mkdir(parents=True, exist_ok=True) SCRIPT = (ASSETS / 'content-style.js').read_text() CSS = (ASSETS / 'content-style.css').read_text() -BASELINE = '\n'.join(p.read_text() for p in sorted(FIXTURES.glob('*.css'))) +BASELINE = '\n'.join(p.read_text() for p in sorted(FIXTURES.glob('*.css'))) + '\n' + (ASSETS / 'app-focus.css').read_text() report = {'mode': 'Synthetic public-page fixtures with captured live CSS; no live mutations', 'checks': [], 'errors': []} def ok(name, condition=True): @@ -22,7 +22,10 @@ def ok(name, condition=True): report['checks'].append(name) BODY = ''' -
CalorieToken

Historical header

+
CalorieToken

Historical header

+ +
rSYNTHETICaccountForLayoutOnly
Original login artwork
Mijn vertrouwde nickname
Saldo: 123 CAL
Rang: 42
Aanmelden via CalorieApp
Open CalorieApp
+

CalorieApp

- + \ No newline at end of file From 68c0b18738954b7649295e709a0e830b43271712 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:01:12 +0200 Subject: [PATCH 138/368] Run participation control state tests in CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae878b27..a2cc245c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -332,6 +332,9 @@ jobs: # forgotten in a manually maintained list. run: node --test tools/tests/*.test.mjs + - name: Test participation control state machine + run: node --test tools/tests/participation_lab_state.test.mjs + - name: Build frontend working-directory: frontend env: From 14bb70bbe41aa972db9ef20ae0352cae15936183 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:02:13 +0200 Subject: [PATCH 139/368] Add synthetic participation growth model --- tools/participation_growth_simulator.py | 105 ++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tools/participation_growth_simulator.py diff --git a/tools/participation_growth_simulator.py b/tools/participation_growth_simulator.py new file mode 100644 index 00000000..a8953bbf --- /dev/null +++ b/tools/participation_growth_simulator.py @@ -0,0 +1,105 @@ +"""Synthetic participation growth simulator. + +Models capacity growth from zero volunteers upward while preserving the hosted +fallback. No real nodes, remote execution, networking, storage, or token +settlement are enabled by this model. +""" +from __future__ import annotations + +from dataclasses import dataclass, asdict +import json + + +@dataclass(frozen=True) +class CapacityPolicy: + hosted_storage_units: int = 100 + hosted_compute_units: int = 100 + storage_units_per_node: int = 4 + compute_units_per_node: int = 3 + max_storage_units_per_node: int = 8 + max_compute_units_per_node: int = 6 + fallback_floor_percent: int = 20 + + +@dataclass(frozen=True) +class GrowthSnapshot: + volunteer_nodes: int + volunteer_storage_units: int + volunteer_compute_units: int + hosted_storage_units: int + hosted_compute_units: int + total_storage_units: int + total_compute_units: int + hosted_fallback_active: bool + app_available: bool + gameverse_available: bool + volunteer_storage_share_percent: int + volunteer_compute_share_percent: int + + +class GrowthModel: + def __init__(self, policy: CapacityPolicy = CapacityPolicy()) -> None: + self.policy = policy + + @staticmethod + def _checked_nodes(volunteer_nodes: int) -> int: + if type(volunteer_nodes) is not int or volunteer_nodes < 0: + raise ValueError("volunteer-nodes-must-be-nonnegative-integer") + return volunteer_nodes + + def snapshot( + self, + volunteer_nodes: int, + *, + storage_units_per_node: int | None = None, + compute_units_per_node: int | None = None, + ) -> GrowthSnapshot: + nodes = self._checked_nodes(volunteer_nodes) + storage_rate = ( + self.policy.storage_units_per_node + if storage_units_per_node is None else storage_units_per_node + ) + compute_rate = ( + self.policy.compute_units_per_node + if compute_units_per_node is None else compute_units_per_node + ) + if not 0 <= storage_rate <= self.policy.max_storage_units_per_node: + raise ValueError("storage-rate-out-of-range") + if not 0 <= compute_rate <= self.policy.max_compute_units_per_node: + raise ValueError("compute-rate-out-of-range") + + volunteer_storage = nodes * storage_rate + volunteer_compute = nodes * compute_rate + + # The first release keeps the hosted core fully available. A future + # policy may taper it, but never below the configured fallback floor. + hosted_storage = self.policy.hosted_storage_units + hosted_compute = self.policy.hosted_compute_units + + total_storage = hosted_storage + volunteer_storage + total_compute = hosted_compute + volunteer_compute + storage_share = 0 if total_storage == 0 else round(volunteer_storage * 100 / total_storage) + compute_share = 0 if total_compute == 0 else round(volunteer_compute * 100 / total_compute) + + return GrowthSnapshot( + volunteer_nodes=nodes, + volunteer_storage_units=volunteer_storage, + volunteer_compute_units=volunteer_compute, + hosted_storage_units=hosted_storage, + hosted_compute_units=hosted_compute, + total_storage_units=total_storage, + total_compute_units=total_compute, + hosted_fallback_active=True, + app_available=True, + gameverse_available=True, + volunteer_storage_share_percent=storage_share, + volunteer_compute_share_percent=compute_share, + ) + + def matrix(self) -> list[GrowthSnapshot]: + return [self.snapshot(nodes) for nodes in (0, 1, 10, 100, 1000)] + + +if __name__ == "__main__": + model = GrowthModel() + print(json.dumps([asdict(item) for item in model.matrix()], sort_keys=True)) From 478af054c038f97136a5238b17b774e18b621d62 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:02:15 +0200 Subject: [PATCH 140/368] Add synthetic participation growth model --- .../test_participation_growth_simulator.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tools/tests/test_participation_growth_simulator.py diff --git a/tools/tests/test_participation_growth_simulator.py b/tools/tests/test_participation_growth_simulator.py new file mode 100644 index 00000000..ffef1272 --- /dev/null +++ b/tools/tests/test_participation_growth_simulator.py @@ -0,0 +1,66 @@ +import unittest + +from tools.participation_growth_simulator import CapacityPolicy, GrowthModel + + +class ParticipationGrowthSimulatorTests(unittest.TestCase): + def setUp(self): + self.model = GrowthModel() + + def test_zero_nodes_is_healthy_normal_state(self): + snap = self.model.snapshot(0) + self.assertEqual(snap.volunteer_storage_units, 0) + self.assertEqual(snap.volunteer_compute_units, 0) + self.assertTrue(snap.hosted_fallback_active) + self.assertTrue(snap.app_available) + self.assertTrue(snap.gameverse_available) + self.assertEqual(snap.total_storage_units, 100) + self.assertEqual(snap.total_compute_units, 100) + + def test_capacity_grows_monotonically_with_participation(self): + snaps = [self.model.snapshot(n) for n in (0, 1, 10, 100, 1000)] + self.assertEqual( + [s.total_storage_units for s in snaps], + sorted(s.total_storage_units for s in snaps), + ) + self.assertEqual( + [s.total_compute_units for s in snaps], + sorted(s.total_compute_units for s in snaps), + ) + self.assertTrue(all(s.hosted_fallback_active for s in snaps)) + + def test_small_participation_adds_capacity_without_replacing_hosted_core(self): + snap = self.model.snapshot(1) + self.assertEqual(snap.volunteer_storage_units, 4) + self.assertEqual(snap.volunteer_compute_units, 3) + self.assertEqual(snap.hosted_storage_units, 100) + self.assertEqual(snap.hosted_compute_units, 100) + + def test_storage_and_compute_growth_can_be_independent(self): + storage_only = self.model.snapshot(10, storage_units_per_node=4, compute_units_per_node=0) + compute_only = self.model.snapshot(10, storage_units_per_node=0, compute_units_per_node=3) + self.assertGreater(storage_only.volunteer_storage_units, 0) + self.assertEqual(storage_only.volunteer_compute_units, 0) + self.assertEqual(compute_only.volunteer_storage_units, 0) + self.assertGreater(compute_only.volunteer_compute_units, 0) + + def test_rates_are_bounded(self): + with self.assertRaisesRegex(ValueError, "storage-rate-out-of-range"): + self.model.snapshot(1, storage_units_per_node=9) + with self.assertRaisesRegex(ValueError, "compute-rate-out-of-range"): + self.model.snapshot(1, compute_units_per_node=7) + + def test_invalid_node_counts_fail_closed(self): + for value in (-1, 1.5, "10"): + with self.assertRaisesRegex(ValueError, "volunteer-nodes"): + self.model.snapshot(value) + + def test_matrix_covers_growth_stages(self): + matrix = self.model.matrix() + self.assertEqual([item.volunteer_nodes for item in matrix], [0, 1, 10, 100, 1000]) + self.assertTrue(all(item.app_available for item in matrix)) + self.assertTrue(all(item.gameverse_available for item in matrix)) + + +if __name__ == "__main__": + unittest.main() From 86ceeed3f4ca8d5a480711a64a58e746a44508b6 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:02:18 +0200 Subject: [PATCH 141/368] Test zero-to-community participation growth model --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2cc245c..09816f4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,7 @@ jobs: python -m unittest tools.tests.test_participation_simulator python -m unittest tools.tests.test_participation_compute_simulator python -m unittest tools.tests.test_participation_ui_adapter + python -m unittest tools.tests.test_participation_growth_simulator wordpress-plugin-release-check: name: WordPress plugin release check From 0fe94ccb6a4851bb24fb2dacd8c20503bd5422f8 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:03:03 +0200 Subject: [PATCH 142/368] Add volunteer node reliability and fallback simulation --- tools/participation_reliability_simulator.py | 133 +++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tools/participation_reliability_simulator.py diff --git a/tools/participation_reliability_simulator.py b/tools/participation_reliability_simulator.py new file mode 100644 index 00000000..44af6bdd --- /dev/null +++ b/tools/participation_reliability_simulator.py @@ -0,0 +1,133 @@ +"""Synthetic volunteer-node reliability and task assignment model. + +Models churn, pause/resume, dropout and rejoin behavior without enabling any real +remote execution, participant storage, networking or token settlement. +""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable + + +@dataclass(frozen=True) +class NodeState: + node_id: str + storage: bool = False + compute: bool = False + paused: bool = False + online: bool = True + storage_capacity: int = 1 + compute_capacity: int = 1 + + def can_store(self) -> bool: + return self.online and not self.paused and self.storage and self.storage_capacity > 0 + + def can_compute(self) -> bool: + return self.online and not self.paused and self.compute and self.compute_capacity > 0 + + +@dataclass(frozen=True) +class Assignment: + task_id: str + task_type: str + node_id: str | None + fallback: bool + reason: str + + +class ReliabilityScheduler: + """Simple deterministic scheduler with safe hosted fallback.""" + + def __init__(self) -> None: + self._storage_cursor = 0 + self._compute_cursor = 0 + + @staticmethod + def _eligible(nodes: Iterable[NodeState], task_type: str) -> list[NodeState]: + if task_type == "storage": + return [n for n in nodes if n.can_store()] + if task_type == "compute": + return [n for n in nodes if n.can_compute()] + raise ValueError("unsupported-task-type") + + def assign(self, task_id: str, task_type: str, nodes: Iterable[NodeState]) -> Assignment: + eligible = sorted(self._eligible(nodes, task_type), key=lambda n: n.node_id) + if not eligible: + return Assignment( + task_id=task_id, + task_type=task_type, + node_id=None, + fallback=True, + reason="hosted-core-fallback", + ) + + if task_type == "storage": + index = self._storage_cursor % len(eligible) + self._storage_cursor += 1 + else: + index = self._compute_cursor % len(eligible) + self._compute_cursor += 1 + + chosen = eligible[index] + return Assignment( + task_id=task_id, + task_type=task_type, + node_id=chosen.node_id, + fallback=False, + reason="volunteer-node", + ) + + def assign_replica_set( + self, + task_id: str, + nodes: Iterable[NodeState], + desired_replicas: int, + ) -> tuple[list[str], int]: + if desired_replicas < 0: + raise ValueError("desired-replicas-must-be-nonnegative") + eligible = sorted(self._eligible(nodes, "storage"), key=lambda n: n.node_id) + selected = [n.node_id for n in eligible[:desired_replicas]] + missing = max(0, desired_replicas - len(selected)) + return selected, missing + + +def lifecycle_scenario() -> dict: + scheduler = ReliabilityScheduler() + nodes = [ + NodeState("sim-a", storage=True, compute=True), + NodeState("sim-b", storage=True, compute=False), + ] + + first = scheduler.assign("task-1", "compute", nodes) + + paused = [ + NodeState("sim-a", storage=True, compute=True, paused=True), + NodeState("sim-b", storage=True, compute=False), + ] + second = scheduler.assign("task-2", "compute", paused) + + offline = [ + NodeState("sim-a", storage=True, compute=True, online=False), + NodeState("sim-b", storage=True, compute=False), + ] + third = scheduler.assign("task-3", "storage", offline) + + rejoined = [ + NodeState("sim-a", storage=True, compute=True, online=True), + NodeState("sim-b", storage=True, compute=False, online=True), + ] + fourth = scheduler.assign("task-4", "compute", rejoined) + + replicas, missing = scheduler.assign_replica_set("blob-1", rejoined, 3) + + return { + "initial_compute": first.__dict__, + "paused_compute": second.__dict__, + "storage_while_compute_node_offline": third.__dict__, + "rejoined_compute": fourth.__dict__, + "replica_nodes": replicas, + "replica_shortfall": missing, + "hosted_core_always_available": True, + "real_network_enabled": False, + "real_token_settlement": False, + } From d298d3eecee9422ba545d0c57ccb4d7b76cc66f9 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:03:05 +0200 Subject: [PATCH 143/368] Add volunteer node reliability and fallback simulation --- ...est_participation_reliability_simulator.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tools/tests/test_participation_reliability_simulator.py diff --git a/tools/tests/test_participation_reliability_simulator.py b/tools/tests/test_participation_reliability_simulator.py new file mode 100644 index 00000000..b477dcc4 --- /dev/null +++ b/tools/tests/test_participation_reliability_simulator.py @@ -0,0 +1,86 @@ +import unittest + +from tools.participation_reliability_simulator import ( + NodeState, ReliabilityScheduler, lifecycle_scenario +) + + +class ParticipationReliabilitySimulatorTests(unittest.TestCase): + def setUp(self): + self.scheduler = ReliabilityScheduler() + + def test_zero_nodes_uses_hosted_fallback(self): + assignment = self.scheduler.assign("t0", "compute", []) + self.assertTrue(assignment.fallback) + self.assertIsNone(assignment.node_id) + self.assertEqual(assignment.reason, "hosted-core-fallback") + + def test_paused_node_receives_no_work(self): + nodes = [NodeState("sim-a", compute=True, paused=True)] + assignment = self.scheduler.assign("t1", "compute", nodes) + self.assertTrue(assignment.fallback) + + def test_offline_node_receives_no_work(self): + nodes = [NodeState("sim-a", storage=True, online=False)] + assignment = self.scheduler.assign("t2", "storage", nodes) + self.assertTrue(assignment.fallback) + + def test_storage_and_compute_eligibility_are_independent(self): + nodes = [ + NodeState("sim-storage", storage=True, compute=False), + NodeState("sim-compute", storage=False, compute=True), + ] + storage = self.scheduler.assign("s1", "storage", nodes) + compute = self.scheduler.assign("c1", "compute", nodes) + self.assertEqual(storage.node_id, "sim-storage") + self.assertEqual(compute.node_id, "sim-compute") + + def test_round_robin_spreads_work_across_eligible_nodes(self): + nodes = [ + NodeState("sim-a", compute=True), + NodeState("sim-b", compute=True), + NodeState("sim-c", compute=True), + ] + assigned = [ + self.scheduler.assign(f"c{i}", "compute", nodes).node_id + for i in range(6) + ] + self.assertEqual(assigned, ["sim-a", "sim-b", "sim-c", "sim-a", "sim-b", "sim-c"]) + + def test_node_can_rejoin_after_pause_or_dropout(self): + paused = [NodeState("sim-a", compute=True, paused=True)] + self.assertTrue(self.scheduler.assign("c1", "compute", paused).fallback) + resumed = [NodeState("sim-a", compute=True, paused=False, online=True)] + assignment = self.scheduler.assign("c2", "compute", resumed) + self.assertFalse(assignment.fallback) + self.assertEqual(assignment.node_id, "sim-a") + + def test_replica_shortfall_falls_back_instead_of_pressuring_users(self): + nodes = [ + NodeState("sim-a", storage=True), + NodeState("sim-b", storage=True, paused=True), + ] + selected, missing = self.scheduler.assign_replica_set("blob-1", nodes, 3) + self.assertEqual(selected, ["sim-a"]) + self.assertEqual(missing, 2) + + def test_replica_set_recovers_as_participants_return(self): + nodes = [ + NodeState("sim-a", storage=True), + NodeState("sim-b", storage=True), + NodeState("sim-c", storage=True), + ] + selected, missing = self.scheduler.assign_replica_set("blob-2", nodes, 3) + self.assertEqual(selected, ["sim-a", "sim-b", "sim-c"]) + self.assertEqual(missing, 0) + + def test_lifecycle_scenario_preserves_fallback(self): + result = lifecycle_scenario() + self.assertFalse(result["initial_compute"]["fallback"]) + self.assertTrue(result["paused_compute"]["fallback"]) + self.assertFalse(result["rejoined_compute"]["fallback"]) + self.assertTrue(result["hosted_core_always_available"]) + + +if __name__ == "__main__": + unittest.main() From 8dcd7e9d2a05ecaf1984ef7b3b22cdd7f943ee99 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:03:08 +0200 Subject: [PATCH 144/368] Test volunteer churn pause resume and fallback behavior --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09816f4f..37b21328 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,7 @@ jobs: python -m unittest tools.tests.test_participation_compute_simulator python -m unittest tools.tests.test_participation_ui_adapter python -m unittest tools.tests.test_participation_growth_simulator + python -m unittest tools.tests.test_participation_reliability_simulator wordpress-plugin-release-check: name: WordPress plugin release check From 30feddc27c3776ba90388a0c20d05ba46d2a6d26 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:05:57 +0200 Subject: [PATCH 145/368] Add user-facing participation contribution summary --- web/participation-lab/state.mjs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/web/participation-lab/state.mjs b/web/participation-lab/state.mjs index 4cb0b0a8..d658aef7 100644 --- a/web/participation-lab/state.mjs +++ b/web/participation-lab/state.mjs @@ -79,3 +79,28 @@ export function hostedFallbackSnapshot(state, volunteerNodes = 0) { current.storage || current.processState === "running", }); } + +export function contributionSummary(state) { + const current = createParticipationState(state); + return Object.freeze({ + storageLabel: current.storage ? "ON" : "OFF", + storageDetail: current.storage + ? `Up to ${current.storageLimitMb >= 1000 ? (current.storageLimitMb / 1000).toFixed(1) + " GB" : current.storageLimitMb + " MB"}` + : "No storage shared", + computeLabel: + current.processState === "running" ? "RUNNING" : + current.processState === "paused" ? "PAUSED" : "OFF", + computeDetail: + current.compute + ? current.processState === "off" + ? `Enabled · waiting for Start · max ${current.computeLimitPercent}%` + : `Max ${current.computeLimitPercent}%` + : "No processing shared", + rewardsLabel: current.rewards ? "ON" : "OFF", + rewardsDetail: current.rewards + ? "Simulated calT accounting only" + : "No reward participation", + contributingNow: + current.storage || current.processState === "running", + }); +} From afa30689977f0693b1237ad7a123242c96a34359 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:05:59 +0200 Subject: [PATCH 146/368] Show personal contribution and local activity in prototype --- web/participation-lab/index.html | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/web/participation-lab/index.html b/web/participation-lab/index.html index d3af2248..38d5c43a 100644 --- a/web/participation-lab/index.html +++ b/web/participation-lab/index.html @@ -11,9 +11,21 @@
CALORIE ECOSYSTEM · OPTIONAL PARTICIPATION

Participation Lab ◌

Use CalorieApp and the Gameverse normally, or voluntarily share a little capacity. You stay in control.

-
NODE STATUSOFF
+
PROCESS STATUSOFF
Nothing is required. Storage, compute and simulated rewards are separate choices. Leaving participation never removes normal app or Gameverse access.
COMMUNITY CAPACITY0 volunteer nodes
Hosted core active
+
+
+ YOUR CHOICES +

Your contribution

+

These are your current voluntary limits, not requirements or guaranteed usage.

+
+
+
StorageOFFNo storage shared
+
ComputeOFFNo processing shared
+
RewardsOFFNo reward participation
+
+
01 · DATA

Storage contribution

@@ -45,6 +57,10 @@

Participation Lab ◌

The ecosystem works at zero participation. More voluntary nodes can gradually add redundancy and capacity without redesigning the core.

+
+
LOCAL PROTOTYPE ACTIVITY

Recent choices

+
  1. Participation is off. Hosted core remains active.
+
You can change these choices at any time.
From 6b6814cb5e18c69909c3ee4c27bac8ec5cfc5aa4 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:06:13 +0200 Subject: [PATCH 147/368] Render personal contribution summary and activity --- web/participation-lab/participation.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/web/participation-lab/participation.js b/web/participation-lab/participation.js index 08fa6840..3d1e8d75 100644 --- a/web/participation-lab/participation.js +++ b/web/participation-lab/participation.js @@ -1,4 +1,5 @@ import { + contributionSummary, createParticipationState, hostedFallbackSnapshot, transition, @@ -6,11 +7,18 @@ import { const $ = id => document.getElementById(id); let state = createParticipationState(); +const activity = ["Participation is off. Hosted core remains active."]; function message(text) { $("message").textContent = text; } +function record(text) { + activity.unshift(text); + activity.splice(5); + $("activityList").innerHTML = activity.map(item => "
  • " + item + "
  • ").join(""); +} + function render() { const active = state.processState === "running" || state.processState === "paused"; $("statusText").textContent = state.processState.toUpperCase(); @@ -34,12 +42,20 @@ function render() { const fallback = hostedFallbackSnapshot(state, 0); $("communityCount").textContent = fallback.volunteerNodes + " volunteer nodes"; + + const summary = contributionSummary(state); + $("storageSummary").textContent = summary.storageLabel; + $("storageDetail").textContent = summary.storageDetail; + $("computeSummary").textContent = summary.computeLabel; + $("computeDetail").textContent = summary.computeDetail; + $("rewardSummary").textContent = summary.rewardsLabel; + $("rewardDetail").textContent = summary.rewardsDetail; } function act(action, successMessage) { try { state = transition(state, action); - if (successMessage) message(successMessage); + if (successMessage) { message(successMessage); record(successMessage); } render(); } catch (error) { if (error.message === "compute-not-enabled") { @@ -59,8 +75,8 @@ $("computeToggle").onchange = e => act({ type: "SET_COMPUTE", value: e.target.ch $("rewardToggle").onchange = e => act({ type: "SET_REWARDS", value: e.target.checked }, e.target.checked ? "Simulated calT rewards enabled." : "Simulated rewards disabled."); -$("storageLimit").oninput = e => act({ type: "SET_STORAGE_LIMIT", value: e.target.value }); -$("computeLimit").oninput = e => act({ type: "SET_COMPUTE_LIMIT", value: e.target.value }); +$("storageLimit").oninput = e => act({ type: "SET_STORAGE_LIMIT", value: e.target.value }, "Storage limit changed to " + e.target.value + " MB."); +$("computeLimit").oninput = e => act({ type: "SET_COMPUTE_LIMIT", value: e.target.value }, "Compute limit changed to " + e.target.value + "%."); $("startBtn").onclick = () => act({ type: "START" }, "Compute participation started within your selected limit."); $("pauseBtn").onclick = () => act({ type: "PAUSE" }, "Compute paused. Storage keeps its own separate setting."); From 2e477ca3a4a048a6d8b8a3d795d5b89d3eab990c Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:06:17 +0200 Subject: [PATCH 148/368] Style contribution summary and activity panel --- web/participation-lab/participation.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/participation-lab/participation.css b/web/participation-lab/participation.css index ed5230fc..f1f773f0 100644 --- a/web/participation-lab/participation.css +++ b/web/participation-lab/participation.css @@ -1 +1,13 @@ :root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;color:#f8fbff;background:#07111f}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:radial-gradient(circle at 18% 0,#20205a 0,transparent 34%),radial-gradient(circle at 90% 15%,#103d45 0,transparent 28%),#07111f}.shell{max-width:1180px;margin:auto;padding:44px 28px}.hero{display:flex;justify-content:space-between;gap:28px;align-items:end;margin-bottom:22px}.eyebrow,small{letter-spacing:.14em;font-weight:800;color:#8aa2bb;font-size:11px}.hero h1{font-size:46px;line-height:1;margin:9px 0}.hero h1 span{color:#7a5cff}.hero p{max-width:690px;color:#aab8c7;font-size:17px}.status{display:flex;align-items:center;gap:12px;background:#101c2b;border:1px solid #25354a;border-radius:18px;padding:14px 18px;min-width:145px}.status i{width:11px;height:11px;border-radius:50%;background:#657488;box-shadow:0 0 0 5px #65748822}.status strong{display:block;margin-top:3px}.notice{border:1px solid #5e55a7;background:#171b39;border-radius:16px;padding:15px 18px;color:#cbd5e1;margin-bottom:18px}.notice strong{color:#a99aff}.grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.card{position:relative;overflow:hidden;background:linear-gradient(145deg,#111e2d,#0b1724);border:1px solid #25374b;border-radius:22px;padding:24px;min-height:250px;box-shadow:0 18px 45px #0004}.card:after{content:"C";position:absolute;right:-8px;bottom:-48px;font-weight:900;font-size:190px;color:#ffffff08;pointer-events:none}.cardhead{display:flex;justify-content:space-between;gap:18px}.card h2{margin:6px 0 10px;font-size:24px}.card p{color:#94a7ba;line-height:1.55}.card label:not(.switch){display:flex;justify-content:space-between;margin-top:25px;font-size:13px}.card label b{color:#66e3b4}input[type=range]{width:100%;accent-color:#6de3b7;margin-top:13px}.ticks{display:flex;justify-content:space-between;color:#61758a;font-size:11px}.switch input{display:none}.switch span{display:block;width:48px;height:27px;border-radius:20px;background:#26384b;position:relative;cursor:pointer}.switch span:after{content:"";position:absolute;width:21px;height:21px;border-radius:50%;background:#9bacc0;top:3px;left:4px;transition:.2s}.switch input:checked+span{background:#315e51}.switch input:checked+span:after{left:23px;background:#6de3b7}.mini{margin-top:20px;padding:12px;border-radius:12px;background:#0a1420;color:#6f8498;font-size:12px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:21px}button{border:1px solid #344a61;background:#17283a;color:#dce8f4;border-radius:11px;padding:10px 16px;font-weight:800;cursor:pointer}button.primary{background:#6b55e8;border-color:#826cff;color:white}button:disabled{opacity:.35;cursor:not-allowed}.balance{margin-top:30px;border-radius:16px;padding:18px;background:#0a1521;border:1px solid #21354a}.balance span{display:block;color:#657b91;font-size:10px;letter-spacing:.12em}.balance strong{font-size:30px}.balance em{font-size:15px;color:#7b67ee;font-style:normal}.steps{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:27px 0}.steps span{padding:8px 10px;border-radius:10px;background:#142536;color:#738aa1;font-size:12px;font-weight:800}.steps .on{background:#183d35;color:#6de3b7}.steps i{color:#475c71}footer{display:flex;align-items:center;gap:18px;margin-top:20px;color:#6f8498;font-size:12px}.danger{border-color:#5b3440;background:#24161d;color:#e39aa9}@media(max-width:760px){.shell{padding:24px 16px}.hero{align-items:start;flex-direction:column}.hero h1{font-size:36px}.grid{grid-template-columns:1fr}.status{width:100%}.card{min-height:auto}footer{align-items:flex-start;flex-direction:column}}.networkbar{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:0 0 18px;padding:14px 18px;border:1px solid #234255;background:#0b1b28;border-radius:16px}.networkbar strong{display:block;margin-top:4px}.fallback{padding:8px 12px;border-radius:10px;background:#173c34;color:#6de3b7;font-size:12px;font-weight:800}@media(max-width:560px){.networkbar{align-items:flex-start;flex-direction:column}} + +.overview,.activity{margin:0 0 18px;padding:20px;border:1px solid #25374b;background:#0c1825;border-radius:18px} +.overview{display:grid;grid-template-columns:1.1fr 2fr;gap:22px;align-items:center} +.overview-copy h2,.activity h2{margin:5px 0 8px}.overview-copy p{margin:0;color:#8ea0b3;font-size:13px} +.overview-items{display:grid;grid-template-columns:repeat(3,1fr);gap:10px} +.overview-items>div{padding:14px;border-radius:14px;background:#101f2e;border:1px solid #203449} +.overview-items span{display:block;color:#71879d;font-size:11px;text-transform:uppercase;letter-spacing:.08em} +.overview-items strong{display:block;margin:5px 0;color:#e8f0f7} +.overview-items small{display:block;letter-spacing:0;color:#7d91a5;font-weight:600;line-height:1.35} +.activity{display:flex;justify-content:space-between;gap:20px;align-items:flex-start} +.activity ol{margin:0;padding-left:22px;color:#8fa2b5;max-width:650px}.activity li{margin:4px 0} +@media(max-width:760px){.overview{grid-template-columns:1fr}.overview-items{grid-template-columns:1fr}.activity{flex-direction:column}} From cf7b9944e1bb33165d2dedf36c1f2f5c559c2cd0 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:06:20 +0200 Subject: [PATCH 149/368] Test user-facing contribution summary --- tools/tests/participation_lab_state.test.mjs | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tools/tests/participation_lab_state.test.mjs b/tools/tests/participation_lab_state.test.mjs index 301b8ca1..d229c360 100644 --- a/tools/tests/participation_lab_state.test.mjs +++ b/tools/tests/participation_lab_state.test.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + contributionSummary, createParticipationState, hostedFallbackSnapshot, transition, @@ -76,3 +77,26 @@ test("invalid limits and invalid process transitions fail closed", () => { assert.throws(() => createParticipationState({ computeLimitPercent: 100 }), /compute-limit-out-of-range/); assert.throws(() => transition(createParticipationState(), { type: "START" }), /compute-not-enabled/); }); + +test("contribution summary remains descriptive and optional", () => { + const off = contributionSummary(createParticipationState()); + assert.equal(off.storageLabel, "OFF"); + assert.equal(off.computeLabel, "OFF"); + assert.equal(off.rewardsLabel, "OFF"); + assert.equal(off.contributingNow, false); + + const active = contributionSummary(createParticipationState({ + storage: true, + compute: true, + rewards: true, + processState: "running", + storageLimitMb: 500, + computeLimitPercent: 30, + })); + assert.equal(active.storageLabel, "ON"); + assert.equal(active.computeLabel, "RUNNING"); + assert.equal(active.rewardsLabel, "ON"); + assert.equal(active.contributingNow, true); + assert.match(active.storageDetail, /500 MB/); + assert.match(active.computeDetail, /30%/); +}); From cf771bccc26b548a2571991eb5845e01cd2ee1ab Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:08:06 +0200 Subject: [PATCH 150/368] Add synthetic community capacity preview states --- web/participation-lab/state.mjs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/web/participation-lab/state.mjs b/web/participation-lab/state.mjs index d658aef7..4d42fbd6 100644 --- a/web/participation-lab/state.mjs +++ b/web/participation-lab/state.mjs @@ -104,3 +104,26 @@ export function contributionSummary(state) { current.storage || current.processState === "running", }); } + +export function communityCapacityPreview(volunteerNodes = 0) { + const nodes = Math.max(0, Number(volunteerNodes) || 0); + let level = "Hosted only"; + let description = "No volunteer capacity yet. The hosted core handles normal service."; + if (nodes >= 1000) { + level = "Strong community"; + description = "Large voluntary capacity is available while the hosted fallback remains active."; + } else if (nodes >= 100) { + level = "Growing network"; + description = "Community capacity is becoming meaningful and can absorb more eligible work."; + } else if (nodes >= 1) { + level = "Early community"; + description = "A small amount of voluntary capacity is available alongside the hosted core."; + } + return Object.freeze({ + nodes, + level, + description, + hostedCoreActive: true, + isSyntheticPreview: true, + }); +} From 70d65a01b608f045b32d86d9802c3153034d1dce Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:08:08 +0200 Subject: [PATCH 151/368] Add clearly labelled synthetic community growth preview --- web/participation-lab/index.html | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/web/participation-lab/index.html b/web/participation-lab/index.html index 38d5c43a..7ba9292f 100644 --- a/web/participation-lab/index.html +++ b/web/participation-lab/index.html @@ -13,7 +13,25 @@

    Participation Lab ◌

    Use CalorieApp and the Gameverse normally, or voluntarily share a little capacity. You stay in control.

    PROCESS STATUSOFF
    -
    Nothing is required. Storage, compute and simulated rewards are separate choices. Leaving participation never removes normal app or Gameverse access.
    COMMUNITY CAPACITY0 volunteer nodes
    Hosted core active
    +
    Nothing is required. Storage, compute and simulated rewards are separate choices. Leaving participation never removes normal app or Gameverse access.
    +
    + COMMUNITY CAPACITY · SYNTHETIC PREVIEW + 0 volunteer nodes + Hosted only +

    No volunteer capacity yet. The hosted core handles normal service.

    +
    +
    + + +
    Hosted core active
    +
    +
    YOUR CHOICES From 43e191818c4fca1dc70c208b25711d898086e322 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:08:10 +0200 Subject: [PATCH 152/368] Render synthetic community growth states in prototype --- web/participation-lab/participation.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/web/participation-lab/participation.js b/web/participation-lab/participation.js index 3d1e8d75..530d0069 100644 --- a/web/participation-lab/participation.js +++ b/web/participation-lab/participation.js @@ -1,4 +1,5 @@ import { + communityCapacityPreview, contributionSummary, createParticipationState, hostedFallbackSnapshot, @@ -7,6 +8,7 @@ import { const $ = id => document.getElementById(id); let state = createParticipationState(); +let syntheticCommunityNodes = 0; const activity = ["Participation is off. Hosted core remains active."]; function message(text) { @@ -40,8 +42,11 @@ function render() { $("resumeBtn").disabled = state.processState !== "paused"; $("stopBtn").disabled = !active; - const fallback = hostedFallbackSnapshot(state, 0); + const fallback = hostedFallbackSnapshot(state, syntheticCommunityNodes); $("communityCount").textContent = fallback.volunteerNodes + " volunteer nodes"; + const community = communityCapacityPreview(syntheticCommunityNodes); + $("communityLevel").textContent = community.level; + $("communityDescription").textContent = community.description; const summary = contributionSummary(state); $("storageSummary").textContent = summary.storageLabel; @@ -66,6 +71,13 @@ function act(action, successMessage) { } } +$("communityPreview").onchange = e => { + syntheticCommunityNodes = Number(e.target.value); + const community = communityCapacityPreview(syntheticCommunityNodes); + record("Synthetic preview changed to " + community.nodes + " volunteer nodes · " + community.level + "."); + render(); +}; + $("storageToggle").onchange = e => act({ type: "SET_STORAGE", value: e.target.checked }, e.target.checked ? "Storage contribution enabled within your selected limit." : "Storage contribution is off."); From e51250eeead425a415d03c07f2b802b3573e9aef Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:08:12 +0200 Subject: [PATCH 153/368] Test synthetic community capacity preview states --- tools/tests/participation_lab_state.test.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tools/tests/participation_lab_state.test.mjs b/tools/tests/participation_lab_state.test.mjs index d229c360..39a23d75 100644 --- a/tools/tests/participation_lab_state.test.mjs +++ b/tools/tests/participation_lab_state.test.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + communityCapacityPreview, contributionSummary, createParticipationState, hostedFallbackSnapshot, @@ -100,3 +101,20 @@ test("contribution summary remains descriptive and optional", () => { assert.match(active.storageDetail, /500 MB/); assert.match(active.computeDetail, /30%/); }); + +test("community capacity preview is always labelled synthetic and preserves hosted fallback", () => { + const zero = communityCapacityPreview(0); + assert.equal(zero.level, "Hosted only"); + assert.equal(zero.hostedCoreActive, true); + assert.equal(zero.isSyntheticPreview, true); + + const early = communityCapacityPreview(1); + assert.equal(early.level, "Early community"); + + const growing = communityCapacityPreview(100); + assert.equal(growing.level, "Growing network"); + + const strong = communityCapacityPreview(1000); + assert.equal(strong.level, "Strong community"); + assert.equal(strong.hostedCoreActive, true); +}); From f7ecd5b586ead9fbb5c964793f2dec967f487f18 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:08:19 +0200 Subject: [PATCH 154/368] Style synthetic community growth preview controls --- web/participation-lab/participation.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/participation-lab/participation.css b/web/participation-lab/participation.css index f1f773f0..330dfd57 100644 --- a/web/participation-lab/participation.css +++ b/web/participation-lab/participation.css @@ -11,3 +11,5 @@ .activity{display:flex;justify-content:space-between;gap:20px;align-items:flex-start} .activity ol{margin:0;padding-left:22px;color:#8fa2b5;max-width:650px}.activity li{margin:4px 0} @media(max-width:760px){.overview{grid-template-columns:1fr}.overview-items{grid-template-columns:1fr}.activity{flex-direction:column}} + +.networkbar>div:first-child{min-width:0}.networkbar #communityLevel{display:inline-block;margin-top:6px;padding:5px 9px;border-radius:999px;background:#152b3b;color:#9eb5ca;font-size:11px;font-weight:800}.networkbar p{margin:7px 0 0;color:#7f93a7;font-size:12px;line-height:1.45;max-width:660px}.network-actions{display:flex;align-items:flex-end;gap:9px;flex-wrap:wrap;justify-content:flex-end}.network-actions label{display:block;width:100%;text-align:right;color:#71879d;font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:800}.network-actions select{appearance:none;border:1px solid #2d455b;background:#102130;color:#dce8f4;border-radius:10px;padding:9px 30px 9px 11px;font-weight:750}.network-actions select:focus{outline:2px solid #6b55e866;outline-offset:2px}@media(max-width:560px){.network-actions{width:100%;align-items:flex-start;justify-content:flex-start}.network-actions label{text-align:left}} From 645de25b6aef2cfac56370abf330bd4a3e4f7ea2 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:08:47 +0200 Subject: [PATCH 155/368] Add illustrative community growth stage visualization --- web/participation-lab/index.html | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/web/participation-lab/index.html b/web/participation-lab/index.html index 7ba9292f..0c99c5fb 100644 --- a/web/participation-lab/index.html +++ b/web/participation-lab/index.html @@ -70,9 +70,17 @@

    Your contribution

    SIMULATED BALANCE0 calT
    - GROW FROM SMALL

    Every contribution can add capacity.

    -
    Hosted core→Storage→Compute→Community scale
    -

    The ecosystem works at zero participation. More voluntary nodes can gradually add redundancy and capacity without redesigning the core.

    + GROW FROM SMALL · ILLUSTRATIVE

    Every contribution can add capacity.

    +
    + Hosted core→ + Early community→ + Growing→ + Strong community +
    +
    +
    +
    +

    The ecosystem works at zero participation. This bar is illustrative only; it does not represent live network capacity.

    From df04bdbaedebcce393b6ad96645b89505af1bd53 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:08:49 +0200 Subject: [PATCH 156/368] Style illustrative community growth meter --- web/participation-lab/participation.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/participation-lab/participation.css b/web/participation-lab/participation.css index 330dfd57..6ecbe2e3 100644 --- a/web/participation-lab/participation.css +++ b/web/participation-lab/participation.css @@ -13,3 +13,5 @@ @media(max-width:760px){.overview{grid-template-columns:1fr}.overview-items{grid-template-columns:1fr}.activity{flex-direction:column}} .networkbar>div:first-child{min-width:0}.networkbar #communityLevel{display:inline-block;margin-top:6px;padding:5px 9px;border-radius:999px;background:#152b3b;color:#9eb5ca;font-size:11px;font-weight:800}.networkbar p{margin:7px 0 0;color:#7f93a7;font-size:12px;line-height:1.45;max-width:660px}.network-actions{display:flex;align-items:flex-end;gap:9px;flex-wrap:wrap;justify-content:flex-end}.network-actions label{display:block;width:100%;text-align:right;color:#71879d;font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:800}.network-actions select{appearance:none;border:1px solid #2d455b;background:#102130;color:#dce8f4;border-radius:10px;padding:9px 30px 9px 11px;font-weight:750}.network-actions select:focus{outline:2px solid #6b55e866;outline-offset:2px}@media(max-width:560px){.network-actions{width:100%;align-items:flex-start;justify-content:flex-start}.network-actions label{text-align:left}} + +.growthmeter{height:9px;border-radius:999px;background:#0a1420;border:1px solid #21354a;overflow:hidden;margin:14px 0 10px}.growthmeter>div{height:100%;width:0;background:linear-gradient(90deg,#6b55e8,#6de3b7);transition:width .25s ease}.steps span.active{background:#183d35;color:#6de3b7}.steps span.past{background:#152b3b;color:#9eb5ca} From 66544831e9168cd5b0561236a830b9224f6d8fdc Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:08:51 +0200 Subject: [PATCH 157/368] Animate illustrative community growth stages --- web/participation-lab/participation.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/participation-lab/participation.js b/web/participation-lab/participation.js index 530d0069..7e3ad12a 100644 --- a/web/participation-lab/participation.js +++ b/web/participation-lab/participation.js @@ -47,6 +47,17 @@ function render() { const community = communityCapacityPreview(syntheticCommunityNodes); $("communityLevel").textContent = community.level; $("communityDescription").textContent = community.description; + const stage = + syntheticCommunityNodes >= 1000 ? "strong" : + syntheticCommunityNodes >= 100 ? "growing" : + syntheticCommunityNodes >= 1 ? "early" : "hosted"; + const stageWidth = { hosted: 0, early: 22, growing: 62, strong: 100 }[stage]; + $("growthFill").style.width = stageWidth + "%"; + document.querySelectorAll("#growthSteps span[data-stage]").forEach(item => { + const order = { hosted: 0, early: 1, growing: 2, strong: 3 }; + item.classList.toggle("active", item.dataset.stage === stage); + item.classList.toggle("past", order[item.dataset.stage] < order[stage]); + }); const summary = contributionSummary(state); $("storageSummary").textContent = summary.storageLabel; From 83c8790b844b777abedfef22aae4e44432d37fe4 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:12:16 +0200 Subject: [PATCH 158/368] Separate storage and compute participation lifecycles --- tools/participation_ui_adapter.py | 82 +++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/tools/participation_ui_adapter.py b/tools/participation_ui_adapter.py index 4aa2a149..60a9c7ca 100644 --- a/tools/participation_ui_adapter.py +++ b/tools/participation_ui_adapter.py @@ -1,8 +1,8 @@ """Local synthetic participation UI state adapter. -Bridges the static Participation Lab prototype to the existing synthetic -storage/compute simulators without enabling remote nodes, networking, token -settlement or arbitrary jobs. +Bridges the Participation Lab prototype to separate synthetic storage/compute +simulators without enabling remote nodes, networking, token settlement or +arbitrary jobs. """ from __future__ import annotations @@ -16,6 +16,9 @@ from tools.participation_simulator import Consent, Coordinator, LocalNode, SimulationError +LIFECYCLE_STATES = {"off", "running", "paused"} + + @dataclass(frozen=True) class ParticipationSelection: storage: bool = False @@ -23,11 +26,12 @@ class ParticipationSelection: rewards: bool = False storage_limit_mb: int = 250 compute_limit_percent: int = 25 - process_state: str = "off" # off|running|paused + storage_state: str = "off" + process_state: str = "off" class ParticipationSession: - """Synthetic-only session with independent storage/compute controls.""" + """Synthetic-only session with independent storage/compute lifecycles.""" def __init__(self, *, enabled: bool = False, age_band: str = "adult", clock=time.time) -> None: @@ -44,12 +48,24 @@ def __init__(self, *, enabled: bool = False, age_band: str = "adult", self.storage_coord.register(self.participant_id, age_band=age_band) self.compute_coord.register(self.participant_id, age_band=age_band) - def _consent(self) -> Consent: + def _storage_consent(self) -> Consent: + selected = self.selection.storage + state = self.selection.storage_state return Consent( - storage=self.selection.storage, - compute=self.selection.compute and self.selection.process_state == "running", + storage=selected and state in {"running", "paused"}, + compute=False, rewards=self.selection.rewards, - paused=self.selection.process_state == "paused", + paused=selected and state == "paused", + ) + + def _compute_consent(self) -> Consent: + selected = self.selection.compute + state = self.selection.process_state + return Consent( + storage=False, + compute=selected and state in {"running", "paused"}, + rewards=self.selection.rewards, + paused=selected and state == "paused", ) def apply(self, selection: ParticipationSelection) -> dict: @@ -57,14 +73,18 @@ def apply(self, selection: ParticipationSelection) -> dict: raise SimulationError("storage-limit-out-of-range") if selection.compute_limit_percent < 5 or selection.compute_limit_percent > 75: raise SimulationError("compute-limit-out-of-range") - if selection.process_state not in {"off", "running", "paused"}: + if selection.storage_state not in LIFECYCLE_STATES: + raise SimulationError("invalid-storage-state") + if selection.process_state not in LIFECYCLE_STATES: raise SimulationError("invalid-process-state") + if selection.storage_state in {"running", "paused"} and not selection.storage: + raise SimulationError("storage-must-be-enabled-before-process") if selection.process_state in {"running", "paused"} and not selection.compute: raise SimulationError("compute-must-be-enabled-before-process") + self.selection = selection - consent = self._consent() - self.storage_coord.set_consent(self.participant_id, consent) - self.compute_coord.set_consent(self.participant_id, consent) + self.storage_coord.set_consent(self.participant_id, self._storage_consent()) + self.compute_coord.set_consent(self.participant_id, self._compute_consent()) return self.snapshot() def snapshot(self) -> dict: @@ -75,7 +95,8 @@ def snapshot(self) -> dict: "normal_app_access": True, "normal_gameverse_access": True, "selection": asdict(self.selection), - "effective_consent": asdict(self._consent()), + "effective_storage_consent": asdict(self._storage_consent()), + "effective_compute_consent": asdict(self._compute_consent()), "real_network_enabled": False, "real_token_settlement": False, } @@ -83,7 +104,17 @@ def snapshot(self) -> dict: def run_storage_probe(self) -> dict: if not self.selection.storage: return {"status": "storage-off"} - node = LocalNode(enabled=True, consent=self._consent(), capacity_bytes=self.selection.storage_limit_mb * 1024 * 1024) + if self.selection.storage_state == "paused": + return {"status": "storage-paused"} + if self.selection.storage_state != "running": + return {"status": "storage-stopped"} + + consent = self._storage_consent() + node = LocalNode( + enabled=True, + consent=consent, + capacity_bytes=self.selection.storage_limit_mb * 1024 * 1024, + ) challenge = self.storage_coord.issue(self.participant_id, "apple") proof = node.accept_and_prove(challenge, now=int(self.clock())) receipt = self.storage_coord.verify(self.participant_id, proof) @@ -96,7 +127,9 @@ def run_compute_probe(self) -> dict: return {"status": "compute-paused"} if self.selection.process_state != "running": return {"status": "compute-stopped"} - node = ComputeNode(enabled=True, consent=self._consent(), clock=self.clock) + + consent = self._compute_consent() + node = ComputeNode(enabled=True, consent=consent, clock=self.clock) task = self.compute_coord.issue(self.participant_id, "apple") proof = node.execute(task, now=int(self.clock())) receipt = self.compute_coord.verify(self.participant_id, proof) @@ -104,9 +137,8 @@ def run_compute_probe(self) -> dict: def exit(self) -> dict: self.selection = ParticipationSelection() - consent = Consent() - self.storage_coord.set_consent(self.participant_id, consent) - self.compute_coord.set_consent(self.participant_id, consent) + self.storage_coord.set_consent(self.participant_id, Consent()) + self.compute_coord.set_consent(self.participant_id, Consent()) return self.snapshot() def close(self) -> None: @@ -119,11 +151,19 @@ def demo_matrix() -> list[dict]: try: cases = [ ParticipationSelection(), - ParticipationSelection(storage=True), + ParticipationSelection(storage=True, storage_state="off"), + ParticipationSelection(storage=True, storage_state="running"), + ParticipationSelection(storage=True, storage_state="paused"), ParticipationSelection(compute=True, process_state="off"), ParticipationSelection(compute=True, process_state="running"), ParticipationSelection(compute=True, process_state="paused"), - ParticipationSelection(storage=True, compute=True, rewards=True, process_state="running"), + ParticipationSelection( + storage=True, + compute=True, + rewards=True, + storage_state="running", + process_state="running", + ), ] results = [] for case in cases: From 0dfac7f0ec3c2f63841eaed0530e9b92f3963b97 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:12:19 +0200 Subject: [PATCH 159/368] Test independent storage and compute pause resume flows --- tools/tests/test_participation_ui_adapter.py | 63 +++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/tools/tests/test_participation_ui_adapter.py b/tools/tests/test_participation_ui_adapter.py index 4e7bd846..da5bdd82 100644 --- a/tools/tests/test_participation_ui_adapter.py +++ b/tools/tests/test_participation_ui_adapter.py @@ -19,10 +19,19 @@ def test_zero_participation_is_normal_and_hosted_core_stays_active(self): self.assertTrue(snap["normal_app_access"]) self.assertTrue(snap["normal_gameverse_access"]) - def test_storage_only(self): - self.session.apply(ParticipationSelection(storage=True)) + def test_storage_enabled_but_not_started(self): + self.session.apply(ParticipationSelection(storage=True, storage_state="off")) + self.assertEqual(self.session.run_storage_probe()["status"], "storage-stopped") + + def test_storage_start_pause_resume_stop(self): + self.session.apply(ParticipationSelection(storage=True, storage_state="running")) + self.assertTrue(self.session.run_storage_probe()["verified"]) + self.session.apply(ParticipationSelection(storage=True, storage_state="paused")) + self.assertEqual(self.session.run_storage_probe()["status"], "storage-paused") + self.session.apply(ParticipationSelection(storage=True, storage_state="running")) self.assertTrue(self.session.run_storage_probe()["verified"]) - self.assertEqual(self.session.run_compute_probe()["status"], "compute-off") + self.session.apply(ParticipationSelection(storage=True, storage_state="off")) + self.assertEqual(self.session.run_storage_probe()["status"], "storage-stopped") def test_compute_enabled_but_not_started(self): self.session.apply(ParticipationSelection(compute=True, process_state="off")) @@ -39,32 +48,70 @@ def test_compute_start_pause_resume_stop(self): self.session.apply(ParticipationSelection(compute=True, process_state="off")) self.assertEqual(self.session.run_compute_probe()["status"], "compute-stopped") - def test_storage_can_remain_on_while_compute_paused(self): - self.session.apply(ParticipationSelection(storage=True, compute=True, process_state="paused")) + def test_storage_can_run_while_compute_is_paused(self): + self.session.apply(ParticipationSelection( + storage=True, + compute=True, + storage_state="running", + process_state="paused", + )) self.assertTrue(self.session.run_storage_probe()["verified"]) self.assertEqual(self.session.run_compute_probe()["status"], "compute-paused") + def test_compute_can_run_while_storage_is_paused(self): + self.session.apply(ParticipationSelection( + storage=True, + compute=True, + storage_state="paused", + process_state="running", + )) + self.assertEqual(self.session.run_storage_probe()["status"], "storage-paused") + self.assertTrue(self.session.run_compute_probe()["verified"]) + def test_exit_keeps_product_access(self): - self.session.apply(ParticipationSelection(storage=True, compute=True, rewards=True, process_state="running")) + self.session.apply(ParticipationSelection( + storage=True, + compute=True, + rewards=True, + storage_state="running", + process_state="running", + )) snap = self.session.exit() self.assertFalse(snap["selection"]["storage"]) self.assertFalse(snap["selection"]["compute"]) + self.assertEqual(snap["selection"]["storage_state"], "off") + self.assertEqual(snap["selection"]["process_state"], "off") self.assertTrue(snap["normal_app_access"]) self.assertTrue(snap["normal_gameverse_access"]) self.assertTrue(snap["hosted_core_active"]) - def test_invalid_limits_or_state_fail_closed(self): + def test_invalid_limits_or_states_fail_closed(self): for selection in ( ParticipationSelection(storage_limit_mb=49), ParticipationSelection(compute_limit_percent=80), + ParticipationSelection(storage=False, storage_state="running"), ParticipationSelection(compute=False, process_state="running"), + ParticipationSelection(storage=True, storage_state="invalid"), ): with self.assertRaises(SimulationError): self.session.apply(selection) + def test_storage_and_compute_pause_are_independent(self): + self.session.apply(ParticipationSelection( + storage=True, + compute=True, + storage_state="running", + process_state="paused", + )) + snap = self.session.snapshot() + self.assertFalse(snap["effective_storage_consent"]["paused"]) + self.assertTrue(snap["effective_storage_consent"]["storage"]) + self.assertTrue(snap["effective_compute_consent"]["paused"]) + self.assertTrue(snap["effective_compute_consent"]["compute"]) + def test_demo_matrix_covers_expected_paths(self): results = demo_matrix() - self.assertGreaterEqual(len(results), 7) + self.assertGreaterEqual(len(results), 9) self.assertTrue(all(r["hosted_core_active"] for r in results)) self.assertTrue(all(r["normal_app_access"] for r in results)) From e6e60dbc0e1c135cbd576c1d009f000afefb9d31 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:12:42 +0200 Subject: [PATCH 160/368] Fix storage adapter against bounded VolunteerNode API --- tools/participation_ui_adapter.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tools/participation_ui_adapter.py b/tools/participation_ui_adapter.py index 60a9c7ca..491e6455 100644 --- a/tools/participation_ui_adapter.py +++ b/tools/participation_ui_adapter.py @@ -13,7 +13,7 @@ import time from tools.participation_compute_simulator import ComputeCoordinator, ComputeNode -from tools.participation_simulator import Consent, Coordinator, LocalNode, SimulationError +from tools.participation_simulator import (\n Consent, Coordinator, FIXTURES, SPEC, SimulationError, VolunteerNode\n) LIFECYCLE_STATES = {"off", "running", "paused"} @@ -110,15 +110,21 @@ def run_storage_probe(self) -> dict: return {"status": "storage-stopped"} consent = self._storage_consent() - node = LocalNode( + node = VolunteerNode( + self.storage_node_dir, enabled=True, consent=consent, - capacity_bytes=self.selection.storage_limit_mb * 1024 * 1024, + clock=self.clock, ) challenge = self.storage_coord.issue(self.participant_id, "apple") - proof = node.accept_and_prove(challenge, now=int(self.clock())) + node.store(challenge.shard, FIXTURES["apple"]) + proof = node.prove(challenge, now=int(self.clock())) receipt = self.storage_coord.verify(self.participant_id, proof) - return {"status": receipt.status, "verified": receipt.verified} + return { + "status": receipt.status, + "verified": receipt.verified, + "synthetic_shard_retained_locally": True, + } def run_compute_probe(self) -> dict: if not self.selection.compute: From 9ceaeb26972aeacc37facd4492ade2400ad4c73e Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:13:32 +0200 Subject: [PATCH 161/368] Add independent storage lifecycle to participation state --- web/participation-lab/state.mjs | 54 ++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/web/participation-lab/state.mjs b/web/participation-lab/state.mjs index 4d42fbd6..9c7f858e 100644 --- a/web/participation-lab/state.mjs +++ b/web/participation-lab/state.mjs @@ -1,4 +1,5 @@ export const PROCESS_STATES = Object.freeze(["off", "running", "paused"]); +export const STORAGE_STATES = PROCESS_STATES; export function createParticipationState(overrides = {}) { const state = { @@ -7,6 +8,7 @@ export function createParticipationState(overrides = {}) { rewards: false, storageLimitMb: 250, computeLimitPercent: 25, + storageState: "off", processState: "off", ...overrides, }; @@ -14,15 +16,17 @@ export function createParticipationState(overrides = {}) { } export function validateParticipationState(state) { - if (!PROCESS_STATES.includes(state.processState)) { - throw new Error("invalid-process-state"); - } + if (!STORAGE_STATES.includes(state.storageState)) throw new Error("invalid-storage-state"); + if (!PROCESS_STATES.includes(state.processState)) throw new Error("invalid-process-state"); if (state.storageLimitMb < 50 || state.storageLimitMb > 2000) { throw new Error("storage-limit-out-of-range"); } if (state.computeLimitPercent < 5 || state.computeLimitPercent > 75) { throw new Error("compute-limit-out-of-range"); } + if ((state.storageState === "running" || state.storageState === "paused") && !state.storage) { + throw new Error("storage-must-be-enabled-before-process"); + } if ((state.processState === "running" || state.processState === "paused") && !state.compute) { throw new Error("compute-must-be-enabled-before-process"); } @@ -32,8 +36,14 @@ export function validateParticipationState(state) { export function transition(state, action) { const current = createParticipationState(state); switch (action.type) { - case "SET_STORAGE": - return createParticipationState({ ...current, storage: Boolean(action.value) }); + case "SET_STORAGE": { + const enabled = Boolean(action.value); + return createParticipationState({ + ...current, + storage: enabled, + storageState: enabled ? current.storageState : "off", + }); + } case "SET_COMPUTE": { const enabled = Boolean(action.value); return createParticipationState({ @@ -48,6 +58,19 @@ export function transition(state, action) { return createParticipationState({ ...current, storageLimitMb: Number(action.value) }); case "SET_COMPUTE_LIMIT": return createParticipationState({ ...current, computeLimitPercent: Number(action.value) }); + case "START_STORAGE": + if (!current.storage) throw new Error("storage-not-enabled"); + if (current.storageState !== "off") throw new Error("storage-not-off"); + return createParticipationState({ ...current, storageState: "running" }); + case "PAUSE_STORAGE": + if (current.storageState !== "running") throw new Error("storage-not-running"); + return createParticipationState({ ...current, storageState: "paused" }); + case "RESUME_STORAGE": + if (current.storageState !== "paused") throw new Error("storage-not-paused"); + return createParticipationState({ ...current, storageState: "running" }); + case "STOP_STORAGE": + if (current.storageState === "off") throw new Error("storage-already-off"); + return createParticipationState({ ...current, storageState: "off" }); case "START": if (!current.compute) throw new Error("compute-not-enabled"); if (current.processState !== "off") throw new Error("process-not-off"); @@ -76,17 +99,26 @@ export function hostedFallbackSnapshot(state, volunteerNodes = 0) { appAvailable: true, gameverseAvailable: true, contributionActive: - current.storage || current.processState === "running", + current.storageState === "running" || current.processState === "running", }); } export function contributionSummary(state) { const current = createParticipationState(state); + const storageSize = + current.storageLimitMb >= 1000 + ? (current.storageLimitMb / 1000).toFixed(1) + " GB" + : current.storageLimitMb + " MB"; return Object.freeze({ - storageLabel: current.storage ? "ON" : "OFF", - storageDetail: current.storage - ? `Up to ${current.storageLimitMb >= 1000 ? (current.storageLimitMb / 1000).toFixed(1) + " GB" : current.storageLimitMb + " MB"}` - : "No storage shared", + storageLabel: + current.storageState === "running" ? "RUNNING" : + current.storageState === "paused" ? "PAUSED" : "OFF", + storageDetail: + current.storage + ? current.storageState === "off" + ? `Enabled · waiting for Start · max ${storageSize}` + : `Up to ${storageSize}` + : "No storage shared", computeLabel: current.processState === "running" ? "RUNNING" : current.processState === "paused" ? "PAUSED" : "OFF", @@ -101,7 +133,7 @@ export function contributionSummary(state) { ? "Simulated calT accounting only" : "No reward participation", contributingNow: - current.storage || current.processState === "running", + current.storageState === "running" || current.processState === "running", }); } From 654e0f4cac8aa6a5b81ed63b6fab187fccd07013 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:13:34 +0200 Subject: [PATCH 162/368] Add storage lifecycle controls and accessible semantics --- web/participation-lab/index.html | 92 +++++++++++++++++++------------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/web/participation-lab/index.html b/web/participation-lab/index.html index 0c99c5fb..2b21f8e4 100644 --- a/web/participation-lab/index.html +++ b/web/participation-lab/index.html @@ -9,29 +9,33 @@
    CALORIE ECOSYSTEM · OPTIONAL PARTICIPATION -

    Participation Lab ◌

    +

    Participation Lab

    Use CalorieApp and the Gameverse normally, or voluntarily share a little capacity. You stay in control.

    -
    PROCESS STATUSOFF
    +
    COMPUTE STATUSOFF
    -
    Nothing is required. Storage, compute and simulated rewards are separate choices. Leaving participation never removes normal app or Gameverse access.
    -
    - COMMUNITY CAPACITY · SYNTHETIC PREVIEW - 0 volunteer nodes - Hosted only -

    No volunteer capacity yet. The hosted core handles normal service.

    -
    -
    - - -
    Hosted core active
    -
    -
    + +
    Nothing is required. Storage, compute and simulated rewards are separate choices. Leaving participation never removes normal app or Gameverse access.
    + +
    +
    + COMMUNITY CAPACITY · SYNTHETIC PREVIEW + 0 volunteer nodes + Hosted only +

    No volunteer capacity yet. The hosted core handles normal service.

    +
    +
    + + +
    Hosted core active
    +
    +
    +
    YOUR CHOICES @@ -44,50 +48,64 @@

    Your contribution

    RewardsOFFNo reward participation
    +
    -
    01 · DATA

    Storage contribution

    +
    01 · DATA

    Storage contribution

    Offer a small, user-limited amount of space for eligible public/synthetic shards.

    - - + +
    50 MB2 GB
    -
    Storage can remain available while compute is stopped.
    +
    + + + + +
    +
    Pause or Stop means no new storage work. Existing assigned data is not silently deleted; safe release is a separate step.
    +
    -
    02 · PROCESS

    Compute contribution

    +
    02 · PROCESS

    Compute contribution

    Run only bounded, approved tasks within the limits you choose.

    - +
    LightHigher
    -
    - - +
    + + + +
    +
    -
    03 · OPTIONAL

    calT test rewards

    +
    03 · OPTIONAL

    calT test rewards

    Opt in to simulated calT accounting for verified participation. This prototype mints and transfers nothing.

    SIMULATED BALANCE0 calT
    +
    GROW FROM SMALL · ILLUSTRATIVE

    Every contribution can add capacity.

    - Hosted core→ - Early community→ - Growing→ + Hosted core + Early community + Growing Strong community
    -
    +

    The ecosystem works at zero participation. This bar is illustrative only; it does not represent live network capacity.

    +
    LOCAL PROTOTYPE ACTIVITY

    Recent choices

    -
    1. Participation is off. Hosted core remains active.
    +
    1. Participation is off. Hosted core remains active.
    -
    You can change these choices at any time.
    + +
    You can change these choices at any time.
    \ No newline at end of file From 7a860d15d86dd6fee52132254bfa58b9faa4591d Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:13:36 +0200 Subject: [PATCH 163/368] Wire independent storage lifecycle and safer UI updates --- web/participation-lab/participation.js | 73 ++++++++++++++++++++------ 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/web/participation-lab/participation.js b/web/participation-lab/participation.js index 7e3ad12a..051180f8 100644 --- a/web/participation-lab/participation.js +++ b/web/participation-lab/participation.js @@ -15,14 +15,25 @@ function message(text) { $("message").textContent = text; } +function renderActivity() { + const list = $("activityList"); + list.replaceChildren(...activity.map(text => { + const item = document.createElement("li"); + item.textContent = text; + return item; + })); +} + function record(text) { activity.unshift(text); activity.splice(5); - $("activityList").innerHTML = activity.map(item => "
  • " + item + "
  • ").join(""); + renderActivity(); } function render() { - const active = state.processState === "running" || state.processState === "paused"; + const computeActive = state.processState === "running" || state.processState === "paused"; + const storageActive = state.storageState === "running" || state.storageState === "paused"; + $("statusText").textContent = state.processState.toUpperCase(); $("statusDot").style.background = state.processState === "running" ? "#6de3b7" : @@ -37,22 +48,29 @@ function render() { state.storageLimitMb >= 1000 ? (state.storageLimitMb / 1000).toFixed(1) + " GB" : state.storageLimitMb + " MB"; $("computeValue").textContent = state.computeLimitPercent + "%"; - $("startBtn").disabled = state.processState !== "off"; + $("storageStartBtn").disabled = !state.storage || state.storageState !== "off"; + $("storagePauseBtn").disabled = state.storageState !== "running"; + $("storageResumeBtn").disabled = state.storageState !== "paused"; + $("storageStopBtn").disabled = !storageActive; + + $("startBtn").disabled = !state.compute || state.processState !== "off"; $("pauseBtn").disabled = state.processState !== "running"; $("resumeBtn").disabled = state.processState !== "paused"; - $("stopBtn").disabled = !active; + $("stopBtn").disabled = !computeActive; const fallback = hostedFallbackSnapshot(state, syntheticCommunityNodes); $("communityCount").textContent = fallback.volunteerNodes + " volunteer nodes"; const community = communityCapacityPreview(syntheticCommunityNodes); $("communityLevel").textContent = community.level; $("communityDescription").textContent = community.description; + const stage = syntheticCommunityNodes >= 1000 ? "strong" : syntheticCommunityNodes >= 100 ? "growing" : syntheticCommunityNodes >= 1 ? "early" : "hosted"; const stageWidth = { hosted: 0, early: 22, growing: 62, strong: 100 }[stage]; $("growthFill").style.width = stageWidth + "%"; + $("growthMeter").setAttribute("aria-valuenow", String(stageWidth)); document.querySelectorAll("#growthSteps span[data-stage]").forEach(item => { const order = { hosted: 0, early: 1, growing: 2, strong: 3 }; item.classList.toggle("active", item.dataset.stage === stage); @@ -68,14 +86,19 @@ function render() { $("rewardDetail").textContent = summary.rewardsDetail; } -function act(action, successMessage) { +function act(action, successMessage, shouldRecord = true) { try { state = transition(state, action); - if (successMessage) { message(successMessage); record(successMessage); } + if (successMessage) { + message(successMessage); + if (shouldRecord) record(successMessage); + } render(); } catch (error) { if (error.message === "compute-not-enabled") { message("Turn on compute contribution first — it never starts automatically."); + } else if (error.message === "storage-not-enabled") { + message("Turn on storage contribution first — it never starts automatically."); } else { message("That action is not available in the current state."); } @@ -89,17 +112,32 @@ $("communityPreview").onchange = e => { render(); }; -$("storageToggle").onchange = e => act({ type: "SET_STORAGE", value: e.target.checked }, - e.target.checked ? "Storage contribution enabled within your selected limit." : "Storage contribution is off."); - -$("computeToggle").onchange = e => act({ type: "SET_COMPUTE", value: e.target.checked }, - e.target.checked ? "Compute is available, but still stopped until you press Start." : "Compute contribution is off."); - -$("rewardToggle").onchange = e => act({ type: "SET_REWARDS", value: e.target.checked }, - e.target.checked ? "Simulated calT rewards enabled." : "Simulated rewards disabled."); - -$("storageLimit").oninput = e => act({ type: "SET_STORAGE_LIMIT", value: e.target.value }, "Storage limit changed to " + e.target.value + " MB."); -$("computeLimit").oninput = e => act({ type: "SET_COMPUTE_LIMIT", value: e.target.value }, "Compute limit changed to " + e.target.value + "%."); +$("storageToggle").onchange = e => act( + { type: "SET_STORAGE", value: e.target.checked }, + e.target.checked + ? "Storage is enabled, but still stopped until you press Start." + : "Storage contribution is off." +); +$("computeToggle").onchange = e => act( + { type: "SET_COMPUTE", value: e.target.checked }, + e.target.checked + ? "Compute is enabled, but still stopped until you press Start." + : "Compute contribution is off." +); +$("rewardToggle").onchange = e => act( + { type: "SET_REWARDS", value: e.target.checked }, + e.target.checked ? "Simulated calT rewards enabled." : "Simulated rewards disabled." +); + +$("storageLimit").oninput = e => act({ type: "SET_STORAGE_LIMIT", value: e.target.value }, null, false); +$("storageLimit").onchange = e => record("Storage limit changed to " + e.target.value + " MB."); +$("computeLimit").oninput = e => act({ type: "SET_COMPUTE_LIMIT", value: e.target.value }, null, false); +$("computeLimit").onchange = e => record("Compute limit changed to " + e.target.value + "%."); + +$("storageStartBtn").onclick = () => act({ type: "START_STORAGE" }, "Storage participation started within your selected limit."); +$("storagePauseBtn").onclick = () => act({ type: "PAUSE_STORAGE" }, "Storage paused. Compute keeps its own separate setting."); +$("storageResumeBtn").onclick = () => act({ type: "RESUME_STORAGE" }, "Storage participation resumed."); +$("storageStopBtn").onclick = () => act({ type: "STOP_STORAGE" }, "Storage work stopped. Existing assigned data is not silently deleted."); $("startBtn").onclick = () => act({ type: "START" }, "Compute participation started within your selected limit."); $("pauseBtn").onclick = () => act({ type: "PAUSE" }, "Compute paused. Storage keeps its own separate setting."); @@ -107,4 +145,5 @@ $("resumeBtn").onclick = () => act({ type: "RESUME" }, "Compute participation re $("stopBtn").onclick = () => act({ type: "STOP" }, "Compute stopped. You can start again whenever you choose."); $("exitBtn").onclick = () => act({ type: "EXIT" }, "Participation exited. CalorieApp and Gameverse access remains unchanged."); +renderActivity(); render(); From 4037c7fbdd5527c3717c93231b7b835b972041bb Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:13:38 +0200 Subject: [PATCH 164/368] Test independent storage and compute UI lifecycles --- tools/tests/participation_lab_state.test.mjs | 64 ++++++++++++++------ 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/tools/tests/participation_lab_state.test.mjs b/tools/tests/participation_lab_state.test.mjs index 39a23d75..741a9a3c 100644 --- a/tools/tests/participation_lab_state.test.mjs +++ b/tools/tests/participation_lab_state.test.mjs @@ -19,6 +19,25 @@ test("zero participation keeps hosted core and products available", () => { assert.equal(snapshot.contributionActive, false); }); +test("storage opt-in does not auto-start storage work", () => { + let state = createParticipationState(); + state = transition(state, { type: "SET_STORAGE", value: true }); + assert.equal(state.storage, true); + assert.equal(state.storageState, "off"); +}); + +test("storage start pause resume stop lifecycle is explicit", () => { + let state = createParticipationState({ storage: true }); + state = transition(state, { type: "START_STORAGE" }); + assert.equal(state.storageState, "running"); + state = transition(state, { type: "PAUSE_STORAGE" }); + assert.equal(state.storageState, "paused"); + state = transition(state, { type: "RESUME_STORAGE" }); + assert.equal(state.storageState, "running"); + state = transition(state, { type: "STOP_STORAGE" }); + assert.equal(state.storageState, "off"); +}); + test("compute opt-in does not auto-start processing", () => { let state = createParticipationState(); state = transition(state, { type: "SET_COMPUTE", value: true }); @@ -26,7 +45,7 @@ test("compute opt-in does not auto-start processing", () => { assert.equal(state.processState, "off"); }); -test("start pause resume stop lifecycle is explicit", () => { +test("compute start pause resume stop lifecycle is explicit", () => { let state = createParticipationState({ compute: true }); state = transition(state, { type: "START" }); assert.equal(state.processState, "running"); @@ -38,25 +57,34 @@ test("start pause resume stop lifecycle is explicit", () => { assert.equal(state.processState, "off"); }); -test("storage remains independently enabled while compute is paused", () => { +test("storage and compute lifecycles remain independent", () => { let state = createParticipationState({ storage: true, compute: true }); + state = transition(state, { type: "START_STORAGE" }); state = transition(state, { type: "START" }); state = transition(state, { type: "PAUSE" }); - assert.equal(state.storage, true); - assert.equal(state.compute, true); + assert.equal(state.storageState, "running"); assert.equal(state.processState, "paused"); + state = transition(state, { type: "PAUSE_STORAGE" }); + state = transition(state, { type: "RESUME" }); + assert.equal(state.storageState, "paused"); + assert.equal(state.processState, "running"); }); -test("disabling compute forces process off without touching storage", () => { +test("disabling a contribution forces only its own lifecycle off", () => { let state = createParticipationState({ storage: true, compute: true, + storageState: "running", processState: "running", }); state = transition(state, { type: "SET_COMPUTE", value: false }); assert.equal(state.storage, true); + assert.equal(state.storageState, "running"); assert.equal(state.compute, false); assert.equal(state.processState, "off"); + state = transition(state, { type: "SET_STORAGE", value: false }); + assert.equal(state.storage, false); + assert.equal(state.storageState, "off"); }); test("exit clears participation but normal access remains", () => { @@ -64,6 +92,7 @@ test("exit clears participation but normal access remains", () => { storage: true, compute: true, rewards: true, + storageState: "running", processState: "running", }); state = transition(state, { type: "EXIT" }); @@ -73,29 +102,35 @@ test("exit clears participation but normal access remains", () => { assert.equal(snapshot.gameverseAvailable, true); }); -test("invalid limits and invalid process transitions fail closed", () => { +test("invalid limits and lifecycle transitions fail closed", () => { assert.throws(() => createParticipationState({ storageLimitMb: 10 }), /storage-limit-out-of-range/); assert.throws(() => createParticipationState({ computeLimitPercent: 100 }), /compute-limit-out-of-range/); + assert.throws(() => transition(createParticipationState(), { type: "START_STORAGE" }), /storage-not-enabled/); assert.throws(() => transition(createParticipationState(), { type: "START" }), /compute-not-enabled/); }); -test("contribution summary remains descriptive and optional", () => { +test("contribution summary distinguishes enabled stopped paused and running", () => { const off = contributionSummary(createParticipationState()); assert.equal(off.storageLabel, "OFF"); assert.equal(off.computeLabel, "OFF"); assert.equal(off.rewardsLabel, "OFF"); assert.equal(off.contributingNow, false); + const enabled = contributionSummary(createParticipationState({ storage: true, compute: true })); + assert.match(enabled.storageDetail, /waiting for Start/); + assert.match(enabled.computeDetail, /waiting for Start/); + const active = contributionSummary(createParticipationState({ storage: true, compute: true, rewards: true, - processState: "running", + storageState: "running", + processState: "paused", storageLimitMb: 500, computeLimitPercent: 30, })); - assert.equal(active.storageLabel, "ON"); - assert.equal(active.computeLabel, "RUNNING"); + assert.equal(active.storageLabel, "RUNNING"); + assert.equal(active.computeLabel, "PAUSED"); assert.equal(active.rewardsLabel, "ON"); assert.equal(active.contributingNow, true); assert.match(active.storageDetail, /500 MB/); @@ -107,13 +142,8 @@ test("community capacity preview is always labelled synthetic and preserves host assert.equal(zero.level, "Hosted only"); assert.equal(zero.hostedCoreActive, true); assert.equal(zero.isSyntheticPreview, true); - - const early = communityCapacityPreview(1); - assert.equal(early.level, "Early community"); - - const growing = communityCapacityPreview(100); - assert.equal(growing.level, "Growing network"); - + assert.equal(communityCapacityPreview(1).level, "Early community"); + assert.equal(communityCapacityPreview(100).level, "Growing network"); const strong = communityCapacityPreview(1000); assert.equal(strong.level, "Strong community"); assert.equal(strong.hostedCoreActive, true); From aa7c41747339c565ff433dddcd947f012bf99633 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:13:46 +0200 Subject: [PATCH 165/368] Improve keyboard focus touch targets and mobile participation controls --- web/participation-lab/participation.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/web/participation-lab/participation.css b/web/participation-lab/participation.css index 6ecbe2e3..88649549 100644 --- a/web/participation-lab/participation.css +++ b/web/participation-lab/participation.css @@ -1,4 +1,4 @@ -:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;color:#f8fbff;background:#07111f}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:radial-gradient(circle at 18% 0,#20205a 0,transparent 34%),radial-gradient(circle at 90% 15%,#103d45 0,transparent 28%),#07111f}.shell{max-width:1180px;margin:auto;padding:44px 28px}.hero{display:flex;justify-content:space-between;gap:28px;align-items:end;margin-bottom:22px}.eyebrow,small{letter-spacing:.14em;font-weight:800;color:#8aa2bb;font-size:11px}.hero h1{font-size:46px;line-height:1;margin:9px 0}.hero h1 span{color:#7a5cff}.hero p{max-width:690px;color:#aab8c7;font-size:17px}.status{display:flex;align-items:center;gap:12px;background:#101c2b;border:1px solid #25354a;border-radius:18px;padding:14px 18px;min-width:145px}.status i{width:11px;height:11px;border-radius:50%;background:#657488;box-shadow:0 0 0 5px #65748822}.status strong{display:block;margin-top:3px}.notice{border:1px solid #5e55a7;background:#171b39;border-radius:16px;padding:15px 18px;color:#cbd5e1;margin-bottom:18px}.notice strong{color:#a99aff}.grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.card{position:relative;overflow:hidden;background:linear-gradient(145deg,#111e2d,#0b1724);border:1px solid #25374b;border-radius:22px;padding:24px;min-height:250px;box-shadow:0 18px 45px #0004}.card:after{content:"C";position:absolute;right:-8px;bottom:-48px;font-weight:900;font-size:190px;color:#ffffff08;pointer-events:none}.cardhead{display:flex;justify-content:space-between;gap:18px}.card h2{margin:6px 0 10px;font-size:24px}.card p{color:#94a7ba;line-height:1.55}.card label:not(.switch){display:flex;justify-content:space-between;margin-top:25px;font-size:13px}.card label b{color:#66e3b4}input[type=range]{width:100%;accent-color:#6de3b7;margin-top:13px}.ticks{display:flex;justify-content:space-between;color:#61758a;font-size:11px}.switch input{display:none}.switch span{display:block;width:48px;height:27px;border-radius:20px;background:#26384b;position:relative;cursor:pointer}.switch span:after{content:"";position:absolute;width:21px;height:21px;border-radius:50%;background:#9bacc0;top:3px;left:4px;transition:.2s}.switch input:checked+span{background:#315e51}.switch input:checked+span:after{left:23px;background:#6de3b7}.mini{margin-top:20px;padding:12px;border-radius:12px;background:#0a1420;color:#6f8498;font-size:12px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:21px}button{border:1px solid #344a61;background:#17283a;color:#dce8f4;border-radius:11px;padding:10px 16px;font-weight:800;cursor:pointer}button.primary{background:#6b55e8;border-color:#826cff;color:white}button:disabled{opacity:.35;cursor:not-allowed}.balance{margin-top:30px;border-radius:16px;padding:18px;background:#0a1521;border:1px solid #21354a}.balance span{display:block;color:#657b91;font-size:10px;letter-spacing:.12em}.balance strong{font-size:30px}.balance em{font-size:15px;color:#7b67ee;font-style:normal}.steps{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:27px 0}.steps span{padding:8px 10px;border-radius:10px;background:#142536;color:#738aa1;font-size:12px;font-weight:800}.steps .on{background:#183d35;color:#6de3b7}.steps i{color:#475c71}footer{display:flex;align-items:center;gap:18px;margin-top:20px;color:#6f8498;font-size:12px}.danger{border-color:#5b3440;background:#24161d;color:#e39aa9}@media(max-width:760px){.shell{padding:24px 16px}.hero{align-items:start;flex-direction:column}.hero h1{font-size:36px}.grid{grid-template-columns:1fr}.status{width:100%}.card{min-height:auto}footer{align-items:flex-start;flex-direction:column}}.networkbar{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:0 0 18px;padding:14px 18px;border:1px solid #234255;background:#0b1b28;border-radius:16px}.networkbar strong{display:block;margin-top:4px}.fallback{padding:8px 12px;border-radius:10px;background:#173c34;color:#6de3b7;font-size:12px;font-weight:800}@media(max-width:560px){.networkbar{align-items:flex-start;flex-direction:column}} +:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;color:#f8fbff;background:#07111f}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:radial-gradient(circle at 18% 0,#20205a 0,transparent 34%),radial-gradient(circle at 90% 15%,#103d45 0,transparent 28%),#07111f}.shell{max-width:1180px;margin:auto;padding:44px 28px}.hero{display:flex;justify-content:space-between;gap:28px;align-items:end;margin-bottom:22px}.eyebrow,small{letter-spacing:.14em;font-weight:800;color:#8aa2bb;font-size:11px}.hero h1{font-size:46px;line-height:1;margin:9px 0}.hero h1 span{color:#7a5cff}.hero p{max-width:690px;color:#aab8c7;font-size:17px}.status{display:flex;align-items:center;gap:12px;background:#101c2b;border:1px solid #25354a;border-radius:18px;padding:14px 18px;min-width:145px}.status i{width:11px;height:11px;border-radius:50%;background:#657488;box-shadow:0 0 0 5px #65748822}.status strong{display:block;margin-top:3px}.notice{border:1px solid #5e55a7;background:#171b39;border-radius:16px;padding:15px 18px;color:#cbd5e1;margin-bottom:18px}.notice strong{color:#a99aff}.grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.card{position:relative;overflow:hidden;background:linear-gradient(145deg,#111e2d,#0b1724);border:1px solid #25374b;border-radius:22px;padding:24px;min-height:250px;box-shadow:0 18px 45px #0004}.card:after{content:"C";position:absolute;right:-8px;bottom:-48px;font-weight:900;font-size:190px;color:#ffffff08;pointer-events:none}.cardhead{display:flex;justify-content:space-between;gap:18px}.card h2{margin:6px 0 10px;font-size:24px}.card p{color:#94a7ba;line-height:1.55}.card label:not(.switch){display:flex;justify-content:space-between;margin-top:25px;font-size:13px}.card label b{color:#66e3b4}input[type=range]{width:100%;accent-color:#6de3b7;margin-top:13px}.ticks{display:flex;justify-content:space-between;color:#61758a;font-size:11px}.switch{position:relative;display:inline-flex;align-items:center}.switch input{position:absolute;inset:0;width:48px;height:27px;margin:0;opacity:0;cursor:pointer;z-index:2}.switch span{display:block;width:48px;height:27px;border-radius:20px;background:#26384b;position:relative;cursor:pointer}.switch span:after{content:"";position:absolute;width:21px;height:21px;border-radius:50%;background:#9bacc0;top:3px;left:4px;transition:.2s}.switch input:checked+span{background:#315e51}.switch input:checked+span:after{left:23px;background:#6de3b7}.mini{margin-top:20px;padding:12px;border-radius:12px;background:#0a1420;color:#6f8498;font-size:12px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:21px}button{border:1px solid #344a61;background:#17283a;color:#dce8f4;border-radius:11px;padding:10px 16px;font-weight:800;cursor:pointer}button.primary{background:#6b55e8;border-color:#826cff;color:white}button:disabled{opacity:.35;cursor:not-allowed}.balance{margin-top:30px;border-radius:16px;padding:18px;background:#0a1521;border:1px solid #21354a}.balance span{display:block;color:#657b91;font-size:10px;letter-spacing:.12em}.balance strong{font-size:30px}.balance em{font-size:15px;color:#7b67ee;font-style:normal}.steps{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:27px 0}.steps span{padding:8px 10px;border-radius:10px;background:#142536;color:#738aa1;font-size:12px;font-weight:800}.steps .on{background:#183d35;color:#6de3b7}.steps i{color:#475c71}footer{display:flex;align-items:center;gap:18px;margin-top:20px;color:#6f8498;font-size:12px}.danger{border-color:#5b3440;background:#24161d;color:#e39aa9}@media(max-width:760px){.shell{padding:24px 16px}.hero{align-items:start;flex-direction:column}.hero h1{font-size:36px}.grid{grid-template-columns:1fr}.status{width:100%}.card{min-height:auto}footer{align-items:flex-start;flex-direction:column}}.networkbar{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:0 0 18px;padding:14px 18px;border:1px solid #234255;background:#0b1b28;border-radius:16px}.networkbar strong{display:block;margin-top:4px}.fallback{padding:8px 12px;border-radius:10px;background:#173c34;color:#6de3b7;font-size:12px;font-weight:800}@media(max-width:560px){.networkbar{align-items:flex-start;flex-direction:column}} .overview,.activity{margin:0 0 18px;padding:20px;border:1px solid #25374b;background:#0c1825;border-radius:18px} .overview{display:grid;grid-template-columns:1.1fr 2fr;gap:22px;align-items:center} @@ -15,3 +15,11 @@ .networkbar>div:first-child{min-width:0}.networkbar #communityLevel{display:inline-block;margin-top:6px;padding:5px 9px;border-radius:999px;background:#152b3b;color:#9eb5ca;font-size:11px;font-weight:800}.networkbar p{margin:7px 0 0;color:#7f93a7;font-size:12px;line-height:1.45;max-width:660px}.network-actions{display:flex;align-items:flex-end;gap:9px;flex-wrap:wrap;justify-content:flex-end}.network-actions label{display:block;width:100%;text-align:right;color:#71879d;font-size:10px;text-transform:uppercase;letter-spacing:.08em;font-weight:800}.network-actions select{appearance:none;border:1px solid #2d455b;background:#102130;color:#dce8f4;border-radius:10px;padding:9px 30px 9px 11px;font-weight:750}.network-actions select:focus{outline:2px solid #6b55e866;outline-offset:2px}@media(max-width:560px){.network-actions{width:100%;align-items:flex-start;justify-content:flex-start}.network-actions label{text-align:left}} .growthmeter{height:9px;border-radius:999px;background:#0a1420;border:1px solid #21354a;overflow:hidden;margin:14px 0 10px}.growthmeter>div{height:100%;width:0;background:linear-gradient(90deg,#6b55e8,#6de3b7);transition:width .25s ease}.steps span.active{background:#183d35;color:#6de3b7}.steps span.past{background:#152b3b;color:#9eb5ca} + +.switch input:focus-visible+span{outline:2px solid #8d7bff;outline-offset:3px} +button:focus-visible,select:focus-visible,input[type=range]:focus-visible{outline:2px solid #8d7bff;outline-offset:3px} +button{min-height:44px} +.actions button{min-width:76px} +input[type=range]{min-height:32px} +@media(max-width:560px){.actions{display:grid;grid-template-columns:1fr 1fr}.actions button{width:100%}.hero h1{font-size:32px;line-height:1.08}.shell{padding:20px 14px}.card{padding:20px}.overview,.activity{padding:17px}} +@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;transition:none!important;animation:none!important}} From 78079301c37d647d2d01437a1ff45caa1cf4c1a2 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:14:07 +0200 Subject: [PATCH 166/368] Add accessibility and mobile regressions for participation lab --- .../participation_lab_accessibility.test.mjs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tools/tests/participation_lab_accessibility.test.mjs diff --git a/tools/tests/participation_lab_accessibility.test.mjs b/tools/tests/participation_lab_accessibility.test.mjs new file mode 100644 index 00000000..4a0c141a --- /dev/null +++ b/tools/tests/participation_lab_accessibility.test.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const htmlPath = new URL("../../web/participation-lab/index.html", import.meta.url); +const cssPath = new URL("../../web/participation-lab/participation.css", import.meta.url); + +test("participation prototype keeps essential controls keyboard and screen-reader discoverable", async () => { + const html = await readFile(htmlPath, "utf8"); + + for (const id of ["storageToggle", "computeToggle", "rewardToggle"]) { + assert.match(html, new RegExp(`id="${id}"[^>]*aria-label="[^"]+"`)); + } + assert.match(html, /
    +
    + + ADVANCED · OPTIONALDevice & network limits + Conservative defaults + +
    +

    Keep the normal experience simple. These extra preferences only matter if you voluntarily participate.

    +

    Synthetic policy preview: real Wi-Fi, battery and idle-state enforcement belongs in the future node runtime.

    +
    +
    +
    + + +
    100 MB10 GB
    +
    + +
    + + + + +
    + +
    + Allowed task classes + + +
    + +
    + Manual start · Wi-Fi only · idle compute +

    Unknown task classes remain blocked. Auto-start is off by default and this web prototype never starts background device work by itself.

    +
    +
    +
    +
    LOCAL PROTOTYPE ACTIVITY

    Recent choices

    1. Participation is off. Hosted core remains active.
    From ca92c432bd64a3e9f4f27d559c7d2dfc109f02e8 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:00:33 +0200 Subject: [PATCH 202/368] Wire advanced optional resource preferences into participation lab --- web/participation-lab/participation.js | 57 ++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/web/participation-lab/participation.js b/web/participation-lab/participation.js index ac3a2372..6e3f7b6c 100644 --- a/web/participation-lab/participation.js +++ b/web/participation-lab/participation.js @@ -44,16 +44,33 @@ function render() { $("rewardToggle").checked = state.rewards; $("storageLimit").value = state.storageLimitMb; $("computeLimit").value = state.computeLimitPercent; + $("bandwidthLimit").value = state.monthlyBandwidthLimitMb; + $("wifiOnly").checked = state.wifiOnly; + $("allowBattery").checked = state.allowBattery; + $("idleComputeOnly").checked = state.idleComputeOnly; + $("autoStart").checked = state.autoStart; + $("allowStorageTasks").checked = state.allowStorageTasks; + $("allowComputeTasks").checked = state.allowComputeTasks; $("storageValue").textContent = state.storageLimitMb >= 1000 ? (state.storageLimitMb / 1000).toFixed(1) + " GB" : state.storageLimitMb + " MB"; $("computeValue").textContent = state.computeLimitPercent + "%"; - - $("storageStartBtn").disabled = !state.storage || state.storageState !== "off"; + $("bandwidthValue").textContent = + state.monthlyBandwidthLimitMb >= 1000 + ? (state.monthlyBandwidthLimitMb / 1000).toFixed(1) + " GB" + : state.monthlyBandwidthLimitMb + " MB"; + $("advancedModeSummary").textContent = [ + state.autoStart ? "Auto-start allowed" : "Manual start", + state.wifiOnly ? "Wi-Fi only" : "Any network", + state.allowBattery ? "Battery allowed" : "Plugged-in preferred", + state.idleComputeOnly ? "Idle compute" : "Compute when active", + ].join(" · "); + + $("storageStartBtn").disabled = !state.storage || !state.allowStorageTasks || state.storageState !== "off"; $("storagePauseBtn").disabled = state.storageState !== "running"; $("storageResumeBtn").disabled = state.storageState !== "paused"; $("storageStopBtn").disabled = !storageActive; - $("startBtn").disabled = !state.compute || state.processState !== "off"; + $("startBtn").disabled = !state.compute || !state.allowComputeTasks || state.processState !== "off"; $("pauseBtn").disabled = state.processState !== "running"; $("resumeBtn").disabled = state.processState !== "paused"; $("stopBtn").disabled = !computeActive; @@ -99,6 +116,10 @@ function act(action, successMessage, shouldRecord = true) { message("Turn on compute contribution first — it never starts automatically."); } else if (error.message === "storage-not-enabled") { message("Turn on storage contribution first — it never starts automatically."); + } else if (error.message === "storage-task-class-disabled") { + message("Enable the storage task class in Advanced limits before starting storage."); + } else if (error.message === "compute-task-class-disabled") { + message("Enable the compute task class in Advanced limits before starting compute."); } else { message("That action is not available in the current state."); } @@ -145,6 +166,36 @@ $("storageReleaseMode").onchange = e => { $("computeLimit").oninput = e => act({ type: "SET_COMPUTE_LIMIT", value: e.target.value }, null, false); $("computeLimit").onchange = e => record("Compute limit changed to " + e.target.value + "%."); +$("bandwidthLimit").oninput = e => act({ type: "SET_BANDWIDTH_LIMIT", value: e.target.value }, null, false); +$("bandwidthLimit").onchange = e => record("Monthly transfer cap changed to " + e.target.value + " MB."); + +$("wifiOnly").onchange = e => act( + { type: "SET_WIFI_ONLY", value: e.target.checked }, + e.target.checked ? "Participation limited to Wi-Fi." : "Wi-Fi-only limit disabled." +); +$("allowBattery").onchange = e => act( + { type: "SET_ALLOW_BATTERY", value: e.target.checked }, + e.target.checked ? "Battery-powered participation allowed." : "Battery-powered participation disabled." +); +$("idleComputeOnly").onchange = e => act( + { type: "SET_IDLE_COMPUTE_ONLY", value: e.target.checked }, + e.target.checked ? "Compute limited to idle-device periods." : "Idle-only compute limit disabled." +); +$("autoStart").onchange = e => act( + { type: "SET_AUTO_START", value: e.target.checked }, + e.target.checked + ? "Auto-start preference enabled for a future node runtime; this prototype still starts nothing in the background." + : "Manual start remains required." +); +$("allowStorageTasks").onchange = e => act( + { type: "SET_STORAGE_TASK_CLASS", value: e.target.checked }, + e.target.checked ? "Storage task class allowed." : "Storage task class blocked." +); +$("allowComputeTasks").onchange = e => act( + { type: "SET_COMPUTE_TASK_CLASS", value: e.target.checked }, + e.target.checked ? "Compute task class allowed." : "Compute task class blocked." +); + $("storageStartBtn").onclick = () => act({ type: "START_STORAGE" }, "Storage participation started within your selected limit."); $("storagePauseBtn").onclick = () => act({ type: "PAUSE_STORAGE" }, "Storage paused. Compute keeps its own separate setting."); $("storageResumeBtn").onclick = () => act({ type: "RESUME_STORAGE" }, "Storage participation resumed."); From 8d9ba554379fe53ac3eb7f43103ec0fc11c11cda Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:00:45 +0200 Subject: [PATCH 203/368] Style collapsed advanced participation preference panel --- web/participation-lab/participation.css | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/web/participation-lab/participation.css b/web/participation-lab/participation.css index 3f7eb0a8..f24ab49c 100644 --- a/web/participation-lab/participation.css +++ b/web/participation-lab/participation.css @@ -28,3 +28,25 @@ input[type=range]{min-height:32px} .release-choice label{display:block!important;margin:0 0 8px!important;color:#c9d7e4;font-size:12px!important;font-weight:800} .release-choice select{width:100%;border:1px solid #2d455b;background:#102130;color:#dce8f4;border-radius:10px;padding:10px 34px 10px 11px;font-weight:700} .release-choice small{display:block;margin-top:8px;letter-spacing:0;line-height:1.45;color:#71879d;font-weight:600} + +.advanced-card{margin:0 0 18px;border:1px solid #29394d;background:#0b1723;border-radius:18px;overflow:hidden} +.advanced-card summary{display:flex;justify-content:space-between;align-items:center;gap:16px;cursor:pointer;padding:18px 20px;list-style:none} +.advanced-card summary::-webkit-details-marker{display:none} +.advanced-card summary span{display:grid;gap:4px} +.advanced-card summary strong{font-size:17px} +.advanced-card summary em{font-style:normal;color:#7890a7;font-size:11px;font-weight:800} +.advanced-card[open] summary{border-bottom:1px solid #213449} +.advanced-intro{display:flex;justify-content:space-between;gap:18px;padding:16px 20px 0} +.advanced-intro p{margin:0;color:#8fa3b7;font-size:12px;line-height:1.5} +.advanced-intro .prototype-note{max-width:500px;color:#6f8498} +.advanced-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;padding:16px 20px 20px} +.advanced-limit,.advanced-options,.task-classes,.advanced-safety{padding:15px;border-radius:14px;background:#0e1d2b;border:1px solid #20354a} +.advanced-limit>label{display:flex;justify-content:space-between;gap:12px;font-size:12px;font-weight:800} +.advanced-limit b{color:#66e3b4} +.advanced-options,.task-classes{display:grid;gap:10px} +.advanced-options label,.task-classes label{display:flex;align-items:flex-start;gap:9px;color:#b8c7d5;font-size:12px;line-height:1.35} +.advanced-options input,.task-classes input{width:18px;height:18px;margin:0;accent-color:#6de3b7;flex:0 0 auto} +.task-classes{margin:0}.task-classes legend{padding:0 6px;color:#8aa2bb;font-size:11px;font-weight:800;letter-spacing:.08em} +.advanced-safety strong{display:block;color:#a99aff;font-size:12px}.advanced-safety p{margin:8px 0 0;color:#7f93a7;font-size:12px;line-height:1.45} +.advanced-card summary:focus-visible{outline:2px solid #8d7bff;outline-offset:-3px} +@media(max-width:760px){.advanced-intro{flex-direction:column}.advanced-grid{grid-template-columns:1fr}.advanced-card summary{align-items:flex-start;flex-direction:column}.advanced-card summary em{margin-top:2px}} From b53925e3a3ad9db133b23d8e6551408283be0ba0 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:00:55 +0200 Subject: [PATCH 204/368] Test conservative advanced participation preferences --- tools/tests/participation_lab_state.test.mjs | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tools/tests/participation_lab_state.test.mjs b/tools/tests/participation_lab_state.test.mjs index 5b8e4eca..07c593c2 100644 --- a/tools/tests/participation_lab_state.test.mjs +++ b/tools/tests/participation_lab_state.test.mjs @@ -166,3 +166,52 @@ test("invalid storage release modes fail closed", () => { /invalid-storage-release-mode/ ); }); + +test("advanced resource preferences use conservative defaults", () => { + const state = createParticipationState(); + assert.equal(state.monthlyBandwidthLimitMb, 500); + assert.equal(state.wifiOnly, true); + assert.equal(state.allowBattery, false); + assert.equal(state.idleComputeOnly, true); + assert.equal(state.autoStart, false); + assert.equal(state.allowStorageTasks, true); + assert.equal(state.allowComputeTasks, true); +}); + +test("advanced preferences are user-changeable without auto-starting work", () => { + let state = createParticipationState({ storage: true, compute: true }); + state = transition(state, { type: "SET_BANDWIDTH_LIMIT", value: 1200 }); + state = transition(state, { type: "SET_WIFI_ONLY", value: false }); + state = transition(state, { type: "SET_ALLOW_BATTERY", value: true }); + state = transition(state, { type: "SET_IDLE_COMPUTE_ONLY", value: false }); + state = transition(state, { type: "SET_AUTO_START", value: true }); + assert.equal(state.monthlyBandwidthLimitMb, 1200); + assert.equal(state.wifiOnly, false); + assert.equal(state.allowBattery, true); + assert.equal(state.idleComputeOnly, false); + assert.equal(state.autoStart, true); + assert.equal(state.storageState, "off"); + assert.equal(state.processState, "off"); +}); + +test("disabling a task class blocks only that task lifecycle", () => { + let state = createParticipationState({ storage: true, compute: true }); + state = transition(state, { type: "SET_STORAGE_TASK_CLASS", value: false }); + assert.throws(() => transition(state, { type: "START_STORAGE" }), /storage-task-class-disabled/); + state = transition(state, { type: "START" }); + assert.equal(state.processState, "running"); + + state = transition(state, { type: "STOP" }); + state = transition(state, { type: "SET_STORAGE_TASK_CLASS", value: true }); + state = transition(state, { type: "SET_COMPUTE_TASK_CLASS", value: false }); + state = transition(state, { type: "START_STORAGE" }); + assert.equal(state.storageState, "running"); + assert.throws(() => transition(state, { type: "START" }), /compute-task-class-disabled/); +}); + +test("advanced resource limits fail closed when malformed", () => { + assert.throws(() => createParticipationState({ monthlyBandwidthLimitMb: 99 }), /bandwidth-limit-out-of-range/); + assert.throws(() => createParticipationState({ monthlyBandwidthLimitMb: 10001 }), /bandwidth-limit-out-of-range/); + assert.throws(() => createParticipationState({ wifiOnly: "yes" }), /invalid-wifiOnly/); + assert.throws(() => createParticipationState({ autoStart: 1 }), /invalid-autoStart/); +}); From e9358933f10b83cbc767794e3cf92dd295dbd569 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:01:04 +0200 Subject: [PATCH 205/368] Cover advanced participation controls in accessibility tests --- tools/tests/participation_lab_accessibility.test.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/tests/participation_lab_accessibility.test.mjs b/tools/tests/participation_lab_accessibility.test.mjs index 909cc131..a222c4ef 100644 --- a/tools/tests/participation_lab_accessibility.test.mjs +++ b/tools/tests/participation_lab_accessibility.test.mjs @@ -15,9 +15,17 @@ test("participation prototype keeps essential controls keyboard and screen-reade assert.match(html, /
    + {selected?.kind === "gallery" ? ( + + ) : null} +
    {copy.ecosystem} From e4449904a25e7422bef6e6dabead06c064e694ef Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:11:25 +0200 Subject: [PATCH 230/368] Style integrated Gameverse Creator Gallery ecosystem --- frontend/app/globals.css | 350 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 315e12b3..13f6da51 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -641,3 +641,353 @@ body { .gameverse-progressbar span, .gameverse-region { transition: none; } } + + +/* World-connected Creator Gallery ecosystem. */ +.gallery-page { + min-height: 100vh; + padding: 1rem; + background: + radial-gradient(circle at 16% 4%, rgba(146,95,168,.13), transparent 26%), + radial-gradient(circle at 86% 18%, rgba(0,141,54,.12), transparent 28%), + #f5f5f5; +} +.gallery-page-shell { + max-width: 1180px; + margin: 0 auto; +} +.gallery-page-topbar { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + margin-bottom: 1rem; +} +.gallery-page-topbar > details { + margin: 0 !important; + max-width: 22rem; +} +.gallery-back { + display: inline-flex; + min-height: 2.75rem; + align-items: center; + border: 1px solid rgba(80,91,169,.2); + border-radius: 999px; + background: white; + padding: .55rem .9rem; + color: var(--brand-secondary); + font-weight: 800; + text-decoration: none; +} +.gallery-age-entry { + max-width: 900px; + margin: 8vh auto 0; +} +.gallery-ecosystem { + border: 1px solid rgba(80,91,169,.15); + border-radius: 2rem; + background: rgba(255,255,255,.94); + padding: clamp(1rem, 3vw, 2rem); + box-shadow: 0 24px 60px rgba(29,43,54,.09); +} +.gallery-ecosystem-head { + display: grid; + grid-template-columns: minmax(0,1fr) minmax(15rem,22rem); + gap: 1rem; + align-items: start; +} +.gallery-kicker { + margin: 0 0 .25rem; + color: #925fa8 !important; + font-size: .67rem !important; + font-weight: 900; + letter-spacing: .12em; + text-transform: uppercase; +} +.gallery-ecosystem h2 { + margin: .1rem 0 .45rem; + color: var(--brand-secondary); + font-size: clamp(1.6rem, 4vw, 2.7rem); +} +.gallery-ecosystem-head > div:first-child > p:last-child { + color: #657786; + line-height: 1.55; +} +.gallery-mode { + display: grid; + gap: .2rem; + border: 1px solid rgba(146,95,168,.2); + border-radius: 1rem; + background: linear-gradient(145deg,#fbf7fc,#fff); + padding: .9rem; +} +.gallery-mode small { + color: #925fa8; +} +.gallery-mode strong { + color: var(--brand-secondary); +} +.gallery-mode span { + color: #647887; + font-size: .75rem; +} +.gallery-safety-note { + display: flex; + justify-content: space-between; + gap: 1rem; + margin: 1rem 0; + border-radius: 1rem; + background: #eef7f1; + padding: .8rem .95rem; + color: #52715d; + font-size: .76rem; +} +.gallery-safety-note strong { + color: #23673d; +} +.gallery-filter { + display: flex; + gap: .45rem; + overflow-x: auto; + padding: .25rem 0 .8rem; + scrollbar-width: thin; +} +.gallery-filter button { + flex: 0 0 auto; + min-height: 2.4rem; + border: 1px solid rgba(80,91,169,.2); + border-radius: 999px; + background: white; + padding: .45rem .75rem; + color: var(--brand-secondary); + font-size: .72rem; + font-weight: 800; +} +.gallery-filter button[aria-pressed="true"] { + border-color: #925fa8; + background: #925fa8; + color: white; +} +.gallery-grid { + display: grid; + grid-template-columns: repeat(4,minmax(0,1fr)); + gap: .8rem; +} +.gallery-asset-card { + overflow: hidden; + border: 1px solid rgba(80,91,169,.14); + border-radius: 1.15rem; + background: white; + box-shadow: 0 10px 28px rgba(29,43,54,.06); +} +.gallery-asset-art { + display: grid; + aspect-ratio: 4/3; + place-items: center; + background: + radial-gradient(circle at 28% 25%, rgba(255,255,255,.6), transparent 20%), + linear-gradient(145deg,#d8efdf,#d7d8f1); + color: var(--brand-secondary); +} +.gallery-asset-art[data-gallery-art="2"] { background: linear-gradient(145deg,#f6ddb0,#dbefd8); } +.gallery-asset-art[data-gallery-art="3"] { background: linear-gradient(145deg,#ddd8f0,#d2e9ee); } +.gallery-asset-art[data-gallery-art="4"] { background: linear-gradient(145deg,#d7eed9,#f2d6dc); } +.gallery-asset-art > span { + display: grid; + width: 3.2rem; + height: 3.2rem; + place-items: center; + border: 4px solid rgba(255,255,255,.82); + border-radius: 50%; + background: rgba(255,255,255,.55); + font-size: 1.45rem; + font-weight: 900; + box-shadow: 0 8px 24px rgba(29,43,54,.12); +} +.gallery-asset-body { + padding: .8rem; +} +.gallery-asset-body small { + color: #925fa8; + font-size: .6rem; + font-weight: 900; + letter-spacing: .08em; + text-transform: uppercase; +} +.gallery-asset-body h3 { + margin: .2rem 0 .25rem; + color: var(--brand-secondary); + font-size: 1rem; +} +.gallery-asset-body p { + margin: .2rem 0; + color: #73838f; + font-size: .72rem; +} +.gallery-tags { + display: flex; + flex-wrap: wrap; + gap: .25rem; + margin-top: .55rem; +} +.gallery-tags span, +.gallery-transfer, +.gameverse-fallback { + border-radius: 999px; +} +.gallery-tags span { + background: #f1f3f7; + padding: .18rem .4rem; + color: #63707b; + font-size: .58rem; +} +.gallery-transfer { + display: inline-block; + margin-top: .55rem !important; + background: #eef7f1; + padding: .2rem .45rem; + color: #23673d !important; + font-weight: 800; +} +.gallery-workshop { + display: grid; + grid-template-columns: minmax(0,1fr) minmax(16rem,.8fr) minmax(14rem,.7fr); + gap: 1rem; + margin-top: 1.2rem; + border-top: 1px solid rgba(80,91,169,.13); + padding-top: 1.2rem; +} +.gallery-workshop h3 { + margin: .25rem 0; + color: var(--brand-secondary); + font-size: 1.35rem; +} +.gallery-workshop-copy > p:not(.gallery-kicker), +.gallery-local-drafts > p { + color: #72828f; + font-size: .8rem; + line-height: 1.5; +} +.gallery-storage-card { + display: grid; + gap: .2rem; + margin-top: .7rem; + border-radius: .9rem; + background: #f6f2f8; + padding: .75rem; +} +.gallery-storage-card strong { + color: #925fa8; +} +.gallery-storage-card span { + color: #75818b; + font-size: .7rem; +} +.gallery-draft-form { + display: grid; + gap: .45rem; +} +.gallery-draft-form label { + color: var(--brand-secondary); + font-size: .72rem; + font-weight: 800; +} +.gallery-draft-form input, +.gallery-draft-form select { + min-height: 2.75rem; + border: 1px solid rgba(80,91,169,.25); + border-radius: .75rem; + background: white; + padding: .55rem .7rem; + color: #314457; +} +.gallery-draft-form button { + min-height: 2.75rem; + border: 0; + border-radius: .8rem; + background: var(--brand-primary); + color: white; + font-weight: 850; +} +.gallery-form-message { + min-height: 1rem; + margin: 0; + color: #23673d; + font-size: .7rem; +} +.gallery-local-drafts { + border-radius: 1rem; + background: #f7f8f9; + padding: .8rem; +} +.gallery-local-drafts > small { + color: #925fa8; + font-weight: 900; + text-transform: uppercase; + letter-spacing: .08em; +} +.gallery-local-drafts ul { + display: grid; + gap: .45rem; + margin: .55rem 0 0; + padding: 0; + list-style: none; +} +.gallery-local-drafts li { + display: flex; + justify-content: space-between; + gap: .5rem; + border-bottom: 1px solid #e4e8ea; + padding-bottom: .4rem; + color: #657786; + font-size: .7rem; +} +.gallery-local-drafts li strong { + color: var(--brand-secondary); +} +.gallery-ecosystem.is-compact { + padding: .85rem; + border-radius: 1.5rem; + box-shadow: 0 12px 30px rgba(29,43,54,.07); +} +.gallery-ecosystem.is-compact .gallery-ecosystem-head { + grid-template-columns: 1fr; +} +.gallery-ecosystem.is-compact .gallery-ecosystem-head h2 { + font-size: 1.25rem; +} +.gallery-ecosystem.is-compact .gallery-safety-note, +.gallery-ecosystem.is-compact .gallery-filter { + display: none; +} +.gallery-ecosystem.is-compact .gallery-grid { + grid-template-columns: 1fr 1fr; +} +.gallery-ecosystem.is-compact .gallery-asset-body { + padding: .55rem; +} +.gallery-ecosystem.is-compact .gallery-asset-body h3 { + font-size: .8rem; +} +.gallery-ecosystem.is-compact .gallery-asset-body p, +.gallery-ecosystem.is-compact .gallery-tags { + display: none; +} +.gallery-ecosystem.is-compact .gallery-mode { + padding: .65rem; +} +@media (max-width: 900px) { + .gallery-grid { grid-template-columns: repeat(2,minmax(0,1fr)); } + .gallery-workshop { grid-template-columns: 1fr 1fr; } + .gallery-local-drafts { grid-column: 1 / -1; } +} +@media (max-width: 640px) { + .gallery-page { padding: .55rem; } + .gallery-page-topbar, + .gallery-ecosystem-head, + .gallery-safety-note { flex-direction: column; } + .gallery-ecosystem-head { grid-template-columns: 1fr; } + .gallery-grid { grid-template-columns: 1fr; } + .gallery-workshop { grid-template-columns: 1fr; } + .gallery-local-drafts { grid-column: auto; } +} From 60192860acc079a0d459d01782efa66ebcdb8815 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:11:40 +0200 Subject: [PATCH 231/368] Test integrated Gallery and Gameverse ecosystem boundaries --- tools/tests/gallery_ecosystem_config.test.mjs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tools/tests/gallery_ecosystem_config.test.mjs diff --git a/tools/tests/gallery_ecosystem_config.test.mjs b/tools/tests/gallery_ecosystem_config.test.mjs new file mode 100644 index 00000000..fd83b601 --- /dev/null +++ b/tools/tests/gallery_ecosystem_config.test.mjs @@ -0,0 +1,85 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const catalogUrl = new URL("../../frontend/config/gallery-assets.json", import.meta.url); +const copyUrl = new URL("../../frontend/config/gallery-copy.json", import.meta.url); +const worldUrl = new URL("../../frontend/config/gameverse-world.json", import.meta.url); +const componentUrl = new URL("../../frontend/components/GameverseCreatorGallery.tsx", import.meta.url); +const gameverseUrl = new URL("../../frontend/components/GameverseWorld.tsx", import.meta.url); +const routeUrl = new URL("../../frontend/app/gallery/page.tsx", import.meta.url); + +async function json(url) { + return JSON.parse(await readFile(url, "utf8")); +} + +test("Gallery ecosystem is world-connected and non-pay-to-win", async () => { + const [catalog, world] = await Promise.all([json(catalogUrl), json(worldUrl)]); + assert.equal(catalog.principles.pay_to_win, false); + assert.equal(catalog.principles.wallet_required_to_browse, false); + assert.equal(catalog.principles.wallet_required_to_create_local_draft, false); + assert.equal(catalog.principles.real_marketplace_enabled, false); + assert.equal(catalog.principles.creator_owns_storage, true); + assert.equal(catalog.principles.world_and_gallery_share_asset_identity, true); + + const galleryRegion = world.regions.find(region => region.id === "creator-gallery"); + assert.ok(galleryRegion); + assert.equal(galleryRegion.destination, "/gallery"); +}); + +test("Gallery catalog covers planned modular creator families without real settlement", async () => { + const catalog = await json(catalogUrl); + for (const type of [ + "recipe", + "menu", + "food-photo", + "3d-model", + "avatar-item", + "digital-livestock-character", + "educational-media", + "provenance-story-media", + ]) { + assert.equal(catalog.asset_types.includes(type), true, `missing gallery type: ${type}`); + } + assert.ok(catalog.assets.length >= 8); + assert.equal(catalog.assets.some(asset => asset.transfer_mode === "game-native"), true); + assert.equal(catalog.assets.some(asset => asset.transfer_mode === "simulated-license"), true); +}); + +test("child Gallery assets never require simulated licensing", async () => { + const catalog = await json(catalogUrl); + const childAssets = catalog.assets.filter(asset => asset.ages.includes("child")); + assert.ok(childAssets.length > 0); + assert.equal(childAssets.every(asset => asset.transfer_mode === "game-native"), true); +}); + +test("all eleven display languages have Gallery shell copy", async () => { + const copy = await json(copyUrl); + const locales = ["en", "zh-Hans", "hi", "es", "ar", "fr", "bn", "pt", "id", "ur", "nl"]; + assert.deepEqual(Object.keys(copy).sort(), [...locales].sort()); + for (const locale of locales) { + for (const key of ["title","intro","browse","workshop","localDraftNote","marketplaceOff","storageNote"]) { + assert.equal(typeof copy[locale][key], "string", `${locale} missing ${key}`); + assert.ok(copy[locale][key].trim().length > 0); + } + } +}); + +test("Gallery UI keeps creation local and explicitly avoids upload mint sale actions", async () => { + const source = await readFile(componentUrl, "utf8"); + assert.match(source, /calorie\.gallery\.local-drafts\.v1/); + assert.match(source, /window\.localStorage/); + assert.match(source, /createLocalGalleryDraft/); + assert.doesNotMatch(source, /mintNFT|submitOffer|walletSign|uploadToProvider/); +}); + +test("Gameverse embeds the Gallery and direct Gallery route returns to the world", async () => { + const [worldSource, routeSource] = await Promise.all([ + readFile(gameverseUrl, "utf8"), + readFile(routeUrl, "utf8"), + ]); + assert.match(worldSource, /GameverseCreatorGallery/); + assert.match(worldSource, /selected\?\.kind === "gallery"/); + assert.match(routeSource, /GameverseCreatorGallery/); + assert.match(routeSource, /href="\/gameverse"/); +}); From ad5f3f288e643364901e6d0830536f93d96422ee Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:12:17 +0200 Subject: [PATCH 232/368] Surface local creator drafts back inside the Gameverse Gallery district --- frontend/components/GameverseCreatorGallery.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/components/GameverseCreatorGallery.tsx b/frontend/components/GameverseCreatorGallery.tsx index 11b531ed..96a140b2 100644 --- a/frontend/components/GameverseCreatorGallery.tsx +++ b/frontend/components/GameverseCreatorGallery.tsx @@ -141,6 +141,19 @@ export function GameverseCreatorGallery({ ))}
    + {compact && drafts.length > 0 ? ( +
    + {copy.localDraft} + {drafts.length} {copy.drafts.toLowerCase()} +
    + {drafts.slice(0, 3).map(draft => ( + {draft.title} + ))} +
    +

    {copy.localDraftNote}

    +
    + ) : null} + {!compact ? (
    From 4108bbe107bfab0b72df243784bedc3cd6f412a7 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:12:24 +0200 Subject: [PATCH 233/368] Style local Gallery drafts inside Gameverse preview --- frontend/app/globals.css | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 13f6da51..178afad6 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -991,3 +991,45 @@ body { .gallery-workshop { grid-template-columns: 1fr; } .gallery-local-drafts { grid-column: auto; } } + +.gallery-world-drafts { + margin-top: .75rem; + border: 1px dashed rgba(146,95,168,.28); + border-radius: .95rem; + background: #fcf9fd; + padding: .7rem; +} +.gallery-world-drafts > small { + display: block; + color: #925fa8; + font-size: .58rem; + font-weight: 900; + letter-spacing: .08em; + text-transform: uppercase; +} +.gallery-world-drafts > strong { + display: block; + margin-top: .15rem; + color: var(--brand-secondary); + font-size: .78rem; +} +.gallery-world-drafts > div { + display: flex; + flex-wrap: wrap; + gap: .3rem; + margin-top: .45rem; +} +.gallery-world-drafts > div span { + border-radius: 999px; + background: white; + padding: .2rem .45rem; + color: #6a5b78; + font-size: .62rem; + box-shadow: inset 0 0 0 1px rgba(146,95,168,.14); +} +.gallery-world-drafts p { + margin: .45rem 0 0 !important; + color: #7b7180 !important; + font-size: .62rem !important; + line-height: 1.35 !important; +} From 2f1206aa79d0451b2739b27f6e5bc6e84ad801c0 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:12:30 +0200 Subject: [PATCH 234/368] Test local Gallery drafts reappear inside Gameverse district --- tools/tests/gallery_ecosystem_config.test.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/tests/gallery_ecosystem_config.test.mjs b/tools/tests/gallery_ecosystem_config.test.mjs index fd83b601..83667da4 100644 --- a/tools/tests/gallery_ecosystem_config.test.mjs +++ b/tools/tests/gallery_ecosystem_config.test.mjs @@ -70,6 +70,8 @@ test("Gallery UI keeps creation local and explicitly avoids upload mint sale act assert.match(source, /calorie\.gallery\.local-drafts\.v1/); assert.match(source, /window\.localStorage/); assert.match(source, /createLocalGalleryDraft/); + assert.match(source, /gallery-world-drafts/); + assert.match(source, /compact && drafts\.length > 0/); assert.doesNotMatch(source, /mintNFT|submitOffer|walletSign|uploadToProvider/); }); From 78e00c1802b5902eeb3079f14d42fda5cc55022d Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:12:54 +0200 Subject: [PATCH 235/368] Bind local Gallery drafts to their age experience --- frontend/lib/galleryEcosystem.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/lib/galleryEcosystem.ts b/frontend/lib/galleryEcosystem.ts index fb4b8c1e..675b12c6 100644 --- a/frontend/lib/galleryEcosystem.ts +++ b/frontend/lib/galleryEcosystem.ts @@ -48,9 +48,12 @@ export type LocalGalleryDraft = { id: string; title: string; type: GalleryAssetType; + age_band: AgeBand; created_at: string; }; +export const GALLERY_DRAFTS_KEY = "calorie.gallery.local-drafts.v1"; + const assetTypes = new Set(galleryConfig.asset_types); export const galleryAssetTypes = galleryConfig.asset_types as GalleryAssetType[]; @@ -109,6 +112,7 @@ export function isGalleryAssetType(value: unknown): value is GalleryAssetType { export function createLocalGalleryDraft(input: { title: string; type: unknown; + ageBand: AgeBand; now?: Date; id?: string; }): LocalGalleryDraft { @@ -120,6 +124,7 @@ export function createLocalGalleryDraft(input: { id: input.id ?? `draft-${now.getTime()}`, title, type: input.type, + age_band: input.ageBand, created_at: now.toISOString(), }; } @@ -136,13 +141,15 @@ export function parseLocalGalleryDrafts(value: string | null): LocalGalleryDraft typeof draft.id !== "string" || typeof draft.title !== "string" || typeof draft.created_at !== "string" || - !isGalleryAssetType(draft.type) + !isGalleryAssetType(draft.type) || + (draft.age_band !== "child" && draft.age_band !== "teen" && draft.age_band !== "adult") ) return []; try { return [createLocalGalleryDraft({ id: draft.id, title: draft.title, type: draft.type, + ageBand: draft.age_band, now: new Date(draft.created_at), })]; } catch { From 66a81cd670a27b577300d8f61353bf0d90e4f0a0 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:13:03 +0200 Subject: [PATCH 236/368] Keep local Gallery drafts inside the matching age world --- .../components/GameverseCreatorGallery.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/frontend/components/GameverseCreatorGallery.tsx b/frontend/components/GameverseCreatorGallery.tsx index 96a140b2..a55c0dcc 100644 --- a/frontend/components/GameverseCreatorGallery.tsx +++ b/frontend/components/GameverseCreatorGallery.tsx @@ -5,6 +5,7 @@ import { useDisplayLanguage } from "@/components/DisplayLanguageProvider"; import type { AgeBand } from "@/lib/ageExperience"; import { createLocalGalleryDraft, + GALLERY_GALLERY_DRAFTS_KEY, galleryAssetsForAge, galleryAssetTypes, galleryCopy, @@ -14,8 +15,6 @@ import { type LocalGalleryDraft, } from "@/lib/galleryEcosystem"; -const DRAFTS_KEY = "calorie.gallery.local-drafts.v1"; - function assetTypeLabel(value: string) { return value.replaceAll("-", " ").replace(/\b\w/g, letter => letter.toUpperCase()); } @@ -39,7 +38,7 @@ export function GameverseCreatorGallery({ useEffect(() => { try { - setDrafts(parseLocalGalleryDrafts(window.localStorage.getItem(DRAFTS_KEY))); + setDrafts(parseLocalGalleryDrafts(window.localStorage.getItem(GALLERY_DRAFTS_KEY))); } catch { setDrafts([]); } @@ -49,17 +48,21 @@ export function GameverseCreatorGallery({ () => galleryAssetsForAge(ageBand, type), [ageBand, type] ); + const visibleDrafts = useMemo( + () => drafts.filter(draft => draft.age_band === ageBand), + [drafts, ageBand] + ); function saveDraft(event: FormEvent) { event.preventDefault(); try { - const draft = createLocalGalleryDraft({ title: draftTitle, type: draftType }); + const draft = createLocalGalleryDraft({ title: draftTitle, type: draftType, ageBand }); const next = [draft, ...drafts].slice(0, 50); setDrafts(next); setDraftTitle(""); setMessage(copy.saved); try { - window.localStorage.setItem(DRAFTS_KEY, JSON.stringify(next)); + window.localStorage.setItem(GALLERY_DRAFTS_KEY, JSON.stringify(next)); } catch { // The workshop stays usable in-memory when browser storage is unavailable. } @@ -141,12 +144,12 @@ export function GameverseCreatorGallery({ ))}
    - {compact && drafts.length > 0 ? ( + {compact && visibleDrafts.length > 0 ? (
    {copy.localDraft} - {drafts.length} {copy.drafts.toLowerCase()} + {visibleDrafts.length} {copy.drafts.toLowerCase()}
    - {drafts.slice(0, 3).map(draft => ( + {visibleDrafts.slice(0, 3).map(draft => ( {draft.title} ))}
    @@ -191,11 +194,11 @@ export function GameverseCreatorGallery({
    {copy.drafts} - {drafts.length === 0 ? ( + {visibleDrafts.length === 0 ? (

    {copy.localDraftNote}

    ) : (
      - {drafts.slice(0, 8).map(draft => ( + {visibleDrafts.slice(0, 8).map(draft => (
    • {draft.title} {assetTypeLabel(draft.type)} From 4a59c51139d992883a639ba313e1fbd27fe879f4 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:13:12 +0200 Subject: [PATCH 237/368] Render local Creator Gallery drafts as world display objects --- frontend/components/GameverseWorld.tsx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frontend/components/GameverseWorld.tsx b/frontend/components/GameverseWorld.tsx index 3a70908d..001c06ab 100644 --- a/frontend/components/GameverseWorld.tsx +++ b/frontend/components/GameverseWorld.tsx @@ -4,6 +4,7 @@ import Image from "next/image";\nimport { useEffect, useMemo, useState } from "r import { AgeExperienceControl, useAgeExperience } from "@/components/AgeExperienceControl"; import { DisplayLanguagePicker, useDisplayLanguage } from "@/components/DisplayLanguageProvider"; import { GameverseCreatorGallery } from "@/components/GameverseCreatorGallery"; +import { GALLERY_DRAFTS_KEY, parseLocalGalleryDrafts, type LocalGalleryDraft } from "@/lib/galleryEcosystem"; import { gameverseCopy, gameverseRegions, @@ -87,6 +88,7 @@ export function GameverseWorld() { const [currentRegionId, setCurrentRegionId] = useState(gameverseWorld.start.region_id); const [selectedRegionId, setSelectedRegionId] = useState(gameverseWorld.start.region_id); const [visited, setVisited] = useState([gameverseWorld.start.region_id]); + const [localGalleryDrafts, setLocalGalleryDrafts] = useState([]); const [loaded, setLoaded] = useState(false); useEffect(() => { @@ -103,6 +105,8 @@ export function GameverseWorld() { })); } + setLocalGalleryDrafts(parseLocalGalleryDrafts(window.localStorage.getItem(GALLERY_DRAFTS_KEY))); + const stored = JSON.parse(window.localStorage.getItem(PROGRESS_KEY) ?? "null") as SavedProgress | null; if (stored?.version === 1 && typeof stored.current_region_id === "string" && Array.isArray(stored.visited_region_ids)) { const safeVisited = uniqueVisited(stored.visited_region_ids); @@ -190,6 +194,24 @@ export function GameverseWorld() { From 926cae877866fcb64e0d3541ead070bba4bc0fa3 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:48:54 +0200 Subject: [PATCH 277/368] Style shared optional participation choice across Gameverse and Gallery --- frontend/app/globals.css | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 015c0f03..e7f72d47 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1198,3 +1198,160 @@ body { margin: .8rem 0; background: rgba(80,91,169,.12); } + + +/* Shared zero-install participation choice for Gameverse and Gallery. */ +.participation-choice { + margin: 1rem 0; + border: 1px solid rgba(80,91,169,.16); + border-radius: 1.5rem; + background: + radial-gradient(circle at 90% 5%, rgba(0,141,54,.08), transparent 26%), + rgba(255,255,255,.94); + padding: 1rem; + box-shadow: 0 14px 34px rgba(29,43,54,.07); +} +.participation-choice-head { + display: flex; + justify-content: space-between; + gap: .8rem; + align-items: flex-start; +} +.participation-choice-head small { + color: var(--brand-primary); + font-size: .62rem; + font-weight: 900; + letter-spacing: .1em; + text-transform: uppercase; +} +.participation-choice-head h3 { + margin: .15rem 0 .35rem; + color: var(--brand-secondary); + font-size: 1.15rem; +} +.participation-choice-head p, +.participation-choice-note, +.participation-choice-inactive p { + margin: 0; + color: #6e7f8b; + font-size: .75rem; + line-height: 1.45; +} +.participation-choice-head > span { + flex: 0 0 auto; + border-radius: 999px; + padding: .3rem .55rem; + font-size: .64rem; + font-weight: 850; +} +.participation-choice-head > span.is-off { + background: #eef0f2; + color: #667580; +} +.participation-choice-head > span.is-on { + background: #e8f5ed; + color: #23673d; +} +.participation-choice-modes { + display: grid; + grid-template-columns: 1fr 1fr; + gap: .5rem; + margin-top: .8rem; +} +.participation-choice-modes button { + min-height: 2.75rem; + border: 1px solid rgba(80,91,169,.2); + border-radius: .8rem; + background: white; + color: var(--brand-secondary); + font-weight: 850; +} +.participation-choice-modes button[aria-pressed="true"] { + border-color: var(--brand-primary); + background: var(--brand-primary); + color: white; +} +.participation-choice-settings { + display: grid; + gap: .7rem; + margin-top: .8rem; +} +.participation-choice-settings fieldset { + display: grid; + gap: .4rem; + margin: 0; + border: 1px solid rgba(80,91,169,.12); + border-radius: .85rem; + padding: .7rem; +} +.participation-choice-settings legend { + padding: 0 .2rem; + color: var(--brand-secondary); + font-size: .68rem; + font-weight: 850; +} +.participation-choice-settings fieldset label { + display: flex; + align-items: center; + gap: .45rem; + color: #516675; + font-size: .74rem; + font-weight: 750; +} +.participation-scope { + display: grid; + gap: .25rem; +} +.participation-scope > span { + color: #526776; + font-size: .68rem; + font-weight: 850; +} +.participation-scope select { + min-height: 2.55rem; + border: 1px solid rgba(80,91,169,.2); + border-radius: .7rem; + background: white; + padding: .45rem .6rem; + color: #314457; +} +.participation-choice-note { + border-radius: .75rem; + background: #f5f7f8; + padding: .6rem .7rem; +} +.participation-choice-message { + min-height: .9rem; + margin: .55rem 0 0; + color: #23673d; + font-size: .66rem; +} +.participation-choice-inactive { + margin-top: .8rem; + border-radius: .85rem; + background: #f4f6f7; + padding: .7rem; +} +.participation-choice-inactive strong { + display: block; + margin-bottom: .2rem; + color: var(--brand-secondary); +} +.participation-choice.is-compact { + margin: 0; + border-radius: 1.5rem; +} +.participation-choice.is-compact .participation-choice-head { + display: grid; +} +.participation-choice.is-compact .participation-choice-head > span { + justify-self: start; +} +@media (max-width: 640px) { + .participation-choice-head { + display: grid; + } + .participation-choice-head > span { + justify-self: start; + } +} From 222914dfa8590d48bc1265978890f57779d2cace Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:49:08 +0200 Subject: [PATCH 278/368] Test shared zero-install participation choice across Gameverse and Gallery --- .../participation_choice_bridge.test.mjs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tools/tests/participation_choice_bridge.test.mjs diff --git a/tools/tests/participation_choice_bridge.test.mjs b/tools/tests/participation_choice_bridge.test.mjs new file mode 100644 index 00000000..b1ba136a --- /dev/null +++ b/tools/tests/participation_choice_bridge.test.mjs @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const browserContractUrl = new URL("../../contracts/participation/v1/browser-runtime.json", import.meta.url); +const networkContractUrl = new URL("../../contracts/participation/v1/network.json", import.meta.url); +const worldUrl = new URL("../../frontend/config/gameverse-world.json", import.meta.url); +const galleryConfigUrl = new URL("../../frontend/config/gallery-assets.json", import.meta.url); +const preferenceUrl = new URL("../../frontend/lib/participationPreference.ts", import.meta.url); +const cardUrl = new URL("../../frontend/components/ParticipationChoiceCard.tsx", import.meta.url); +const gameverseUrl = new URL("../../frontend/components/GameverseWorld.tsx", import.meta.url); +const galleryPageUrl = new URL("../../frontend/app/gallery/page.tsx", import.meta.url); + +async function json(url) { + return JSON.parse(await readFile(url, "utf8")); +} + +test("one shared link is the entry model for participants and non-participants", async () => { + const [browser, network, world] = await Promise.all([ + json(browserContractUrl), + json(networkContractUrl), + json(worldUrl), + ]); + + assert.equal(browser.entry.install_required, false); + assert.equal(browser.entry.shared_entry_for_participants_and_nonparticipants, true); + assert.equal(browser.entry.separate_node_or_validator_url_required, false); + assert.equal(browser.entry.default_mode, "non-participant"); + assert.equal(network.official_product_boundary.single_shared_gameverse_entry_for_all_users, true); + assert.equal(network.official_product_boundary.separate_node_download_flow_required, false); + assert.equal(world.principles.single_link_for_everyone, true); + assert.equal(world.principles.participant_and_nonparticipant_share_same_world_entry, true); +}); + +test("browser participation offers temporary or remembered scopes without raw OS access", async () => { + const browser = await json(browserContractUrl); + assert.deepEqual(browser.consent_scopes.storage_access, [ + "session-temporary", + "persistent-until-revoked", + ]); + assert.deepEqual(browser.consent_scopes.compute_access, [ + "session-only", + "remember-preference-until-revoked", + ]); + assert.equal(browser.consent_scopes.storage_and_compute_independent, true); + assert.match(browser.runtime.processor_access_semantics, /Web Worker\/WASM/); + assert.match(browser.runtime.storage_access_semantics, /browser-managed site storage/); +}); + +test("Gallery and Gameverse render the same shared optional participation chooser", async () => { + const [gameverse, galleryPage, card, preference] = await Promise.all([ + readFile(gameverseUrl, "utf8"), + readFile(galleryPageUrl, "utf8"), + readFile(cardUrl, "utf8"), + readFile(preferenceUrl, "utf8"), + ]); + assert.match(gameverse, /ParticipationChoiceCard ageBand=\{ageBand\} compact/); + assert.match(galleryPage, /ParticipationChoiceCard ageBand=\{ageBand\}/); + assert.match(card, /PARTICIPATION_CHOICE_KEY/); + assert.match(preference, /calorie\.participation\.choice\.v1/); + assert.match(preference, /mode: "nonparticipant"/); +}); + +test("resource sharing remains adult-gated and defaults to no participation", async () => { + const [preference, card] = await Promise.all([ + readFile(preferenceUrl, "utf8"), + readFile(cardUrl, "utf8"), + ]); + assert.match(preference, /if \(ageBand !== "adult"\)/); + assert.match(preference, /return \{\.\.\.defaultParticipationChoice\}/); + assert.match(card, /mode === "participant" && ageBand !== "adult"/); + assert.match(card, /No resources|copy\.none/); +}); + +test("Gallery contract inherits the same zero-install optional participation model", async () => { + const gallery = await json(galleryConfigUrl); + assert.equal(gallery.principles.participation_optional, true); + assert.equal(gallery.principles.shared_participation_preference_with_gameverse, true); + assert.equal(gallery.principles.install_required_for_participation, false); + assert.equal(gallery.principles.default_mode, "nonparticipant"); +}); From 42ba846788d0aa71d07a5569d8f15256327c232c Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:57:14 +0200 Subject: [PATCH 279/368] Use Next links for internal Gameverse navigation --- frontend/components/GameverseWorld.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/components/GameverseWorld.tsx b/frontend/components/GameverseWorld.tsx index f6d352e5..06be6cc5 100644 --- a/frontend/components/GameverseWorld.tsx +++ b/frontend/components/GameverseWorld.tsx @@ -1,6 +1,7 @@ "use client"; import Image from "next/image"; +import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { AgeExperienceControl, useAgeExperience } from "@/components/AgeExperienceControl"; import { DisplayLanguagePicker, useDisplayLanguage } from "@/components/DisplayLanguageProvider"; @@ -175,10 +176,10 @@ export function GameverseWorld() {
      @@ -296,7 +297,7 @@ export function GameverseWorld() { {selected.id === currentRegionId ? "● " + copy.visited : copy.walk} {selected.destination && interactive && !mazeLocked ? ( - {copy.open} + {copy.open} ) : null}
      {selected.kind === "maze" ? ( From 301a231d72f7585cadc9c6480bfce0aeac34f7a9 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:57:18 +0200 Subject: [PATCH 280/368] Use Next link for Gallery return navigation --- frontend/app/gallery/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/app/gallery/page.tsx b/frontend/app/gallery/page.tsx index 717d677b..436dde08 100644 --- a/frontend/app/gallery/page.tsx +++ b/frontend/app/gallery/page.tsx @@ -1,5 +1,7 @@ "use client"; +import Link from "next/link"; + import { AgeExperienceControl, useAgeExperience } from "@/components/AgeExperienceControl"; import { DisplayLanguagePicker } from "@/components/DisplayLanguageProvider"; import { GameverseCreatorGallery } from "@/components/GameverseCreatorGallery"; @@ -14,7 +16,7 @@ export default function GalleryPage() {
      - ← Gameverse + ← Gameverse
      {!ageBand ? ( From d15f8383ce6d5abb28473270071b986ca16e1e2c Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:57:21 +0200 Subject: [PATCH 281/368] Use Next link for CalorieApp Gameverse portal --- frontend/components/GameversePortalLink.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/components/GameversePortalLink.tsx b/frontend/components/GameversePortalLink.tsx index 9aa20be4..bf031115 100644 --- a/frontend/components/GameversePortalLink.tsx +++ b/frontend/components/GameversePortalLink.tsx @@ -1,5 +1,7 @@ "use client"; +import Link from "next/link"; + import { useDisplayLanguage } from "@/components/DisplayLanguageProvider"; import { gameverseCopy } from "@/lib/gameverseWorld"; @@ -7,7 +9,7 @@ export function GameversePortalLink() { const display = useDisplayLanguage(); const { copy } = gameverseCopy(display.enabled ? display.locale : "en"); return ( - @@ -16,6 +18,6 @@ export function GameversePortalLink() { {copy.subtitle} - + ); } From 686bec13a2006367931688770ae19cf3a458361e Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:57:56 +0200 Subject: [PATCH 282/368] Define Gameverse as a freedom-first player-driven world --- docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md index 74c92e25..aec79caa 100644 --- a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md +++ b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md @@ -67,6 +67,40 @@ The intended arc is therefore: while remaining recognizably the **same Gameverse**. +## Freedom-first world design + +The Gameverse should feel highly open and self-directed: a world where people +can explore, create, cooperate, trade where permitted, contribute resources, or +simply play without being pushed into a prescribed lifestyle or system. + +This is a **player-autonomy design principle**, not a political campaign or party +position. In product terms it means: + +- free exploration and multiple valid routes instead of one mandatory questline; +- no required faction, ideology, creator role, node role, wallet, token, market + activity or social participation to enjoy the core world; +- participation in storage, compute, validation, creator systems, community + projects and future economies is opt-in and reversible; +- players may change their mind, pause a role, leave a group or return to ordinary + play without losing core access or unrelated progress; +- broad avatar, creator and self-expression choices are encouraged within clear + age, safety, rights, moderation and performance boundaries; +- creator ownership, provenance and user control over their own local data and + permissions are preferred over platform lock-in; +- future trade and marketplace systems must use voluntary exchange, transparent + terms and no pay-to-win pressure; +- social features should support communities and cooperation without forcing + public visibility, voice, direct messages or persistent presence; +- the world should tolerate many play styles: explorer, builder, creator, learner, + farmer, collector, helper, competitive player, observer or participant node; +- restrictions should exist only where needed for safety, minors, abuse + prevention, legal compliance, technical integrity or the rights of other + players. + +The practical rule is: **maximum meaningful player freedom, minimum necessary +coercion**. Freedom for one participant must not remove the safety, access, +privacy or agency of another. + ## Preserve the starting character while the world grows Scaling and continued development must not make the project feel like a different From 5cbd87b63adcc5dc9bf6ef64f7ba5e1c81d081e9 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:57:59 +0200 Subject: [PATCH 283/368] Encode freedom-first Gameverse principles --- frontend/config/gameverse-world.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/config/gameverse-world.json b/frontend/config/gameverse-world.json index ff60ddb3..773b6531 100644 --- a/frontend/config/gameverse-world.json +++ b/frontend/config/gameverse-world.json @@ -23,7 +23,18 @@ "participation_runtime_must_be_browser_accessible": true, "single_link_for_everyone": true, "participant_and_nonparticipant_share_same_world_entry": true, - "node_or_validator_install_required": false + "node_or_validator_install_required": false, + "freedom_first_world": true, + "multiple_valid_playstyles": true, + "mandatory_faction": false, + "mandatory_social_participation": false, + "mandatory_creator_role": false, + "voluntary_economy_only": true, + "pay_to_win": false, + "reversible_optional_roles": true, + "player_expression_with_safety_boundaries": true, + "privacy_and_permission_control": true, + "minimum_necessary_restrictions": true }, "start": { "x": 11, From e0010bf3f0c53605692931fc034ea50e101b65c5 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:58:04 +0200 Subject: [PATCH 284/368] Test freedom-first Gameverse invariants --- tools/tests/gameverse_world_config.test.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/tests/gameverse_world_config.test.mjs b/tools/tests/gameverse_world_config.test.mjs index 7b623ba1..59d8cce1 100644 --- a/tools/tests/gameverse_world_config.test.mjs +++ b/tools/tests/gameverse_world_config.test.mjs @@ -32,6 +32,21 @@ test("starter world combines the planned Calorie ecosystem surfaces", async () = assert.equal(world.principles.zero_participation_path, true); }); +test("Gameverse is freedom-first without forcing roles or pay-to-win", async () => { + const world = await json(worldUrl); + assert.equal(world.principles.freedom_first_world, true); + assert.equal(world.principles.multiple_valid_playstyles, true); + assert.equal(world.principles.mandatory_faction, false); + assert.equal(world.principles.mandatory_social_participation, false); + assert.equal(world.principles.mandatory_creator_role, false); + assert.equal(world.principles.voluntary_economy_only, true); + assert.equal(world.principles.pay_to_win, false); + assert.equal(world.principles.reversible_optional_roles, true); + assert.equal(world.principles.player_expression_with_safety_boundaries, true); + assert.equal(world.principles.privacy_and_permission_control, true); + assert.equal(world.principles.minimum_necessary_restrictions, true); +}); + test("starting character and original world are compatibility invariants", async () => { const world = await json(worldUrl); assert.equal(world.starter_character.starter_character_id, "starter-original"); From eaf9732515ad53a4bdf28afcd13157f5d23a5799 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:58:35 +0200 Subject: [PATCH 285/368] Make in-world participation controls compact until chosen --- .../components/ParticipationChoiceCard.tsx | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/components/ParticipationChoiceCard.tsx b/frontend/components/ParticipationChoiceCard.tsx index 7f465623..0722a874 100644 --- a/frontend/components/ParticipationChoiceCard.tsx +++ b/frontend/components/ParticipationChoiceCard.tsx @@ -35,6 +35,7 @@ export function ParticipationChoiceCard({ const [choice, setChoice] = useState({...defaultParticipationChoice}); const [ready, setReady] = useState(false); const [message, setMessage] = useState(""); + const [expanded, setExpanded] = useState(!compact); useEffect(() => { try { @@ -113,13 +114,26 @@ export function ParticipationChoiceCard({
      - {choice.mode === "participant" ? ( + {compact && choice.mode === "participant" && !expanded ? ( + + ) : null} + + {choice.mode === "participant" && (!compact || expanded) ? (
      {copy.roles} @@ -186,6 +200,16 @@ export function ParticipationChoiceCard({ ) : null}

      {copy.none}

      + + {compact ? ( + + ) : null}
      ) : null} From 88b844c876c2714d95ad54157b2ddb4356889767 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:58:39 +0200 Subject: [PATCH 286/368] Turn Participation Lodge into the full voluntary control location --- frontend/components/GameverseWorld.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/components/GameverseWorld.tsx b/frontend/components/GameverseWorld.tsx index 06be6cc5..173fdb3f 100644 --- a/frontend/components/GameverseWorld.tsx +++ b/frontend/components/GameverseWorld.tsx @@ -314,6 +314,10 @@ export function GameverseWorld() { ) : null} + {selected?.kind === "participation" && ageBand === "adult" ? ( + + ) : null} + {selected?.kind === "maze" && mazeReady ? ( Date: Sat, 19 Sep 2026 18:58:42 +0200 Subject: [PATCH 287/368] Keep Gallery participation choice available but unobtrusive --- frontend/app/gallery/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/gallery/page.tsx b/frontend/app/gallery/page.tsx index 436dde08..58f90568 100644 --- a/frontend/app/gallery/page.tsx +++ b/frontend/app/gallery/page.tsx @@ -26,7 +26,7 @@ export default function GalleryPage() { ) : ( <> - + )} From cca0c1bf21d36cf66921a32965cd10faf841b67b Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 18:58:52 +0200 Subject: [PATCH 288/368] Polish unobtrusive in-world participation controls --- frontend/app/globals.css | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index e7f72d47..a7d57ab6 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1355,3 +1355,24 @@ body { justify-self: start; } } + +.participation-choice-configure, +.participation-choice-collapse { + width: 100%; + min-height: 2.5rem; + margin-top: .55rem; + border: 1px solid rgba(80,91,169,.18); + border-radius: .75rem; + background: white; + color: var(--brand-secondary); + font-size: .7rem; + font-weight: 850; +} +.participation-choice-configure:hover, +.participation-choice-collapse:hover { + border-color: var(--brand-primary); +} +.participation-choice.is-compact .participation-choice-settings { + border-top: 1px solid rgba(80,91,169,.1); + padding-top: .65rem; +} From 184ececf1c5a6fe48ebe7362fb319036ce391eb9 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:01:16 +0200 Subject: [PATCH 289/368] Name the living metaverse CalorieVerse across all locales --- frontend/config/gameverse-copy.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/config/gameverse-copy.json b/frontend/config/gameverse-copy.json index 6f27142e..4fe14fef 100644 --- a/frontend/config/gameverse-copy.json +++ b/frontend/config/gameverse-copy.json @@ -1,6 +1,6 @@ { "en": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "Walk through the Calorie ecosystem as one connected world.", "world": "Starter World", "identity": "Starter character", @@ -46,7 +46,7 @@ "routeReady": "Core landmarks found. The maze route is open." }, "nl": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "Loop door het Calorie-ecosysteem als één verbonden wereld.", "world": "Startwereld", "identity": "Startkarakter", @@ -92,7 +92,7 @@ "routeReady": "De kernplekken zijn gevonden. De doolhofroute is open." }, "zh-Hans": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "把 Calorie 生态系统作为一个相连的世界来探索。", "world": "起始世界", "identity": "初始角色", @@ -128,7 +128,7 @@ "routeReady": "核心地标已找到。迷宫路线已开放。" }, "hi": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "Calorie इकोसिस्टम को एक जुड़ी हुई दुनिया की तरह खोजें।", "world": "आरंभिक दुनिया", "identity": "आरंभिक पात्र", @@ -164,7 +164,7 @@ "routeReady": "मुख्य स्थलचिह्न मिल गए। भूलभुलैया मार्ग खुला है।" }, "es": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "Recorre el ecosistema Calorie como un mundo conectado.", "world": "Mundo inicial", "identity": "Personaje inicial", @@ -200,7 +200,7 @@ "routeReady": "Se encontraron los lugares principales. La ruta del laberinto está abierta." }, "ar": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "استكشف منظومة Calorie كعالم واحد مترابط.", "world": "عالم البداية", "identity": "شخصية البداية", @@ -236,7 +236,7 @@ "routeReady": "تم العثور على المعالم الأساسية. مسار المتاهة مفتوح." }, "fr": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "Parcourez l’écosystème Calorie comme un monde connecté.", "world": "Monde de départ", "identity": "Personnage de départ", @@ -272,7 +272,7 @@ "routeReady": "Les repères principaux sont trouvés. La route du labyrinthe est ouverte." }, "bn": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "Calorie ইকোসিস্টেমকে একটি সংযুক্ত জগৎ হিসেবে ঘুরে দেখুন।", "world": "শুরুর জগৎ", "identity": "শুরুর চরিত্র", @@ -308,7 +308,7 @@ "routeReady": "মূল ল্যান্ডমার্ক পাওয়া গেছে। গোলকধাঁধার পথ খুলেছে।" }, "pt": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "Percorra o ecossistema Calorie como um mundo conectado.", "world": "Mundo inicial", "identity": "Personagem inicial", @@ -344,7 +344,7 @@ "routeReady": "Os pontos principais foram encontrados. A rota do labirinto está aberta." }, "id": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "Jelajahi ekosistem Calorie sebagai satu dunia yang terhubung.", "world": "Dunia awal", "identity": "Karakter awal", @@ -380,7 +380,7 @@ "routeReady": "Landmark utama ditemukan. Rute labirin terbuka." }, "ur": { - "title": "Calorie Gameverse", + "title": "CalorieVerse", "subtitle": "Calorie ایکوسسٹم کو ایک جڑی ہوئی دنیا کے طور پر دریافت کریں۔", "world": "ابتدائی دنیا", "identity": "ابتدائی کردار", From e1849727c0e86f1c9c7f0afadc568771341eca55 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:01:19 +0200 Subject: [PATCH 290/368] Use CalorieVerse as the public metaverse name --- frontend/app/gameverse/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/gameverse/page.tsx b/frontend/app/gameverse/page.tsx index e59e38d2..7fadbe7f 100644 --- a/frontend/app/gameverse/page.tsx +++ b/frontend/app/gameverse/page.tsx @@ -2,8 +2,8 @@ import type { Metadata } from "next"; import { GameverseWorld } from "@/components/GameverseWorld"; export const metadata: Metadata = { - title: "Calorie Gameverse", - description: "A first playable Calorie ecosystem world.", + title: "CalorieVerse", + description: "The living metaverse of the Calorie ecosystem.", }; export default function GameversePage() { From ce626ed5b03bb33c7acf0f37c898a3bc5c9d62ec Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:01:22 +0200 Subject: [PATCH 291/368] Bind CalorieVerse and CalorieApp to one Calorie ecosystem --- frontend/config/gameverse-world.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/config/gameverse-world.json b/frontend/config/gameverse-world.json index 773b6531..fa6837fd 100644 --- a/frontend/config/gameverse-world.json +++ b/frontend/config/gameverse-world.json @@ -34,7 +34,9 @@ "reversible_optional_roles": true, "player_expression_with_safety_boundaries": true, "privacy_and_permission_control": true, - "minimum_necessary_restrictions": true + "minimum_necessary_restrictions": true, + "metaverse_and_app_share_same_ecosystem": true, + "creator_gallery_is_native_ecosystem_surface": true }, "start": { "x": 11, @@ -205,5 +207,13 @@ "destination": null, "hidden_until": "maze-complete" } - ] + ], + "public_name": "CalorieVerse", + "ecosystem_id": "calorie-ecosystem", + "product_family": { + "ecosystem": "Calorie Ecosystem", + "app": "CalorieApp", + "metaverse": "CalorieVerse", + "creator_surface": "Creator Gallery" + } } From ae526c722c2ac681cfec1f60ec79010f466a6db0 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:01:24 +0200 Subject: [PATCH 292/368] Add Calorie ecosystem product-family naming contract --- contracts/ecosystem/v2/product-family.json | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 contracts/ecosystem/v2/product-family.json diff --git a/contracts/ecosystem/v2/product-family.json b/contracts/ecosystem/v2/product-family.json new file mode 100644 index 00000000..8e3bde79 --- /dev/null +++ b/contracts/ecosystem/v2/product-family.json @@ -0,0 +1,37 @@ +{ + "contract_id": "calorie.ecosystem.product-family", + "contract_version": "1.0.0", + "status": "product-naming-and-relationship-contract", + "umbrella": { + "id": "calorie-ecosystem", + "public_name": "Calorie Ecosystem", + "role": "shared ecosystem layer for products, identity-compatible services, creator surfaces and future voluntary network participation" + }, + "products": { + "calorie_app": { + "public_name": "CalorieApp", + "role": "food, nutrition and ecosystem application", + "runs_on_ecosystem": "calorie-ecosystem" + }, + "calorie_verse": { + "public_name": "CalorieVerse", + "internal_legacy_term": "Gameverse", + "role": "persistent live metaverse and open-world experience", + "runs_on_ecosystem": "calorie-ecosystem", + "route_compatibility": "/gameverse" + }, + "creator_gallery": { + "public_name": "Creator Gallery", + "role": "creator and asset surface available inside CalorieVerse and directly in the ecosystem", + "runs_on_ecosystem": "calorie-ecosystem", + "separate_ecosystem": false + } + }, + "invariants": { + "one_shared_ecosystem": true, + "app_and_metaverse_are_sibling_surfaces": true, + "gallery_is_not_a_competing_ecosystem": true, + "metaverse_name_is_stable": "CalorieVerse", + "internal_routes_and_ids_may_keep_gameverse_for_backward_compatibility": true + } +} From 3ddbe29c085a456a7503a67d288326e2687e97a8 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:01:35 +0200 Subject: [PATCH 293/368] Define CalorieVerse within the Calorie ecosystem product family --- docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md index aec79caa..d9f845de 100644 --- a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md +++ b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md @@ -35,6 +35,23 @@ production quality as features are added: The local participation simulator has no end-user visual interface. Its technical output does not establish the style of the future game or participation controls. +## Product family and public names + +The shared umbrella is the **Calorie Ecosystem**. + +- **CalorieApp** is the food/nutrition application running on that ecosystem. +- **CalorieVerse** is the public name of the persistent live metaverse/open world. +- **Creator Gallery** is a creator surface inside CalorieVerse and can also be + opened directly, but it is not a separate competing ecosystem. + +The existing internal term `Gameverse`, route `/gameverse`, file names and +stable IDs may remain where changing them would add migration risk. Public-facing +copy should use **CalorieVerse**. + +This gives one clear hierarchy: + +`Calorie Ecosystem -> CalorieApp + CalorieVerse -> Creator Gallery and other ecosystem surfaces`. + ## One continuously evolving live Gameverse The Gameverse is designed as **one persistent live world**, not as a franchise From 38761470160bd16ae9bf8bbfa9a4cd6a5d87bfe3 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:01:38 +0200 Subject: [PATCH 294/368] Test CalorieVerse and CalorieApp ecosystem naming --- tools/tests/gameverse_world_config.test.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/tests/gameverse_world_config.test.mjs b/tools/tests/gameverse_world_config.test.mjs index 59d8cce1..718f0884 100644 --- a/tools/tests/gameverse_world_config.test.mjs +++ b/tools/tests/gameverse_world_config.test.mjs @@ -47,6 +47,18 @@ test("Gameverse is freedom-first without forcing roles or pay-to-win", async () assert.equal(world.principles.minimum_necessary_restrictions, true); }); +test("public product family is Calorie Ecosystem with CalorieApp and CalorieVerse", async () => { + const world = await json(worldUrl); + assert.equal(world.public_name, "CalorieVerse"); + assert.equal(world.ecosystem_id, "calorie-ecosystem"); + assert.equal(world.product_family.ecosystem, "Calorie Ecosystem"); + assert.equal(world.product_family.app, "CalorieApp"); + assert.equal(world.product_family.metaverse, "CalorieVerse"); + assert.equal(world.product_family.creator_surface, "Creator Gallery"); + assert.equal(world.principles.metaverse_and_app_share_same_ecosystem, true); + assert.equal(world.principles.creator_gallery_is_native_ecosystem_surface, true); +}); + test("starting character and original world are compatibility invariants", async () => { const world = await json(worldUrl); assert.equal(world.starter_character.starter_character_id, "starter-original"); From fa59a541a73c94afa2144c9f4aa39f6a95a3fe86 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:09:02 +0200 Subject: [PATCH 295/368] Update Gallery tests for shared local draft key and age-filtered drafts --- tools/tests/gallery_ecosystem_config.test.mjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/tests/gallery_ecosystem_config.test.mjs b/tools/tests/gallery_ecosystem_config.test.mjs index 83667da4..284fd69d 100644 --- a/tools/tests/gallery_ecosystem_config.test.mjs +++ b/tools/tests/gallery_ecosystem_config.test.mjs @@ -6,6 +6,7 @@ const catalogUrl = new URL("../../frontend/config/gallery-assets.json", import.m const copyUrl = new URL("../../frontend/config/gallery-copy.json", import.meta.url); const worldUrl = new URL("../../frontend/config/gameverse-world.json", import.meta.url); const componentUrl = new URL("../../frontend/components/GameverseCreatorGallery.tsx", import.meta.url); +const helperUrl = new URL("../../frontend/lib/galleryEcosystem.ts", import.meta.url); const gameverseUrl = new URL("../../frontend/components/GameverseWorld.tsx", import.meta.url); const routeUrl = new URL("../../frontend/app/gallery/page.tsx", import.meta.url); @@ -66,12 +67,15 @@ test("all eleven display languages have Gallery shell copy", async () => { }); test("Gallery UI keeps creation local and explicitly avoids upload mint sale actions", async () => { - const source = await readFile(componentUrl, "utf8"); - assert.match(source, /calorie\.gallery\.local-drafts\.v1/); + const [source, helper] = await Promise.all([ + readFile(componentUrl, "utf8"), + readFile(helperUrl, "utf8"), + ]); + assert.match(helper, /calorie\.gallery\.local-drafts\.v1/); assert.match(source, /window\.localStorage/); assert.match(source, /createLocalGalleryDraft/); assert.match(source, /gallery-world-drafts/); - assert.match(source, /compact && drafts\.length > 0/); + assert.match(source, /compact && visibleDrafts\.length > 0/); assert.doesNotMatch(source, /mintNFT|submitOffer|walletSign|uploadToProvider/); }); From 76da0338b7085a191fde97eddb529adf21fc6bb5 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:09:45 +0200 Subject: [PATCH 296/368] Add optional interconnected CalorieVerse world interactions --- .../config/calorieverse-interactions.json | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 frontend/config/calorieverse-interactions.json diff --git a/frontend/config/calorieverse-interactions.json b/frontend/config/calorieverse-interactions.json new file mode 100644 index 00000000..83254c79 --- /dev/null +++ b/frontend/config/calorieverse-interactions.json @@ -0,0 +1,93 @@ +{ + "version": "0.1.0", + "status": "optional-local-first-world-interactions", + "principles": { + "optional": true, + "no_wallet_required": true, + "no_participation_required": true, + "no_pay_to_win": true, + "no_core_route_gate": true, + "local_progress_only": true, + "systems_overlap": true + }, + "interactions": [ + { + "id": "food-data-inspection", + "region_id": "food-data-grove", + "ages": [ + "child", + "teen", + "adult" + ], + "effect": "food-data-understood", + "supports": [ + "farm-market-link", + "helpbot-context" + ] + }, + { + "id": "farm-water-balance", + "region_id": "community-farm", + "ages": [ + "child", + "teen", + "adult" + ], + "effect": "farm-water-balanced", + "supports": [ + "farm-market-link", + "nature-state" + ] + }, + { + "id": "gallery-world-display", + "region_id": "creator-gallery", + "ages": [ + "child", + "teen", + "adult" + ], + "effect": "gallery-world-display-ready", + "supports": [ + "creator-presence" + ] + }, + { + "id": "helpbot-local-hint", + "region_id": "helpbot-garden", + "ages": [ + "child", + "teen", + "adult" + ], + "effect": "helpbot-context-learned", + "supports": [ + "route-awareness" + ] + } + ], + "derived_effects": [ + { + "id": "farm-market-link", + "requires": [ + "food-data-inspection", + "farm-water-balance" + ], + "description": "A small produce stand appears between food data and the community farm." + }, + { + "id": "creator-beacon", + "requires": [ + "gallery-world-display" + ], + "description": "A creator beacon appears near the Gallery district." + }, + { + "id": "route-whisper", + "requires": [ + "helpbot-local-hint" + ], + "description": "Subtle route guidance becomes available without forcing a quest order." + } + ] +} From feb93a09157c1227a66d26874e3103faa6d2c577 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:09:47 +0200 Subject: [PATCH 297/368] Add multilingual copy for CalorieVerse world interactions --- .../config/calorieverse-interaction-copy.json | 365 ++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 frontend/config/calorieverse-interaction-copy.json diff --git a/frontend/config/calorieverse-interaction-copy.json b/frontend/config/calorieverse-interaction-copy.json new file mode 100644 index 00000000..901e7953 --- /dev/null +++ b/frontend/config/calorieverse-interaction-copy.json @@ -0,0 +1,365 @@ +{ + "en": { + "title": "World interaction", + "optional": "Optional world activity · never required for the main route", + "completed": "Completed", + "progress": "world interactions", + "reset": "Replay locally", + "interactions": { + "food-data-inspection": { + "title": "Inspect a public food record", + "body": "Compare the kind of information a food record can carry and connect it to the world around you.", + "action": "Inspect record", + "done": "A food-data clue now links into the farm and market systems." + }, + "farm-water-balance": { + "title": "Balance water for the fields", + "body": "Adjust a small water route so crops and nature can share the same local resource.", + "action": "Balance water", + "done": "The farm water route is balanced and the landscape responds." + }, + "gallery-world-display": { + "title": "Curate a Gallery display", + "body": "Choose to place a simple creator marker in the world. It stays local in this first slice.", + "action": "Place display", + "done": "A creator beacon now appears in the Gallery district." + }, + "helpbot-local-hint": { + "title": "Ask for a local hint", + "body": "Ask Helpbot for one contextual clue about nearby places without turning the world into a guided tour.", + "action": "Ask Helpbot", + "done": "A subtle route whisper is now available; your route remains your choice." + } + } + }, + "nl": { + "title": "Wereldinteractie", + "optional": "Optionele wereldactiviteit · nooit verplicht voor de hoofdroute", + "completed": "Voltooid", + "progress": "wereldinteracties", + "reset": "Lokaal opnieuw spelen", + "interactions": { + "food-data-inspection": { + "title": "Bekijk een openbaar voedingsrecord", + "body": "Bekijk welke informatie een voedingsrecord kan bevatten en verbind die met de wereld om je heen.", + "action": "Record bekijken", + "done": "Een food-data-aanwijzing koppelt nu aan de boerderij- en marktsystemen." + }, + "farm-water-balance": { + "title": "Breng het water voor de velden in balans", + "body": "Pas een kleine waterroute aan zodat gewassen en natuur dezelfde lokale bron kunnen delen.", + "action": "Water balanceren", + "done": "De waterroute van de boerderij is in balans en het landschap reageert." + }, + "gallery-world-display": { + "title": "Stel een Gallery-display samen", + "body": "Plaats vrijwillig een eenvoudige creator-marker in de wereld. In deze eerste versie blijft die lokaal.", + "action": "Display plaatsen", + "done": "Er verschijnt nu een creator-beacon bij het Gallery-district." + }, + "helpbot-local-hint": { + "title": "Vraag om een lokale hint", + "body": "Vraag Helpbot om één contextuele aanwijzing over plekken in de buurt zonder de wereld in een rondleiding te veranderen.", + "action": "Vraag Helpbot", + "done": "Er is nu een subtiele routehint beschikbaar; de route blijft jouw keuze." + } + } + }, + "zh-Hans": { + "title": "世界互动", + "optional": "可选世界活动 · 主路线从不强制", + "completed": "已完成", + "progress": "世界互动", + "reset": "本地重玩", + "interactions": { + "food-data-inspection": { + "title": "查看公共食品记录", + "body": "了解食品记录能包含哪些信息,并把它与周围世界联系起来。", + "action": "查看记录", + "done": "食品数据线索现在与农场和市场系统相连。" + }, + "farm-water-balance": { + "title": "平衡田地用水", + "body": "调整一条小型水路,让作物和自然共享本地资源。", + "action": "平衡用水", + "done": "农场水路已平衡,景观也会响应。" + }, + "gallery-world-display": { + "title": "策划 Gallery 展示", + "body": "选择在世界中放置一个简单的创作者标记。此版本仅保存在本地。", + "action": "放置展示", + "done": "Gallery 区域现在出现创作者信标。" + }, + "helpbot-local-hint": { + "title": "询问本地提示", + "body": "向 Helpbot 获取附近地点的一条情境提示,而不是把世界变成导览。", + "action": "询问 Helpbot", + "done": "现在有一条细微路线提示;路线仍由你选择。" + } + } + }, + "hi": { + "title": "वर्ल्ड इंटरैक्शन", + "optional": "वैकल्पिक गतिविधि · मुख्य मार्ग के लिए कभी जरूरी नहीं", + "completed": "पूरा", + "progress": "वर्ल्ड इंटरैक्शन", + "reset": "लोकली फिर खेलें", + "interactions": { + "food-data-inspection": { + "title": "पब्लिक फूड रिकॉर्ड देखें", + "body": "देखें कि फूड रिकॉर्ड किस तरह की जानकारी रख सकता है और उसे आसपास की दुनिया से जोड़ें।", + "action": "रिकॉर्ड देखें", + "done": "फूड-डेटा संकेत अब फार्म और मार्केट सिस्टम से जुड़ता है।" + }, + "farm-water-balance": { + "title": "खेतों के पानी को संतुलित करें", + "body": "छोटे जल मार्ग को समायोजित करें ताकि फसल और प्रकृति स्थानीय संसाधन साझा कर सकें।", + "action": "पानी संतुलित करें", + "done": "फार्म का जल मार्ग संतुलित है और परिदृश्य प्रतिक्रिया देता है।" + }, + "gallery-world-display": { + "title": "Gallery डिस्प्ले तैयार करें", + "body": "दुनिया में एक सरल creator marker रखने का विकल्प चुनें। इस चरण में यह लोकल रहता है।", + "action": "डिस्प्ले रखें", + "done": "Gallery क्षेत्र में creator beacon दिखाई देता है।" + }, + "helpbot-local-hint": { + "title": "लोकल संकेत पूछें", + "body": "Helpbot से पास की जगहों के बारे में एक संकेत लें, बिना दुनिया को guided tour बनाए।", + "action": "Helpbot से पूछें", + "done": "हल्का route hint उपलब्ध है; रास्ता आपका चुनाव है।" + } + } + }, + "es": { + "title": "Interacción del mundo", + "optional": "Actividad opcional · nunca obligatoria para la ruta principal", + "completed": "Completada", + "progress": "interacciones del mundo", + "reset": "Repetir localmente", + "interactions": { + "food-data-inspection": { + "title": "Inspecciona un registro público de alimentos", + "body": "Compara la información que puede contener un registro y conéctala con el mundo.", + "action": "Inspeccionar registro", + "done": "Una pista de datos conecta ahora con la granja y el mercado." + }, + "farm-water-balance": { + "title": "Equilibra el agua de los campos", + "body": "Ajusta una pequeña ruta de agua para que cultivos y naturaleza compartan el recurso.", + "action": "Equilibrar agua", + "done": "La ruta de agua queda equilibrada y el paisaje responde." + }, + "gallery-world-display": { + "title": "Crea una muestra de Gallery", + "body": "Coloca voluntariamente un marcador de creador. En esta primera versión permanece local.", + "action": "Colocar muestra", + "done": "Aparece una baliza de creador en el distrito Gallery." + }, + "helpbot-local-hint": { + "title": "Pide una pista local", + "body": "Pide a Helpbot una pista contextual sin convertir el mundo en una visita guiada.", + "action": "Preguntar a Helpbot", + "done": "Hay una pista de ruta sutil; el camino sigue siendo tu elección." + } + } + }, + "ar": { + "title": "تفاعل العالم", + "optional": "نشاط اختياري · غير مطلوب للمسار الرئيسي", + "completed": "مكتمل", + "progress": "تفاعلات العالم", + "reset": "إعادة اللعب محليًا", + "interactions": { + "food-data-inspection": { + "title": "افحص سجل طعام عامًا", + "body": "تعرّف على المعلومات التي يمكن أن يحملها سجل الطعام واربطها بالعالم.", + "action": "فحص السجل", + "done": "أصبحت إشارة بيانات الطعام مرتبطة بالمزرعة والسوق." + }, + "farm-water-balance": { + "title": "وازن مياه الحقول", + "body": "عدّل مسارًا مائيًا صغيرًا لتشارك المحاصيل والطبيعة المورد المحلي.", + "action": "موازنة المياه", + "done": "تمت موازنة مسار مياه المزرعة واستجابت البيئة." + }, + "gallery-world-display": { + "title": "نسّق عرض Gallery", + "body": "اختر وضع علامة بسيطة للمبدع في العالم. تبقى محلية في هذه المرحلة.", + "action": "وضع العرض", + "done": "تظهر الآن منارة للمبدعين قرب منطقة Gallery." + }, + "helpbot-local-hint": { + "title": "اطلب تلميحًا محليًا", + "body": "اطلب من Helpbot تلميحًا سياقيًا واحدًا دون تحويل العالم إلى جولة موجهة.", + "action": "اسأل Helpbot", + "done": "أصبح تلميح مسار خفيف متاحًا؛ الطريق يبقى اختيارك." + } + } + }, + "fr": { + "title": "Interaction du monde", + "optional": "Activité facultative · jamais requise pour la route principale", + "completed": "Terminée", + "progress": "interactions du monde", + "reset": "Rejouer localement", + "interactions": { + "food-data-inspection": { + "title": "Inspecter une fiche alimentaire publique", + "body": "Découvrez les informations d’une fiche alimentaire et reliez-les au monde environnant.", + "action": "Inspecter", + "done": "Un indice de données relie maintenant ferme et marché." + }, + "farm-water-balance": { + "title": "Équilibrer l’eau des champs", + "body": "Ajustez un petit trajet d’eau afin que cultures et nature partagent la ressource.", + "action": "Équilibrer l’eau", + "done": "Le trajet d’eau est équilibré et le paysage réagit." + }, + "gallery-world-display": { + "title": "Composer une présentation Gallery", + "body": "Placez volontairement un repère créateur simple. Il reste local dans cette première version.", + "action": "Placer la présentation", + "done": "Une balise créateur apparaît près du quartier Gallery." + }, + "helpbot-local-hint": { + "title": "Demander un indice local", + "body": "Demandez à Helpbot un indice contextuel sans transformer le monde en visite guidée.", + "action": "Demander à Helpbot", + "done": "Un indice discret est disponible ; votre route reste votre choix." + } + } + }, + "bn": { + "title": "ওয়ার্ল্ড ইন্টারঅ্যাকশন", + "optional": "ঐচ্ছিক কার্যক্রম · মূল রুটের জন্য কখনো বাধ্যতামূলক নয়", + "completed": "সম্পন্ন", + "progress": "ওয়ার্ল্ড ইন্টারঅ্যাকশন", + "reset": "লোকালি আবার খেলুন", + "interactions": { + "food-data-inspection": { + "title": "পাবলিক ফুড রেকর্ড দেখুন", + "body": "ফুড রেকর্ডে কী তথ্য থাকতে পারে দেখুন এবং সেটিকে চারপাশের বিশ্বের সাথে যুক্ত করুন।", + "action": "রেকর্ড দেখুন", + "done": "ফুড-ডেটা ইঙ্গিত এখন ফার্ম ও মার্কেট সিস্টেমের সাথে যুক্ত।" + }, + "farm-water-balance": { + "title": "ক্ষেতের পানি ভারসাম্য করুন", + "body": "ছোট পানির পথ সামঞ্জস্য করুন যাতে ফসল ও প্রকৃতি একই স্থানীয় সম্পদ ভাগ করতে পারে।", + "action": "পানি ভারসাম্য করুন", + "done": "ফার্মের পানির পথ ভারসাম্যপূর্ণ এবং দৃশ্যপট সাড়া দেয়।" + }, + "gallery-world-display": { + "title": "Gallery ডিসপ্লে সাজান", + "body": "বিশ্বে একটি সহজ creator marker রাখুন। প্রথম সংস্করণে এটি লোকাল থাকে।", + "action": "ডিসপ্লে রাখুন", + "done": "Gallery এলাকায় creator beacon দেখা যায়।" + }, + "helpbot-local-hint": { + "title": "লোকাল হিন্ট নিন", + "body": "বিশ্বকে guided tour না বানিয়ে Helpbot থেকে কাছের জায়গা সম্পর্কে একটি হিন্ট নিন।", + "action": "Helpbot-কে জিজ্ঞেস করুন", + "done": "হালকা route hint পাওয়া যায়; রুট আপনার পছন্দ।" + } + } + }, + "pt": { + "title": "Interação do mundo", + "optional": "Atividade opcional · nunca obrigatória para a rota principal", + "completed": "Concluída", + "progress": "interações do mundo", + "reset": "Repetir localmente", + "interactions": { + "food-data-inspection": { + "title": "Inspecione um registo público de alimentos", + "body": "Veja que informação um registo pode conter e ligue-a ao mundo.", + "action": "Inspecionar", + "done": "Uma pista de dados liga agora a quinta e o mercado." + }, + "farm-water-balance": { + "title": "Equilibre a água dos campos", + "body": "Ajuste uma pequena rota de água para culturas e natureza partilharem o recurso.", + "action": "Equilibrar água", + "done": "A rota de água está equilibrada e a paisagem responde." + }, + "gallery-world-display": { + "title": "Organize uma mostra Gallery", + "body": "Coloque voluntariamente um marcador simples de criador. Nesta versão fica local.", + "action": "Colocar mostra", + "done": "Uma baliza de criador aparece no distrito Gallery." + }, + "helpbot-local-hint": { + "title": "Peça uma dica local", + "body": "Peça ao Helpbot uma dica contextual sem transformar o mundo numa visita guiada.", + "action": "Perguntar ao Helpbot", + "done": "Uma dica subtil está disponível; o percurso continua a ser sua escolha." + } + } + }, + "id": { + "title": "Interaksi dunia", + "optional": "Aktivitas opsional · tidak pernah wajib untuk rute utama", + "completed": "Selesai", + "progress": "interaksi dunia", + "reset": "Main ulang lokal", + "interactions": { + "food-data-inspection": { + "title": "Periksa catatan makanan publik", + "body": "Lihat informasi yang dapat dibawa catatan makanan dan hubungkan dengan dunia.", + "action": "Periksa catatan", + "done": "Petunjuk data makanan kini terhubung ke sistem farm dan market." + }, + "farm-water-balance": { + "title": "Seimbangkan air untuk ladang", + "body": "Atur jalur air kecil agar tanaman dan alam berbagi sumber daya lokal.", + "action": "Seimbangkan air", + "done": "Jalur air farm seimbang dan lanskap merespons." + }, + "gallery-world-display": { + "title": "Kurasi tampilan Gallery", + "body": "Pilih menempatkan marker kreator sederhana. Pada tahap ini tetap lokal.", + "action": "Tempatkan tampilan", + "done": "Beacon kreator muncul di distrik Gallery." + }, + "helpbot-local-hint": { + "title": "Minta petunjuk lokal", + "body": "Minta satu petunjuk kontekstual dari Helpbot tanpa mengubah dunia menjadi tur terpandu.", + "action": "Tanya Helpbot", + "done": "Petunjuk rute halus tersedia; rute tetap pilihanmu." + } + } + }, + "ur": { + "title": "دنیا کی تعامل", + "optional": "اختیاری سرگرمی · مرکزی راستے کے لیے کبھی لازمی نہیں", + "completed": "مکمل", + "progress": "ورلڈ انٹرایکشنز", + "reset": "لوکل دوبارہ کھیلیں", + "interactions": { + "food-data-inspection": { + "title": "پبلک فوڈ ریکارڈ دیکھیں", + "body": "دیکھیں فوڈ ریکارڈ میں کیا معلومات ہو سکتی ہیں اور انہیں دنیا سے جوڑیں۔", + "action": "ریکارڈ دیکھیں", + "done": "فوڈ ڈیٹا اشارہ اب فارم اور مارکیٹ سسٹم سے جڑتا ہے۔" + }, + "farm-water-balance": { + "title": "کھیتوں کے پانی کو متوازن کریں", + "body": "چھوٹا آبی راستہ ایڈجسٹ کریں تاکہ فصل اور فطرت مقامی وسیلہ بانٹ سکیں۔", + "action": "پانی متوازن کریں", + "done": "فارم کا پانی متوازن ہے اور منظر ردعمل دیتا ہے۔" + }, + "gallery-world-display": { + "title": "Gallery ڈسپلے ترتیب دیں", + "body": "دنیا میں ایک سادہ creator marker رکھنے کا انتخاب کریں۔ یہ فی الحال لوکل رہتا ہے۔", + "action": "ڈسپلے رکھیں", + "done": "Gallery ضلع میں creator beacon ظاہر ہوتا ہے۔" + }, + "helpbot-local-hint": { + "title": "لوکل ہنٹ پوچھیں", + "body": "دنیا کو guided tour بنائے بغیر Helpbot سے ایک سیاقی ہنٹ لیں۔", + "action": "Helpbot سے پوچھیں", + "done": "ہلکا route hint دستیاب ہے؛ راستہ آپ کی پسند رہتا ہے۔" + } + } + } +} From 3379c90b5af1ed1fd59d76ec7ddbca609fe14545 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:10:05 +0200 Subject: [PATCH 298/368] Add local-first CalorieVerse interaction state and world effects --- frontend/lib/calorieVerseInteractions.ts | 111 +++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 frontend/lib/calorieVerseInteractions.ts diff --git a/frontend/lib/calorieVerseInteractions.ts b/frontend/lib/calorieVerseInteractions.ts new file mode 100644 index 00000000..89f54260 --- /dev/null +++ b/frontend/lib/calorieVerseInteractions.ts @@ -0,0 +1,111 @@ +import interactionConfig from "@/config/calorieverse-interactions.json"; +import copyConfig from "@/config/calorieverse-interaction-copy.json"; +import type { AgeBand } from "@/lib/ageExperience"; +import { resolveLocale } from "@/lib/locales"; + +export const CALORIEVERSE_INTERACTION_KEY = "calorie.calorieverse.interactions.v1"; + +export type CalorieVerseInteraction = { + id: string; + region_id: string; + ages: AgeBand[]; + effect: string; + supports: string[]; +}; + +export type CalorieVerseInteractionProgress = { + version: 1; + completed_ids: string[]; +}; + +export type CalorieVerseInteractionCopy = { + title: string; + optional: string; + completed: string; + progress: string; + reset: string; + interactions: Record; +}; + +export type CalorieVerseWorldEffects = { + farmMarketLink: boolean; + creatorBeacon: boolean; + routeWhisper: boolean; +}; + +const interactions = interactionConfig.interactions as CalorieVerseInteraction[]; +const knownIds = new Set(interactions.map(item => item.id)); + +export function interactionCopy(locale?: string | null): CalorieVerseInteractionCopy { + const resolved = resolveLocale(locale); + const source = copyConfig as unknown as Record; + return source[resolved] ?? source.en; +} + +export function interactionForRegion( + regionId: string, + ageBand: AgeBand +): CalorieVerseInteraction | null { + return interactions.find(item => + item.region_id === regionId && item.ages.includes(ageBand) + ) ?? null; +} + +export function parseInteractionProgress(value: string | null): CalorieVerseInteractionProgress { + if (!value) return {version: 1, completed_ids: []}; + try { + const parsed = JSON.parse(value) as Partial; + if (parsed.version !== 1 || !Array.isArray(parsed.completed_ids)) { + return {version: 1, completed_ids: []}; + } + return { + version: 1, + completed_ids: [...new Set( + parsed.completed_ids.filter(id => typeof id === "string" && knownIds.has(id)) + )], + }; + } catch { + return {version: 1, completed_ids: []}; + } +} + +export function completeInteraction( + progress: CalorieVerseInteractionProgress, + interactionId: string +): CalorieVerseInteractionProgress { + if (!knownIds.has(interactionId)) return progress; + return { + version: 1, + completed_ids: [...new Set([...progress.completed_ids, interactionId])], + }; +} + +export function resetInteraction( + progress: CalorieVerseInteractionProgress, + interactionId: string +): CalorieVerseInteractionProgress { + return { + version: 1, + completed_ids: progress.completed_ids.filter(id => id !== interactionId), + }; +} + +export function deriveWorldEffects( + progress: CalorieVerseInteractionProgress +): CalorieVerseWorldEffects { + const completed = new Set(progress.completed_ids); + return { + farmMarketLink: + completed.has("food-data-inspection") && + completed.has("farm-water-balance"), + creatorBeacon: completed.has("gallery-world-display"), + routeWhisper: completed.has("helpbot-local-hint"), + }; +} + +export const calorieVerseInteractionConfig = interactionConfig; From 7a359be9b0b4fdc4e6eadd01660be2c046bd9efb Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:10:07 +0200 Subject: [PATCH 299/368] Build optional contextual CalorieVerse interaction card --- .../CalorieVerseInteractionCard.tsx | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 frontend/components/CalorieVerseInteractionCard.tsx diff --git a/frontend/components/CalorieVerseInteractionCard.tsx b/frontend/components/CalorieVerseInteractionCard.tsx new file mode 100644 index 00000000..bb05abac --- /dev/null +++ b/frontend/components/CalorieVerseInteractionCard.tsx @@ -0,0 +1,66 @@ +"use client"; + +import type { AgeBand } from "@/lib/ageExperience"; +import { + interactionCopy, + interactionForRegion, + type CalorieVerseInteractionProgress, +} from "@/lib/calorieVerseInteractions"; + +export function CalorieVerseInteractionCard({ + regionId, + ageBand, + locale, + progress, + onComplete, + onReset, +}: { + regionId: string; + ageBand: AgeBand; + locale: string; + progress: CalorieVerseInteractionProgress; + onComplete(interactionId: string): void; + onReset(interactionId: string): void; +}) { + const interaction = interactionForRegion(regionId, ageBand); + if (!interaction) return null; + + const copy = interactionCopy(locale); + const itemCopy = copy.interactions[interaction.id]; + if (!itemCopy) return null; + + const completed = progress.completed_ids.includes(interaction.id); + + return ( +
      +
      +
      + {copy.title} +

      {itemCopy.title}

      +
      + {progress.completed_ids.length} / 4 {copy.progress} +
      + +

      {completed ? itemCopy.done : itemCopy.body}

      +

      {copy.optional}

      + +
      + {completed ? ( + <> + ✓ {copy.completed} + + + ) : ( + + )} +
      +
      + ); +} From 2517a531f9cd480c438cbe354d67a4dd547a6dc0 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:10:24 +0200 Subject: [PATCH 300/368] Connect optional CalorieVerse interactions to persistent world effects --- frontend/components/GameverseWorld.tsx | 68 +++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/frontend/components/GameverseWorld.tsx b/frontend/components/GameverseWorld.tsx index 173fdb3f..ead52265 100644 --- a/frontend/components/GameverseWorld.tsx +++ b/frontend/components/GameverseWorld.tsx @@ -8,7 +8,16 @@ import { DisplayLanguagePicker, useDisplayLanguage } from "@/components/DisplayL import { GameverseCreatorGallery } from "@/components/GameverseCreatorGallery"; import { GameverseMazeRoute } from "@/components/GameverseMazeRoute"; import { ParticipationChoiceCard } from "@/components/ParticipationChoiceCard"; +import { CalorieVerseInteractionCard } from "@/components/CalorieVerseInteractionCard"; import { GALLERY_DRAFTS_KEY, parseLocalGalleryDrafts, type LocalGalleryDraft } from "@/lib/galleryEcosystem"; +import { + CALORIEVERSE_INTERACTION_KEY, + completeInteraction, + deriveWorldEffects, + parseInteractionProgress, + resetInteraction, + type CalorieVerseInteractionProgress, +} from "@/lib/calorieVerseInteractions"; import { coreRouteProgress, gameverseCopy, @@ -100,6 +109,7 @@ export function GameverseWorld() { const [visited, setVisited] = useState([gameverseWorld.start.region_id]); const [localGalleryDrafts, setLocalGalleryDrafts] = useState([]); const [mazeCompleteState, setMazeCompleteState] = useState(false); + const [interactionProgress, setInteractionProgress] = useState({version: 1, completed_ids: []}); const [loaded, setLoaded] = useState(false); useEffect(() => { @@ -117,6 +127,7 @@ export function GameverseWorld() { } setLocalGalleryDrafts(parseLocalGalleryDrafts(window.localStorage.getItem(GALLERY_DRAFTS_KEY))); + setInteractionProgress(parseInteractionProgress(window.localStorage.getItem(CALORIEVERSE_INTERACTION_KEY))); const stored = JSON.parse(window.localStorage.getItem(PROGRESS_KEY) ?? "null") as SavedProgress | null; if (stored?.version === 1 && typeof stored.current_region_id === "string" && Array.isArray(stored.visited_region_ids)) { @@ -150,6 +161,18 @@ export function GameverseWorld() { } }, [loaded, starterId, currentRegionId, visited, mazeCompleteState]); + useEffect(() => { + if (!loaded) return; + try { + window.localStorage.setItem( + CALORIEVERSE_INTERACTION_KEY, + JSON.stringify(interactionProgress) + ); + } catch { + // Optional interactions remain usable in-memory when storage is blocked. + } + }, [loaded, interactionProgress]); + const regions = useMemo( () => ageBand ? regionsForAge(ageBand).filter(region => region.id !== "misty-ridge" || mazeCompleteState) @@ -161,6 +184,7 @@ export function GameverseWorld() { const mazeReady = mazeUnlocked(visited); const coreProgress = coreRouteProgress(visited); const coreRouteIds = new Set(gameverseWorld.progression.maze_required_regions as string[]); + const worldEffects = deriveWorldEffects(interactionProgress); function walkTo(region: GameverseRegion) { if (!ageBand || !regionIsInteractive(region, ageBand)) return; @@ -170,6 +194,14 @@ export function GameverseWorld() { setVisited(previous => uniqueVisited([...previous, region.id])); } + function finishInteraction(interactionId: string) { + setInteractionProgress(previous => completeInteraction(previous, interactionId)); + } + + function replayInteraction(interactionId: string) { + setInteractionProgress(previous => resetInteraction(previous, interactionId)); + } + if (!ageResolved) return null; return ( @@ -202,7 +234,16 @@ export function GameverseWorld() { -
      +
    + {selected ? ( + + ) : null} + {selected?.kind === "gallery" ? ( ) : null} From 641f88f05ff6b5d18397945812531e43e687e60e Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:10:40 +0200 Subject: [PATCH 301/368] Style optional CalorieVerse interactions and world responses --- frontend/app/globals.css | 173 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index a7d57ab6..399f563f 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1376,3 +1376,176 @@ body { border-top: 1px solid rgba(80,91,169,.1); padding-top: .65rem; } + + +/* Optional CalorieVerse world interactions and derived world-state cues. */ +.calorieverse-interaction { + border: 1px solid rgba(80,91,169,.15); + border-radius: 1.35rem; + background: + radial-gradient(circle at 88% 10%, rgba(249,178,51,.1), transparent 24%), + rgba(255,255,255,.94); + padding: .9rem; + box-shadow: 0 12px 28px rgba(29,43,54,.06); +} +.calorieverse-interaction.is-complete { + border-color: rgba(0,141,54,.28); + background: + radial-gradient(circle at 88% 10%, rgba(0,141,54,.1), transparent 24%), + rgba(255,255,255,.96); +} +.calorieverse-interaction-head { + display: flex; + justify-content: space-between; + gap: .75rem; + align-items: flex-start; +} +.calorieverse-interaction-head small { + color: var(--brand-primary); + font-size: .58rem; + font-weight: 900; + letter-spacing: .09em; + text-transform: uppercase; +} +.calorieverse-interaction-head h3 { + margin: .18rem 0 .35rem; + color: var(--brand-secondary); + font-size: 1rem; +} +.calorieverse-interaction-head > span { + flex: 0 0 auto; + border-radius: 999px; + background: #f1f3f6; + padding: .25rem .45rem; + color: #627280; + font-size: .6rem; + font-weight: 800; +} +.calorieverse-interaction > p { + margin: .25rem 0; + color: #687b88; + font-size: .75rem; + line-height: 1.45; +} +.calorieverse-interaction-optional { + color: #8a7958 !important; + font-size: .66rem !important; +} +.calorieverse-interaction-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: .55rem; + margin-top: .7rem; +} +.calorieverse-interaction-actions button { + min-height: 2.5rem; + border: 1px solid rgba(80,91,169,.2); + border-radius: .75rem; + background: var(--brand-secondary); + padding: .45rem .75rem; + color: white; + font-size: .72rem; + font-weight: 850; +} +.calorieverse-interaction.is-complete .calorieverse-interaction-actions button { + background: white; + color: var(--brand-secondary); +} +.calorieverse-interaction-actions strong { + color: #23673d; + font-size: .72rem; +} +.gameverse-farm-market-link { + position: absolute; + z-index: 3; + left: 54%; + top: 46%; + display: flex; + width: 9rem; + align-items: center; + gap: .3rem; + transform: rotate(-7deg); + opacity: .86; +} +.gameverse-farm-market-link > span { + display: grid; + width: 1.3rem; + height: 1.3rem; + place-items: center; + border: 2px solid rgba(255,255,255,.9); + border-radius: 50%; + background: var(--brand-primary); + color: white; + font-size: .55rem; +} +.gameverse-farm-market-link > span:last-child { + border-radius: .3rem; + background: var(--brand-accent); + color: #67522c; +} +.gameverse-farm-market-link > i { + flex: 1; + height: 3px; + border-radius: 999px; + background: repeating-linear-gradient( + 90deg, + rgba(87,110,73,.85) 0 8px, + transparent 8px 13px + ); +} +.gameverse-creator-beacon { + position: absolute; + z-index: 6; + left: 72%; + top: 57%; + display: grid; + width: 2rem; + height: 2rem; + place-items: center; + transform: rotate(45deg); + border: 2px solid rgba(255,255,255,.9); + border-radius: .4rem; + background: #925fa8; + color: white; + font-weight: 900; + box-shadow: 0 0 0 5px rgba(146,95,168,.12), 0 10px 22px rgba(75,49,88,.22); +} +.gameverse-creator-beacon::after { + content: ""; + position: absolute; + width: .45rem; + height: 3.2rem; + top: 1.5rem; + border-radius: 999px; + background: linear-gradient(180deg,rgba(146,95,168,.35),transparent); +} +.gameverse-route-whisper { + position: absolute; + z-index: 2; + left: 80%; + top: 50%; + display: flex; + gap: .28rem; + transform: rotate(-10deg); +} +.gameverse-route-whisper i { + display: block; + width: 1.15rem; + height: .2rem; + border-radius: 999px; + background: rgba(80,91,169,.46); +} +.gameverse-route-whisper i:nth-child(2) { opacity: .68; } +.gameverse-route-whisper i:nth-child(3) { opacity: .42; } +@media (max-width: 640px) { + .calorieverse-interaction-head { + display: grid; + } + .calorieverse-interaction-head > span { + justify-self: start; + } + .gameverse-farm-market-link { + width: 6rem; + } +} From a605ba48a5d2ffdfacffc48fb05564d566f39b02 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:11:01 +0200 Subject: [PATCH 302/368] Test optional interconnected CalorieVerse world interactions --- .../tests/calorieverse_interactions.test.mjs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tools/tests/calorieverse_interactions.test.mjs diff --git a/tools/tests/calorieverse_interactions.test.mjs b/tools/tests/calorieverse_interactions.test.mjs new file mode 100644 index 00000000..23cefbf1 --- /dev/null +++ b/tools/tests/calorieverse_interactions.test.mjs @@ -0,0 +1,85 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const configUrl = new URL("../../frontend/config/calorieverse-interactions.json", import.meta.url); +const copyUrl = new URL("../../frontend/config/calorieverse-interaction-copy.json", import.meta.url); +const helperUrl = new URL("../../frontend/lib/calorieVerseInteractions.ts", import.meta.url); +const cardUrl = new URL("../../frontend/components/CalorieVerseInteractionCard.tsx", import.meta.url); +const worldUrl = new URL("../../frontend/components/GameverseWorld.tsx", import.meta.url); + +async function json(url) { + return JSON.parse(await readFile(url, "utf8")); +} + +test("CalorieVerse interactions stay optional and do not gate the core route", async () => { + const config = await json(configUrl); + assert.equal(config.principles.optional, true); + assert.equal(config.principles.no_wallet_required, true); + assert.equal(config.principles.no_participation_required, true); + assert.equal(config.principles.no_pay_to_win, true); + assert.equal(config.principles.no_core_route_gate, true); + assert.equal(config.principles.local_progress_only, true); +}); + +test("first interaction slice covers data, farm, Gallery and Helpbot systems", async () => { + const config = await json(configUrl); + const regions = new Set(config.interactions.map(item => item.region_id)); + for (const region of [ + "food-data-grove", + "community-farm", + "creator-gallery", + "helpbot-garden", + ]) { + assert.equal(regions.has(region), true, `missing optional interaction for ${region}`); + } +}); + +test("derived world effects require the intended system combinations", async () => { + const source = await readFile(helperUrl, "utf8"); + assert.match(source, /food-data-inspection/); + assert.match(source, /farm-water-balance/); + assert.match(source, /farmMarketLink/); + assert.match(source, /creatorBeacon/); + assert.match(source, /routeWhisper/); + assert.match(source, /calorie\.calorieverse\.interactions\.v1/); +}); + +test("all eleven display languages ship complete interaction copy", async () => { + const config = await json(configUrl); + const copy = await json(copyUrl); + const locales = ["en","zh-Hans","hi","es","ar","fr","bn","pt","id","ur","nl"]; + assert.deepEqual(Object.keys(copy).sort(), [...locales].sort()); + + for (const locale of locales) { + assert.equal(typeof copy[locale].title, "string"); + assert.equal(typeof copy[locale].optional, "string"); + for (const interaction of config.interactions) { + const item = copy[locale].interactions[interaction.id]; + assert.ok(item, `${locale} missing ${interaction.id}`); + for (const key of ["title","body","action","done"]) { + assert.equal(typeof item[key], "string", `${locale} ${interaction.id} missing ${key}`); + assert.ok(item[key].trim().length > 0); + } + } + } +}); + +test("interaction UI is contextual and can be replayed without becoming mandatory", async () => { + const source = await readFile(cardUrl, "utf8"); + assert.match(source, /interactionForRegion/); + assert.match(source, /onComplete\(interaction\.id\)/); + assert.match(source, /onReset\(interaction\.id\)/); + assert.match(source, /copy\.optional/); +}); + +test("CalorieVerse persists interaction progress and renders world-state cues", async () => { + const source = await readFile(worldUrl, "utf8"); + assert.match(source, /CALORIEVERSE_INTERACTION_KEY/); + assert.match(source, /parseInteractionProgress/); + assert.match(source, /deriveWorldEffects/); + assert.match(source, /CalorieVerseInteractionCard/); + assert.match(source, /gameverse-farm-market-link/); + assert.match(source, /gameverse-creator-beacon/); + assert.match(source, /gameverse-route-whisper/); +}); From a0f3cfac6acdb79f62cebea2e464aa0458bb208e Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:11:23 +0200 Subject: [PATCH 303/368] Document first optional interconnected CalorieVerse world systems --- docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md index d9f845de..1bda6f3e 100644 --- a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md +++ b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md @@ -324,6 +324,36 @@ This benchmark set is a design reference, not a feature checklist. Features are only adopted when they strengthen the Calorie world's food, nature, community, creator and ecosystem identity. +## First interconnected world-systems slice + +The first live CalorieVerse interactions are deliberately small, handcrafted and +optional. They prove that the world can respond to what a player does without +turning every activity into a mandatory quest. + +The initial four-system slice is: + +- **Food Data Grove** — inspect a public food-data record and create a world clue; +- **Community Farm** — balance a small water route between crops and nature; +- **Creator Gallery** — curate a local-first creator display that becomes visible + in the world; +- **Helpbot Garden** — request one contextual hint without switching the world + into a guided-tour mode. + +These interactions do **not** gate the core route, wallet access, participation, +the maze or ordinary exploration. They can be ignored, replayed and completed in +different orders. + +World systems may overlap. For example, completing both the food-data and farm +interactions can create a small visual link between farm and market activity, +while Gallery and Helpbot interactions add creator and route-awareness cues. +This is the seed of the larger systemic-world model: future events should reuse +shared food, water, creator, nature, community and data state instead of becoming +isolated minigames. + +The first implementation stores this interaction progress locally only. Server +sync or multiplayer world-state adoption requires a later versioned design and +must preserve offline/zero-login play. + ## Non-negotiable world structure 1. The player starts in a large open world with many explorable places, moving characters, nature, water, villages, markets, farms, cities, F&B businesses, CalorieApp activity, Helpbot guidance and global cultural variety. From d435037dfc678380e8002471deee328daae3ccab Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:17:41 +0200 Subject: [PATCH 304/368] Add open ecosystem IP stewardship and sustainable funding plan --- .../ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md diff --git a/docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md b/docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md new file mode 100644 index 00000000..bb4cf4c5 --- /dev/null +++ b/docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md @@ -0,0 +1,227 @@ +# Open ecosystem stewardship, IP and sustainable funding + +Status: design and legal-readiness baseline +Scope: Calorie Ecosystem, CalorieApp, CalorieVerse, Creator Gallery and future ecosystem extensions + +This document records the intended operating model. It is not a licence grant, +trade-mark filing, tax ruling, regulatory approval, or substitute for Dutch/EU +legal advice. + +## Core objective + +The project should support two goals at the same time: + +1. people must be able to keep building independent applications, worlds, + extensions and creator experiences around the ecosystem; and +2. the official products need a sustainable revenue model that can compensate + the people funding and building the project and, over time, fund an + increasingly independent ecosystem treasury/stewardship layer. + +Open participation does not require giving away the official brand, private +user data, production credentials, or every line of official product code. + +## Product and ecosystem boundary + +The current product family is: + +- **Calorie Ecosystem** — shared protocols, compatible data/capability + boundaries and the long-term ecosystem layer; +- **CalorieApp** — the official food/nutrition application; +- **CalorieVerse** — the persistent live metaverse/open world; +- **Creator Gallery** — a creator surface inside CalorieVerse and directly in + the ecosystem. + +Independent projects may build compatible experiences under their own +namespace and branding. Compatibility does not automatically make a project an +official Calorie product. + +## Open-building path + +The long-term target is to make the ecosystem interfaces genuinely reusable +without requiring permission from the operator for every independent project. + +The preferred structure is: + +- public, versioned protocols and schemas; +- stable extension points and capability contracts; +- a separately identified set of reference SDKs/examples suitable for a + recognised open-source licence after provenance review; +- portable creator assets and user-controlled data where legally possible; +- no requirement to use official hosted infrastructure merely to remain + protocol-compatible. + +The **existing repository is not automatically relicensed** by this document. +Its current repository licence remains controlling. Before any existing file is +moved into an open component, maintainers must verify authorship, contributor +rights, third-party code, asset provenance and patent/licence compatibility. + +For newly cleared ecosystem protocol/SDK components, Apache-2.0 is the current +licence candidate because it is permissive and includes an express contributor +patent grant. Activation requires a file-level/component-level licence manifest +and explicit rights-holder approval. Documentation/content may need a different +licence and must be reviewed separately. + +## Trade marks and official identity + +Open ecosystem code and protocols do not imply an unrestricted right to use the +official names, logos or trade dress in a way that suggests endorsement. + +The intended long-term model is: + +- independent implementations may describe truthful compatibility; +- official marks remain separately governed; +- a future independent stewardship body may receive ownership of, or a durable + licence to administer, ecosystem marks if the owner deliberately approves + that transfer/licence and the required registry recordals are completed; +- no ownership transfer is automatic merely because governance becomes more + distributed. + +A neutral trade-mark policy should eventually distinguish: +- nominative compatibility use; +- community/project naming; +- certification/conformance badges; +- official product branding; +- prohibited misleading or impersonating use. + +## Creator copyright and user-generated content + +Creator ownership should be the default for original Creator Gallery works. +The platform should obtain only the rights it needs through clear terms, for +example a non-exclusive licence to host, cache, transform for technical display, +moderate, and show the work in CalorieVerse. Any commercial sublicensing, +marketplace sale or broader promotional reuse should require separately stated +terms. + +Before public user-generated-content hosting is enabled, production terms must +cover at least: + +- contributor authority and third-party rights; +- prohibited uploads and infringement reporting; +- notice-and-action / takedown handling; +- moderation and appeal; +- licence scope, duration, termination and technical backup/cache survival; +- attribution and provenance where applicable; +- treatment of AI-assisted/generated material; +- minors and age-appropriate creator features. + +## Sustainable commercial model + +The ecosystem can remain open while official services are commercial. + +Candidate revenue streams include: + +- optional premium CalorieApp or Creator Gallery features; +- optional premium creator tools, storage, rendering and collaboration; +- managed hosting, API/data services and enterprise support; +- managed node/compute/storage services where lawful; +- optional cosmetic/world content that is not pay-to-win; +- marketplace or transaction service fees only after separate legal, + consumer, tax and regulatory review; +- sponsorships, partnerships, grants and paid implementation/support work. + +Core Gameverse/CalorieVerse entry, ordinary ecosystem exploration and ordinary +Creator Gallery browsing should not require a wallet, token purchase or node +participation. Premium features must remain additive rather than degrading the +free core. + +## Founder/developer recovery and ecosystem funding + +The commercial model explicitly recognises that the current builder/funder may +need to recover documented project costs and receive reasonable development and +operating compensation. That is compatible with a long-term decentralisation +goal when the rules are transparent. + +The intended funding path is phased: + +### Phase A — build and recovery + +Revenue may primarily fund: +- hosting and operating costs; +- legal, security and compliance work; +- documented development costs; +- reasonable founder/developer compensation; +- reserves needed to keep official services operating. + +### Phase B — shared growth + +As recurring revenue becomes dependable, an explicit share can be allocated to +an ecosystem treasury while founder/developer compensation continues under +documented terms. + +### Phase C — mature stewardship + +The long-term goal is that a substantial and potentially primary share of +ecosystem-derived surplus funds ecosystem maintenance, public infrastructure, +grants, moderation/security, creator tooling and approved community development. +The founder/operator may still be paid for real services, licences or defined +roles, but should not need indefinite unilateral treasury control for the +ecosystem to function. + +No fixed percentages are declared yet. Percentages, definitions of revenue vs +profit, cost-recovery caps, related-party payments, treasury custody and +governance authority require explicit recorded approval plus accounting/tax +review before activation. + +## Separation of money and governance + +Paying for premium services does not buy governance authority. Token ownership, +premium status, marketplace spend and node contribution do not automatically +grant control over official product state or private user data. + +A future treasury or stewardship body should have: +- a written purpose and spending mandate; +- transparent revenue categories and related-party disclosures; +- auditable accounts; +- conflict-of-interest rules; +- a documented transition/handover process; +- no ability to override privacy, security, legal or user-consent boundaries. + +A Dutch stichting/foundation or another independent legal vehicle is one +possible future stewardship structure, but no entity choice is made by this +document. Corporate, tax, governance and liability advice is required before a +transfer. + +## Paid digital services and EU launch gates + +Before charging EU consumers for subscriptions, premium digital tools, virtual +items or marketplace services, the launch checklist must address: + +- consumer information, conformity/remedy and cancellation rules for digital + content/services; +- VAT place-of-supply, invoicing and OSS obligations where applicable; +- privacy/GDPR and data-minimisation; +- Digital Services Act duties if the service hosts/intermediates creator + content or operates marketplace/platform functions; +- age and parental-consent design where minors are served; +- refund, chargeback and fraud handling; +- clear distinction between game/premium purchases and regulated financial or + crypto-asset services. + +Any CAL/XRP payment rail, token reward, exchange, custody, transfer execution or +other crypto-asset service remains behind the separate MiCA/regulatory review +boundary already recorded in the repository. + +## Evidence and launch checklist + +Before the public commercial launch of CalorieVerse/Creator Gallery: + +1. perform an exact EUIPO/TMview/BOIP clearance search for the final public + names and relevant goods/services; +2. do not claim CalorieVerse is registered unless a registration actually + exists; do not use the registered symbol for an unregistered mark; +3. retain the official register extracts/certificates for existing registered + marks; +4. complete asset and contributor provenance for all shipped visual/audio/3D + content; +5. create the open-component licence manifest before claiming a component is + open source; +6. publish production Terms, Privacy, creator/content rules and IP-reporting + process; +7. document paid-product VAT/consumer-law handling; +8. perform the separate crypto/MiCA review before activating real-token rails; +9. record any future transfer/licence of marks or other IP to a stewardship + entity with the appropriate signed instruments and registry recordals. + +The engineering repository can prepare these boundaries, but filings, +assignments, tax positions and final consumer/legal terms require the relevant +rights holder and qualified advisers. From eea0db78a76a4aed7163bdd5791b4b746134a57b Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:17:44 +0200 Subject: [PATCH 305/368] Add machine-readable open stewardship and funding boundary --- .../v2/open-stewardship-and-funding.json | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 contracts/ecosystem/v2/open-stewardship-and-funding.json diff --git a/contracts/ecosystem/v2/open-stewardship-and-funding.json b/contracts/ecosystem/v2/open-stewardship-and-funding.json new file mode 100644 index 00000000..b9dbffe4 --- /dev/null +++ b/contracts/ecosystem/v2/open-stewardship-and-funding.json @@ -0,0 +1,107 @@ +{ + "contract_id": "calorie.ecosystem.open-stewardship-and-funding", + "contract_version": "1.0.0", + "status": "design-boundary-no-automatic-licence-or-ip-transfer", + "ecosystem": "calorie-ecosystem", + "products": [ + "CalorieApp", + "CalorieVerse", + "Creator Gallery" + ], + "open_building": { + "independent_compatible_projects_allowed": true, + "official_preapproval_required_for_independent_project": false, + "own_namespace_and_non_misleading_branding_required": true, + "public_versioned_protocols_target": true, + "open_reference_sdk_target": true, + "existing_repository_automatically_relicensed": false, + "candidate_open_software_licence": "Apache-2.0", + "licence_activation_requires_provenance_review": true, + "licence_activation_requires_explicit_rights_holder_approval": true, + "file_or_component_manifest_required": true + }, + "intellectual_property": { + "official_trademarks_separate_from_code_licences": true, + "creator_original_work_default_owner": "creator", + "platform_creator_licence_target": "narrow-non-exclusive-host-display-moderate-technical-transform", + "commercial_sublicensing_requires_separate_terms": true, + "compatibility_does_not_equal_official_status": true, + "future_ip_stewardship_transfer_possible": true, + "future_transfer_automatic": false, + "registry_recordal_required_when_applicable": true + }, + "free_core": { + "calorieverse_entry_requires_payment": false, + "ordinary_gallery_browsing_requires_payment": false, + "wallet_required": false, + "node_participation_required": false, + "premium_features_must_be_additive": true, + "pay_to_win": false + }, + "commercial_sustainability": { + "premium_app_features_allowed": true, + "premium_creator_tools_allowed": true, + "managed_hosting_api_data_services_allowed": true, + "enterprise_support_allowed": true, + "cosmetic_world_content_allowed_if_non_pay_to_win": true, + "marketplace_fees_require_separate_review": true, + "founder_cost_recovery_recognised": true, + "founder_developer_compensation_recognised": true, + "ecosystem_treasury_long_term_funding_goal": true, + "fixed_revenue_percentages_set": false, + "percentages_require_explicit_approval": true, + "accounting_and_tax_review_required": true + }, + "funding_phases": [ + { + "id": "build-and-recovery", + "priority": [ + "operating-costs", + "legal-security-compliance", + "documented-development-costs", + "reasonable-founder-developer-compensation", + "service-reserve" + ] + }, + { + "id": "shared-growth", + "priority": [ + "continued-operations-and-compensation", + "recorded-ecosystem-treasury-allocation" + ] + }, + { + "id": "mature-stewardship", + "priority": [ + "ecosystem-infrastructure", + "security-and-moderation", + "creator-tooling", + "community-grants", + "approved-maintenance" + ], + "ecosystem_surplus_may_be_primary_beneficiary": true, + "indefinite_unilateral_founder_treasury_control_required": false + } + ], + "governance_separation": { + "premium_purchase_grants_governance": false, + "token_ownership_grants_automatic_governance": false, + "node_contribution_grants_automatic_governance": false, + "privacy_or_consent_may_be_overridden_by_governance": false + }, + "future_stewardship_entity": { + "legal_vehicle_selected": false, + "dutch_stichting_is_candidate_not_decision": true, + "legal_tax_governance_review_required": true, + "trademark_or_ip_transfer_requires_signed_instrument": true + }, + "eu_commercial_launch_gates": [ + "consumer-digital-content-and-service-terms", + "vat-and-oss-review", + "gdpr-privacy-review", + "dsa-platform-ugc-review-when-applicable", + "minor-age-safety-review", + "refund-chargeback-fraud-process", + "separate-mica-crypto-review-before-real-token-rails" + ] +} From 8c9510810be908eb59cdb6e35b116eca78fde112 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:18:02 +0200 Subject: [PATCH 306/368] Separate open ecosystem building from official repository contributions --- CONTRIBUTING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9812d8e1..aaa3593a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,22 @@ Do not submit code, documentation, designs, data, or other material unless you have the right to do so and the project owner has agreed in writing to review the contribution. +## Independent ecosystem projects + +Building an independent compatible project is different from contributing code +to the official repository. + +An independent project does not need prior operator approval merely to create +its own lawful implementation against public ecosystem interfaces, provided it +uses its own namespace/branding, respects applicable licences and third-party +rights, and does not claim official status or access private production state. + +The official repository still has its own inbound contribution requirements +below. Open compatibility does not silently relicense official source code. + +See `docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md` and +`contracts/ecosystem/v2/open-stewardship-and-funding.json`. + ## No implied acceptance or transfer - Opening an issue or pull request does not mean a contribution is accepted. From ac33d874254bc5798da14a27c7392e65eb33dc0b Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:18:05 +0200 Subject: [PATCH 307/368] Record CalorieVerse trademark status and future stewardship boundary --- TRADEMARKS.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/TRADEMARKS.md b/TRADEMARKS.md index 9c0afc86..b2d95cd2 100644 --- a/TRADEMARKS.md +++ b/TRADEMARKS.md @@ -32,3 +32,27 @@ Registration of a figurative mark does not by itself establish copyright authorship or assignment. The copyright chain for the finished CalorieToken logo artwork remains subject to separate provenance and assignment verification. Trade-mark rights and copyright in artwork are distinct rights. + + +## CalorieVerse and future ecosystem marks + +`CalorieVerse` is the current proposed public name for the project's live +metaverse/open-world product. This repository does **not** claim that +CalorieVerse is a registered trade mark. The name requires an exact clearance +search for the intended goods/services before any registration or registered +symbol is claimed. A general web search is not a freedom-to-operate opinion. + +Open ecosystem compatibility does not itself grant a right to use official +branding in a misleading way. Independent implementations may need a future +trade-mark policy for truthful compatibility references, community naming and +conformance badges. + +Long-term decentralisation does not require the current proprietor to abandon +marks. A future stewardship entity may receive ownership of, or a documented +licence to administer, marks only through an explicit legal instrument and any +required registry recordal. No transfer occurs automatically through code +governance, token ownership, community participation or publication of an open +protocol. + +Until a separate decision and legal instrument exist, the current ownership +records control. From 48fccab33ab8e5a0ba9b723a2de238ae370ae38d Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:18:08 +0200 Subject: [PATCH 308/368] Add CalorieVerse and open ecosystem IP clearance gates --- IP_CLEARANCE.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/IP_CLEARANCE.md b/IP_CLEARANCE.md index a3b84595..81aaa113 100644 --- a/IP_CLEARANCE.md +++ b/IP_CLEARANCE.md @@ -5,3 +5,23 @@ This repository publishes reviewed application expression; it does not claim own Contributions require documented provenance and compatible rights. Dependencies, datasets, images, names and marks retain their own licences and restrictions. Open Food Facts reuse must preserve the attribution and database-licence requirements described in `DATA_LICENSING.md`. A fresh legal and technical review is required before adding financial execution, wallet custody, regulated claims, bulk datasets, biometric or health profiling, new branding, restrictive SDKs or copied third-party material. This document is an engineering boundary, not legal advice or a freedom-to-operate opinion. + + +## CalorieVerse / open ecosystem clearance gates + +The proposed CalorieVerse name has not been declared cleared merely because no +obvious conflict appeared in a general web search. Before a commercial public +launch or filing, perform an exact trade-mark clearance search across the +relevant EUIPO/TMview/BOIP records, classes and similar names, and retain the +search evidence. + +The long-term intention to let third parties build on ecosystem protocols does +not automatically change this repository's current licence. Existing code, +assets and documentation require a rights/provenance audit before they can be +placed into an open-licence manifest. Newly created protocol/SDK components may +use a recognised open-source licence only after the relevant rights holder has +approved that licence and the component is explicitly identified. + +Creator-uploaded content needs its own production terms, rights warranty, +licence scope, notice/takedown process and moderation rules before public +hosting is enabled. From 90d05434a01312c410c0ecbb10746c7aaf0e6457 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:18:24 +0200 Subject: [PATCH 309/368] Enforce open stewardship and funding legal boundaries in CI --- tools/check_legal_boundaries.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tools/check_legal_boundaries.py b/tools/check_legal_boundaries.py index d2cbd196..ecd3f4c0 100644 --- a/tools/check_legal_boundaries.py +++ b/tools/check_legal_boundaries.py @@ -56,6 +56,8 @@ def main() -> None: "THIRD_PARTY_NOTICES.md", "ASSET_PROVENANCE.md", "IP_CLEARANCE.md", + "docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md", + "contracts/ecosystem/v2/open-stewardship-and-funding.json", "contracts/identity-bridge/v1/code-provenance.json", "contracts/identity-bridge/v1/evidence/xummlogin-public-1.3.0-similarity.json", "contracts/identity-bridge/v1/evidence/xummlogin-live-1.3.1-similarity.json", @@ -77,6 +79,26 @@ def main() -> None: require_text("DATA_LICENSING.md", ("Open Database License", "share-alike")) require_text("THIRD_PARTY_NOTICES.md", ("software bill of materials", "GPL-2.0-or-later")) require_text("IP_CLEARANCE.md", ("general ideas such as calorie tracking", "freedom-to-operate")) + require_text( + "docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md", + ("existing repository is not automatically relicensed", "Founder/developer recovery", "No fixed percentages are declared yet"), + ) + stewardship = require_json_object( + json.loads( + (ROOT / "contracts" / "ecosystem" / "v2" / "open-stewardship-and-funding.json").read_text(encoding="utf-8") + ), + "Open stewardship and funding contract", + ) + open_building = require_json_object(stewardship.get("open_building"), "open stewardship open_building") + if open_building.get("existing_repository_automatically_relicensed") is not False: + raise SystemExit("Open ecosystem policy must not silently relicense the existing repository") + commercial = require_json_object(stewardship.get("commercial_sustainability"), "open stewardship commercial_sustainability") + if commercial.get("founder_cost_recovery_recognised") is not True: + raise SystemExit("Sustainability model must retain founder cost recovery") + if commercial.get("ecosystem_treasury_long_term_funding_goal") is not True: + raise SystemExit("Sustainability model must retain long-term ecosystem funding") + if commercial.get("fixed_revenue_percentages_set") is not False: + raise SystemExit("Revenue percentages require separate explicit approval") require_text( "wordpress-plugins/calorieapp-identity-bridge/calorieapp-identity-bridge.php", ("License: GPL-2.0-or-later",), From 8e687b53e5d3ccfe3d470c0823a074ae19fa1e34 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:18:28 +0200 Subject: [PATCH 310/368] Add premium digital, UGC platform and crypto launch review gates --- REGULATORY.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/REGULATORY.md b/REGULATORY.md index 409ed7d6..a73464ba 100644 --- a/REGULATORY.md +++ b/REGULATORY.md @@ -8,3 +8,19 @@ investment services or financial advice. External identity may be used only for authentication context. Any future financial, token, reward, health-profiling or regulated feature requires separate legal, privacy, security and architecture review before public claims or implementation. This summary is not legal advice and does not claim certification or regulatory approval. + + +## CalorieVerse / Creator Gallery commercial boundary + +Optional premium digital features, creator tools or managed services may be +introduced as a sustainability model, but charging EU consumers requires a +separate consumer-contract, VAT/OSS, privacy and refund review before launch. + +If Creator Gallery or CalorieVerse begins hosting/intermediating public +user-generated content or marketplace activity, Digital Services Act and +platform/content-moderation obligations must be reviewed before production +activation. + +Real CAL/XRP payment rails, crypto-asset rewards, exchange/custody/transfer +services or other token-linked commercial functions remain outside the current +non-financial boundary and require separate MiCA/financial-regulatory review. From 57fc21b506944e9ca68a4668780520cdfd4836d1 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:18:31 +0200 Subject: [PATCH 311/368] Connect open ecosystem evolution to sustainable funding stewardship --- contracts/ecosystem/v2/evolution-guardrails.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/contracts/ecosystem/v2/evolution-guardrails.json b/contracts/ecosystem/v2/evolution-guardrails.json index 5fca155d..38f04a7f 100644 --- a/contracts/ecosystem/v2/evolution-guardrails.json +++ b/contracts/ecosystem/v2/evolution-guardrails.json @@ -29,7 +29,9 @@ "official_service_credentials_required": false, "must_use_own_namespace_and_non_official_branding": true, "official_rejection_may_delete_or_technically_block_independent_work": false, - "applicable_licences_privacy_law_and_third_party_rights_still_apply": true + "applicable_licences_privacy_law_and_third_party_rights_still_apply": true, + "open_ecosystem_protocol_goal": true, + "official_product_commercial_services_may_coexist": true }, "official_state_boundary": { "official_namespace": "calorieapp-official", @@ -89,7 +91,10 @@ "wealth_weighted_governance_required": false, "governance_may_override_privacy_consent_or_data_rights": false, "future_shared_governance_requires_separate_versioned_design": true, - "operator_continuity_and_succession_controls_still_required": true + "operator_continuity_and_succession_controls_still_required": true, + "long_term_ecosystem_treasury_funding_goal": true, + "founder_cost_recovery_and_compensation_may_precede_broader_treasury_allocation": true, + "revenue_percentage_changes_require_recorded_approval": true }, "future_dao_candidate": { "status": "v2-limited-tool-preparation-v3-full-dao-only", @@ -251,5 +256,6 @@ "official_system_may_refuse_unsafe_or_incompatible_federation": true, "refusal_to_federate_may_rewrite_or_destroy_remote_state": false, "goal": "Allow broad experimentation while keeping official trust, personal data and canonical provenance deliberately protected." - } + }, + "open_stewardship_and_funding_contract": "contracts/ecosystem/v2/open-stewardship-and-funding.json" } From 76a0c99be622030817cef6e9bc023a98f4095d2c Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:19:04 +0200 Subject: [PATCH 312/368] Add CalorieVerse and Creator Gallery legal-readiness evidence register --- docs/CALORIEVERSE_LEGAL_READINESS.md | 210 +++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 docs/CALORIEVERSE_LEGAL_READINESS.md diff --git a/docs/CALORIEVERSE_LEGAL_READINESS.md b/docs/CALORIEVERSE_LEGAL_READINESS.md new file mode 100644 index 00000000..4534af7a --- /dev/null +++ b/docs/CALORIEVERSE_LEGAL_READINESS.md @@ -0,0 +1,210 @@ +# CalorieVerse / Creator Gallery legal-readiness evidence register + +Status: engineering evidence register — not a legal opinion or launch clearance +Last design update: 2026-09-19 + +This file separates what the repository can currently evidence from what still +requires an external filing, signed instrument or professional review. + +## Trade marks + +### CalorieToken + +Repository evidence currently records two EU trade-mark registrations identified +by the project owner: + +- EUTM application/registration reference `019137415`; +- EUTM application/registration reference `019125433`. + +`TRADEMARKS.md` records the claimed filing/registration dates and identifies +Pieter Hendrikse as proprietor. The official EUIPO register extract/certificate +controls if any repository statement differs. + +Evidence status: **repository record exists; retain official register evidence +outside Git history as controlling proof**. + +### CalorieVerse + +CalorieVerse is the current proposed public metaverse name. + +Evidence status: **not claimed registered and not declared cleared**. + +Required before using a registered symbol or claiming exclusive registered +rights: + +1. exact EUIPO/TMview/BOIP search for identical and similar marks; +2. review of the actual goods/services classes needed for software, online + entertainment/metaverse, creator/platform services and any commercial + services; +3. opposition/conflict assessment by qualified trade-mark counsel where needed; +4. filing/registration evidence if protection is pursued. + +A general web search is not a freedom-to-operate search. + +## Copyright and repository code + +Current repository boundary: + +- original CalorieApp portions created by or lawfully assigned to ICTHendrikse + are recorded as copyright-controlled material; +- repository visibility does not create a public-domain or open-source grant; +- WordPress plugin components have their own GPL-2.0-or-later declarations; +- contributor rights remain separate unless a written licence/assignment applies; +- Git history is technical provenance, not conclusive legal authorship proof. + +Evidence status: **documented rights boundary, but not a universal chain-of-title +certificate for every historical file**. + +Open ecosystem direction: + +- independent projects may implement public interfaces under their own code and + branding; +- existing official source is not automatically relicensed; +- protocol/SDK components intended for open licensing need an explicit + component manifest, provenance review and rights-holder approval first; +- Apache-2.0 is the current software-licence candidate for cleared protocol/SDK + components, not an active blanket licence. + +## Logos, campaign art, 3D assets, audio and generated media + +`ASSET_PROVENANCE.md` requires source, permission, licence and attribution +checks before distribution. The repository explicitly does not make a blanket +exclusive-copyright claim for historical CalorieToken logo artwork where the +authorship/assignment chain remains incomplete. + +Before CalorieVerse public launch, each shipped asset pack should have an +asset manifest containing: + +- asset ID and file digest; +- creator/source; +- creation/commission date; +- written assignment or licence; +- AI-assisted/generated status and human review; +- third-party materials/models/fonts/music; +- attribution requirements; +- permitted commercial/metaverse/marketing uses; +- territory/duration restrictions if any. + +Evidence status: **policy exists; full CalorieVerse asset-pack manifest still +required**. + +## Food and public data rights + +`DATA_LICENSING.md` records the current source-specific approach: + +- Open Food Facts database reuse remains subject to ODbL/database-content + conditions, attribution and share-alike analysis; +- Open Food Facts images have separate CC BY-SA/third-party packaging concerns; +- USDA FoodData Central provenance remains separate and current reference data + is documented independently; +- private user food history is not public ecosystem data by default. + +Evidence status: **source rules documented; any expanded CalorieVerse/Creator +Gallery data redistribution needs source-by-source review**. + +## Creator Gallery user-generated content + +Production public uploads are not legally ready merely because the local creator +prototype works. + +Before open hosting, publish and implement: + +- creator Terms / content licence; +- rights warranty and third-party-content rules; +- notice-and-action / copyright reporting; +- moderation and appeal; +- repeat-abuse/infringement handling where appropriate; +- attribution/provenance rules; +- age/minor safeguards; +- privacy rules; +- commercial-sale/marketplace terms if monetisation is enabled. + +Creator copyright should remain with the creator by default; the platform should +receive only the clearly stated rights needed for hosting/display/technical +processing unless a separate commercial licence is agreed. + +Evidence status: **architecture defined; production creator terms and workflow +not yet complete**. + +## Premium services and revenue + +The intended commercial model permits optional premium app/creator features, +managed hosting/API/data services, non-pay-to-win cosmetic/world content and +future reviewed marketplace services. + +The revenue model also recognises: + +- documented project-cost recovery; +- reasonable founder/developer compensation; +- operating/legal/security reserves; +- a growing ecosystem-treasury allocation; +- a long-term objective for ecosystem infrastructure/community development to + become a substantial and potentially primary beneficiary of ecosystem surplus. + +No percentages are legally or technically activated yet. + +Before EU consumer charging is enabled, complete: + +- digital-content/service consumer terms and remedies; +- cancellation/withdrawal analysis for the exact product; +- transparent price/renewal/refund flows; +- VAT place-of-supply and OSS/accounting treatment; +- privacy/GDPR review; +- age/minor purchase controls; +- DSA/platform review if hosting or marketplace functions apply; +- chargeback/fraud process. + +Evidence status: **commercial architecture approved as direction; exact paid +product terms/accounting still required**. + +## Crypto and token boundary + +Real CAL/XRP/token-linked payment, rewards, custody, transfers, exchange or other +crypto-asset service functionality is not cleared by the premium-service model. + +`REGULATORY.md` keeps those features behind a separate MiCA/financial/legal, +privacy and security review. + +Evidence status: **separate review gate remains active**. + +## Decentralised stewardship + +The long-term ecosystem does not have to remain permanently under ICTHendrikse +or the current operator. + +A future independent stewardship body may receive specific responsibilities, +revenue, trade-mark rights or licences, but only through deliberate documented +steps. For EU trade marks, an ownership change must be implemented through the +appropriate transfer/recordal process; governance code alone does not change the +registered proprietor. + +Possible future vehicle: a Dutch stichting/foundation or another suitable +independent structure. This is a candidate only, not a current legal decision. + +Before handover: + +- define stewardship charter and protected invariants; +- define which IP/marks are transferred versus licensed; +- sign the relevant assignments/licences; +- complete registry recordals where applicable; +- establish treasury custody, accounting and tax treatment; +- define conflicts, audit, security and succession; +- retain privacy/security rights that governance may not override. + +Evidence status: **designed for later transfer; no automatic transfer has +occurred**. + +## Current blockers before a commercial public CalorieVerse launch + +1. formal CalorieVerse trade-mark clearance/filing decision; +2. final CalorieVerse/Creator Gallery asset-provenance manifest; +3. explicit open-component licence manifest before claiming an open-source SDK; +4. Creator Gallery production Terms/content licence/takedown workflow; +5. premium-service consumer/VAT/accounting terms for the exact offers; +6. DSA review once the actual UGC/platform model is fixed; +7. separate MiCA review before real-token commercial rails; +8. external legal review of the final launch package. + +The repository can make these items auditable and fail-closed, but it cannot +replace signed legal instruments, official registers, tax filings or qualified +professional advice. From 1b6c4af9f3890adb435d075c4aa883c8dae04df1 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:19:30 +0200 Subject: [PATCH 313/368] Bind CalorieVerse open-building to sustainable founder and ecosystem funding --- docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md index 1bda6f3e..26175fe6 100644 --- a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md +++ b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md @@ -199,6 +199,39 @@ with no blackholing now. Issuer, amounts and any 100-billion ceiling remain undecided; see `CALT_TESTNET_SUPPLY_DISCUSSION.md`. This is design discussion, not authorization or implementation of on-chain issuance. +## Open ecosystem with sustainable founder and treasury funding + +CalorieVerse and Creator Gallery are intended to remain open to independent +builders and voluntary participants while the official products retain a +sustainable commercial model. + +The commercial principle is **free core, optional paid services**: + +- entering and exploring CalorieVerse remains free; +- ordinary Creator Gallery browsing and core creator participation stay free; +- optional premium CalorieApp/Creator Gallery tooling may be sold; +- official managed hosting, rendering, API/data and support may be sold; +- cosmetic/world content may be sold only where it does not create pay-to-win; +- marketplace/service fees remain a later separately reviewed capability. + +Revenue may first recover documented project investment and pay reasonable +development/operating compensation. As recurring revenue matures, an explicit +ecosystem-treasury allocation should grow. The long-term goal is for ecosystem +infrastructure, security, creator tooling and community development to become a +substantial and potentially primary beneficiary of ecosystem surplus. + +No revenue percentage, treasury controller or legal stewardship entity is +hard-coded yet. Those decisions require recorded approval, accounting/tax review +and the future governance/handover process. + +The ecosystem's open-building path and the official products' commercial rights +are deliberately separate. Independent builders can create compatible projects +under their own code/branding, while official marks, hosted services and +commercial product presentation remain governed separately. + +See `docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md` and +`contracts/ecosystem/v2/open-stewardship-and-funding.json`. + ## Long-term ecosystem revenue model The user clarified that the goal is a revenue model that can fund CalEco From b948ecfef3662fe449b3dbe30bce6e9f7993f965 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:26:37 +0200 Subject: [PATCH 314/368] Rename the creator surface to CalorieStudio --- frontend/config/gallery-copy.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/frontend/config/gallery-copy.json b/frontend/config/gallery-copy.json index d25af2bf..3afbfbb0 100644 --- a/frontend/config/gallery-copy.json +++ b/frontend/config/gallery-copy.json @@ -1,7 +1,7 @@ { "en": { - "title": "Creator Gallery", - "intro": "A world-connected gallery for recipes, food media, 3D objects, avatar items and future Calorie creator work.", + "title": "CalorieStudio", + "intro": "A world-connected studio for recipes, food media, 3D objects, avatar items and future Calorie creator work.", "browse": "Browse world assets", "workshop": "Creator workshop", "localDraft": "Local draft", @@ -26,8 +26,8 @@ "drafts": "Local drafts" }, "nl": { - "title": "Creator Gallery", - "intro": "Een met de wereld verbonden gallery voor recepten, foodmedia, 3D-objecten, avatar-items en toekomstig Calorie-creatorwerk.", + "title": "CalorieStudio", + "intro": "Een met de wereld verbonden studio voor recepten, foodmedia, 3D-objecten, avatar-items en toekomstig Calorie-creatorwerk.", "browse": "Bekijk wereldassets", "workshop": "Creator-workshop", "localDraft": "Lokaal concept", @@ -52,7 +52,7 @@ "drafts": "Lokale concepten" }, "zh-Hans": { - "title": "创作者画廊", + "title": "CalorieStudio", "intro": "与世界相连的画廊,用于食谱、食品媒体、3D 对象、头像物品和未来的 Calorie 创作。", "browse": "浏览世界资产", "workshop": "创作者工作室", @@ -78,7 +78,7 @@ "drafts": "本地草稿" }, "hi": { - "title": "क्रिएटर गैलरी", + "title": "CalorieStudio", "intro": "रेसिपी, फूड मीडिया, 3D ऑब्जेक्ट, अवतार आइटम और भविष्य के Calorie क्रिएटर काम के लिए दुनिया से जुड़ी गैलरी।", "browse": "वर्ल्ड एसेट देखें", "workshop": "क्रिएटर वर्कशॉप", @@ -104,7 +104,7 @@ "drafts": "लोकल ड्राफ्ट" }, "es": { - "title": "Galería de creadores", + "title": "CalorieStudio", "intro": "Una galería conectada al mundo para recetas, medios de comida, objetos 3D, artículos de avatar y futuras creaciones de Calorie.", "browse": "Explorar activos del mundo", "workshop": "Taller de creadores", @@ -130,7 +130,7 @@ "drafts": "Borradores locales" }, "ar": { - "title": "معرض المبدعين", + "title": "CalorieStudio", "intro": "معرض متصل بالعالم للوصفات ووسائط الطعام والعناصر ثلاثية الأبعاد وعناصر الصورة الرمزية وأعمال Calorie الإبداعية المستقبلية.", "browse": "تصفح أصول العالم", "workshop": "ورشة المبدعين", @@ -156,7 +156,7 @@ "drafts": "مسودات محلية" }, "fr": { - "title": "Galerie des créateurs", + "title": "CalorieStudio", "intro": "Une galerie reliée au monde pour recettes, médias culinaires, objets 3D, éléments d’avatar et futures créations Calorie.", "browse": "Parcourir les créations du monde", "workshop": "Atelier créateur", @@ -182,7 +182,7 @@ "drafts": "Brouillons locaux" }, "bn": { - "title": "ক্রিয়েটর গ্যালারি", + "title": "CalorieStudio", "intro": "রেসিপি, ফুড মিডিয়া, 3D অবজেক্ট, অ্যাভাটার আইটেম ও ভবিষ্যৎ Calorie সৃষ্টির জন্য বিশ্ব-সংযুক্ত গ্যালারি।", "browse": "ওয়ার্ল্ড অ্যাসেট দেখুন", "workshop": "ক্রিয়েটর ওয়ার্কশপ", @@ -208,7 +208,7 @@ "drafts": "লোকাল ড্রাফট" }, "pt": { - "title": "Galeria de criadores", + "title": "CalorieStudio", "intro": "Uma galeria ligada ao mundo para receitas, mídia de comida, objetos 3D, itens de avatar e futuras criações Calorie.", "browse": "Explorar ativos do mundo", "workshop": "Oficina de criadores", @@ -234,7 +234,7 @@ "drafts": "Rascunhos locais" }, "id": { - "title": "Galeri Kreator", + "title": "CalorieStudio", "intro": "Galeri yang terhubung ke dunia untuk resep, media makanan, objek 3D, item avatar, dan karya kreator Calorie di masa depan.", "browse": "Jelajahi aset dunia", "workshop": "Lokakarya kreator", @@ -260,7 +260,7 @@ "drafts": "Draf lokal" }, "ur": { - "title": "کریئیٹر گیلری", + "title": "CalorieStudio", "intro": "ریسپی، فوڈ میڈیا، 3D آبجیکٹس، اوتار آئٹمز اور مستقبل کے Calorie تخلیقی کام کے لیے دنیا سے جڑی گیلری۔", "browse": "ورلڈ اثاثے دیکھیں", "workshop": "کریئیٹر ورکشاپ", From 5a0a81768e9fb33a81fb616bac174422142738bf Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:26:41 +0200 Subject: [PATCH 315/368] Use CalorieStudio as the creator district name in CalorieVerse --- frontend/config/gameverse-copy.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/frontend/config/gameverse-copy.json b/frontend/config/gameverse-copy.json index 4fe14fef..d3259e06 100644 --- a/frontend/config/gameverse-copy.json +++ b/frontend/config/gameverse-copy.json @@ -24,7 +24,7 @@ "helpbot-garden": "Helpbot Garden", "food-data-grove": "Food Data Grove", "community-farm": "Community Farm", - "creator-gallery": "Creator Gallery", + "creator-gallery": "CalorieStudio", "participation-lodge": "Participation Lodge", "maze-gate": "Maze Gate", "misty-ridge": "Misty Ridge" @@ -35,7 +35,7 @@ "helpbot-garden": "Ask for guidance and learn what different parts of the ecosystem do.", "food-data-grove": "Discover how public food data from OFF and USDA can support CalorieDB over time.", "community-farm": "Real food, farms, water, biodiversity and community projects stay central.", - "creator-gallery": "Recipes, art, food media, 3D objects and future creator work live here without pay-to-win.", + "creator-gallery": "Create and discover recipes, art, food media, 3D objects and future world content in CalorieStudio without pay-to-win.", "participation-lodge": "See voluntary storage and compute participation. Adult contribution controls stay opt-in.", "maze-gate": "A real route toward the still-hidden mountain area; no teleport shortcut is announced.", "misty-ridge": "A quiet approach above the maze. The mountains are closer, but their Calorie colours are still concealed by natural rock, cloud and mist." @@ -70,7 +70,7 @@ "helpbot-garden": "Helpbot-tuin", "food-data-grove": "Voedingsdatabos", "community-farm": "Community-boerderij", - "creator-gallery": "Creator Gallery", + "creator-gallery": "CalorieStudio", "participation-lodge": "Participatiehuis", "maze-gate": "Doolhofpoort", "misty-ridge": "Mistige kam" @@ -81,7 +81,7 @@ "helpbot-garden": "Vraag om begeleiding en leer wat de verschillende delen van het ecosysteem doen.", "food-data-grove": "Ontdek hoe openbare voedingsdata van OFF en USDA CalorieDB in de tijd kan ondersteunen.", "community-farm": "Echt eten, boerderijen, water, biodiversiteit en communityprojecten blijven centraal.", - "creator-gallery": "Recepten, kunst, foodmedia, 3D-objecten en toekomstig creatorwerk zonder pay-to-win.", + "creator-gallery": "Maak en ontdek recepten, kunst, foodmedia, 3D-objecten en toekomstige wereldcontent in CalorieStudio zonder pay-to-win.", "participation-lodge": "Bekijk vrijwillige opslag- en compute-deelname. Volwassenen houden volledige opt-in controle.", "maze-gate": "Een echte route naar het nog verborgen berggebied; geen aangekondigde teleport-shortcut.", "misty-ridge": "Een rustige route boven het doolhof. De bergen zijn dichterbij, maar hun Calorie-kleuren blijven nog verborgen door natuurlijke rotsen, wolken en mist." @@ -116,7 +116,7 @@ "helpbot-garden": "Helpbot 花园", "food-data-grove": "食品数据林", "community-farm": "社区农场", - "creator-gallery": "创作者画廊", + "creator-gallery": "CalorieStudio", "participation-lodge": "参与小屋", "maze-gate": "迷宫入口", "misty-ridge": "雾岭" @@ -152,7 +152,7 @@ "helpbot-garden": "Helpbot गार्डन", "food-data-grove": "फूड डेटा ग्रोव", "community-farm": "कम्युनिटी फार्म", - "creator-gallery": "क्रिएटर गैलरी", + "creator-gallery": "CalorieStudio", "participation-lodge": "पार्टिसिपेशन लॉज", "maze-gate": "भूलभुलैया द्वार", "misty-ridge": "धुंधली रिज" @@ -188,7 +188,7 @@ "helpbot-garden": "Jardín Helpbot", "food-data-grove": "Bosque de datos alimentarios", "community-farm": "Granja comunitaria", - "creator-gallery": "Galería de creadores", + "creator-gallery": "CalorieStudio", "participation-lodge": "Refugio de participación", "maze-gate": "Puerta del laberinto", "misty-ridge": "Cresta brumosa" @@ -224,7 +224,7 @@ "helpbot-garden": "حديقة Helpbot", "food-data-grove": "بستان بيانات الغذاء", "community-farm": "مزرعة المجتمع", - "creator-gallery": "معرض المبدعين", + "creator-gallery": "CalorieStudio", "participation-lodge": "نُزل المشاركة", "maze-gate": "بوابة المتاهة", "misty-ridge": "الحافة الضبابية" @@ -260,7 +260,7 @@ "helpbot-garden": "Jardin Helpbot", "food-data-grove": "Bosquet des données alimentaires", "community-farm": "Ferme communautaire", - "creator-gallery": "Galerie des créateurs", + "creator-gallery": "CalorieStudio", "participation-lodge": "Pavillon de participation", "maze-gate": "Porte du labyrinthe", "misty-ridge": "Crête brumeuse" @@ -296,7 +296,7 @@ "helpbot-garden": "Helpbot বাগান", "food-data-grove": "ফুড ডেটা গ্রোভ", "community-farm": "কমিউনিটি ফার্ম", - "creator-gallery": "ক্রিয়েটর গ্যালারি", + "creator-gallery": "CalorieStudio", "participation-lodge": "পার্টিসিপেশন লজ", "maze-gate": "গোলকধাঁধা দ্বার", "misty-ridge": "কুয়াশাচ্ছন্ন রিজ" @@ -332,7 +332,7 @@ "helpbot-garden": "Jardim Helpbot", "food-data-grove": "Bosque de dados alimentares", "community-farm": "Fazenda comunitária", - "creator-gallery": "Galeria de criadores", + "creator-gallery": "CalorieStudio", "participation-lodge": "Pavilhão de participação", "maze-gate": "Portão do labirinto", "misty-ridge": "Crista enevoada" @@ -368,7 +368,7 @@ "helpbot-garden": "Taman Helpbot", "food-data-grove": "Rimbun data makanan", "community-farm": "Ladang komunitas", - "creator-gallery": "Galeri kreator", + "creator-gallery": "CalorieStudio", "participation-lodge": "Pondok partisipasi", "maze-gate": "Gerbang labirin", "misty-ridge": "Punggung bukit berkabut" @@ -404,7 +404,7 @@ "helpbot-garden": "Helpbot باغ", "food-data-grove": "فوڈ ڈیٹا گروو", "community-farm": "کمیونٹی فارم", - "creator-gallery": "کریئیٹر گیلری", + "creator-gallery": "CalorieStudio", "participation-lodge": "پارٹیسپیشن لاج", "maze-gate": "بھول بھلیاں دروازہ", "misty-ridge": "دھندلا پہاڑی کنارہ" From 58f434af8e0bb84f70fd1f65c22d02068fefa9b1 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:26:44 +0200 Subject: [PATCH 316/368] Define CalorieStudio as the native CalorieVerse creator surface --- frontend/config/gallery-assets.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/config/gallery-assets.json b/frontend/config/gallery-assets.json index 9454ef77..0647a385 100644 --- a/frontend/config/gallery-assets.json +++ b/frontend/config/gallery-assets.json @@ -11,7 +11,9 @@ "participation_optional": true, "shared_participation_preference_with_gameverse": true, "install_required_for_participation": false, - "default_mode": "nonparticipant" + "default_mode": "nonparticipant", + "caloriestudio_is_native_calorieverse_surface": true, + "separate_product_ecosystem": false }, "asset_types": [ "recipe", @@ -179,5 +181,8 @@ "story" ] } - ] + ], + "public_name": "CalorieStudio", + "internal_legacy_term": "Creator Gallery", + "route_compatibility": "/gallery" } From 20587e9f4f5dd2bbf61a6547f3295cb15ce3ecb8 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:27:10 +0200 Subject: [PATCH 317/368] Use CalorieStudio as the public creator surface name --- frontend/config/gameverse-world.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/config/gameverse-world.json b/frontend/config/gameverse-world.json index fa6837fd..b891e705 100644 --- a/frontend/config/gameverse-world.json +++ b/frontend/config/gameverse-world.json @@ -36,7 +36,8 @@ "privacy_and_permission_control": true, "minimum_necessary_restrictions": true, "metaverse_and_app_share_same_ecosystem": true, - "creator_gallery_is_native_ecosystem_surface": true + "creator_gallery_is_native_ecosystem_surface": true, + "caloriestudio_is_native_ecosystem_surface": true }, "start": { "x": 11, @@ -214,6 +215,8 @@ "ecosystem": "Calorie Ecosystem", "app": "CalorieApp", "metaverse": "CalorieVerse", - "creator_surface": "Creator Gallery" + "creator_surface": "CalorieStudio", + "creator_surface_route": "/gallery", + "creator_surface_internal_legacy_term": "Creator Gallery" } } From 55e8b23c57944a77535f88de54acd36a072940ef Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:27:13 +0200 Subject: [PATCH 318/368] Rename the creator product surface to CalorieStudio --- contracts/ecosystem/v2/product-family.json | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/ecosystem/v2/product-family.json b/contracts/ecosystem/v2/product-family.json index 8e3bde79..f5d00793 100644 --- a/contracts/ecosystem/v2/product-family.json +++ b/contracts/ecosystem/v2/product-family.json @@ -1,6 +1,6 @@ { "contract_id": "calorie.ecosystem.product-family", - "contract_version": "1.0.0", + "contract_version": "1.0.1", "status": "product-naming-and-relationship-contract", "umbrella": { "id": "calorie-ecosystem", @@ -21,10 +21,12 @@ "route_compatibility": "/gameverse" }, "creator_gallery": { - "public_name": "Creator Gallery", - "role": "creator and asset surface available inside CalorieVerse and directly in the ecosystem", + "public_name": "CalorieStudio", + "role": "native creator, workshop and asset surface inside CalorieVerse and directly reachable in the Calorie Ecosystem", "runs_on_ecosystem": "calorie-ecosystem", - "separate_ecosystem": false + "separate_ecosystem": false, + "internal_legacy_term": "Creator Gallery", + "route_compatibility": "/gallery" } }, "invariants": { @@ -32,6 +34,8 @@ "app_and_metaverse_are_sibling_surfaces": true, "gallery_is_not_a_competing_ecosystem": true, "metaverse_name_is_stable": "CalorieVerse", - "internal_routes_and_ids_may_keep_gameverse_for_backward_compatibility": true + "internal_routes_and_ids_may_keep_gameverse_for_backward_compatibility": true, + "creator_surface_name_is_stable": "CalorieStudio", + "internal_gallery_ids_and_route_may_remain_for_backward_compatibility": true } } From f3b36af8cfcfc898c7aed5cad6c245aa6a1967b4 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:27:16 +0200 Subject: [PATCH 319/368] Adopt CalorieStudio throughout the CalorieVerse master plan --- docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md index 26175fe6..da027cd5 100644 --- a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md +++ b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md @@ -41,16 +41,17 @@ The shared umbrella is the **Calorie Ecosystem**. - **CalorieApp** is the food/nutrition application running on that ecosystem. - **CalorieVerse** is the public name of the persistent live metaverse/open world. -- **Creator Gallery** is a creator surface inside CalorieVerse and can also be +- **CalorieStudio** is a creator surface inside CalorieVerse and can also be opened directly, but it is not a separate competing ecosystem. -The existing internal term `Gameverse`, route `/gameverse`, file names and -stable IDs may remain where changing them would add migration risk. Public-facing -copy should use **CalorieVerse**. +The existing internal terms `Gameverse` and `Creator Gallery`, routes `/gameverse` +and `/gallery`, file names and stable IDs may remain where changing them would +add migration risk. Public-facing copy should use **CalorieVerse** and +**CalorieStudio**. This gives one clear hierarchy: -`Calorie Ecosystem -> CalorieApp + CalorieVerse -> Creator Gallery and other ecosystem surfaces`. +`Calorie Ecosystem -> CalorieApp + CalorieVerse -> CalorieStudio and other ecosystem surfaces`. ## One continuously evolving live Gameverse @@ -201,15 +202,15 @@ authorization or implementation of on-chain issuance. ## Open ecosystem with sustainable founder and treasury funding -CalorieVerse and Creator Gallery are intended to remain open to independent +CalorieVerse and CalorieStudio are intended to remain open to independent builders and voluntary participants while the official products retain a sustainable commercial model. The commercial principle is **free core, optional paid services**: - entering and exploring CalorieVerse remains free; -- ordinary Creator Gallery browsing and core creator participation stay free; -- optional premium CalorieApp/Creator Gallery tooling may be sold; +- ordinary CalorieStudio browsing and core creator participation stay free; +- optional premium CalorieApp/CalorieStudio tooling may be sold; - official managed hosting, rendering, API/data and support may be sold; - cosmetic/world content may be sold only where it does not create pay-to-win; - marketplace/service fees remain a later separately reviewed capability. @@ -367,7 +368,7 @@ The initial four-system slice is: - **Food Data Grove** — inspect a public food-data record and create a world clue; - **Community Farm** — balance a small water route between crops and nature; -- **Creator Gallery** — curate a local-first creator display that becomes visible +- **CalorieStudio** — curate a local-first creator display that becomes visible in the world; - **Helpbot Garden** — request one contextual hint without switching the world into a guided-tour mode. From d61438ff17837f578e0dc870551b24ae0ebe3d84 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:27:20 +0200 Subject: [PATCH 320/368] Use CalorieStudio in open stewardship and funding policy --- docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md b/docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md index bb4cf4c5..31a7d919 100644 --- a/docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md +++ b/docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md @@ -1,7 +1,7 @@ # Open ecosystem stewardship, IP and sustainable funding Status: design and legal-readiness baseline -Scope: Calorie Ecosystem, CalorieApp, CalorieVerse, Creator Gallery and future ecosystem extensions +Scope: Calorie Ecosystem, CalorieApp, CalorieVerse, CalorieStudio and future ecosystem extensions This document records the intended operating model. It is not a licence grant, trade-mark filing, tax ruling, regulatory approval, or substitute for Dutch/EU @@ -28,7 +28,7 @@ The current product family is: boundaries and the long-term ecosystem layer; - **CalorieApp** — the official food/nutrition application; - **CalorieVerse** — the persistent live metaverse/open world; -- **Creator Gallery** — a creator surface inside CalorieVerse and directly in +- **CalorieStudio** — a creator surface inside CalorieVerse and directly in the ecosystem. Independent projects may build compatible experiences under their own @@ -85,7 +85,7 @@ A neutral trade-mark policy should eventually distinguish: ## Creator copyright and user-generated content -Creator ownership should be the default for original Creator Gallery works. +Creator ownership should be the default for original CalorieStudio works. The platform should obtain only the rights it needs through clear terms, for example a non-exclusive licence to host, cache, transform for technical display, moderate, and show the work in CalorieVerse. Any commercial sublicensing, @@ -110,7 +110,7 @@ The ecosystem can remain open while official services are commercial. Candidate revenue streams include: -- optional premium CalorieApp or Creator Gallery features; +- optional premium CalorieApp or CalorieStudio features; - optional premium creator tools, storage, rendering and collaboration; - managed hosting, API/data services and enterprise support; - managed node/compute/storage services where lawful; @@ -120,7 +120,7 @@ Candidate revenue streams include: - sponsorships, partnerships, grants and paid implementation/support work. Core Gameverse/CalorieVerse entry, ordinary ecosystem exploration and ordinary -Creator Gallery browsing should not require a wallet, token purchase or node +CalorieStudio browsing should not require a wallet, token purchase or node participation. Premium features must remain additive rather than degrading the free core. @@ -203,7 +203,7 @@ boundary already recorded in the repository. ## Evidence and launch checklist -Before the public commercial launch of CalorieVerse/Creator Gallery: +Before the public commercial launch of CalorieVerse/CalorieStudio: 1. perform an exact EUIPO/TMview/BOIP clearance search for the final public names and relevant goods/services; From 7aa7740938d0bac8f7cb06592409d951ed0cd5a1 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:27:24 +0200 Subject: [PATCH 321/368] Rename creator legal-readiness surface to CalorieStudio --- docs/CALORIEVERSE_LEGAL_READINESS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CALORIEVERSE_LEGAL_READINESS.md b/docs/CALORIEVERSE_LEGAL_READINESS.md index 4534af7a..15abd1ac 100644 --- a/docs/CALORIEVERSE_LEGAL_READINESS.md +++ b/docs/CALORIEVERSE_LEGAL_READINESS.md @@ -1,4 +1,4 @@ -# CalorieVerse / Creator Gallery legal-readiness evidence register +# CalorieVerse / CalorieStudio legal-readiness evidence register Status: engineering evidence register — not a legal opinion or launch clearance Last design update: 2026-09-19 @@ -102,7 +102,7 @@ required**. Evidence status: **source rules documented; any expanded CalorieVerse/Creator Gallery data redistribution needs source-by-source review**. -## Creator Gallery user-generated content +## CalorieStudio user-generated content Production public uploads are not legally ready merely because the local creator prototype works. @@ -197,9 +197,9 @@ occurred**. ## Current blockers before a commercial public CalorieVerse launch 1. formal CalorieVerse trade-mark clearance/filing decision; -2. final CalorieVerse/Creator Gallery asset-provenance manifest; +2. final CalorieVerse/CalorieStudio asset-provenance manifest; 3. explicit open-component licence manifest before claiming an open-source SDK; -4. Creator Gallery production Terms/content licence/takedown workflow; +4. CalorieStudio production Terms/content licence/takedown workflow; 5. premium-service consumer/VAT/accounting terms for the exact offers; 6. DSA review once the actual UGC/platform model is fixed; 7. separate MiCA review before real-token commercial rails; From 5a935b98023af64f43ec29fc4cad77eb37c12d76 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:27:37 +0200 Subject: [PATCH 322/368] Test CalorieStudio public naming with legacy route compatibility --- tools/tests/gameverse_world_config.test.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/tests/gameverse_world_config.test.mjs b/tools/tests/gameverse_world_config.test.mjs index 718f0884..9990c382 100644 --- a/tools/tests/gameverse_world_config.test.mjs +++ b/tools/tests/gameverse_world_config.test.mjs @@ -54,9 +54,12 @@ test("public product family is Calorie Ecosystem with CalorieApp and CalorieVers assert.equal(world.product_family.ecosystem, "Calorie Ecosystem"); assert.equal(world.product_family.app, "CalorieApp"); assert.equal(world.product_family.metaverse, "CalorieVerse"); - assert.equal(world.product_family.creator_surface, "Creator Gallery"); + assert.equal(world.product_family.creator_surface, "CalorieStudio"); + assert.equal(world.product_family.creator_surface_route, "/gallery"); + assert.equal(world.product_family.creator_surface_internal_legacy_term, "Creator Gallery"); assert.equal(world.principles.metaverse_and_app_share_same_ecosystem, true); assert.equal(world.principles.creator_gallery_is_native_ecosystem_surface, true); + assert.equal(world.principles.caloriestudio_is_native_ecosystem_surface, true); }); test("starting character and original world are compatibility invariants", async () => { From 1c59611ce1ad4e9a0549fa7caf97639b3db7c4b3 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:27:41 +0200 Subject: [PATCH 323/368] Test CalorieStudio naming and legacy Gallery compatibility --- tools/tests/gallery_ecosystem_config.test.mjs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/tests/gallery_ecosystem_config.test.mjs b/tools/tests/gallery_ecosystem_config.test.mjs index 284fd69d..e8fa81d9 100644 --- a/tools/tests/gallery_ecosystem_config.test.mjs +++ b/tools/tests/gallery_ecosystem_config.test.mjs @@ -14,7 +14,7 @@ async function json(url) { return JSON.parse(await readFile(url, "utf8")); } -test("Gallery ecosystem is world-connected and non-pay-to-win", async () => { +test("CalorieStudio is world-connected and non-pay-to-win", async () => { const [catalog, world] = await Promise.all([json(catalogUrl), json(worldUrl)]); assert.equal(catalog.principles.pay_to_win, false); assert.equal(catalog.principles.wallet_required_to_browse, false); @@ -22,13 +22,17 @@ test("Gallery ecosystem is world-connected and non-pay-to-win", async () => { assert.equal(catalog.principles.real_marketplace_enabled, false); assert.equal(catalog.principles.creator_owns_storage, true); assert.equal(catalog.principles.world_and_gallery_share_asset_identity, true); + assert.equal(catalog.public_name, "CalorieStudio"); + assert.equal(catalog.internal_legacy_term, "Creator Gallery"); + assert.equal(catalog.route_compatibility, "/gallery"); + assert.equal(catalog.principles.caloriestudio_is_native_calorieverse_surface, true); const galleryRegion = world.regions.find(region => region.id === "creator-gallery"); assert.ok(galleryRegion); assert.equal(galleryRegion.destination, "/gallery"); }); -test("Gallery catalog covers planned modular creator families without real settlement", async () => { +test("CalorieStudio catalog covers planned modular creator families without real settlement", async () => { const catalog = await json(catalogUrl); for (const type of [ "recipe", @@ -47,14 +51,14 @@ test("Gallery catalog covers planned modular creator families without real settl assert.equal(catalog.assets.some(asset => asset.transfer_mode === "simulated-license"), true); }); -test("child Gallery assets never require simulated licensing", async () => { +test("child CalorieStudio assets never require simulated licensing", async () => { const catalog = await json(catalogUrl); const childAssets = catalog.assets.filter(asset => asset.ages.includes("child")); assert.ok(childAssets.length > 0); assert.equal(childAssets.every(asset => asset.transfer_mode === "game-native"), true); }); -test("all eleven display languages have Gallery shell copy", async () => { +test("all eleven display languages have CalorieStudio shell copy", async () => { const copy = await json(copyUrl); const locales = ["en", "zh-Hans", "hi", "es", "ar", "fr", "bn", "pt", "id", "ur", "nl"]; assert.deepEqual(Object.keys(copy).sort(), [...locales].sort()); @@ -66,7 +70,7 @@ test("all eleven display languages have Gallery shell copy", async () => { } }); -test("Gallery UI keeps creation local and explicitly avoids upload mint sale actions", async () => { +test("CalorieStudio keeps creation local and explicitly avoids upload mint sale actions", async () => { const [source, helper] = await Promise.all([ readFile(componentUrl, "utf8"), readFile(helperUrl, "utf8"), @@ -79,7 +83,7 @@ test("Gallery UI keeps creation local and explicitly avoids upload mint sale act assert.doesNotMatch(source, /mintNFT|submitOffer|walletSign|uploadToProvider/); }); -test("Gameverse embeds the Gallery and direct Gallery route returns to the world", async () => { +test("CalorieVerse embeds CalorieStudio and its compatibility route returns to the world", async () => { const [worldSource, routeSource] = await Promise.all([ readFile(gameverseUrl, "utf8"), readFile(routeUrl, "utf8"), From 22814bdd70565a0a1a3e6b62f147b98a60259776 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:27:45 +0200 Subject: [PATCH 324/368] Record CalorieStudio trademark status and clearance requirement --- TRADEMARKS.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/TRADEMARKS.md b/TRADEMARKS.md index b2d95cd2..ffc4bcf2 100644 --- a/TRADEMARKS.md +++ b/TRADEMARKS.md @@ -56,3 +56,16 @@ protocol. Until a separate decision and legal instrument exist, the current ownership records control. + + +### CalorieStudio + +`CalorieStudio` is the current proposed public name for the creator/workshop +surface within CalorieVerse. The internal route `/gallery` and legacy +"Creator Gallery" identifiers may remain for compatibility. + +This repository does **not** claim that CalorieStudio is a registered trade +mark or that the name has received a formal freedom-to-operate clearance. +Before a commercial brand filing or registered-symbol claim, perform an exact +EUIPO/TMview/BOIP clearance search for the intended goods and services and +retain the evidence. From 650a054de18c714821df4bd9e651ed01859dc03f Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:28:05 +0200 Subject: [PATCH 325/368] Rename creator funding surface to CalorieStudio --- contracts/ecosystem/v2/open-stewardship-and-funding.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/ecosystem/v2/open-stewardship-and-funding.json b/contracts/ecosystem/v2/open-stewardship-and-funding.json index b9dbffe4..5e4c133c 100644 --- a/contracts/ecosystem/v2/open-stewardship-and-funding.json +++ b/contracts/ecosystem/v2/open-stewardship-and-funding.json @@ -1,12 +1,12 @@ { "contract_id": "calorie.ecosystem.open-stewardship-and-funding", - "contract_version": "1.0.0", + "contract_version": "1.0.1", "status": "design-boundary-no-automatic-licence-or-ip-transfer", "ecosystem": "calorie-ecosystem", "products": [ "CalorieApp", "CalorieVerse", - "Creator Gallery" + "CalorieStudio" ], "open_building": { "independent_compatible_projects_allowed": true, @@ -36,7 +36,8 @@ "wallet_required": false, "node_participation_required": false, "premium_features_must_be_additive": true, - "pay_to_win": false + "pay_to_win": false, + "ordinary_caloriestudio_browsing_requires_payment": false }, "commercial_sustainability": { "premium_app_features_allowed": true, From 5d4b32f15377e860a9c678491c10e97648ee2023 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:28:10 +0200 Subject: [PATCH 326/368] Use CalorieStudio in commercial regulatory boundary --- REGULATORY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/REGULATORY.md b/REGULATORY.md index a73464ba..7c4312a8 100644 --- a/REGULATORY.md +++ b/REGULATORY.md @@ -10,13 +10,13 @@ External identity may be used only for authentication context. Any future financ This summary is not legal advice and does not claim certification or regulatory approval. -## CalorieVerse / Creator Gallery commercial boundary +## CalorieVerse / CalorieStudio commercial boundary Optional premium digital features, creator tools or managed services may be introduced as a sustainability model, but charging EU consumers requires a separate consumer-contract, VAT/OSS, privacy and refund review before launch. -If Creator Gallery or CalorieVerse begins hosting/intermediating public +If CalorieStudio or CalorieVerse begins hosting/intermediating public user-generated content or marketplace activity, Digital Services Act and platform/content-moderation obligations must be reviewed before production activation. From b0fb2e5ea57aeec54f548ca43437ce8402c5be3c Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:28:14 +0200 Subject: [PATCH 327/368] Enforce CalorieStudio naming in legal boundary checks --- tools/check_legal_boundaries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/check_legal_boundaries.py b/tools/check_legal_boundaries.py index ecd3f4c0..c492bcb4 100644 --- a/tools/check_legal_boundaries.py +++ b/tools/check_legal_boundaries.py @@ -75,13 +75,13 @@ def main() -> None: ("NO GENERAL LICENCE GRANTED", "GPL-2.0-or-later", "no permission is granted", "PATENTS AND PUBLIC DISCLOSURE"), ) require_text("COPYRIGHT.md", ("ICTHendrikse", "technical provenance", "GPL-2.0-or-later")) - require_text("TRADEMARKS.md", ("Pieter Hendrikse", "019137415", "019125433", "No repository licence grants")) + require_text("TRADEMARKS.md", ("Pieter Hendrikse", "019137415", "019125433", "No repository licence grants", "CalorieStudio")) require_text("DATA_LICENSING.md", ("Open Database License", "share-alike")) require_text("THIRD_PARTY_NOTICES.md", ("software bill of materials", "GPL-2.0-or-later")) require_text("IP_CLEARANCE.md", ("general ideas such as calorie tracking", "freedom-to-operate")) require_text( "docs/ECOSYSTEM_OPEN_STEWARDSHIP_AND_FUNDING.md", - ("existing repository is not automatically relicensed", "Founder/developer recovery", "No fixed percentages are declared yet"), + ("existing repository is not automatically relicensed", "Founder/developer recovery", "No fixed percentages are declared yet", "CalorieStudio"), ) stewardship = require_json_object( json.loads( From 7ef7d6ce33c155d119bf7ac2af543d835936678c Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:29:19 +0200 Subject: [PATCH 328/368] Add CalorieStudio as the public creator component --- frontend/components/CalorieStudio.tsx | 214 ++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 frontend/components/CalorieStudio.tsx diff --git a/frontend/components/CalorieStudio.tsx b/frontend/components/CalorieStudio.tsx new file mode 100644 index 00000000..1a8e6a55 --- /dev/null +++ b/frontend/components/CalorieStudio.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { FormEvent, useEffect, useMemo, useState } from "react"; +import { useDisplayLanguage } from "@/components/DisplayLanguageProvider"; +import type { AgeBand } from "@/lib/ageExperience"; +import { + createLocalGalleryDraft, + GALLERY_DRAFTS_KEY, + galleryAssetsForAge, + galleryAssetTypes, + galleryCopy, + galleryModeForAge, + parseLocalGalleryDrafts, + type GalleryAssetType, + type LocalGalleryDraft, +} from "@/lib/galleryEcosystem"; + +function assetTypeLabel(value: string) { + return value.replaceAll("-", " ").replace(/\b\w/g, letter => letter.toUpperCase()); +} + +export function CalorieStudio({ + ageBand, + compact = false, +}: { + ageBand: AgeBand; + compact?: boolean; +}) { + const display = useDisplayLanguage(); + const locale = display.enabled ? display.locale : "en"; + const { copy } = galleryCopy(locale); + const mode = galleryModeForAge(ageBand); + const [type, setType] = useState("all"); + const [drafts, setDrafts] = useState([]); + const [draftTitle, setDraftTitle] = useState(""); + const [draftType, setDraftType] = useState(galleryAssetTypes[0]); + const [message, setMessage] = useState(""); + + useEffect(() => { + try { + setDrafts(parseLocalGalleryDrafts(window.localStorage.getItem(GALLERY_DRAFTS_KEY))); + } catch { + setDrafts([]); + } + }, []); + + const assets = useMemo( + () => galleryAssetsForAge(ageBand, type), + [ageBand, type] + ); + const visibleDrafts = useMemo( + () => drafts.filter(draft => draft.age_band === ageBand), + [drafts, ageBand] + ); + + function saveDraft(event: FormEvent) { + event.preventDefault(); + try { + const draft = createLocalGalleryDraft({ title: draftTitle, type: draftType, ageBand }); + const next = [draft, ...drafts].slice(0, 50); + setDrafts(next); + setDraftTitle(""); + setMessage(copy.saved); + try { + window.localStorage.setItem(GALLERY_DRAFTS_KEY, JSON.stringify(next)); + } catch { + // The workshop stays usable in-memory when browser storage is unavailable. + } + } catch { + setMessage(copy.localDraftNote); + } + } + + return ( +
    +
    +
    +

    {copy.browse}

    +

    {copy.title}

    +

    {copy.intro}

    +
    +
    + {copy.mode} + {copy[mode.labelKey]} + {copy.noWallet} +
    +
    + +
    + {copy.marketplaceOff} + {copy.storageNote} +
    + +
    + + {galleryAssetTypes.map(assetType => ( + + ))} +
    + +
    + {assets.slice(0, compact ? 4 : assets.length).map((asset, index) => ( +
    + +
    + {assetTypeLabel(asset.type)} +

    {asset.title}

    +

    {copy.creator}: {asset.creator}

    +
    + {asset.tags.slice(0, 3).map(tag => {tag})} +
    +

    {asset.transfer_mode === "game-native" ? "Game-native" : "Simulated licence"}

    +
    +
    + ))} +
    + + {compact && visibleDrafts.length > 0 ? ( +
    + {copy.localDraft} + {visibleDrafts.length} {copy.drafts.toLowerCase()} +
    + {visibleDrafts.slice(0, 3).map(draft => ( + {draft.title} + ))} +
    +

    {copy.localDraftNote}

    +
    + ) : null} + + {!compact ? ( +
    +
    +

    {copy.workshop}

    + +

    {copy.localDraftNote}

    +
    + {copy.storage} + {copy.storageNote} +
    +
    + +
    + + setDraftTitle(event.target.value)} + required + /> + + + +

    {message}

    +
    + +
    + {copy.drafts} + {visibleDrafts.length === 0 ? ( +

    {copy.localDraftNote}

    + ) : ( +
      + {visibleDrafts.slice(0, 8).map(draft => ( +
    • + {draft.title} + {assetTypeLabel(draft.type)} +
    • + ))} +
    + )} +
    +
    + ) : null} +
    + ); +} From 798b3758211af5d05f1aa111b6f153ba75d747a4 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:29:22 +0200 Subject: [PATCH 329/368] Keep legacy Creator Gallery component name as a compatibility alias --- .../components/GameverseCreatorGallery.tsx | 215 +----------------- 1 file changed, 1 insertion(+), 214 deletions(-) diff --git a/frontend/components/GameverseCreatorGallery.tsx b/frontend/components/GameverseCreatorGallery.tsx index 1eac83fb..a78b21cf 100644 --- a/frontend/components/GameverseCreatorGallery.tsx +++ b/frontend/components/GameverseCreatorGallery.tsx @@ -1,214 +1 @@ -"use client"; - -import { FormEvent, useEffect, useMemo, useState } from "react"; -import { useDisplayLanguage } from "@/components/DisplayLanguageProvider"; -import type { AgeBand } from "@/lib/ageExperience"; -import { - createLocalGalleryDraft, - GALLERY_DRAFTS_KEY, - galleryAssetsForAge, - galleryAssetTypes, - galleryCopy, - galleryModeForAge, - parseLocalGalleryDrafts, - type GalleryAssetType, - type LocalGalleryDraft, -} from "@/lib/galleryEcosystem"; - -function assetTypeLabel(value: string) { - return value.replaceAll("-", " ").replace(/\b\w/g, letter => letter.toUpperCase()); -} - -export function GameverseCreatorGallery({ - ageBand, - compact = false, -}: { - ageBand: AgeBand; - compact?: boolean; -}) { - const display = useDisplayLanguage(); - const locale = display.enabled ? display.locale : "en"; - const { copy } = galleryCopy(locale); - const mode = galleryModeForAge(ageBand); - const [type, setType] = useState("all"); - const [drafts, setDrafts] = useState([]); - const [draftTitle, setDraftTitle] = useState(""); - const [draftType, setDraftType] = useState(galleryAssetTypes[0]); - const [message, setMessage] = useState(""); - - useEffect(() => { - try { - setDrafts(parseLocalGalleryDrafts(window.localStorage.getItem(GALLERY_DRAFTS_KEY))); - } catch { - setDrafts([]); - } - }, []); - - const assets = useMemo( - () => galleryAssetsForAge(ageBand, type), - [ageBand, type] - ); - const visibleDrafts = useMemo( - () => drafts.filter(draft => draft.age_band === ageBand), - [drafts, ageBand] - ); - - function saveDraft(event: FormEvent) { - event.preventDefault(); - try { - const draft = createLocalGalleryDraft({ title: draftTitle, type: draftType, ageBand }); - const next = [draft, ...drafts].slice(0, 50); - setDrafts(next); - setDraftTitle(""); - setMessage(copy.saved); - try { - window.localStorage.setItem(GALLERY_DRAFTS_KEY, JSON.stringify(next)); - } catch { - // The workshop stays usable in-memory when browser storage is unavailable. - } - } catch { - setMessage(copy.localDraftNote); - } - } - - return ( -
    -
    -
    -

    {copy.browse}

    -

    {copy.title}

    -

    {copy.intro}

    -
    -
    - {copy.mode} - {copy[mode.labelKey]} - {copy.noWallet} -
    -
    - -
    - {copy.marketplaceOff} - {copy.storageNote} -
    - -
    - - {galleryAssetTypes.map(assetType => ( - - ))} -
    - -
    - {assets.slice(0, compact ? 4 : assets.length).map((asset, index) => ( -
    - -
    - {assetTypeLabel(asset.type)} -

    {asset.title}

    -

    {copy.creator}: {asset.creator}

    -
    - {asset.tags.slice(0, 3).map(tag => {tag})} -
    -

    {asset.transfer_mode === "game-native" ? "Game-native" : "Simulated licence"}

    -
    -
    - ))} -
    - - {compact && visibleDrafts.length > 0 ? ( -
    - {copy.localDraft} - {visibleDrafts.length} {copy.drafts.toLowerCase()} -
    - {visibleDrafts.slice(0, 3).map(draft => ( - {draft.title} - ))} -
    -

    {copy.localDraftNote}

    -
    - ) : null} - - {!compact ? ( -
    -
    -

    {copy.workshop}

    - -

    {copy.localDraftNote}

    -
    - {copy.storage} - {copy.storageNote} -
    -
    - -
    - - setDraftTitle(event.target.value)} - required - /> - - - -

    {message}

    -
    - -
    - {copy.drafts} - {visibleDrafts.length === 0 ? ( -

    {copy.localDraftNote}

    - ) : ( -
      - {visibleDrafts.slice(0, 8).map(draft => ( -
    • - {draft.title} - {assetTypeLabel(draft.type)} -
    • - ))} -
    - )} -
    -
    - ) : null} -
    - ); -} +export { CalorieStudio as GameverseCreatorGallery } from "@/components/CalorieStudio"; From 34edd1c581e1afd5960ce0452bd839e80ca70afc Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:29:25 +0200 Subject: [PATCH 330/368] Add reusable CalorieStudio page surface --- frontend/components/CalorieStudioPage.tsx | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/components/CalorieStudioPage.tsx diff --git a/frontend/components/CalorieStudioPage.tsx b/frontend/components/CalorieStudioPage.tsx new file mode 100644 index 00000000..d5ebeac5 --- /dev/null +++ b/frontend/components/CalorieStudioPage.tsx @@ -0,0 +1,36 @@ +"use client"; + +import Link from "next/link"; + +import { AgeExperienceControl, useAgeExperience } from "@/components/AgeExperienceControl"; +import { CalorieStudio } from "@/components/CalorieStudio"; +import { DisplayLanguagePicker } from "@/components/DisplayLanguageProvider"; +import { ParticipationChoiceCard } from "@/components/ParticipationChoiceCard"; + +export function CalorieStudioPage() { + const [ageBand, setAgeBand, resolved = true] = useAgeExperience(); + + if (!resolved) return null; + + return ( +
    +
    +
    + ← CalorieVerse + +
    + {!ageBand ? ( +
    + +
    + ) : ( + <> + + + + + )} +
    +
    + ); +} From 44f7aa1231cba549f995f78773d177f7ae4f81c4 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:29:28 +0200 Subject: [PATCH 331/368] Add canonical CalorieStudio route --- frontend/app/studio/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 frontend/app/studio/page.tsx diff --git a/frontend/app/studio/page.tsx b/frontend/app/studio/page.tsx new file mode 100644 index 00000000..813f889f --- /dev/null +++ b/frontend/app/studio/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; +import { CalorieStudioPage } from "@/components/CalorieStudioPage"; + +export const metadata: Metadata = { + title: "CalorieStudio", + description: "Create and discover world content inside the Calorie ecosystem.", +}; + +export default function StudioPage() { + return ; +} From 34395e428044b4dedd80ff97e701e226188b3adf Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:29:32 +0200 Subject: [PATCH 332/368] Keep the old Gallery route as a CalorieStudio compatibility entry --- frontend/app/gallery/page.tsx | 41 +++++++---------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/frontend/app/gallery/page.tsx b/frontend/app/gallery/page.tsx index 58f90568..0209953d 100644 --- a/frontend/app/gallery/page.tsx +++ b/frontend/app/gallery/page.tsx @@ -1,36 +1,11 @@ -"use client"; +import type { Metadata } from "next"; +import { CalorieStudioPage } from "@/components/CalorieStudioPage"; -import Link from "next/link"; +export const metadata: Metadata = { + title: "CalorieStudio", + description: "Compatibility route for CalorieStudio.", +}; -import { AgeExperienceControl, useAgeExperience } from "@/components/AgeExperienceControl"; -import { DisplayLanguagePicker } from "@/components/DisplayLanguageProvider"; -import { GameverseCreatorGallery } from "@/components/GameverseCreatorGallery"; -import { ParticipationChoiceCard } from "@/components/ParticipationChoiceCard"; - -export default function GalleryPage() { - const [ageBand, setAgeBand, resolved = true] = useAgeExperience(); - - if (!resolved) return null; - - return ( -
    -
    -
    - ← Gameverse - -
    - {!ageBand ? ( -
    - -
    - ) : ( - <> - - - - - )} -
    -
    - ); +export default function GalleryCompatibilityPage() { + return ; } From 41574bb6cc79481b6c2c0f3c7fe5060d05acce6c Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:29:43 +0200 Subject: [PATCH 333/368] Use CalorieStudio directly inside CalorieVerse --- frontend/components/GameverseWorld.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/components/GameverseWorld.tsx b/frontend/components/GameverseWorld.tsx index ead52265..4c6ff9d0 100644 --- a/frontend/components/GameverseWorld.tsx +++ b/frontend/components/GameverseWorld.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { AgeExperienceControl, useAgeExperience } from "@/components/AgeExperienceControl"; import { DisplayLanguagePicker, useDisplayLanguage } from "@/components/DisplayLanguageProvider"; -import { GameverseCreatorGallery } from "@/components/GameverseCreatorGallery"; +import { CalorieStudio } from "@/components/CalorieStudio"; import { GameverseMazeRoute } from "@/components/GameverseMazeRoute"; import { ParticipationChoiceCard } from "@/components/ParticipationChoiceCard"; import { CalorieVerseInteractionCard } from "@/components/CalorieVerseInteractionCard"; @@ -377,7 +377,7 @@ export function GameverseWorld() { ) : null} {selected?.kind === "gallery" ? ( - + ) : null} {selected?.kind === "participation" && ageBand === "adult" ? ( From 6e3ec3e06dd12a6e098871472ce8dd719e59e17a Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:29:46 +0200 Subject: [PATCH 334/368] Make studio the canonical CalorieStudio route --- frontend/config/gameverse-world.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/config/gameverse-world.json b/frontend/config/gameverse-world.json index b891e705..1911b742 100644 --- a/frontend/config/gameverse-world.json +++ b/frontend/config/gameverse-world.json @@ -156,7 +156,8 @@ "teen", "adult" ], - "destination": "/gallery" + "destination": "/studio", + "legacy_destination": "/gallery" }, { "id": "participation-lodge", @@ -216,7 +217,8 @@ "app": "CalorieApp", "metaverse": "CalorieVerse", "creator_surface": "CalorieStudio", - "creator_surface_route": "/gallery", - "creator_surface_internal_legacy_term": "Creator Gallery" + "creator_surface_route": "/studio", + "creator_surface_internal_legacy_term": "Creator Gallery", + "creator_surface_legacy_route": "/gallery" } } From 2431f8425867e3fa232e413a56d8cd6ff1c10b83 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:29:50 +0200 Subject: [PATCH 335/368] Set studio as canonical CalorieStudio route with Gallery compatibility --- contracts/ecosystem/v2/product-family.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/ecosystem/v2/product-family.json b/contracts/ecosystem/v2/product-family.json index f5d00793..b0b23b8a 100644 --- a/contracts/ecosystem/v2/product-family.json +++ b/contracts/ecosystem/v2/product-family.json @@ -1,6 +1,6 @@ { "contract_id": "calorie.ecosystem.product-family", - "contract_version": "1.0.1", + "contract_version": "1.0.2", "status": "product-naming-and-relationship-contract", "umbrella": { "id": "calorie-ecosystem", @@ -26,7 +26,8 @@ "runs_on_ecosystem": "calorie-ecosystem", "separate_ecosystem": false, "internal_legacy_term": "Creator Gallery", - "route_compatibility": "/gallery" + "route_compatibility": "/gallery", + "canonical_route": "/studio" } }, "invariants": { From ec151c87740b47700c5de99e18e78dac1dc44560 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:30:03 +0200 Subject: [PATCH 336/368] Test CalorieStudio canonical and legacy routes --- tools/tests/gameverse_world_config.test.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/tests/gameverse_world_config.test.mjs b/tools/tests/gameverse_world_config.test.mjs index 9990c382..c8514894 100644 --- a/tools/tests/gameverse_world_config.test.mjs +++ b/tools/tests/gameverse_world_config.test.mjs @@ -55,7 +55,8 @@ test("public product family is Calorie Ecosystem with CalorieApp and CalorieVers assert.equal(world.product_family.app, "CalorieApp"); assert.equal(world.product_family.metaverse, "CalorieVerse"); assert.equal(world.product_family.creator_surface, "CalorieStudio"); - assert.equal(world.product_family.creator_surface_route, "/gallery"); + assert.equal(world.product_family.creator_surface_route, "/studio"); + assert.equal(world.product_family.creator_surface_legacy_route, "/gallery"); assert.equal(world.product_family.creator_surface_internal_legacy_term, "Creator Gallery"); assert.equal(world.principles.metaverse_and_app_share_same_ecosystem, true); assert.equal(world.principles.creator_gallery_is_native_ecosystem_surface, true); From 08e4ca0fef97b55ef7372497ca28bc2850ffe949 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:30:07 +0200 Subject: [PATCH 337/368] Test CalorieStudio canonical route and legacy Gallery compatibility --- tools/tests/gallery_ecosystem_config.test.mjs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tools/tests/gallery_ecosystem_config.test.mjs b/tools/tests/gallery_ecosystem_config.test.mjs index e8fa81d9..0ac5dbf5 100644 --- a/tools/tests/gallery_ecosystem_config.test.mjs +++ b/tools/tests/gallery_ecosystem_config.test.mjs @@ -5,10 +5,12 @@ import test from "node:test"; const catalogUrl = new URL("../../frontend/config/gallery-assets.json", import.meta.url); const copyUrl = new URL("../../frontend/config/gallery-copy.json", import.meta.url); const worldUrl = new URL("../../frontend/config/gameverse-world.json", import.meta.url); -const componentUrl = new URL("../../frontend/components/GameverseCreatorGallery.tsx", import.meta.url); +const componentUrl = new URL("../../frontend/components/CalorieStudio.tsx", import.meta.url); +const legacyComponentUrl = new URL("../../frontend/components/GameverseCreatorGallery.tsx", import.meta.url); const helperUrl = new URL("../../frontend/lib/galleryEcosystem.ts", import.meta.url); const gameverseUrl = new URL("../../frontend/components/GameverseWorld.tsx", import.meta.url); -const routeUrl = new URL("../../frontend/app/gallery/page.tsx", import.meta.url); +const routeUrl = new URL("../../frontend/app/studio/page.tsx", import.meta.url); +const legacyRouteUrl = new URL("../../frontend/app/gallery/page.tsx", import.meta.url); async function json(url) { return JSON.parse(await readFile(url, "utf8")); @@ -29,7 +31,8 @@ test("CalorieStudio is world-connected and non-pay-to-win", async () => { const galleryRegion = world.regions.find(region => region.id === "creator-gallery"); assert.ok(galleryRegion); - assert.equal(galleryRegion.destination, "/gallery"); + assert.equal(galleryRegion.destination, "/studio"); + assert.equal(galleryRegion.legacy_destination, "/gallery"); }); test("CalorieStudio catalog covers planned modular creator families without real settlement", async () => { @@ -83,13 +86,16 @@ test("CalorieStudio keeps creation local and explicitly avoids upload mint sale assert.doesNotMatch(source, /mintNFT|submitOffer|walletSign|uploadToProvider/); }); -test("CalorieVerse embeds CalorieStudio and its compatibility route returns to the world", async () => { - const [worldSource, routeSource] = await Promise.all([ +test("CalorieVerse embeds CalorieStudio and legacy Gallery names remain compatibility-only", async () => { + const [worldSource, routeSource, legacyRoute, legacyComponent] = await Promise.all([ readFile(gameverseUrl, "utf8"), readFile(routeUrl, "utf8"), + readFile(legacyRouteUrl, "utf8"), + readFile(legacyComponentUrl, "utf8"), ]); - assert.match(worldSource, /GameverseCreatorGallery/); + assert.match(worldSource, /CalorieStudio/); assert.match(worldSource, /selected\?\.kind === "gallery"/); - assert.match(routeSource, /GameverseCreatorGallery/); - assert.match(routeSource, /href="\/gameverse"/); + assert.match(routeSource, /CalorieStudioPage/); + assert.match(legacyRoute, /CalorieStudioPage/); + assert.match(legacyComponent, /CalorieStudio as GameverseCreatorGallery/); }); From bc8747a974a98173e1ac50c78dc748220cf45034 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:30:10 +0200 Subject: [PATCH 338/368] Document CalorieStudio canonical route and Gallery compatibility --- docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md index da027cd5..e977a238 100644 --- a/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md +++ b/docs/SHOWCASE_OPEN_WORLD_MASTERPLAN.md @@ -44,9 +44,9 @@ The shared umbrella is the **Calorie Ecosystem**. - **CalorieStudio** is a creator surface inside CalorieVerse and can also be opened directly, but it is not a separate competing ecosystem. -The existing internal terms `Gameverse` and `Creator Gallery`, routes `/gameverse` -and `/gallery`, file names and stable IDs may remain where changing them would -add migration risk. Public-facing copy should use **CalorieVerse** and +The existing internal terms `Gameverse` and `Creator Gallery`, routes `/gameverse` and legacy `/gallery`, file names and stable IDs may remain +where changing them would add migration risk. The canonical direct creator route +is `/studio`. Public-facing copy should use **CalorieVerse** and **CalorieStudio**. This gives one clear hierarchy: From 95e553990967440ea8349ccadd19e1d58fe56a8b Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:30:39 +0200 Subject: [PATCH 339/368] Use CalorieStudio in CalorieVerse interaction effects --- frontend/config/calorieverse-interactions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/config/calorieverse-interactions.json b/frontend/config/calorieverse-interactions.json index 83254c79..b3aa33ad 100644 --- a/frontend/config/calorieverse-interactions.json +++ b/frontend/config/calorieverse-interactions.json @@ -80,7 +80,7 @@ "requires": [ "gallery-world-display" ], - "description": "A creator beacon appears near the Gallery district." + "description": "A creator beacon appears near the CalorieStudio district." }, { "id": "route-whisper", From 0af89a664998095d793fee77008c3d77748c7154 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:30:42 +0200 Subject: [PATCH 340/368] Rename Gallery interaction copy to CalorieStudio in all locales --- .../config/calorieverse-interaction-copy.json | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/frontend/config/calorieverse-interaction-copy.json b/frontend/config/calorieverse-interaction-copy.json index 901e7953..473a68cf 100644 --- a/frontend/config/calorieverse-interaction-copy.json +++ b/frontend/config/calorieverse-interaction-copy.json @@ -19,10 +19,10 @@ "done": "The farm water route is balanced and the landscape responds." }, "gallery-world-display": { - "title": "Curate a Gallery display", + "title": "Curate a CalorieStudio display", "body": "Choose to place a simple creator marker in the world. It stays local in this first slice.", "action": "Place display", - "done": "A creator beacon now appears in the Gallery district." + "done": "A creator beacon now appears in the CalorieStudio district." }, "helpbot-local-hint": { "title": "Ask for a local hint", @@ -52,10 +52,10 @@ "done": "De waterroute van de boerderij is in balans en het landschap reageert." }, "gallery-world-display": { - "title": "Stel een Gallery-display samen", + "title": "Stel een CalorieStudio-display samen", "body": "Plaats vrijwillig een eenvoudige creator-marker in de wereld. In deze eerste versie blijft die lokaal.", "action": "Display plaatsen", - "done": "Er verschijnt nu een creator-beacon bij het Gallery-district." + "done": "Er verschijnt nu een creator-beacon bij het CalorieStudio-district." }, "helpbot-local-hint": { "title": "Vraag om een lokale hint", @@ -85,10 +85,10 @@ "done": "农场水路已平衡,景观也会响应。" }, "gallery-world-display": { - "title": "策划 Gallery 展示", + "title": "策划 CalorieStudio 展示", "body": "选择在世界中放置一个简单的创作者标记。此版本仅保存在本地。", "action": "放置展示", - "done": "Gallery 区域现在出现创作者信标。" + "done": "CalorieStudio 区域现在出现创作者信标。" }, "helpbot-local-hint": { "title": "询问本地提示", @@ -118,10 +118,10 @@ "done": "फार्म का जल मार्ग संतुलित है और परिदृश्य प्रतिक्रिया देता है।" }, "gallery-world-display": { - "title": "Gallery डिस्प्ले तैयार करें", + "title": "CalorieStudio डिस्प्ले तैयार करें", "body": "दुनिया में एक सरल creator marker रखने का विकल्प चुनें। इस चरण में यह लोकल रहता है।", "action": "डिस्प्ले रखें", - "done": "Gallery क्षेत्र में creator beacon दिखाई देता है।" + "done": "CalorieStudio क्षेत्र में creator beacon दिखाई देता है।" }, "helpbot-local-hint": { "title": "लोकल संकेत पूछें", @@ -151,10 +151,10 @@ "done": "La ruta de agua queda equilibrada y el paisaje responde." }, "gallery-world-display": { - "title": "Crea una muestra de Gallery", + "title": "Crea una muestra de CalorieStudio", "body": "Coloca voluntariamente un marcador de creador. En esta primera versión permanece local.", "action": "Colocar muestra", - "done": "Aparece una baliza de creador en el distrito Gallery." + "done": "Aparece una baliza de creador en el distrito CalorieStudio." }, "helpbot-local-hint": { "title": "Pide una pista local", @@ -184,10 +184,10 @@ "done": "تمت موازنة مسار مياه المزرعة واستجابت البيئة." }, "gallery-world-display": { - "title": "نسّق عرض Gallery", + "title": "نسّق عرض CalorieStudio", "body": "اختر وضع علامة بسيطة للمبدع في العالم. تبقى محلية في هذه المرحلة.", "action": "وضع العرض", - "done": "تظهر الآن منارة للمبدعين قرب منطقة Gallery." + "done": "تظهر الآن منارة للمبدعين قرب منطقة CalorieStudio." }, "helpbot-local-hint": { "title": "اطلب تلميحًا محليًا", @@ -217,10 +217,10 @@ "done": "Le trajet d’eau est équilibré et le paysage réagit." }, "gallery-world-display": { - "title": "Composer une présentation Gallery", + "title": "Composer une présentation CalorieStudio", "body": "Placez volontairement un repère créateur simple. Il reste local dans cette première version.", "action": "Placer la présentation", - "done": "Une balise créateur apparaît près du quartier Gallery." + "done": "Une balise créateur apparaît près du quartier CalorieStudio." }, "helpbot-local-hint": { "title": "Demander un indice local", @@ -250,10 +250,10 @@ "done": "ফার্মের পানির পথ ভারসাম্যপূর্ণ এবং দৃশ্যপট সাড়া দেয়।" }, "gallery-world-display": { - "title": "Gallery ডিসপ্লে সাজান", + "title": "CalorieStudio ডিসপ্লে সাজান", "body": "বিশ্বে একটি সহজ creator marker রাখুন। প্রথম সংস্করণে এটি লোকাল থাকে।", "action": "ডিসপ্লে রাখুন", - "done": "Gallery এলাকায় creator beacon দেখা যায়।" + "done": "CalorieStudio এলাকায় creator beacon দেখা যায়।" }, "helpbot-local-hint": { "title": "লোকাল হিন্ট নিন", @@ -283,10 +283,10 @@ "done": "A rota de água está equilibrada e a paisagem responde." }, "gallery-world-display": { - "title": "Organize uma mostra Gallery", + "title": "Organize uma mostra CalorieStudio", "body": "Coloque voluntariamente um marcador simples de criador. Nesta versão fica local.", "action": "Colocar mostra", - "done": "Uma baliza de criador aparece no distrito Gallery." + "done": "Uma baliza de criador aparece no distrito CalorieStudio." }, "helpbot-local-hint": { "title": "Peça uma dica local", @@ -316,10 +316,10 @@ "done": "Jalur air farm seimbang dan lanskap merespons." }, "gallery-world-display": { - "title": "Kurasi tampilan Gallery", + "title": "Kurasi tampilan CalorieStudio", "body": "Pilih menempatkan marker kreator sederhana. Pada tahap ini tetap lokal.", "action": "Tempatkan tampilan", - "done": "Beacon kreator muncul di distrik Gallery." + "done": "Beacon kreator muncul di distrik CalorieStudio." }, "helpbot-local-hint": { "title": "Minta petunjuk lokal", @@ -349,10 +349,10 @@ "done": "فارم کا پانی متوازن ہے اور منظر ردعمل دیتا ہے۔" }, "gallery-world-display": { - "title": "Gallery ڈسپلے ترتیب دیں", + "title": "CalorieStudio ڈسپلے ترتیب دیں", "body": "دنیا میں ایک سادہ creator marker رکھنے کا انتخاب کریں۔ یہ فی الحال لوکل رہتا ہے۔", "action": "ڈسپلے رکھیں", - "done": "Gallery ضلع میں creator beacon ظاہر ہوتا ہے۔" + "done": "CalorieStudio ضلع میں creator beacon ظاہر ہوتا ہے۔" }, "helpbot-local-hint": { "title": "لوکل ہنٹ پوچھیں", From 0c6c7ef1285a7e7efc9bf8bf267984f8762bef28 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:30:46 +0200 Subject: [PATCH 341/368] Update interaction test language to CalorieStudio --- tools/tests/calorieverse_interactions.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tests/calorieverse_interactions.test.mjs b/tools/tests/calorieverse_interactions.test.mjs index 23cefbf1..bb152ee6 100644 --- a/tools/tests/calorieverse_interactions.test.mjs +++ b/tools/tests/calorieverse_interactions.test.mjs @@ -22,7 +22,7 @@ test("CalorieVerse interactions stay optional and do not gate the core route", a assert.equal(config.principles.local_progress_only, true); }); -test("first interaction slice covers data, farm, Gallery and Helpbot systems", async () => { +test("first interaction slice covers data, farm, CalorieStudio and Helpbot systems", async () => { const config = await json(configUrl); const regions = new Set(config.interactions.map(item => item.region_id)); for (const region of [ From 20b0e293c44a5f6e037fd89917cc2b7d1a25c062 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:32:00 +0200 Subject: [PATCH 342/368] Stop overriding Next.js exact PostCSS dependency --- frontend/package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 672d057a..3b2af91f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,8 +29,5 @@ "postcss": "8.5.23", "tailwindcss": "3.4.7", "typescript": "5.5.4" - }, - "overrides": { - "postcss": "8.5.23" } } From a4d1d5f568e29d18b2075602383319319966b452 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:32:07 +0200 Subject: [PATCH 343/368] Restore Next.js nested PostCSS dependency for a valid npm tree --- frontend/package-lock.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c6c60fa9..6e45e69f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6690,6 +6690,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "url": "https://opencollective.com/postcss/", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/funding/github/npm/postcss", + "type": "tidelift" + }, + { + "url": "https://github.com/sponsors/ai", + "type": "github" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } } } } From 6e24cb4c1fea36acabf81a5a250124357f31e79c Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:32:48 +0200 Subject: [PATCH 344/368] Add bounded free-roam movement rules for CalorieVerse --- frontend/lib/calorieVerseMovement.ts | 89 ++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 frontend/lib/calorieVerseMovement.ts diff --git a/frontend/lib/calorieVerseMovement.ts b/frontend/lib/calorieVerseMovement.ts new file mode 100644 index 00000000..20fb8ec4 --- /dev/null +++ b/frontend/lib/calorieVerseMovement.ts @@ -0,0 +1,89 @@ +export type WorldPosition = { x: number; y: number }; +export type WorldDirection = "up" | "down" | "left" | "right"; + +export type WorldRegionPoint = { + id: string; + x: number; + y: number; +}; + +export const WORLD_MOVEMENT = Object.freeze({ + minX: 4, + maxXBeforeRidge: 90, + maxXAfterRidge: 96, + minY: 22, + maxY: 88, + step: 2.4, + discoveryRadius: 5.8, +}); + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +export function safeWorldPosition( + value: unknown, + fallback: WorldPosition +): WorldPosition { + if (!value || typeof value !== "object" || Array.isArray(value)) return {...fallback}; + const candidate = value as Record; + if ( + typeof candidate.x !== "number" || + !Number.isFinite(candidate.x) || + typeof candidate.y !== "number" || + !Number.isFinite(candidate.y) + ) return {...fallback}; + return { + x: clamp(candidate.x, WORLD_MOVEMENT.minX, WORLD_MOVEMENT.maxXAfterRidge), + y: clamp(candidate.y, WORLD_MOVEMENT.minY, WORLD_MOVEMENT.maxY), + }; +} + +export function moveWorld( + position: WorldPosition, + direction: WorldDirection, + ridgeUnlocked: boolean +): WorldPosition { + const delta = direction === "up" ? {x: 0, y: -WORLD_MOVEMENT.step} + : direction === "down" ? {x: 0, y: WORLD_MOVEMENT.step} + : direction === "left" ? {x: -WORLD_MOVEMENT.step, y: 0} + : {x: WORLD_MOVEMENT.step, y: 0}; + + const maxX = ridgeUnlocked + ? WORLD_MOVEMENT.maxXAfterRidge + : WORLD_MOVEMENT.maxXBeforeRidge; + + return { + x: clamp(position.x + delta.x, WORLD_MOVEMENT.minX, maxX), + y: clamp(position.y + delta.y, WORLD_MOVEMENT.minY, WORLD_MOVEMENT.maxY), + }; +} + +export function worldKeyDirection(key: string): WorldDirection | null { + const normalized = key.toLowerCase(); + if (key === "ArrowUp" || normalized === "w") return "up"; + if (key === "ArrowDown" || normalized === "s") return "down"; + if (key === "ArrowLeft" || normalized === "a") return "left"; + if (key === "ArrowRight" || normalized === "d") return "right"; + return null; +} + +export function nearestWorldRegion( + position: WorldPosition, + regions: readonly T[], + radius = WORLD_MOVEMENT.discoveryRadius +): T | null { + let nearest: T | null = null; + let nearestDistance = Number.POSITIVE_INFINITY; + + for (const region of regions) { + const dx = region.x - position.x; + const dy = (region.y - position.y) * 0.82; + const distance = Math.hypot(dx, dy); + if (distance <= radius && distance < nearestDistance) { + nearest = region; + nearestDistance = distance; + } + } + return nearest; +} From c36e331d5e49a1219e3036cf5acf71e9d6c8c60a Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:32:50 +0200 Subject: [PATCH 345/368] Test CalorieVerse free-roam movement boundaries --- tools/tests/calorieverse_movement.test.mjs | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tools/tests/calorieverse_movement.test.mjs diff --git a/tools/tests/calorieverse_movement.test.mjs b/tools/tests/calorieverse_movement.test.mjs new file mode 100644 index 00000000..ede93c54 --- /dev/null +++ b/tools/tests/calorieverse_movement.test.mjs @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const helperUrl = new URL("../../frontend/lib/calorieVerseMovement.ts", import.meta.url); +const worldUrl = new URL("../../frontend/components/GameverseWorld.tsx", import.meta.url); + +test("free-roam movement supports keyboard and mobile-friendly cardinal directions", async () => { + const source = await readFile(helperUrl, "utf8"); + assert.match(source, /ArrowUp/); + assert.match(source, /ArrowDown/); + assert.match(source, /ArrowLeft/); + assert.match(source, /ArrowRight/); + assert.match(source, /normalized === "w"/); + assert.match(source, /normalized === "a"/); + assert.match(source, /normalized === "s"/); + assert.match(source, /normalized === "d"/); +}); + +test("ridge boundary stays closed before maze completion", async () => { + const source = await readFile(helperUrl, "utf8"); + assert.match(source, /maxXBeforeRidge: 90/); + assert.match(source, /maxXAfterRidge: 96/); + assert.match(source, /ridgeUnlocked/); +}); + +test("world discovery uses proximity rather than forcing a fixed quest order", async () => { + const source = await readFile(helperUrl, "utf8"); + assert.match(source, /nearestWorldRegion/); + assert.match(source, /discoveryRadius/); + assert.match(source, /Math\.hypot/); +}); + +test("CalorieVerse integrates free-roam state without removing click-to-walk accessibility", async () => { + const source = await readFile(worldUrl, "utf8"); + assert.match(source, /moveWorld/); + assert.match(source, /worldKeyDirection/); + assert.match(source, /nearestWorldRegion/); + assert.match(source, /walkTo/); + assert.match(source, /gameverse-free-roam-controls/); +}); From b3f4a193292f68c4a67d1a8465e9e3936f329d66 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:33:09 +0200 Subject: [PATCH 346/368] Add multilingual free-roam controls for CalorieVerse --- frontend/config/gameverse-copy.json | 77 ++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/frontend/config/gameverse-copy.json b/frontend/config/gameverse-copy.json index d3259e06..c8d18c4e 100644 --- a/frontend/config/gameverse-copy.json +++ b/frontend/config/gameverse-copy.json @@ -43,7 +43,12 @@ "route": "Ecosystem route", "routeProgress": "landmarks found", "routeHint": "Choose your own order. Four ecosystem landmarks open the maze route.", - "routeReady": "Core landmarks found. The maze route is open." + "routeReady": "Core landmarks found. The maze route is open.", + "freeRoam": "Free roam", + "moveUp": "Move up", + "moveDown": "Move down", + "moveLeft": "Move left", + "moveRight": "Move right" }, "nl": { "title": "CalorieVerse", @@ -89,7 +94,12 @@ "route": "Ecosysteemroute", "routeProgress": "herkenningspunten gevonden", "routeHint": "Kies je eigen volgorde. Vier ecosysteemplekken openen de doolhofroute.", - "routeReady": "De kernplekken zijn gevonden. De doolhofroute is open." + "routeReady": "De kernplekken zijn gevonden. De doolhofroute is open.", + "freeRoam": "Vrij bewegen", + "moveUp": "Omhoog", + "moveDown": "Omlaag", + "moveLeft": "Naar links", + "moveRight": "Naar rechts" }, "zh-Hans": { "title": "CalorieVerse", @@ -125,7 +135,12 @@ "route": "生态路线", "routeProgress": "已找到地标", "routeHint": "按自己的顺序探索。四个生态地标会开启迷宫路线。", - "routeReady": "核心地标已找到。迷宫路线已开放。" + "routeReady": "核心地标已找到。迷宫路线已开放。", + "freeRoam": "自由探索", + "moveUp": "向上移动", + "moveDown": "向下移动", + "moveLeft": "向左移动", + "moveRight": "向右移动" }, "hi": { "title": "CalorieVerse", @@ -161,7 +176,12 @@ "route": "इकोसिस्टम मार्ग", "routeProgress": "स्थलचिह्न मिले", "routeHint": "अपना क्रम चुनें। चार इकोसिस्टम स्थलचिह्न भूलभुलैया मार्ग खोलते हैं।", - "routeReady": "मुख्य स्थलचिह्न मिल गए। भूलभुलैया मार्ग खुला है।" + "routeReady": "मुख्य स्थलचिह्न मिल गए। भूलभुलैया मार्ग खुला है।", + "freeRoam": "स्वतंत्र घूमना", + "moveUp": "ऊपर जाएँ", + "moveDown": "नीचे जाएँ", + "moveLeft": "बाएँ जाएँ", + "moveRight": "दाएँ जाएँ" }, "es": { "title": "CalorieVerse", @@ -197,7 +217,12 @@ "route": "Ruta del ecosistema", "routeProgress": "lugares encontrados", "routeHint": "Elige tu propio orden. Cuatro lugares del ecosistema abren la ruta del laberinto.", - "routeReady": "Se encontraron los lugares principales. La ruta del laberinto está abierta." + "routeReady": "Se encontraron los lugares principales. La ruta del laberinto está abierta.", + "freeRoam": "Exploración libre", + "moveUp": "Mover arriba", + "moveDown": "Mover abajo", + "moveLeft": "Mover a la izquierda", + "moveRight": "Mover a la derecha" }, "ar": { "title": "CalorieVerse", @@ -233,7 +258,12 @@ "route": "مسار المنظومة", "routeProgress": "معالم تم العثور عليها", "routeHint": "اختر ترتيبك بنفسك. أربعة معالم من المنظومة تفتح مسار المتاهة.", - "routeReady": "تم العثور على المعالم الأساسية. مسار المتاهة مفتوح." + "routeReady": "تم العثور على المعالم الأساسية. مسار المتاهة مفتوح.", + "freeRoam": "تجول حر", + "moveUp": "تحرك للأعلى", + "moveDown": "تحرك للأسفل", + "moveLeft": "تحرك لليسار", + "moveRight": "تحرك لليمين" }, "fr": { "title": "CalorieVerse", @@ -269,7 +299,12 @@ "route": "Route de l’écosystème", "routeProgress": "repères trouvés", "routeHint": "Choisissez votre ordre. Quatre repères de l’écosystème ouvrent la route du labyrinthe.", - "routeReady": "Les repères principaux sont trouvés. La route du labyrinthe est ouverte." + "routeReady": "Les repères principaux sont trouvés. La route du labyrinthe est ouverte.", + "freeRoam": "Exploration libre", + "moveUp": "Monter", + "moveDown": "Descendre", + "moveLeft": "Aller à gauche", + "moveRight": "Aller à droite" }, "bn": { "title": "CalorieVerse", @@ -305,7 +340,12 @@ "route": "ইকোসিস্টেম পথ", "routeProgress": "ল্যান্ডমার্ক পাওয়া গেছে", "routeHint": "নিজের ক্রম বেছে নিন। চারটি ইকোসিস্টেম ল্যান্ডমার্ক গোলকধাঁধার পথ খুলবে।", - "routeReady": "মূল ল্যান্ডমার্ক পাওয়া গেছে। গোলকধাঁধার পথ খুলেছে।" + "routeReady": "মূল ল্যান্ডমার্ক পাওয়া গেছে। গোলকধাঁধার পথ খুলেছে।", + "freeRoam": "স্বাধীন চলাফেরা", + "moveUp": "উপরে যান", + "moveDown": "নিচে যান", + "moveLeft": "বামে যান", + "moveRight": "ডানে যান" }, "pt": { "title": "CalorieVerse", @@ -341,7 +381,12 @@ "route": "Rota do ecossistema", "routeProgress": "pontos encontrados", "routeHint": "Escolha a sua ordem. Quatro pontos do ecossistema abrem a rota do labirinto.", - "routeReady": "Os pontos principais foram encontrados. A rota do labirinto está aberta." + "routeReady": "Os pontos principais foram encontrados. A rota do labirinto está aberta.", + "freeRoam": "Exploração livre", + "moveUp": "Mover para cima", + "moveDown": "Mover para baixo", + "moveLeft": "Mover para a esquerda", + "moveRight": "Mover para a direita" }, "id": { "title": "CalorieVerse", @@ -377,7 +422,12 @@ "route": "Rute ekosistem", "routeProgress": "landmark ditemukan", "routeHint": "Pilih urutanmu sendiri. Empat landmark ekosistem membuka rute labirin.", - "routeReady": "Landmark utama ditemukan. Rute labirin terbuka." + "routeReady": "Landmark utama ditemukan. Rute labirin terbuka.", + "freeRoam": "Jelajah bebas", + "moveUp": "Bergerak ke atas", + "moveDown": "Bergerak ke bawah", + "moveLeft": "Bergerak ke kiri", + "moveRight": "Bergerak ke kanan" }, "ur": { "title": "CalorieVerse", @@ -413,6 +463,11 @@ "route": "ایکوسسٹم راستہ", "routeProgress": "نشانات ملے", "routeHint": "اپنی ترتیب خود چنیں۔ چار ایکوسسٹم مقامات بھول بھلیاں کا راستہ کھولتے ہیں۔", - "routeReady": "بنیادی نشانات مل گئے۔ بھول بھلیاں کا راستہ کھل گیا ہے۔" + "routeReady": "بنیادی نشانات مل گئے۔ بھول بھلیاں کا راستہ کھل گیا ہے۔", + "freeRoam": "آزاد گھومنا", + "moveUp": "اوپر جائیں", + "moveDown": "نیچے جائیں", + "moveLeft": "بائیں جائیں", + "moveRight": "دائیں جائیں" } } From d4cab54ae7045e0dc319c11f35c29ba53c66342d Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:33:13 +0200 Subject: [PATCH 347/368] Type CalorieVerse free-roam copy --- frontend/lib/gameverseWorld.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/lib/gameverseWorld.ts b/frontend/lib/gameverseWorld.ts index 7f7deaaf..46b23e41 100644 --- a/frontend/lib/gameverseWorld.ts +++ b/frontend/lib/gameverseWorld.ts @@ -36,6 +36,11 @@ export type GameverseCopy = { routeProgress: string; routeHint: string; routeReady: string; + freeRoam: string; + moveUp: string; + moveDown: string; + moveLeft: string; + moveRight: string; locations: Record; descriptions: Record; }; From 5b4b35e5e63401adba3fadce6091c31cf5ba2251 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:33:41 +0200 Subject: [PATCH 348/368] Add free-roam keyboard and touch movement to CalorieVerse --- frontend/components/GameverseWorld.tsx | 56 ++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/frontend/components/GameverseWorld.tsx b/frontend/components/GameverseWorld.tsx index 4c6ff9d0..4f86a9f1 100644 --- a/frontend/components/GameverseWorld.tsx +++ b/frontend/components/GameverseWorld.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import Link from "next/link"; -import { useEffect, useMemo, useState } from "react"; +import { type KeyboardEvent, useEffect, useMemo, useState } from "react"; import { AgeExperienceControl, useAgeExperience } from "@/components/AgeExperienceControl"; import { DisplayLanguagePicker, useDisplayLanguage } from "@/components/DisplayLanguageProvider"; import { CalorieStudio } from "@/components/CalorieStudio"; @@ -10,6 +10,7 @@ import { GameverseMazeRoute } from "@/components/GameverseMazeRoute"; import { ParticipationChoiceCard } from "@/components/ParticipationChoiceCard"; import { CalorieVerseInteractionCard } from "@/components/CalorieVerseInteractionCard"; import { GALLERY_DRAFTS_KEY, parseLocalGalleryDrafts, type LocalGalleryDraft } from "@/lib/galleryEcosystem"; +import { moveWorld, nearestWorldRegion, safeWorldPosition, worldKeyDirection, type WorldDirection, type WorldPosition } from "@/lib/calorieVerseMovement"; import { CALORIEVERSE_INTERACTION_KEY, completeInteraction, @@ -41,6 +42,7 @@ type SavedProgress = { current_region_id: string; visited_region_ids: string[]; maze_complete?: boolean; + position?: WorldPosition; }; function ageLabel(ageBand: "child" | "teen" | "adult", copy: ReturnType["copy"]) { @@ -106,6 +108,7 @@ export function GameverseWorld() { const [starterId, setStarterId] = useState(starterIdentity().starter_character_id); const [currentRegionId, setCurrentRegionId] = useState(gameverseWorld.start.region_id); const [selectedRegionId, setSelectedRegionId] = useState(gameverseWorld.start.region_id); + const [playerPosition, setPlayerPosition] = useState({x: gameverseWorld.start.x, y: gameverseWorld.start.y}); const [visited, setVisited] = useState([gameverseWorld.start.region_id]); const [localGalleryDrafts, setLocalGalleryDrafts] = useState([]); const [mazeCompleteState, setMazeCompleteState] = useState(false); @@ -133,9 +136,16 @@ export function GameverseWorld() { if (stored?.version === 1 && typeof stored.current_region_id === "string" && Array.isArray(stored.visited_region_ids)) { const safeVisited = uniqueVisited(stored.visited_region_ids); const currentExists = gameverseRegions.some(region => region.id === stored.current_region_id); + const restoredRegion = currentExists + ? gameverseRegions.find(region => region.id === stored.current_region_id) + : gameverseRegions.find(region => region.id === gameverseWorld.start.region_id); + const fallbackPosition = restoredRegion + ? {x: restoredRegion.x, y: restoredRegion.y} + : {x: gameverseWorld.start.x, y: gameverseWorld.start.y}; setVisited(safeVisited.length ? safeVisited : [gameverseWorld.start.region_id]); setCurrentRegionId(currentExists ? stored.current_region_id : gameverseWorld.start.region_id); setSelectedRegionId(currentExists ? stored.current_region_id : gameverseWorld.start.region_id); + setPlayerPosition(safeWorldPosition(stored.position, fallbackPosition)); setMazeCompleteState(stored.maze_complete === true); } } catch { @@ -154,12 +164,13 @@ export function GameverseWorld() { current_region_id: currentRegionId, visited_region_ids: uniqueVisited(visited), maze_complete: mazeCompleteState, + position: playerPosition, }; window.localStorage.setItem(PROGRESS_KEY, JSON.stringify(progress)); } catch { // Playing without persistent browser storage remains supported. } - }, [loaded, starterId, currentRegionId, visited, mazeCompleteState]); + }, [loaded, starterId, currentRegionId, visited, mazeCompleteState, playerPosition]); useEffect(() => { if (!loaded) return; @@ -180,7 +191,7 @@ export function GameverseWorld() { [ageBand, mazeCompleteState] ); const selected = regions.find(region => region.id === selectedRegionId) ?? regions[0]; - const current = gameverseRegions.find(region => region.id === currentRegionId) ?? gameverseRegions[0]; + const nearbyRegion = nearestWorldRegion(playerPosition, regions); const mazeReady = mazeUnlocked(visited); const coreProgress = coreRouteProgress(visited); const coreRouteIds = new Set(gameverseWorld.progression.maze_required_regions as string[]); @@ -189,11 +200,29 @@ export function GameverseWorld() { function walkTo(region: GameverseRegion) { if (!ageBand || !regionIsInteractive(region, ageBand)) return; if (region.kind === "maze" && !mazeReady) return; + setPlayerPosition({x: region.x, y: region.y}); setCurrentRegionId(region.id); setSelectedRegionId(region.id); setVisited(previous => uniqueVisited([...previous, region.id])); } + function movePlayer(direction: WorldDirection) { + const next = moveWorld(playerPosition, direction, mazeCompleteState); + setPlayerPosition(next); + const discovered = nearestWorldRegion(next, regions); + if (!discovered) return; + setCurrentRegionId(discovered.id); + setSelectedRegionId(discovered.id); + setVisited(previous => uniqueVisited([...previous, discovered.id])); + } + + function handleWorldKeyDown(event: KeyboardEvent) { + const direction = worldKeyDirection(event.key); + if (!direction) return; + event.preventDefault(); + movePlayer(direction); + } + function finishInteraction(interactionId: string) { setInteractionProgress(previous => completeInteraction(previous, interactionId)); } @@ -242,7 +271,8 @@ export function GameverseWorld() { worldEffects.routeWhisper ? "has-route-whisper" : "", ].filter(Boolean).join(" ")} tabIndex={0} - aria-label={copy.world} + onKeyDown={handleWorldKeyDown} + aria-label={copy.world + " · " + copy.freeRoam} >
    From afb1e7f93b168eb3d24e626dc0c57846078b994e Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 20:12:15 +0200 Subject: [PATCH 365/368] Style CalorieVerse nickname and stable starter identity together --- frontend/app/globals.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index ee0d1d68..bad0663e 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1778,3 +1778,12 @@ body { grid-template-columns: 1fr; } } + +.gameverse-starter-id { + display: block; + margin-top: .15rem; + color: #7c8992; + font-size: .62rem; + line-height: 1.25; + overflow-wrap: anywhere; +} From f8711ee1a7d3c6e43c300fc6b1123ebe0329ad44 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sat, 19 Sep 2026 20:12:23 +0200 Subject: [PATCH 366/368] Test CalorieApp nickname continuity inside CalorieVerse --- .../calorieverse_identity_presence.test.mjs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tools/tests/calorieverse_identity_presence.test.mjs diff --git a/tools/tests/calorieverse_identity_presence.test.mjs b/tools/tests/calorieverse_identity_presence.test.mjs new file mode 100644 index 00000000..01e170d0 --- /dev/null +++ b/tools/tests/calorieverse_identity_presence.test.mjs @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const identityUrl = new URL("../../frontend/components/CalorieVerseIdentityPresence.tsx", import.meta.url); +const worldUrl = new URL("../../frontend/components/GameverseWorld.tsx", import.meta.url); + +test("CalorieVerse nickname follows authenticated adult identity without wallet exposure", async () => { + const source = await readFile(identityUrl,"utf8"); + assert.match(source,/\/api\/backend\/api\/identity\/me/); + assert.match(source,/ageBand !== "adult"/); + assert.match(source,/safeNickname/); + assert.doesNotMatch(source,/wallet|account_address|classic_address/i); +}); + +test("stable starter identity remains visible when nickname is present", async () => { + const source = await readFile(identityUrl,"utf8"); + assert.match(source,/nickname \? \(/); + assert.match(source,/data-calorieverse-starter-id/); + assert.match(source,/starterId/); +}); + +test("Gameverse identity card uses the shared CalorieVerse nickname presence component", async () => { + const source = await readFile(worldUrl,"utf8"); + assert.match(source,/CalorieVerseIdentityPresence/); + assert.match(source,/ageBand=\{ageBand\}/); + assert.match(source,/starterId=\{starterId\}/); +}); From 84f8a43243a9a9a85b75cb4042a44ff88121cf44 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:11:23 +0200 Subject: [PATCH 367/368] Continue CalorieVerse meadow preview with additive food activity --- .github/copilot-instructions.md | 24 ++ docs/CALORIEVERSE_PREVIEW_CHECKPOINT.md | 110 +++++ frontend/app/gameverse/preview/page.tsx | 12 + .../components/CalorieVerseMeadow.module.css | 61 +++ frontend/components/CalorieVerseMeadow.tsx | 276 +++++++++++++ .../config/calorieverse-food-activity.json | 15 + frontend/config/calorieverse-food-copy.json | 134 ++++++ frontend/config/calorieverse-meadow-copy.json | 387 ++++++++++++++++++ frontend/config/calorieverse-meadow.json | 18 + frontend/lib/calorieVerseActivities.ts | 35 ++ frontend/lib/calorieVerseMeadow.ts | 79 ++++ tools/tests/calorieverse_activities.test.mjs | 82 ++++ tools/tests/calorieverse_meadow.test.mjs | 80 ++++ tools/tests/calorieverse_meadow_ui.test.mjs | 105 +++++ .../participation_choice_bridge.test.mjs | 7 +- 15 files changed, 1423 insertions(+), 2 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 docs/CALORIEVERSE_PREVIEW_CHECKPOINT.md create mode 100644 frontend/app/gameverse/preview/page.tsx create mode 100644 frontend/components/CalorieVerseMeadow.module.css create mode 100644 frontend/components/CalorieVerseMeadow.tsx create mode 100644 frontend/config/calorieverse-food-activity.json create mode 100644 frontend/config/calorieverse-food-copy.json create mode 100644 frontend/config/calorieverse-meadow-copy.json create mode 100644 frontend/config/calorieverse-meadow.json create mode 100644 frontend/lib/calorieVerseActivities.ts create mode 100644 frontend/lib/calorieVerseMeadow.ts create mode 100644 tools/tests/calorieverse_activities.test.mjs create mode 100644 tools/tests/calorieverse_meadow.test.mjs create mode 100644 tools/tests/calorieverse_meadow_ui.test.mjs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..5a6729d0 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,24 @@ +# CalorieApp project continuity + +- Continue the existing checkpoint; never rebuild the CalorieVerse concept from + scratch. Read `docs/CALORIEVERSE_PREVIEW_CHECKPOINT.md` before changing it. +- Draft PR #150, branch `feat/participation-node-simulator`, continues from + `f8711ee1a7d3c6e43c300fc6b1123ebe0329ad44`. No merge or deployment is authorized + for this work. Keep the PR a draft; do not change live services or WordPress. +- `/gameverse/preview` is the small meadow review route. `/gameverse` retains + the larger existing draft with Studio, identity and participation integrations. + The extra route is a review boundary, not a second world or product version. +- CalorieVerse grows continuously: preserve the original world/starter IDs, + module IDs, storage keys and earned progress. Never require a player reset. +- Reuse the eleven-language display registry. Keep UI copy out of reducers. +- Content packs, pure activity reducers, rendering and persistence are separate. + Unknown module fields must survive; unsupported formats must not be overwritten. +- Ordinary play requires no account, wallet, storage contribution, compute role + or reward. Participation is separately opt-in, capped and reversible, with + independent storage/compute consent and pause/resume/full-stop controls. +- Local guest progress is untrusted and cannot authorize calT/CAL/XRP rewards. + Synthetic simulators are not an active network or settlement system. +- Never place private food logs, identity, credentials or age records in public + participant data. Preserve the existing private PostgreSQL boundary. +- Keep deferred architecture, publication and governance work in repository docs; + never store secrets or temporary chat credentials here. diff --git a/docs/CALORIEVERSE_PREVIEW_CHECKPOINT.md b/docs/CALORIEVERSE_PREVIEW_CHECKPOINT.md new file mode 100644 index 00000000..37d4ecfb --- /dev/null +++ b/docs/CALORIEVERSE_PREVIEW_CHECKPOINT.md @@ -0,0 +1,110 @@ +# CalorieVerse preview checkpoint — 21 September 2026 + +## Baseline and scope + +Continue draft PR #150, `feat/participation-node-simulator`, directly from +`f8711ee1a7d3c6e43c300fc6b1123ebe0329ad44`. The user explicitly prohibited merge +and deployment. This change adds a review route; it does not replace existing +`/gameverse`, Gallery/Studio, identity, participation or backend code. + +Recovered meadow source: commits `e23f9d8896e1f0565be8e006b3e618defd686c87` +and `7f60f3222da93bfe13dba3ddc7d9aafbe3a655f3` from the existing local release +checkout. Reuse is limited to its meadow component/CSS, copy/config, pure state +module and tests. The release branch and unrelated WordPress changes are not +merged into this draft. This document does not assert the current live site state. + +## Review the small slice + +From `frontend`, run `npm ci`, then `npm run dev -- --port 3100`. +Open `http://localhost:3100/gameverse/preview?ui_lang=nl` on that machine. +The normal shared display-language picker supports all eleven locales. + +1. Walk with arrow/WASD keys, the touch pad, or a click/tap on the meadow. +2. Collect the three original apples; plant, water and harvest the garden. +3. Visit the new food-data stand beside the path. An object click approaches it; + the action card enables the activity when the character is nearby. +4. Match the food illustration to its name. An incorrect answer offers a retry; + a correct answer completes the local activity once. +5. Pause/resume, reload and revisit: original garden and food activity progress + remain. The CalorieApp link carries the selected display language. + +The example is invented and clearly labelled as practice. It is not an OFF/USDA +record, database edit, real contribution proof or token reward. No participation +runtime, account request or food-data API is invoked by the meadow component. +Progress belongs to this browser/origin. This preview does not sync accounts, +implement multiplayer or distribute data to volunteers. No new infrastructure +or paid service is required. + +## Implemented growth boundaries + +| Layer | Existing or new boundary | +| --- | --- | +| World identity | Existing `calorie-gameverse-starter-world` and `welcome-meadow` | +| Original activity | Existing `welcome-meadow.garden` and permanent object IDs | +| New content | `config/calorieverse-food-activity.json`, module `welcome-meadow.food-data` | +| Existing F&B connection | Reuses `food-data-reviewer` role ID; does not change selected F&B role | +| Logic | Pure `calorieVerseActivities.ts` reducer; accepts only nearby, correct, first completion | +| Rendering | Recovered `CalorieVerseMeadow`; same scene, one additional stand | +| Persistence | Existing `calorie.calorieverse.meadow.v1`; additive `modules[module_id]` records | +| Compatibility | Unknown modules/fields retained; unsupported activity formats are left untouched | +| Localization | Shared provider/locale registry and separate eleven-language copy packs | + +The starter-character key is retained without replacing a previous choice. Other +world, Gallery/Studio, F&B and participation keys remain untouched. Activity +completion does not reset or gate the garden, and the garden does not gate the +new activity. Movement is ephemeral and saves remain debounced plus flushed on +page exit. Keyboard motion also stops when focus leaves the scene. + +There is no new consumer-facing version or replacement world. The separate URL +allows this small slice to be reviewed against the larger draft before any +future entry-route decision. Schema numbers are internal compatibility markers. + +## Scaling from here + +Keep `docs/DECENTRALIZED_PARTICIPATION_ARCHITECTURE.md`, the participation +contracts and simulator tools as the larger foundation. This change establishes +an additive module boundary; it does not claim distributed scalability is tested. + +- Add a new content pack and deterministic activity reducer under its own stable + module ID. Keep provider/network calls outside the game reducer and movement. +- For real food data, introduce a read-only adapter with source/provenance and + redistribution eligibility. Do not relabel this invented sample as live data. +- For account persistence, add an authenticated progress adapter and explicit + guest-import choice. Separate public content from private player state and + define conflict handling/idempotency before synchronizing multiple devices. +- Add shared presence separately from durable progress; never send position + updates as reward evidence or write each animation frame to a database. +- Connect eligible public-data work to the existing synthetic participation + adapters only after review. Consent, bounded work, independent verification, + resource limits, pause/full stop and zero-volunteer hosted fallback remain + mandatory. Browser participation, XRPL validators and settlement are distinct. +- Real Testnet settlement, a token peg/supply decision, DAO governance, revenue + policy and changes to public data authority remain separate deferred decisions. + No automatic paid scaling, deployment or network-phase promotion is enabled. + +## Verification and limits + +- 76 focused Node tests passed for CalorieVerse, Gameverse and participation UI. + These include 14 meadow/activity tests: full garden loop, preservation/reload, + repeat protection, proximity, wrong-answer retry, future saves, pause/resume, + retained starter identity, eleven-locale coverage and Arabic RTL DOM behavior. +- 124 Python tests passed for storage/compute, UI adapter, growth, reliability, + public-data release, resource policy and character identity simulators. +- TypeScript and production build passed. Lint reports only two existing + `react-hooks/exhaustive-deps` warnings in `TestnetEntry.tsx:135`. +- A pre-existing test expected the participation chooser inside Gallery's route + file. It now follows Gallery's actual `CalorieStudioPage` delegation and still + checks the shared chooser; no Gallery/Studio product behavior was changed. +- Cloud browser navigation to the local preview failed with + `net::ERR_BLOCKED_BY_CLIENT`. Visual desktop/mobile and real browser input QA + remain unverified. The React DOM interaction tests do not replace that check. +- No merge, deployment, WordPress edit, paid infrastructure change, financial + action or live participation activation was performed. + +## Next bounded step + +Open this existing preview in a browser with access to the local server and +check desktop/mobile movement, touch controls and the stand placement. Then +review one read-only food-data adapter against the existing F&B role contract. +Keep this meadow and its progress; do not restart the design or merge/deploy as +part of continuation without a new instruction. diff --git a/frontend/app/gameverse/preview/page.tsx b/frontend/app/gameverse/preview/page.tsx new file mode 100644 index 00000000..56562595 --- /dev/null +++ b/frontend/app/gameverse/preview/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; +import { CalorieVerseMeadow } from "@/components/CalorieVerseMeadow"; + +export const metadata: Metadata = { + title: "CalorieVerse — meadow preview", + description: "Explore the meadow and try a small food-data activity.", + robots: { index: false, follow: false }, +}; + +export default function MeadowPreviewPage() { + return ; +} diff --git a/frontend/components/CalorieVerseMeadow.module.css b/frontend/components/CalorieVerseMeadow.module.css new file mode 100644 index 00000000..002321d0 --- /dev/null +++ b/frontend/components/CalorieVerseMeadow.module.css @@ -0,0 +1,61 @@ +.page { min-height:100svh; padding:clamp(12px,2.5vw,32px); background:radial-gradient(ellipse at top right,#ecf4ec,transparent 55%),#f4f6f9; color:#263d37; } +.header { max-width:1420px; margin:0 auto 24px; display:flex; align-items:center; justify-content:space-between; gap:16px; } +.brand { display:flex; align-items:center; gap:12px; text-decoration:none; color:#505ba9; font-size:1.35rem; font-weight:800; letter-spacing:-.04em; } +.brand small { display:block; font-size:.875rem; font-weight:400; letter-spacing:0; color:#546963; margin-top:2px; } +.headerActions { display:flex; align-items:center; gap:10px; } +.headerActions > details { margin:0 !important; max-width:260px; } +.page button { font:inherit; cursor:pointer; } +.page button:focus-visible,.page a:focus-visible { outline:3px solid #f9b233; outline-offset:4px; } +.page button:disabled { cursor:default; } +.headerActions > button,.worldHeading > button { min-height:44px; padding:9px 17px; border:1px solid #505ba930; background:white; border-radius:24px; color:#505ba9; font-size:.875rem; font-weight:700; } +.layout { display:grid; grid-template-columns:minmax(0,1fr) 290px; gap:22px; max-width:1420px; margin:auto; align-items:start; } +.worldPanel { overflow:hidden; border:1px solid #dae4db; border-radius:24px; background:#fff; box-shadow:0 14px 38px #2545350d; } +.worldHeading { display:flex; justify-content:space-between; align-items:center; gap:12px; padding:20px 24px; } +.eyebrow { display:block; font-size:.75rem; letter-spacing:.1em; text-transform:uppercase; font-weight:800; color:#467052; } +.worldHeading h1 { font-size:clamp(1.4rem,3vw,2rem); margin:3px 0 0; letter-spacing:-.04em; line-height:1.12; color:#263d37; } +.stage { position:relative; aspect-ratio:1000/650; min-height:380px; overflow:hidden; isolation:isolate; background:#96bd80; outline:none; touch-action:manipulation; direction:ltr; } +.stage:focus-visible { box-shadow:inset 0 0 0 4px #f9b233; } +.terrain { position:absolute; inset:0; width:100%; height:100%; pointer-events:none; } +.worldSign { position:absolute; top:10%; left:43%; color:#3e6454; font-size:clamp(.7rem,1.4vw,1.15rem); font-weight:800; transform:rotate(-5deg); letter-spacing:.06em; pointer-events:none; opacity:.65; } +.object { position:absolute; transform:translate(-50%,-50%); border:0; background:transparent; padding:0; display:grid; place-items:center; min-width:48px; min-height:48px; z-index:3; transition:filter .16s; } +.object:hover { filter:brightness(1.12); } +.apple { border-radius:50%; background:#fffc; box-shadow:0 3px 0 #63814b,0 7px 14px #2b56332d; } +.apple.selected { outline:3px solid #f9b233; outline-offset:4px; } +.apple.collected { opacity:.35; background:transparent; box-shadow:none; } +.collected > img { visibility:hidden; } +.objectLabel { white-space:nowrap; color:#253b31; font-weight:700; font-size:.75rem; background:#fffef2e8; border-radius:12px; padding:3px 8px; position:absolute; top:100%; margin-top:7px; } +.objectLabel:empty { display:none; } +.bed { width:85px; height:57px; display:flex; align-items:center; justify-content:center; background:repeating-linear-gradient(0deg,#98734f 0 8px,#aa845d 9px 16px); border:5px solid #ccad7b; box-shadow:0 5px 0 #6a7942; border-radius:10px; color:#e7f9c3; font-size:1.8rem; } +.selected .bed { outline:3px solid #f9b233; outline-offset:4px; } +.guideIcon { display:grid; place-items:center; position:relative; width:50px; height:52px; background:#505ba9; color:white; border-radius:21px 21px 17px 17px; font-size:2.1rem; font-weight:800; box-shadow:0 5px 0 #384271,0 10px 15px #2b56332d; } +.guideIcon > span { position:absolute; font-size:.7rem; letter-spacing:1px; top:14px; left:26px; } +.player { position:absolute; transform:translate(-50%,-85%); width:42px; height:56px; z-index:4; pointer-events:none; display:grid; place-items:center; } +.player > span { display:grid; place-items:center; width:37px; height:44px; background:#f9b233; color:#334258; font-size:1.4rem; font-weight:900; border:3px solid #fff5cc; border-radius:17px 17px 12px 12px; box-shadow:0 5px 0 #b5812c,0 13px 5px -3px #375e4660; } +.player small { position:absolute; top:62px; padding:2px 9px; border-radius:10px; background:#fffdef; font-size:.7rem; font-weight:800; color:#384a36; white-space:nowrap; } +.pad { position:absolute; bottom:16px; left:16px; display:grid; grid-template-columns:repeat(3,42px); grid-template-rows:repeat(2,42px); gap:4px; z-index:6; } +.pad button { background:#ffffffe6; border:1px solid #fff; border-radius:12px; box-shadow:0 3px 0 #496c4655; color:#334d3f; font-size:1.3rem; user-select:none; touch-action:none; } +.pad .up { grid-column:2; grid-row:1; }.pad .left { grid-column:1;grid-row:2; }.pad .down { grid-column:2;grid-row:2; }.pad .right { grid-column:3;grid-row:2; } +.pauseOverlay { position:absolute; inset:0; z-index:7; display:flex; align-items:center; justify-content:center; flex-direction:column; gap:16px; backdrop-filter:blur(4px); background:#1c443365; color:white; font-size:1.75rem; } +.pauseOverlay button,.actionCard button { border:0; border-radius:22px; background:#008d36; color:#fff; padding:12px 20px; min-height:44px; font-size:1rem; font-weight:700; } +.pauseOverlay button { background:#fff; color:#2f5841; }.actionCard button:disabled { opacity:.5; } +.instructions { padding:15px 22px; display:flex; flex-wrap:wrap; justify-content:space-between; gap:6px 16px; color:#445d51; font-size:.875rem; }.instructions small { font-size:.75rem; color:#596d63; } +.side { display:grid; gap:16px; } +.quest,.actionCard { border:1px solid #d9e3db; background:#fff; border-radius:22px; padding:23px; } +.quest { background:linear-gradient(145deg,#fff 45%,#f0f8ee); } +.side h2 { font-size:1.3rem; color:#2c4d3a; letter-spacing:-.025em; margin:6px 0 12px; } +.side p { font-size:.9375rem; line-height:1.65; margin:10px 0 18px; } +.questRow { display:flex; align-items:center; gap:9px; padding:10px 0; font-size:.875rem; border-top:1px solid #e4ede5; }.questRow strong { margin-inline-start:auto; font-variant-numeric:tabular-nums; } +.progress { background:#deebdd; height:6px; border-radius:8px; margin-top:15px; overflow:hidden; }.progress span { display:block; height:100%; background:#008d36; transition:width .3s; } +.actionCard .announcement { color:#007b32; font-size:.875rem; margin:14px 0 0; }.announcement:empty { display:none; } +.side .saveNote { font-size:.8rem; color:#5b6a64; padding:0 10px; margin:0; } +.appLink { font-size:.875rem; color:#505ba9; font-weight:700; padding:0 10px; } +@media(max-width:1050px) { .layout { grid-template-columns:minmax(0,1fr) 250px;gap:14px; }.quest,.actionCard { padding:18px; }.header { margin-bottom:16px; } } +@media(max-width:820px) { .layout { grid-template-columns:1fr; }.side { grid-template-columns:1fr 1fr; }.saveNote,.appLink { grid-column:1/-1; }.stage { min-height:370px; }.brand small { max-width:220px; }.headerActions > button { display:none; } } +@media(max-width:480px) { .page { padding:10px; }.header { align-items:start;gap:8px; }.brand { gap:7px; font-size:1.1rem; }.brand small { font-size:.75rem; max-width:165px; }.brand img { width:28px;height:28px; }.headerActions { max-width:145px; }.worldHeading { padding:16px; }.worldHeading h1 { font-size:1.4rem; }.worldPanel { border-radius:18px; }.stage { min-height:390px; }.side { grid-template-columns:1fr; }.bed { width:68px; }.apple { min-width:42px;min-height:42px; }.apple img { width:35px;height:35px; }.guideIcon { width:40px;height:44px;font-size:1.8rem; }.objectLabel { font-size:.65rem;padding:3px 5px; }.instructions { padding:13px 16px; }.pad { grid-template-columns:repeat(3,40px);grid-template-rows:repeat(2,40px);bottom:10px;left:10px; }.player { transform:translate(-50%,-95%) scale(.88); } } + +.foodStand { display:flex; align-items:center; gap:6px; background:#fff7db; border:4px solid #d7b875; border-top:10px solid #505ba9; border-radius:10px; padding:7px; box-shadow:0 5px 0 #746745; color:#008d36; font-weight:900; } +.selected .foodStand { outline:3px solid #f9b233; outline-offset:4px; } +.foodChoices { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; } +.actionCard .sampleNote { font-size:.8rem; color:#5b6a64; } +.actionCard a { color:#505ba9; font-weight:700; } +@media(max-width:480px) { .foodStand { padding:4px; }.foodStand img { width:28px;height:28px; } } diff --git a/frontend/components/CalorieVerseMeadow.tsx b/frontend/components/CalorieVerseMeadow.tsx new file mode 100644 index 00000000..604f76b0 --- /dev/null +++ b/frontend/components/CalorieVerseMeadow.tsx @@ -0,0 +1,276 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; +import { DisplayLanguagePicker, useDisplayLanguage } from "@/components/DisplayLanguageProvider"; +import { localeDirection } from "@/lib/locales"; +import translations from "@/config/calorieverse-meadow-copy.json"; +import foodTranslations from "@/config/calorieverse-food-copy.json"; +import { foodActivity, foodActivityStatus, reviewFoodSample } from "@/lib/calorieVerseActivities"; +import { + appleCount, initialMeadow, interact, meadow, meadowComplete, MEADOW_KEY, + moveToward, nearby, position, restoreMeadow, STARTER_KEY, + type MeadowObject, type MeadowState, type Point, +} from "@/lib/calorieVerseMeadow"; +import styles from "./CalorieVerseMeadow.module.css"; + +type Copy = typeof translations.en; +type SceneObject = MeadowObject | typeof foodActivity.object; +const sceneObjects: SceneObject[] = [...meadow.objects, foodActivity.object]; +const directionKeys: Record = { + ArrowUp: {x: 0, y: -1}, w: {x: 0, y: -1}, + ArrowDown: {x: 0, y: 1}, s: {x: 0, y: 1}, + ArrowLeft: {x: -1, y: 0}, a: {x: -1, y: 0}, + ArrowRight: {x: 1, y: 0}, d: {x: 1, y: 0}, +}; + +// A local scene renderer. Motion, public scenery and input never need server writes. +function MeadowTerrain() { + const ref = useRef(null); + useEffect(() => { + const ctx = ref.current?.getContext("2d"); + if (!ctx) return; + const gradient = ctx.createLinearGradient(0, 0, 0, 650); + gradient.addColorStop(0, "#b9d5a7"); gradient.addColorStop(1, "#74ac68"); + ctx.fillStyle = gradient; ctx.fillRect(0, 0, 1000, 650); + // Soft natural hills beyond the meadow; no mountain reveal or brand colours. + for (const [x,y,r,color] of [[100,-30,230,"#759481"],[380,-90,280,"#8fa99b"],[770,-60,300,"#90a89a"]] as const) { + ctx.fillStyle = color; ctx.beginPath(); ctx.ellipse(x,y,r,r*.65,0,0,Math.PI*2); ctx.fill(); + } + ctx.lineCap = "round"; + const path = () => { ctx.beginPath(); ctx.moveTo(95,650); ctx.bezierCurveTo(50,445,380,440,480,370); ctx.bezierCurveTo(610,285,490,195,565,117); }; + path(); ctx.strokeStyle = "#6b925a"; ctx.lineWidth = 67; ctx.stroke(); + path(); ctx.strokeStyle = "#d7c394"; ctx.lineWidth = 58; ctx.stroke(); + path(); ctx.strokeStyle = "#e2d0a5"; ctx.lineWidth = 44; ctx.stroke(); + ctx.beginPath(); ctx.moveTo(1000,545); ctx.bezierCurveTo(825,555,890,677,630,665); + ctx.strokeStyle = "#486f67"; ctx.lineWidth = 100; ctx.stroke(); + ctx.strokeStyle = "#7bbec7"; ctx.lineWidth = 83; ctx.stroke(); + ctx.strokeStyle = "#bbe0d7"; ctx.lineWidth = 3; ctx.stroke(); + for (let i=0; i<90; i++) { + const x=(i*137+29)%970, y=130+(i*83)%455; + ctx.fillStyle = i%5===0 ? "#eae0a1" : "#639656"; + ctx.beginPath(); ctx.ellipse(x,y,i%5===0?2.8:2,1.6,0,0,Math.PI*2); ctx.fill(); + } + const trees = [[45,200,1.1],[106,118,.95],[250,128,.95],[850,161,1.2],[944,275,1.2],[933,420,.85],[325,218,1.1]]; + for (const [x,y,s] of trees) { + ctx.fillStyle="#456a4940";ctx.beginPath();ctx.ellipse(x+12,y+48*s,38*s,15*s,0,0,Math.PI*2);ctx.fill(); + ctx.fillStyle="#76583b";ctx.fillRect(x-6*s,y,12*s,45*s); + for (const [dx,dy,r,c] of [[0,-22,40,"#40764c"],[-22,-9,28,"#467e4b"],[21,-16,29,"#528d53"],[0,-43,28,"#639b5f"]] as const) { + ctx.fillStyle=c;ctx.beginPath();ctx.arc(x+dx*s,y+dy*s,r*s,0,Math.PI*2);ctx.fill(); + } + } + }, []); + return