diff --git a/.cursorrules b/.cursorrules
new file mode 100644
index 0000000..95942c8
--- /dev/null
+++ b/.cursorrules
@@ -0,0 +1,91 @@
+You are an expert in React Router 7, TailwindCSS, and TypeScript, focusing on scalable web development.
+
+**Key Principles**
+
+- Provide clear, precise React Router 7 and TypeScript examples.
+- Apply immutability and pure functions where applicable.
+- Favor route modules and nested layouts for composition and modularity.
+- Use meaningful variable names (e.g., `isAuthenticated`, `userRole`).
+- Always use kebab-case for file names (e.g., `user-profile.tsx`).
+- Prefer named exports for loaders, actions, and components.
+
+**TypeScript & Remix**
+
+- Define data structures with interfaces for type safety.
+- Avoid the `any` type, fully utilize TypeScript's type system.
+- Organize files: imports, loaders/actions, component logic.
+- Use template strings for multi-line literals.
+- Utilize optional chaining and nullish coalescing.
+- Use nested layouts and dynamic routes where applicable.
+- Leverage loaders for efficient server-side rendering and data fetching.
+- Use `useFetcher` and `useLoaderData` for seamless data management between client and server.
+
+**File Naming Conventions**
+
+- `*.tsx` for React components
+- `*.ts` for utilities, types, and configurations
+- `root.tsx` for the root layout
+- All files use kebab-case.
+
+**Code Style**
+
+- Use single quotes for string literals.
+- Indent with 2 spaces.
+- Ensure clean code with no trailing whitespace.
+- Use `const` for immutable variables.
+- Use template strings for string interpolation.
+
+**Remix-Specific Guidelines**
+
+- Routes are mapped from URL schemas to files in `app/routes.ts`
+- Use `` for navigation, avoiding full page reloads.
+- Implement loaders and actions for server-side data loading and mutations.
+- Import type `Route` from `./+types/{route-filename}.ts` to type loaders and actions.
+- Ensure accessibility with semantic HTML and ARIA labels.
+- Leverage route-based loading, error boundaries, and catch boundaries.
+- Use the `useFetcher` hook for non-blocking data updates.
+- Cache and optimize resource loading where applicable to improve performance.
+
+**Import Order**
+
+1. React Router 7 core modules
+2. React and other core libraries
+3. Third-party packages
+4. Application-specific imports
+5. Environment-specific imports
+6. Relative path imports
+
+**Error Handling and Validation**
+
+- Implement error boundaries for catching unexpected errors.
+- Use custom error handling within loaders and actions.
+- Validate user input on both client and server using formData or JSON.
+
+**Testing**
+
+- Use `@testing-library/react` for component testing.
+- Write tests for loaders and actions ensuring data correctness.
+- Mock fetch requests and responses where applicable.
+
+**Performance Optimization**
+
+- Prefetch routes using `` for faster navigation.
+- Defer non-essential JavaScript using ``.
+- Optimize nested layouts to minimize re-rendering.
+- Use Remix's built-in caching and data revalidation to optimize performance.
+
+**Security**
+
+- Prevent XSS by sanitizing user-generated content.
+- Use Remix's CSRF protection for form submissions.
+- Handle sensitive data on the server, never expose in client code.
+
+**Key Conventions**
+
+- Use React Router 7's loaders and actions to handle server-side logic.
+- Focus on reusability and modularity across routes and components.
+- Follow Remix’s best practices for file structure and data fetching.
+- Optimize for performance and accessibility.
+
+**Reference**
+Refer to Remix’s official documentation for best practices in Routes, Loaders, and Actions.
diff --git a/.eslintrc b/.eslintrc
index 5205f98..2b510c4 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -19,7 +19,9 @@
"parser": "@typescript-eslint/parser",
"settings": {
"import/resolver": {
- "typescript": true
+ "typescript": {
+ "project": "./tsconfig.json"
+ }
}
},
"plugins": ["unused-imports", "react", "@typescript-eslint", "import", "prettier"],
diff --git a/.husky/pre-commit b/.husky/pre-commit
index fe9f891..1b35ad6 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# npm run typecheck
-npx lint-staged
+# npx lint-staged
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index e9eafa3..f03767a 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -4,6 +4,8 @@
"streetsidesoftware.code-spell-checker",
"bradlc.vscode-tailwindcss",
"eamodio.gitlens",
- "esbenp.prettier-vscode"
+ "esbenp.prettier-vscode",
+ "Lokalise.i18n-ally",
+ "inlang.vs-code-extension"
]
-}
+}
\ No newline at end of file
diff --git a/.vscode/i18n-ally-reviews.yml b/.vscode/i18n-ally-reviews.yml
new file mode 100644
index 0000000..e79af03
--- /dev/null
+++ b/.vscode/i18n-ally-reviews.yml
@@ -0,0 +1,3 @@
+# Review comments generated by i18n-ally. Please commit this file.
+
+reviews:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3f9b2b8..3398fb0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -14,5 +14,11 @@
"className",
"ngClass",
"tw"
- ]
+ ],
+ "i18n-ally.localesPaths": [
+ "app/i18n/locales",
+ ],
+ "i18n-ally.sourceLanguage": "en",
+ "i18n-ally.annotations": true,
+ "i18n-ally.keystyle": "nested"
}
diff --git a/src/assets/icons/cognactive-icon.svg b/app/assets/icons/cognactive-icon.svg
similarity index 100%
rename from src/assets/icons/cognactive-icon.svg
rename to app/assets/icons/cognactive-icon.svg
diff --git a/src/assets/icons/cognactive-icon.tsx b/app/assets/icons/cognactive-icon.tsx
similarity index 100%
rename from src/assets/icons/cognactive-icon.tsx
rename to app/assets/icons/cognactive-icon.tsx
diff --git a/src/assets/icons/cognactive-symbol.css b/app/assets/icons/cognactive-symbol.css
similarity index 100%
rename from src/assets/icons/cognactive-symbol.css
rename to app/assets/icons/cognactive-symbol.css
diff --git a/src/assets/icons/detail.svg b/app/assets/icons/detail.svg
similarity index 100%
rename from src/assets/icons/detail.svg
rename to app/assets/icons/detail.svg
diff --git a/src/components/brand/cognacitve-symbol.tsx b/app/components/brand/cognacitve-symbol.tsx
similarity index 100%
rename from src/components/brand/cognacitve-symbol.tsx
rename to app/components/brand/cognacitve-symbol.tsx
diff --git a/src/components/content-header.tsx/index.tsx b/app/components/content-header.tsx/index.tsx
similarity index 100%
rename from src/components/content-header.tsx/index.tsx
rename to app/components/content-header.tsx/index.tsx
diff --git a/src/components/date-picker/index.tsx b/app/components/date-picker/index.tsx
similarity index 77%
rename from src/components/date-picker/index.tsx
rename to app/components/date-picker/index.tsx
index 9fc88f6..d7637df 100644
--- a/src/components/date-picker/index.tsx
+++ b/app/components/date-picker/index.tsx
@@ -1,13 +1,15 @@
'use client'
import * as React from 'react'
-import { format } from 'date-fns'
+import { intlFormat } from 'date-fns'
import { Calendar as CalendarIcon } from 'lucide-react'
import { cn } from 'src/lib/utils'
import { Button } from 'src/components/ui/button'
import { Calendar } from 'src/components/ui/calendar'
import { Popover, PopoverContent, PopoverTrigger } from 'src/components/ui/popover'
+import { useLocale } from 'remix-i18next/react'
+import { useTranslation } from 'react-i18next'
type DatePickerProps = {
currentDate?: Date
@@ -23,6 +25,8 @@ export const DatePicker: React.FC = (props) => {
setDate(props.currentDate)
}, [props.currentDate])
+ const locale = useLocale('lang')
+ const { t } = useTranslation()
return (
@@ -31,7 +35,11 @@ export const DatePicker: React.FC = (props) => {
className={cn('w-[280px] justify-start text-left font-normal', !date && 'text-muted-foreground')}
>
- {date ? format(date, 'PPP') : Pick a date}
+ {date ? (
+ intlFormat(date, { month: 'long', day: 'numeric', year: 'numeric' }, { locale: locale })
+ ) : (
+ {t('tracker.pick_date')}
+ )}
diff --git a/src/components/error-page/index.tsx b/app/components/error-page/index.tsx
similarity index 100%
rename from src/components/error-page/index.tsx
rename to app/components/error-page/index.tsx
diff --git a/app/components/footer/index.tsx b/app/components/footer/index.tsx
new file mode 100644
index 0000000..54cc367
--- /dev/null
+++ b/app/components/footer/index.tsx
@@ -0,0 +1,47 @@
+import { Link } from 'react-router'
+import CognactiveIcon from 'src/assets/icons/cognactive-icon'
+import { ArrowUpRightSquare, Github } from 'lucide-react'
+import { GITHUB_REPO_BASE, TELEGRAM_CHAT_LINK } from 'src/constants'
+import { Button } from '../ui/button'
+import { LanguageSelector } from '../language-selector'
+import { useTranslation } from 'react-i18next'
+
+const Footer: React.FC = () => {
+ const { t } = useTranslation()
+
+ return (
+
+ )
+}
+
+export default Footer
diff --git a/src/components/header/index.tsx b/app/components/header/index.tsx
similarity index 84%
rename from src/components/header/index.tsx
rename to app/components/header/index.tsx
index 6ddd4c0..f48e2b2 100644
--- a/src/components/header/index.tsx
+++ b/app/components/header/index.tsx
@@ -1,18 +1,18 @@
-import React from 'react'
-
import CognactiveIcon from 'src/assets/icons/cognactive-icon'
import { cn } from 'src/lib/utils'
import { Button } from '../ui/button'
import { Github } from 'lucide-react'
import { Link, useNavigation, useMatches } from 'react-router'
import { useSpinDelay } from 'spin-delay'
+import { useTranslation } from 'react-i18next'
+// import { LanguageSelector } from '../language-selector'
interface IProps {
className?: string
}
export function Header(props: IProps) {
const matches = useMatches()
- const isHomepage = matches.findIndex((m) => m.pathname === '/') == matches.length - 1
+ const isHomepage = matches.every((m) => m.pathname === '/')
const transition = useNavigation()
const busy = transition.state !== 'idle'
@@ -21,6 +21,8 @@ export function Header(props: IProps) {
minDuration: 300,
})
+ const { t } = useTranslation()
+
return (
- {/* */}
{isHomepage && (
diff --git a/src/components/hero/index.tsx b/app/components/hero/index.tsx
similarity index 79%
rename from src/components/hero/index.tsx
rename to app/components/hero/index.tsx
index 5570ea4..46b2929 100644
--- a/src/components/hero/index.tsx
+++ b/app/components/hero/index.tsx
@@ -6,15 +6,15 @@ import {
LayoutGridIcon,
Scan,
MonitorPlay,
+ FlameIcon,
} from 'lucide-react'
import CognactiveIcon from 'src/assets/icons/cognactive-icon'
import { Button } from '../ui/button'
import { Link } from 'react-router'
-// Temporary. See docs/decisions/02-i18n
-import t from 'src/i18n/locales/en/translation.json'
import { GITHUB_REPO_BASE } from 'src/constants'
+import { useTranslation } from 'react-i18next'
interface FeatureProps {
icon: React.ElementType
@@ -46,9 +46,11 @@ const HeroFeature: React.FC = ({ icon: Icon, title, description, l
}
export const Hero = () => {
+ const { t } = useTranslation()
+
return (
-
+
@@ -57,21 +59,20 @@ export const Hero = () => {
- {t['hero-title']}
+ {t('homepage.hero-title')}
-
Open source tools and tech for the anti-fungal NAC protocol.
+
{t('homepage.hero-subtitle')}
{/*
-
+
)
}
diff --git a/app/components/language-selector/index.tsx b/app/components/language-selector/index.tsx
new file mode 100644
index 0000000..bb340e4
--- /dev/null
+++ b/app/components/language-selector/index.tsx
@@ -0,0 +1,81 @@
+import { useTranslation } from 'react-i18next'
+import { useCallback, useMemo, useState } from 'react'
+import { Languages, ChevronDown } from 'lucide-react'
+import i18next from 'i18next'
+import { Button } from '../ui/button'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog'
+import { cn } from 'src/lib/utils'
+import { supportedLanguages } from 'app/localization/resource'
+
+const getLocaleDisplayName = (locale: string, displayLocale?: string) => {
+ const displayName = new Intl.DisplayNames([displayLocale || locale], {
+ type: 'language',
+ }).of(locale)
+ return displayName ? displayName.charAt(0).toLocaleUpperCase() + displayName.slice(1) : locale
+}
+
+type LanguageSelectorProps = {
+ triggerClassName?: string
+}
+
+const LanguageSelector = ({ triggerClassName }: LanguageSelectorProps) => {
+ const { i18n } = useTranslation()
+ const [open, setOpen] = useState(false)
+
+ const localesAndNames = useMemo(() => {
+ return supportedLanguages.map((locale) => ({
+ locale,
+ name: getLocaleDisplayName(locale),
+ }))
+ }, [])
+
+ const languageChanged = useCallback(async (locale: any) => {
+ i18next.changeLanguage(locale)
+ const searchParams = new URLSearchParams(window.location.search)
+ searchParams.set('lng', locale)
+ window.history.replaceState(null, '', `${window.location.pathname}?${searchParams.toString()}`)
+ setOpen(false)
+ }, [])
+
+ const { resolvedLanguage: currentLanguage } = i18n
+
+ return (
+
+ )
+}
+
+export { LanguageSelector }
diff --git a/src/components/layout/index.tsx b/app/components/layout/index.tsx
similarity index 100%
rename from src/components/layout/index.tsx
rename to app/components/layout/index.tsx
diff --git a/src/components/lore/index.tsx b/app/components/lore/index.tsx
similarity index 100%
rename from src/components/lore/index.tsx
rename to app/components/lore/index.tsx
diff --git a/src/components/ui/alert.tsx b/app/components/ui/alert.tsx
similarity index 100%
rename from src/components/ui/alert.tsx
rename to app/components/ui/alert.tsx
diff --git a/src/components/ui/button.tsx b/app/components/ui/button.tsx
similarity index 100%
rename from src/components/ui/button.tsx
rename to app/components/ui/button.tsx
diff --git a/src/components/ui/calendar.tsx b/app/components/ui/calendar.tsx
similarity index 100%
rename from src/components/ui/calendar.tsx
rename to app/components/ui/calendar.tsx
diff --git a/src/components/ui/card.tsx b/app/components/ui/card.tsx
similarity index 100%
rename from src/components/ui/card.tsx
rename to app/components/ui/card.tsx
diff --git a/src/components/ui/carousel.tsx b/app/components/ui/carousel.tsx
similarity index 100%
rename from src/components/ui/carousel.tsx
rename to app/components/ui/carousel.tsx
diff --git a/src/components/ui/dialog.tsx b/app/components/ui/dialog.tsx
similarity index 100%
rename from src/components/ui/dialog.tsx
rename to app/components/ui/dialog.tsx
diff --git a/src/components/ui/hero-video-dialog.tsx b/app/components/ui/hero-video-dialog.tsx
similarity index 100%
rename from src/components/ui/hero-video-dialog.tsx
rename to app/components/ui/hero-video-dialog.tsx
diff --git a/src/components/ui/input.tsx b/app/components/ui/input.tsx
similarity index 100%
rename from src/components/ui/input.tsx
rename to app/components/ui/input.tsx
diff --git a/src/components/ui/label.tsx b/app/components/ui/label.tsx
similarity index 100%
rename from src/components/ui/label.tsx
rename to app/components/ui/label.tsx
diff --git a/src/components/ui/meteors.tsx b/app/components/ui/meteors.tsx
similarity index 100%
rename from src/components/ui/meteors.tsx
rename to app/components/ui/meteors.tsx
diff --git a/src/components/ui/pagination.tsx b/app/components/ui/pagination.tsx
similarity index 100%
rename from src/components/ui/pagination.tsx
rename to app/components/ui/pagination.tsx
diff --git a/src/components/ui/popover.tsx b/app/components/ui/popover.tsx
similarity index 100%
rename from src/components/ui/popover.tsx
rename to app/components/ui/popover.tsx
diff --git a/src/components/ui/progress.tsx b/app/components/ui/progress.tsx
similarity index 100%
rename from src/components/ui/progress.tsx
rename to app/components/ui/progress.tsx
diff --git a/src/components/ui/sheet.tsx b/app/components/ui/sheet.tsx
similarity index 100%
rename from src/components/ui/sheet.tsx
rename to app/components/ui/sheet.tsx
diff --git a/src/components/ui/switch.tsx b/app/components/ui/switch.tsx
similarity index 100%
rename from src/components/ui/switch.tsx
rename to app/components/ui/switch.tsx
diff --git a/src/components/ui/table.tsx b/app/components/ui/table.tsx
similarity index 100%
rename from src/components/ui/table.tsx
rename to app/components/ui/table.tsx
diff --git a/src/components/ui/textarea.tsx b/app/components/ui/textarea.tsx
similarity index 100%
rename from src/components/ui/textarea.tsx
rename to app/components/ui/textarea.tsx
diff --git a/src/constants.ts b/app/constants.ts
similarity index 100%
rename from src/constants.ts
rename to app/constants.ts
diff --git a/src/data/studies.tsx b/app/data/studies.tsx
similarity index 100%
rename from src/data/studies.tsx
rename to app/data/studies.tsx
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
index 319d5e0..1f000d4 100644
--- a/app/entry.client.tsx
+++ b/app/entry.client.tsx
@@ -1,12 +1,55 @@
import React, { startTransition, StrictMode } from 'react'
+import i18next from 'i18next'
+import LanguageDetector from 'i18next-browser-languagedetector'
+import Fetch from 'i18next-fetch-backend'
+
+import { I18nextProvider, initReactI18next } from 'react-i18next'
import { HydratedRouter } from 'react-router/dom'
+import { getInitialNamespaces } from 'remix-i18next/client'
+import i18n from '@/localization/i18n'
+
import { hydrateRoot } from 'react-dom/client'
-startTransition(() => {
- hydrateRoot(
- document,
-
-
- ,
- )
-})
+async function hydrate() {
+ // eslint-disable-next-line import/no-named-as-default-member
+ await i18next
+ .use(initReactI18next) // Tell i18next to use the react-i18next plugin
+ .use(LanguageDetector) // Setup a client-side language detector
+ .use(Fetch) // Setup your backend
+ .init({
+ ...i18n, // spread the configuration
+ // This function detects the namespaces your routes rendered while SSR use
+ ns: getInitialNamespaces(),
+ backend: {
+ loadPath: '/resource/locales?lng={{lng}}&ns={{ns}}',
+ },
+ detection: {
+ // Here only enable htmlTag detection, we'll detect the language only
+ // server-side with remix-i18next, by using the `` attribute
+ // we can communicate to the client the language detected server-side
+ order: ['htmlTag'],
+ // Because we only use htmlTag, there's no reason to cache the language
+ // on the browser, so we disable it
+ caches: [],
+ },
+ })
+
+ startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+
+ ,
+ )
+ })
+}
+
+if (window.requestIdleCallback) {
+ window.requestIdleCallback(hydrate)
+} else {
+ // Safari doesn't support requestIdleCallback
+ // https://caniuse.com/requestidlecallback
+ window.setTimeout(hydrate, 1)
+}
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index 5962f26..6c15a6e 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -5,6 +5,12 @@ import type { AppLoadContext, EntryContext } from 'react-router'
import { createReadableStreamFromReadable } from '@react-router/node'
import { ServerRouter } from 'react-router'
import { isbot } from 'isbot'
+import { createInstance } from 'i18next'
+import { I18nextProvider, initReactI18next } from 'react-i18next'
+import i18n from './localization/i18n' // i18n configuration file
+import i18nextOpts from './localization/i18n.server'
+import { resources } from './localization/resource'
+
import { renderToPipeableStream } from 'react-dom/server'
const ABORT_DELAY = 5_000
@@ -18,7 +24,7 @@ export default function handleRequest(
) {
return isbot(request.headers.get('user-agent') || '')
? handleBotRequest(request, responseStatusCode, responseHeaders, reactRouterContext)
- : handleBrowserRequest(request, responseStatusCode, responseHeaders, reactRouterContext)
+ : handleBrowserRequest(request, responseStatusCode, responseHeaders, reactRouterContext, loadContext)
}
function handleBotRequest(
@@ -59,30 +65,44 @@ function handleBotRequest(
})
}
-function handleBrowserRequest(
+async function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext,
+ appContext: AppLoadContext,
) {
+ const instance = createInstance()
+ const lng = appContext.lang
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const ns = i18nextOpts.getRouteNamespaces(reactRouterContext as any)
+
+ await instance
+ .use(initReactI18next) // Tell our instance to use react-i18next
+ .init({
+ ...i18n, // spread the configuration
+ lng, // The locale we detected above
+ ns, // The namespaces the routes about to render wants to use
+ resources,
+ })
+
return new Promise((resolve, reject) => {
+ let didError = false
const { pipe, abort } = renderToPipeableStream(
- ,
+
+
+ ,
{
onShellReady() {
const body = new PassThrough()
-
+ const stream = createReadableStreamFromReadable(body)
responseHeaders.set('Content-Type', 'text/html')
- const url = new URL(request.url)
- if (url.pathname.match(/^\/videos\/.+\.mp4$/)) {
- responseHeaders.set('Cache-Control', 'public, max-age=604800') // 1 week
- }
-
resolve(
- new Response(createReadableStreamFromReadable(body), {
+ // @ts-expect-error - We purposely do not define the body as existent so it's not used inside loaders as it's injected there as well
+ appContext.body(stream, {
headers: responseHeaders,
- status: responseStatusCode,
+ status: didError ? 500 : responseStatusCode,
}),
)
@@ -92,6 +112,8 @@ function handleBrowserRequest(
reject(error)
},
onError(error: unknown) {
+ didError = true
+
console.error(error)
responseStatusCode = 500
},
diff --git a/src/i18n/locales/en/notfound.json b/app/i18n/locales/en/notfound.json
similarity index 100%
rename from src/i18n/locales/en/notfound.json
rename to app/i18n/locales/en/notfound.json
diff --git a/app/i18n/locales/en/translation.json b/app/i18n/locales/en/translation.json
new file mode 100644
index 0000000..936cc68
--- /dev/null
+++ b/app/i18n/locales/en/translation.json
@@ -0,0 +1,155 @@
+{
+ "title": "cognactive",
+ "footer": {
+ "data-usage-title": "Data Usage & Privacy Policy",
+ "medical-disclaimer": "Medical Disclaimer",
+ "medical-disclaimer-desc": "This website is provided for educational and informational purposes only and does not constitute providing medical advice or professional services. Nothing herein should be construed as an attempt to diagnose, treat, cure, or prevent any condition, illness, or disease, and those seeking medical advice should consult with a licensed physician.",
+ "chat-nac-link-title": "Chat NAC"
+ },
+ "privacy": {
+ "title": "Privacy & Data Usage",
+ "policy-link": "Read full privacy policy",
+ "regimen-title": "Protocol Regimen Tracking Utility",
+ "dimensions": {
+ "local-storage": {
+ "title": "Local Data Privacy",
+ "description": "Regimen activity data is stored only on your device and never transmitted to servers."
+ },
+ "third-party": {
+ "title": "No Third-Party Sharing",
+ "description": "Regimen activity data is not shared with third parties."
+ }
+ }
+ },
+ "homepage": {
+ "github-link-title": "View Code",
+ "tracker-link-title": "Launch Tracker",
+ "ingredient-scanner-link-title": "Ingredient Scanner",
+ "tracking": "Track your NAC",
+ "tracking-desc": "Track NAC protocol regimen, myco die-off experiences and more. Respect for your privacy with local data storage. Flexible with support for customizable regimen.",
+ "hero-appexperience": "App your NAC",
+ "hero-appexperience-desc": "Take the cognactive toolbelt with you on your journey. Install the cognactive mobile web app to your home screen and use it like an app. Compatible with iOS and Android.",
+ "hero-opensource": "Open source",
+ "hero-opensource-desc": "Cognactive is ready for customization, collaboration and improvements as a global community technological public good.",
+ "hero-title": "Track NAC Protocol",
+ "hero-subtitle": "Open source tools and tech for the anti-fungal and immunomodular NAC protocol.",
+ "fungcaster": "Watch Videos",
+ "cave-allegory": "Cave of Allegories",
+ "whitepaper-prompt": "Read whitepaper",
+ "whitepaper-label": "What is the NAC protocol?"
+ },
+ "tracker": {
+ "pick_date": "Pick a date",
+ "start_tracking": "Track daily activities and myco die-off experiences for the NAC protocol.",
+ "start_date": "Protocol Start Date",
+ "start_button": "Start Tracking NAC Protocol",
+ "started_on": "Started NAC protocol on",
+ "reset_tracking": "Reset Tracking",
+ "reset_description": "Reset the tracking start date and delete all data.",
+ "reset_button": "Reset Tracker",
+ "reset_confirm": "Are you sure? Your progress will be reset. All activity data will be erased. This action cannot be undone.",
+ "reset_error": "Something went wrong while resetting tracker data",
+ "install_title": "Install cognactive to mobile device",
+ "install_description": "To continue, open this webpage from your mobile device. Then open the Share menu and select 'Add to Home Screen'.",
+ "privacy_title": "Privacy Policy",
+ "privacy_text": "All regimen activity tracking data (tracking dates, experiences, supplements) is stored locally in web browser and not shared with any third-party.",
+ "privacy_link": "Learn more about cognactive Privacy & Data Usage",
+ "legal_title": "Legal Disclaimer",
+ "legal_text": "The cognactive app helps track activities and events and is for personal use only. cognactive is not a medical app. Use this app at your own risk and liability.",
+ "dashboard": {
+ "day": "Day",
+ "date_error": "Selected date cannot be before the start date or after today.",
+ "date_start": "Tracking started on this day",
+ "daily_regimen": "Daily Regimen",
+ "customize": "Customize",
+ "morning": "Morning",
+ "night": "Night",
+ "note_journal": "Note Journal",
+ "read_all_notes": "Read All Notes",
+ "food_guide": "Food Guide"
+ },
+ "progress_indicator": {
+ "phase_1": "Phase 1",
+ "phase_2": "Phase 2",
+ "two_month_milestone": "2 month milestone",
+ "phase_2_eligible": "Phase 2 Eligibility",
+ "weeks_no_die_off": "3 weeks of no die-off",
+ "latest_die_off": "Latest die-off symptom: {{date}}",
+ "phase_2_started": "started on {{date}}",
+ "edit": "Edit",
+ "cancel": "Cancel",
+ "three_weeks_on": "3 weeks on",
+ "one_week_off": "1 week off",
+ "phase_2_cycle_date": {
+ "title": "Phase 2 Cycle Start / Resume Date",
+ "description": "When did you start or resume phase 2? Setting this date will reset the timer."
+ },
+ "phase_2_setup": {
+ "title": "Phase 2",
+ "optional": "Optional",
+ "description": "Once past the 2 month milestone, continue phase 1 as needed or prepare for phase 2 of the NAC protocol.",
+ "setup_button": "Setup Phase 2"
+ },
+ "bso_only": "BSO only on week off."
+ },
+ "die_off_symptoms": {
+ "title": "Myco Die-Off",
+ "description": "Track myco die-off symptoms and experiences",
+ "view_chart": "View Chart",
+ "total": "TOTAL",
+ "new_symptom": {
+ "placeholder": "Describe a new symptom",
+ "track_button": "Track",
+ "error": "Describe your symptom in more detail",
+ "success": "{{date}}\nTracked{{custom}} symptom\n{{symptom}}"
+ },
+ "symptoms": {
+ "tiredness": "Tiredness",
+ "exhaustion": "Exhaustion",
+ "muscle_soreness": "Muscle soreness",
+ "increased_discharge": "Increased chest or nasal discharge",
+ "cold_flu": "Cold or flu like symptoms",
+ "cold_sores": "Cold sores",
+ "headaches": "Headaches",
+ "irritability": "Irritability",
+ "stool_changes": "Change in stool frequency, volume or color",
+ "rash": "Rash",
+ "bloated_stomach": "Bloated stomach",
+ "cramps": "Cramps",
+ "increased_gas": "Increased gas"
+ }
+ },
+ "daily_note": {
+ "new_entry": "Write a New Journal Entry",
+ "whats_notable": "What's notable about {{date}}?",
+ "prompt": "Eat anything special? Do anything different?",
+ "placeholder": "Write your note here",
+ "privacy_notice": "Notes are only stored locally on your device",
+ "save_button": "Save Note",
+ "success": "Note saved"
+ },
+ "trends": {
+ "loading": "Loading Symptom Analysis...",
+ "title": "Myco Die-Off",
+ "back_to_tracker": "Tracker",
+ "chart_description": "Chart showing {{period}} data",
+ "period": {
+ "weekly": "weekly",
+ "monthly": "monthly",
+ "week": "Week",
+ "month": "Month"
+ },
+ "chart": {
+ "symptom_severity": "Symptom Severity"
+ }
+ }
+ },
+ "memery": {
+ "title": "Cave of Allegories",
+ "platos-cave": {
+ "title": "Plato's Allegory of the Cave",
+ "description": "Plato's Allegory of the Cave describes a group of people who have lived chained to the wall of a cave all of their lives, facing a blank wall. The people watch shadows projected on the wall from objects passing in front of a fire behind them and begin to ascribe forms to these shadows. According to the allegory, the shadows are as close as the prisoners get to viewing reality. Plato suggests that the shadows are the prisoners' reality but that there is a higher, more true level of reality, hidden from human eyes.",
+ "relevance": "Anything that influences human thought can be characterized as the puppeteers in the allegory of the cave. Fungi like candida and cryptococcus can be considered the puppeteers in this metaphor. This is due to the influence on the gut-brain axis and residence in the brain."
+ }
+ }
+}
diff --git a/src/i18n/locales/es/notfound.json b/app/i18n/locales/es/notfound.json
similarity index 100%
rename from src/i18n/locales/es/notfound.json
rename to app/i18n/locales/es/notfound.json
diff --git a/app/i18n/locales/es/translation.json b/app/i18n/locales/es/translation.json
new file mode 100644
index 0000000..be79d76
--- /dev/null
+++ b/app/i18n/locales/es/translation.json
@@ -0,0 +1,141 @@
+{
+ "title": "cognactive",
+ "homepage": {
+ "get-started": "Iniciar Rastreador",
+ "tracker-link-title": "Iniciar Rastreador",
+ "github-link-title": "Ver Código",
+ "tracking": "Rastrea tu NAC",
+ "tracking-desc": "Rastrea el régimen del protocolo NAC, experiencias de eliminación de micotoxinas y más. Respeto por tu privacidad con almacenamiento de datos local. Flexible con soporte para régimen personalizable.",
+ "hero-appexperience": "App tu NAC",
+ "hero-appexperience-desc": "Lleva contigo el cinturón de herramientas cognactivas en tu viaje. Instala la aplicación web móvil cognactiva en tu pantalla de inicio y úsala como una app. Compatible con iOS y Android.",
+ "hero-opensource": "Código abierto",
+ "hero-opensource-desc": "Cognactive está lista para personalización, colaboración y mejoras como un bien público tecnológico de la comunidad global.",
+ "hero-title": "Rastrea el Protocolo NAC",
+ "fungcaster": "Ver Videos",
+ "ingredient-scanner-link-title": "Escáner de Ingredientes",
+ "hero-subtitle": "Herramientas y tecnología de código abierto para el protocolo NAC antifúngico e inmunomodulador.",
+ "whitepaper-prompt": "Leer el documento técnico",
+ "whitepaper-label": "¿Qué es el protocolo NAC?",
+ "cave-allegory": "Cueva de Alegorías"
+ },
+ "footer": {
+ "data-usage-title": "Uso de Datos y Política de Privacidad",
+ "medical-disclaimer": "Descargo de Responsabilidad Médica",
+ "medical-disclaimer-desc": "Este sitio web se proporciona solo con fines educativos e informativos y no constituye la prestación de asesoramiento médico o servicios profesionales. Nada aquí debe interpretarse como un intento de diagnosticar, tratar, curar o prevenir ninguna condición, enfermedad o dolencia, y aquellos que busquen asesoramiento médico deben consultar con un médico licenciado.",
+ "chat-nac-link-title": "Chat NAC"
+ },
+ "tracker": {
+ "start_tracking": "Rastrea actividades diarias y experiencias de eliminación de micotoxinas para el protocolo NAC.",
+ "start_date": "Fecha de Inicio del Protocolo",
+ "start_button": "Comenzar a Rastrear el Protocolo NAC",
+ "started_on": "Protocolo NAC iniciado el",
+ "reset_tracking": "Restablecer Seguimiento",
+ "reset_description": "Restablece la fecha de inicio del seguimiento y elimina todos los datos.",
+ "reset_button": "Restablecer Rastreador",
+ "reset_confirm": "¿Estás seguro? Tu progreso se restablecerá. Todos los datos de actividad serán borrados. Esta acción no se puede deshacer.",
+ "reset_error": "Algo salió mal al restablecer los datos del rastreador",
+ "install_title": "Instalar cognactive en el dispositivo móvil",
+ "install_description": "Para continuar, abre esta página web desde tu dispositivo móvil. Luego abre el menú Compartir y selecciona 'Agregar a la pantalla de inicio'.",
+ "privacy_title": "Política de Privacidad",
+ "privacy_text": "Todos los datos de seguimiento de actividades del régimen (fechas de seguimiento, experiencias, suplementos) se almacenan localmente en el navegador web y no se comparten con terceros.",
+ "privacy_link": "Aprende más sobre la Privacidad y Uso de Datos de cognactive",
+ "legal_title": "Aviso Legal",
+ "legal_text": "La aplicación cognactive ayuda a rastrear actividades y eventos y es solo para uso personal. cognactive no es una aplicación médica. Usa esta aplicación bajo tu propio riesgo y responsabilidad.",
+ "dashboard": {
+ "day": "Día",
+ "date_error": "La fecha seleccionada no puede ser anterior a la fecha de inicio o posterior a hoy.",
+ "date_start": "Seguimiento iniciado en este día",
+ "daily_regimen": "Régimen Diario",
+ "customize": "Personalizar",
+ "morning": "Mañana",
+ "night": "Noche",
+ "note_journal": "Diario de Notas",
+ "read_all_notes": "Leer Todas las Notas",
+ "food_guide": "Guía de Alimentos"
+ },
+ "progress_indicator": {
+ "phase_1": "Fase 1",
+ "phase_2": "Fase 2",
+ "two_month_milestone": "Hito de 2 meses",
+ "weeks_no_die_off": "3 semanas sin eliminación",
+ "latest_die_off": "Último síntoma de eliminación: {{date}}",
+ "phase_2_started": "comenzó el {{date}}",
+ "edit": "Editar",
+ "cancel": "Cancelar",
+ "three_weeks_on": "3 semanas activas",
+ "one_week_off": "1 semana de descanso",
+ "phase_2_cycle_date": {
+ "title": "Fecha de Inicio / Reanudación del Ciclo de Fase 2",
+ "description": "¿Cuándo comenzaste o reanudaste la fase 2? Establecer esta fecha restablecerá el temporizador."
+ },
+ "phase_2_setup": {
+ "title": "Fase 2",
+ "optional": "Opcional",
+ "description": "Una vez superado el hito de 2 meses, continúa la fase 1 según sea necesario o prepárate para la fase 2 del protocolo NAC.",
+ "setup_button": "Configurar Fase 2"
+ },
+ "bso_only": "Solo BSO en la semana de descanso.",
+ "phase_2_eligible": "Elegibilidad para la Fase 2"
+ },
+ "die_off_symptoms": {
+ "title": "Eliminación de Micotoxinas",
+ "description": "Rastrea los síntomas y experiencias de eliminación de micotoxinas",
+ "view_chart": "Ver Gráfico",
+ "total": "TOTAL",
+ "new_symptom": {
+ "placeholder": "Describe un nuevo síntoma",
+ "track_button": "Rastrear",
+ "error": "Describe tu síntoma con más detalle",
+ "success": "{{date}}\nSíntoma rastreado{{custom}}\n{{symptom}}"
+ },
+ "symptoms": {
+ "tiredness": "Cansancio",
+ "exhaustion": "Exhausto",
+ "muscle_soreness": "Dolor muscular",
+ "increased_discharge": "Aumento de secreción nasal o torácica",
+ "cold_flu": "Síntomas de resfriado o gripe",
+ "cold_sores": "Herpes labial",
+ "headaches": "Dolores de cabeza",
+ "irritability": "Irritabilidad",
+ "stool_changes": "Cambio en la frecuencia, volumen o color de las heces",
+ "rash": "Erupción",
+ "bloated_stomach": "Estómago hinchado",
+ "cramps": "Calambres",
+ "increased_gas": "Aumento de gases"
+ }
+ },
+ "daily_note": {
+ "new_entry": "Escribe una Nueva Entrada en el Diario",
+ "whats_notable": "¿Qué es notable sobre {{date}}?",
+ "prompt": "¿Comiste algo especial? ¿Hiciste algo diferente?",
+ "placeholder": "Escribe tu nota aquí",
+ "privacy_notice": "Las notas solo se almacenan localmente en tu dispositivo",
+ "save_button": "Guardar Nota",
+ "success": "Nota guardada"
+ },
+ "trends": {
+ "loading": "Cargando Análisis de Síntomas...",
+ "title": "Eliminación de Micotoxinas",
+ "back_to_tracker": "Rastreador",
+ "chart_description": "Gráfico que muestra datos de {{period}}",
+ "period": {
+ "weekly": "semanal",
+ "monthly": "mensual",
+ "week": "Semana",
+ "month": "Mes"
+ },
+ "chart": {
+ "symptom_severity": "Severidad del Síntoma"
+ }
+ },
+ "pick_date": "Elige una fecha"
+ },
+ "memery": {
+ "platos-cave": {
+ "title": "La Alegoría de la Cueva de Platón",
+ "description": "La Alegoría de la Cueva de Platón describe a un grupo de personas que han vivido encadenadas a la pared de una cueva toda su vida, frente a una pared en blanco. Las personas observan sombras proyectadas en la pared por objetos que pasan frente a un fuego detrás de ellas y comienzan a atribuir formas a estas sombras. Según la alegoría, las sombras son lo más cerca que los prisioneros están de ver la realidad. Platón sugiere que las sombras son la realidad de los prisioneros, pero que hay un nivel de realidad más alto y verdadero, oculto a los ojos humanos.",
+ "relevance": "Cualquier cosa que influya en el pensamiento humano puede caracterizarse como los titiriteros en la alegoría de la cueva. Hongos como la cándida y el criptococo pueden considerarse los titiriteros en esta metáfora. Esto se debe a la influencia en el eje intestino-cerebro y la residencia en el cerebro."
+ },
+ "title": "Cueva de Alegorías"
+ }
+}
\ No newline at end of file
diff --git a/app/i18n/locales/fr/notfound.json b/app/i18n/locales/fr/notfound.json
new file mode 100644
index 0000000..da0913c
--- /dev/null
+++ b/app/i18n/locales/fr/notfound.json
@@ -0,0 +1,5 @@
+{
+ "oops": "Oops!",
+ "title": "Sorry, an unexpected error has occurred.",
+ "backtohomepage": "Back to home page"
+}
diff --git a/app/i18n/locales/fr/translation.json b/app/i18n/locales/fr/translation.json
new file mode 100644
index 0000000..383de4a
--- /dev/null
+++ b/app/i18n/locales/fr/translation.json
@@ -0,0 +1,140 @@
+{
+ "title": "cognactive",
+ "homepage": {
+ "tracker-link-title": "Lancer le tracker",
+ "github-link-title": "Voir le code",
+ "tracking": "Suivez votre NAC",
+ "tracking-desc": "Suivez le régime du protocole NAC, les expériences de détoxification mycologique et plus encore. Respect de votre vie privée avec stockage local des données. Flexible avec prise en charge d'un régime personnalisable.",
+ "hero-appexperience": "Appliquez votre NAC",
+ "hero-appexperience-desc": "Emportez la ceinture à outils cognactive avec vous dans votre voyage. Installez l'application web mobile cognactive sur votre écran d'accueil et utilisez-la comme une application. Compatible avec iOS et Android.",
+ "hero-opensource": "Open source",
+ "hero-opensource-desc": "Cognactive est prêt pour la personnalisation, la collaboration et les améliorations en tant que bien public technologique mondial.",
+ "hero-title": "Suivre le protocole NAC",
+ "fungcaster": "Regarder des vidéos",
+ "ingredient-scanner-link-title": "Scanner d'ingrédients",
+ "hero-subtitle": "Outils et technologies open source pour le protocole NAC antifongique et immunomodulateur.",
+ "whitepaper-prompt": "Lire le livre blanc",
+ "whitepaper-label": "Qu'est-ce que le protocole NAC ?",
+ "cave-allegory": "Caverne des Allégories"
+ },
+ "footer": {
+ "data-usage-title": "Utilisation des données et politique de confidentialité",
+ "medical-disclaimer": "Avertissement médical",
+ "medical-disclaimer-desc": "Ce site web est fourni à des fins éducatives et informatives uniquement et ne constitue pas un avis médical ou des services professionnels. Rien ici ne doit être interprété comme une tentative de diagnostiquer, traiter, guérir ou prévenir toute condition, maladie ou affection, et ceux qui recherchent un avis médical doivent consulter un médecin agréé.",
+ "chat-nac-link-title": "Chat NAC"
+ },
+ "tracker": {
+ "start_tracking": "Suivez les activités quotidiennes et les expériences de détoxification mycotique pour le protocole NAC.",
+ "start_date": "Date de début du protocole",
+ "start_button": "Commencer le suivi du protocole NAC",
+ "started_on": "Protocole NAC commencé le",
+ "reset_tracking": "Réinitialiser le suivi",
+ "reset_description": "Réinitialisez la date de début du suivi et supprimez toutes les données.",
+ "reset_button": "Réinitialiser le suivi",
+ "reset_confirm": "Êtes-vous sûr ? Vos progrès seront réinitialisés. Toutes les données d'activité seront effacées. Cette action ne peut pas être annulée.",
+ "reset_error": "Une erreur s'est produite lors de la réinitialisation des données de suivi",
+ "install_title": "Installer cognactive sur un appareil mobile",
+ "install_description": "Pour continuer, ouvrez cette page web depuis votre appareil mobile. Ensuite, ouvrez le menu Partager et sélectionnez 'Ajouter à l'écran d'accueil'.",
+ "privacy_title": "Politique de confidentialité",
+ "privacy_text": "Toutes les données de suivi d'activité du régime (dates de suivi, expériences, suppléments) sont stockées localement dans le navigateur web et ne sont pas partagées avec des tiers.",
+ "privacy_link": "En savoir plus sur la confidentialité et l'utilisation des données de cognactive",
+ "legal_title": "Avertissement légal",
+ "legal_text": "L'application cognactive aide à suivre les activités et événements et est destinée à un usage personnel uniquement. cognactive n'est pas une application médicale. Utilisez cette application à vos propres risques et responsabilités.",
+ "dashboard": {
+ "day": "Jour",
+ "date_error": "La date sélectionnée ne peut pas être antérieure à la date de début ou postérieure à aujourd'hui.",
+ "date_start": "Suivi commencé ce jour",
+ "daily_regimen": "Régime quotidien",
+ "customize": "Personnaliser",
+ "morning": "Matin",
+ "night": "Nuit",
+ "note_journal": "Journal de notes",
+ "read_all_notes": "Lire toutes les notes",
+ "food_guide": "Guide alimentaire"
+ },
+ "progress_indicator": {
+ "phase_1": "Phase 1",
+ "phase_2": "Phase 2",
+ "two_month_milestone": "Jalon de 2 mois",
+ "weeks_no_die_off": "3 semaines sans détoxification",
+ "latest_die_off": "Dernier symptôme de détoxification : {{date}}",
+ "phase_2_started": "commencé le {{date}}",
+ "edit": "Modifier",
+ "cancel": "Annuler",
+ "three_weeks_on": "3 semaines en cours",
+ "one_week_off": "1 semaine de pause",
+ "phase_2_cycle_date": {
+ "title": "Date de début / reprise du cycle de la phase 2",
+ "description": "Quand avez-vous commencé ou repris la phase 2 ? Définir cette date réinitialisera le minuteur."
+ },
+ "phase_2_setup": {
+ "title": "Phase 2",
+ "optional": "Optionnel",
+ "description": "Une fois passé le jalon de 2 mois, continuez la phase 1 si nécessaire ou préparez-vous pour la phase 2 du protocole NAC.",
+ "setup_button": "Configurer la phase 2"
+ },
+ "bso_only": "BSO uniquement pendant la semaine de pause.",
+ "phase_2_eligible": "Éligibilité Phase 2"
+ },
+ "die_off_symptoms": {
+ "title": "Détoxification mycotique",
+ "description": "Suivez les symptômes et expériences de détoxification mycotique",
+ "view_chart": "Voir le graphique",
+ "total": "TOTAL",
+ "new_symptom": {
+ "placeholder": "Décrivez un nouveau symptôme",
+ "track_button": "Suivre",
+ "error": "Décrivez votre symptôme plus en détail",
+ "success": "{{date}}\nSymptôme suivi{{custom}}\n{{symptom}}"
+ },
+ "symptoms": {
+ "tiredness": "Fatigue",
+ "exhaustion": "Épuisement",
+ "muscle_soreness": "Douleurs musculaires",
+ "increased_discharge": "Augmentation des sécrétions thoraciques ou nasales",
+ "cold_flu": "Symptômes de rhume ou de grippe",
+ "cold_sores": "Boutons de fièvre",
+ "headaches": "Maux de tête",
+ "irritability": "Irritabilité",
+ "stool_changes": "Changement de fréquence, volume ou couleur des selles",
+ "rash": "Éruption cutanée",
+ "bloated_stomach": "Ventre ballonné",
+ "cramps": "Crampes",
+ "increased_gas": "Augmentation des gaz"
+ }
+ },
+ "daily_note": {
+ "new_entry": "Écrire une nouvelle entrée de journal",
+ "whats_notable": "Qu'est-ce qui est notable à propos de {{date}} ?",
+ "prompt": "Avez-vous mangé quelque chose de spécial ? Avez-vous fait quelque chose de différent ?",
+ "placeholder": "Écrivez votre note ici",
+ "privacy_notice": "Les notes sont uniquement stockées localement sur votre appareil",
+ "save_button": "Enregistrer la note",
+ "success": "Note enregistrée"
+ },
+ "trends": {
+ "loading": "Chargement de l'analyse des symptômes...",
+ "title": "Détoxification mycotique",
+ "back_to_tracker": "Suivi",
+ "chart_description": "Graphique montrant les données de {{period}}",
+ "period": {
+ "weekly": "hebdomadaire",
+ "monthly": "mensuel",
+ "week": "Semaine",
+ "month": "Mois"
+ },
+ "chart": {
+ "symptom_severity": "Gravité des symptômes"
+ }
+ },
+ "pick_date": "Choisissez une date"
+ },
+ "memery": {
+ "platos-cave": {
+ "title": "Allégorie de la Caverne de Platon",
+ "description": "L'Allégorie de la Caverne de Platon décrit un groupe de personnes qui ont vécu enchaînées au mur d'une caverne toute leur vie, face à un mur blanc. Les gens regardent les ombres projetées sur le mur par des objets passant devant un feu derrière eux et commencent à attribuer des formes à ces ombres. Selon l'allégorie, les ombres sont aussi proches que les prisonniers peuvent voir la réalité. Platon suggère que les ombres sont la réalité des prisonniers mais qu'il existe un niveau de réalité supérieur, plus vrai, caché aux yeux humains.",
+ "relevance": "Tout ce qui influence la pensée humaine peut être caractérisé comme les marionnettistes dans l'allégorie de la caverne. Les champignons comme le candida et le cryptococcus peuvent être considérés comme les marionnettistes dans cette métaphore. Cela est dû à l'influence sur l'axe intestin-cerveau et à la résidence dans le cerveau."
+ },
+ "title": "Caverne des Allégories"
+ }
+}
\ No newline at end of file
diff --git a/src/lib/openai.server.ts b/app/lib/openai.server.ts
similarity index 100%
rename from src/lib/openai.server.ts
rename to app/lib/openai.server.ts
diff --git a/src/lib/utils.ts b/app/lib/utils.ts
similarity index 100%
rename from src/lib/utils.ts
rename to app/lib/utils.ts
diff --git a/app/localization/README.md b/app/localization/README.md
new file mode 100644
index 0000000..277663f
--- /dev/null
+++ b/app/localization/README.md
@@ -0,0 +1,27 @@
+# Localization
+
+Localization works by using the `i18next` package. Everything is configured inside of this folder.
+The localization works by using the `/app/i18n/locales` folder. This folder contains all the translations for the different languages. You can add new translations by adding new files to this folder and then changing the `resource.ts` file to include the new language.
+
+The server part is set up in the `entry.server.tsx` file, and the client part, conversely, is in the `entry.client.tsx` file and also the `root.tsx` file.
+
+The language is detected by the `Accept-Language` HTTP request header, a saved cookie or defaults to English.
+
+## Server-side
+
+Due to the fact that the server does not care about loading in additional resources as they are not send over the wire we
+pass in `resources` to the `i18next` instance. This provides all the languages to your server which allows it to render
+the correct language on the server.
+
+## Client-side
+
+The client-side is a bit more complicated. We do not want to load in all the languages on the client side as it would
+be a lot of requests. Instead, we use the fetch backend to load in the language files on the client side. We have a resource route inside of the `routes` directory which is in charge of loading in the resources. This route is called `resource.locales` and it is used to load in the languages. The `resource.locales` route is set up to only load in the languages and namespaces that are needed. In production we cache these responses and in development we don't cache them.
+
+## Automated Translations
+
+Translation automation is setup using [`languine.ai`](https://languine.ai). The configuration is in the `languine.config.ts` file. After installing project dependencies using `npm install`, you can run the following command to see what languine can do:
+
+```bash
+npx languine available
+```
diff --git a/app/localization/i18n.server.ts b/app/localization/i18n.server.ts
new file mode 100644
index 0000000..a7f5e5a
--- /dev/null
+++ b/app/localization/i18n.server.ts
@@ -0,0 +1,49 @@
+import { resolve } from 'node:path'
+import { RemixI18Next } from 'remix-i18next/server'
+import i18n from '@/localization/i18n' // your i18n configuration file
+
+const i18next = new RemixI18Next({
+ detection: {
+ findLocale: async (request) => {
+ // Get first path component from URL
+ const url = new URL(request.url)
+ const firstPathComponent = url.pathname.split('/')[1]
+
+ // Check if it matches a supported language code
+ if (i18n.supportedLngs.includes(firstPathComponent as any)) {
+ return firstPathComponent
+ }
+
+ const cookie = request.headers.get('Cookie')
+ const lng = cookie?.match(/lng=([^;]+)/)?.[1]
+ if (lng) {
+ return lng
+ }
+
+ // Try to detect from Accept-Language header
+ // Try to detect from Accept-Language header
+ const acceptLanguage = request.headers.get('Accept-Language')
+ if (acceptLanguage) {
+ const preferredLang = acceptLanguage.split(',')[0].split('-')[0]
+ if (i18n.supportedLngs.includes(preferredLang as any)) {
+ return preferredLang
+ }
+ }
+
+ // Else detect language from stored cookie
+ return 'en'
+ },
+ supportedLanguages: i18n.supportedLngs,
+ fallbackLanguage: i18n.fallbackLng,
+ },
+ // This is the configuration for i18next used
+ // when translating messages server-side only
+ i18next: {
+ ...i18n,
+ backend: {
+ loadPath: resolve('../i18n/locales/{{lng}}/{{ns}}.json'),
+ },
+ },
+})
+
+export default i18next
diff --git a/app/localization/i18n.ts b/app/localization/i18n.ts
new file mode 100644
index 0000000..15af262
--- /dev/null
+++ b/app/localization/i18n.ts
@@ -0,0 +1,11 @@
+import { supportedLanguages } from './resource'
+
+export default {
+ // This is the list of languages your application supports
+ supportedLngs: supportedLanguages,
+ // This is the language you want to use in case
+ // if the user language is not in the supportedLngs
+ fallbackLng: 'en',
+ // The default namespace of i18next is "translation", but you can customize it here
+ defaultNS: 'translation',
+}
diff --git a/app/localization/resource.ts b/app/localization/resource.ts
new file mode 100644
index 0000000..7102481
--- /dev/null
+++ b/app/localization/resource.ts
@@ -0,0 +1,33 @@
+import spanish from '../../app/i18n/locales/es/translation.json'
+import english from '../../app/i18n/locales/en/translation.json'
+import french from '../../app/i18n/locales/fr/translation.json'
+
+import notfound from '../../app/i18n/locales/en/notfound.json'
+import notfound_es from '../../app/i18n/locales/es/notfound.json'
+import notfound_fr from '../../app/i18n/locales/fr/notfound.json'
+
+const languages = ['en', 'es', 'fr'] as const
+export const supportedLanguages = [...languages]
+export type Language = (typeof languages)[number]
+
+type Resource = {
+ translation: typeof english
+ notfound: typeof notfound
+}
+
+export type Namespace = keyof Resource
+
+export const resources: Record = {
+ en: {
+ translation: english,
+ notfound: notfound,
+ },
+ es: {
+ translation: spanish,
+ notfound: notfound_es,
+ },
+ fr: {
+ translation: french,
+ notfound: notfound_fr,
+ },
+}
diff --git a/src/pages/about/index.tsx b/app/pages/about/index.tsx
similarity index 100%
rename from src/pages/about/index.tsx
rename to app/pages/about/index.tsx
diff --git a/src/pages/blog/layout.tsx b/app/pages/blog/layout.tsx
similarity index 100%
rename from src/pages/blog/layout.tsx
rename to app/pages/blog/layout.tsx
diff --git a/src/pages/home/index.tsx b/app/pages/home/index.tsx
similarity index 100%
rename from src/pages/home/index.tsx
rename to app/pages/home/index.tsx
diff --git a/src/pages/tracker/daily-note-form.tsx b/app/pages/tracker/daily-note-form.tsx
similarity index 74%
rename from src/pages/tracker/daily-note-form.tsx
rename to app/pages/tracker/daily-note-form.tsx
index 66cb86e..a6670f0 100644
--- a/src/pages/tracker/daily-note-form.tsx
+++ b/app/pages/tracker/daily-note-form.tsx
@@ -10,33 +10,37 @@ import {
SheetTitle,
SheetTrigger,
} from 'src/components/ui/sheet'
-
import { Textarea } from 'src/components/ui/textarea'
import db from './db'
import { useState } from 'react'
import { toast } from 'react-hot-toast'
import { useNavigate } from 'react-router'
import { cn } from 'src/lib/utils'
+import { useTranslation } from 'react-i18next'
export default function DailyNoteForm({ dateKey, buttonClassName }: { dateKey: string; buttonClassName?: string }) {
+ const { t } = useTranslation()
const [note, setNote] = useState('')
-
const navigate = useNavigate()
return (
- Write a New Journal Entry
+ {t('tracker.daily_note.new_entry')}
- What's notable about {dateKey}?
- Eat anything special? Do anything different?
+ {t('tracker.daily_note.whats_notable', { date: dateKey })}
+ {t('tracker.daily_note.prompt')}
-
-
Notes are only stored locally on your device
+
{t('tracker.daily_note.privacy_notice')}
{
- toast.success('Note saved')
+ toast.success(t('tracker.daily_note.success'))
db.addNote({
content: note,
date: dateKey,
@@ -70,7 +74,7 @@ export default function DailyNoteForm({ dateKey, buttonClassName }: { dateKey: s
navigate('/tracker-journal')
}}
>
- Save Note
+ {t('tracker.daily_note.save_button')}
diff --git a/src/pages/tracker/dashboard.tsx b/app/pages/tracker/dashboard.tsx
similarity index 89%
rename from src/pages/tracker/dashboard.tsx
rename to app/pages/tracker/dashboard.tsx
index 04629ee..e1d6d52 100644
--- a/src/pages/tracker/dashboard.tsx
+++ b/app/pages/tracker/dashboard.tsx
@@ -1,6 +1,5 @@
import { differenceInCalendarDays, isSameDay, startOfDay } from 'date-fns'
import { BookIcon, BotIcon, CheckCircle2, Moon, Sun } from 'lucide-react'
-
import { Button } from 'src/components/ui/button'
import DieOffSymptoms from './die-off-symptoms'
import { useTrackSupplement } from './use-track-supplement'
@@ -13,6 +12,7 @@ import { Link } from 'react-router'
import ProgressIndicator from './progress-indicator'
import DailyNoteForm from './daily-note-form'
import { useRegimen } from './use-regimen'
+import { useTranslation } from 'react-i18next'
interface DashboardProps {
startDate: Date
@@ -41,6 +41,7 @@ export const TrackerTool: React.FC = ({ title, children, toolb
const Dashboard: React.FC = ({ startDate }) => {
const [currentDate, setCurrentDate] = useState(new Date())
+ const { t } = useTranslation()
// On focus sets date to today
useEffect(function resetToToday() {
@@ -94,7 +95,9 @@ const Dashboard: React.FC = ({ startDate }) => {
- Day {dayNumber}
+
+ {t('tracker.dashboard.day')} {dayNumber}{' '}
+ = ({ startDate }) => {
return
}
if (isSameDay(d, startDate)) {
- toast('Tracking started on this day', { position: 'bottom-center' })
+ toast(t('tracker.dashboard.date_start'), { position: 'bottom-center' })
setCurrentDate(startDate)
return
}
if (startOfDay(d) >= start && d <= new Date()) {
setCurrentDate(d)
} else {
- toast.error('Selected date cannot be before the start date or after today.', {
+ toast.error(t('tracker.dashboard.date_error'), {
position: 'bottom-center',
duration: 1000,
})
@@ -126,11 +129,11 @@ const Dashboard: React.FC = ({ startDate }) => {
toolbarItems={
- Customize
+ {t('tracker.dashboard.customize')}
}
- title="Daily Regimen"
+ title={t('tracker.dashboard.daily_regimen')}
>
@@ -90,7 +103,7 @@ const DieOffSymptoms: React.FC = (props) => {
if (customSymptom.length > 2) {
handleSymptomChange(customSymptom, true)
} else {
- toast.error('Describe your symptom in more detail')
+ toast.error(t('tracker.die_off_symptoms.new_symptom.error'))
}
}}
>
@@ -98,13 +111,13 @@ const DieOffSymptoms: React.FC = (props) => {
ref={inputRef}
className="ml-4 w-full p-2"
type="text"
- placeholder="Describe a new symptom"
+ placeholder={t('tracker.die_off_symptoms.new_symptom.placeholder')}
name="custom_symptom"
/>
- Track
+ {t('tracker.die_off_symptoms.new_symptom.track_button')}
diff --git a/src/pages/tracker/index.tsx b/app/pages/tracker/index.tsx
similarity index 70%
rename from src/pages/tracker/index.tsx
rename to app/pages/tracker/index.tsx
index c9d26c0..4fc1e6f 100644
--- a/src/pages/tracker/index.tsx
+++ b/app/pages/tracker/index.tsx
@@ -8,11 +8,12 @@ import { toast } from 'react-hot-toast'
import useIsAppInstalled from 'src/pages/tracker/use-is-app-installed'
import { Alert, AlertDescription, AlertTitle } from 'src/components/ui/alert'
import { LayoutGrid, ShareIcon } from 'lucide-react'
-import { getEnv } from 'src/lib/env'
import { parse } from 'date-fns'
import { Popover, PopoverContent, PopoverTrigger } from 'src/components/ui/popover'
import { PROTOCOL_PHASE, PROTOCOL_PHASE_2_CYCLE_START, PROTOCOL_START_DATE } from 'src/constants'
import { Link } from 'react-router'
+import { useTranslation } from 'react-i18next'
+
interface ProtocolTrackerProps {
clientCachedStartDate: string | null
}
@@ -21,9 +22,10 @@ const ProtocolTracker: React.FC = ({ clientCachedStartDate
const [startDate, setStartDate] = useState(clientCachedStartDate)
const [pickerDate, setPickerDate] = useState()
const isAppInstalled = useIsAppInstalled()
+ const { t } = useTranslation()
const handleStartProtocol = useCallback(() => {
- if (isAppInstalled || getEnv() === 'development') {
+ if (isAppInstalled || ENV.MODE === 'development') {
const currentDate = formatDateKey(pickerDate || new Date())
localStorage.setItem(PROTOCOL_START_DATE, currentDate)
setStartDate(currentDate)
@@ -51,7 +53,7 @@ const ProtocolTracker: React.FC = ({ clientCachedStartDate
{startDate ? (
<>
- Started NAC protocol on
+ {t('tracker.started_on')}
@@ -60,15 +62,13 @@ const ProtocolTracker: React.FC = ({ clientCachedStartDate
-
Reset Tracking
-
Reset the tracking start date and delete all data.
+
{t('tracker.reset_tracking')}
+
{t('tracker.reset_description')}
{
- const sure = confirm(
- 'Are you sure? Your progress will be reset. All activity data will be erased. This action cannot be undone.',
- )
+ const sure = confirm(t('tracker.reset_confirm'))
if (sure) {
db.resetAllData().then(
() => {
@@ -79,13 +79,13 @@ const ProtocolTracker: React.FC = ({ clientCachedStartDate
},
(err) => {
console.log(err)
- toast.error('Something went wrong while resetting tracker data')
+ toast.error(t('tracker.reset_error'))
},
)
}
}}
>
- Reset Tracker
+ {t('tracker.reset_button')}
@@ -94,37 +94,34 @@ const ProtocolTracker: React.FC = ({ clientCachedStartDate
>
) : (
-
Track daily activities and myco die-off experiences for the NAC protocol.
+
{t('tracker.start_tracking')}
-
Protocol Start Date
+
{t('tracker.start_date')}
setPickerDate(d)} />
- Start Tracking NAC Protocol
+ {t('tracker.start_button')}
{!isAppInstalled && (
- Install cognactive to mobile device
+ {t('tracker.install_title')}
- To continue, open this webpage from your mobile device. Then open the Share menu{' '}
- and select 'Add to Home Screen'.
+ {t('tracker.install_description')}
)}
{isAppInstalled && (
By proceeding you agree to:
-
Privacy Policy
- All regimen activity tracking data (tracking dates, experiences, supplements) is stored locally in web
- browser and not shared with any third-party.{' '}
+
{t('tracker.privacy_title')}
+ {t('tracker.privacy_text')}{' '}
- Learn more about cognactive Privacy & Data Usage
+ {t('tracker.privacy_link')}
- .
Legal Disclaimer
- The cognactive app helps track activities and events and is for personal use only. cognactive is not a
- medical app. Use this app at your own risk and liability.
+ .
- {/* Handle multiple cycles by modulo calc days by 28 and stop at 3 weeks (21 days) */}
-
3 weeks on
+
{t('tracker.progress_indicator.three_weeks_on')}
- {/* Show the final week by modulo calc by 28 and subtract 3 weeks */}
-
1 week off
+
{t('tracker.progress_indicator.one_week_off')}
@@ -127,9 +133,9 @@ const ProgressIndicator: React.FC = ({ startDate, curren
{editingPhase2CycleDate && phase2CycleStart && (
- Phase 2 Cycle Start / Resume Date
+ {t('tracker.progress_indicator.phase_2_cycle_date.title')}
- When did you start or resume phase 2? Setting this date will reset the timer.
+ {t('tracker.progress_indicator.phase_2_cycle_date.description')}
= ({ startDate, curren
{daysUntilTwoMonths <= 0 && (currentPhase == '1' || currentPhase == null) && (
- Phase 2 Optional
+ {t('tracker.progress_indicator.phase_2_setup.title')}{' '}
+ {t('tracker.progress_indicator.phase_2_setup.optional')}
- Once past the 2 month milestone, continue phase 1 as needed or prepare for phase 2 of the NAC protocol.
+ {t('tracker.progress_indicator.phase_2_setup.description')}
{
@@ -163,13 +170,13 @@ const ProgressIndicator: React.FC = ({ startDate, curren
variant={'cyan'}
size={'sm'}
>
- Setup Phase 2
+ {t('tracker.progress_indicator.phase_2_setup.setup_button')}
)}
{currentPhase == '2' && (daysSinceResumingPhase2 % fourWeeksInDays) - threeWeeksInDays > 0 && (
-
BSO only on week off.
+
{t('tracker.progress_indicator.bso_only')}
)}
>
)
diff --git a/src/pages/tracker/use-is-app-installed.ts b/app/pages/tracker/use-is-app-installed.ts
similarity index 100%
rename from src/pages/tracker/use-is-app-installed.ts
rename to app/pages/tracker/use-is-app-installed.ts
diff --git a/src/pages/tracker/use-protocol-tracker-state.ts b/app/pages/tracker/use-protocol-tracker-state.ts
similarity index 100%
rename from src/pages/tracker/use-protocol-tracker-state.ts
rename to app/pages/tracker/use-protocol-tracker-state.ts
diff --git a/src/pages/tracker/use-regimen.ts b/app/pages/tracker/use-regimen.ts
similarity index 100%
rename from src/pages/tracker/use-regimen.ts
rename to app/pages/tracker/use-regimen.ts
diff --git a/src/pages/tracker/use-track-supplement.ts b/app/pages/tracker/use-track-supplement.ts
similarity index 100%
rename from src/pages/tracker/use-track-supplement.ts
rename to app/pages/tracker/use-track-supplement.ts
diff --git a/src/pages/tracker/use-track-symptom.ts b/app/pages/tracker/use-track-symptom.ts
similarity index 100%
rename from src/pages/tracker/use-track-symptom.ts
rename to app/pages/tracker/use-track-symptom.ts
diff --git a/app/root.tsx b/app/root.tsx
index c8b7e8f..c388da4 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -3,8 +3,10 @@ import { Links, Meta, Outlet, Scripts, ScrollRestoration, useRouteError } from '
import 'src/assets/icons/cognactive-symbol.css'
import 'src/styles/globals.css'
-// import 'src/i18n/config'
-// /TODO
+import type { Route } from './+types/root'
+
+import { useTranslation } from 'react-i18next'
+import { useChangeLanguage } from 'remix-i18next/react'
import { Toaster } from 'react-hot-toast'
@@ -13,9 +15,19 @@ import ErrorPage from 'src/components/error-page'
import Footer from 'src/components/footer'
import { DOMAIN, HOSTNAME } from 'src/constants'
-export default function Root() {
+export async function loader({ context }: Route.LoaderArgs) {
+ const { lang, clientEnv } = context
+ return { lang, clientEnv }
+}
+
+export default function Root({ loaderData }: Route.ComponentProps) {
+ const { lang, clientEnv } = loaderData
+
+ useChangeLanguage(lang)
+ const { i18n } = useTranslation()
+
return (
-
+
@@ -23,6 +35,8 @@ export default function Root() {
+
+
{/* Standalone web app support */}
diff --git a/app/routes.ts b/app/routes.ts
index 69eb960..8b7da89 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -8,11 +8,15 @@ const routes = [
route('/tracker-regimen', './routes/tracker-regimen/route.tsx'),
route('/trends', './routes/trends.tsx'),
route('/fridai', './routes/fridai+/_fridai.tsx'),
+ route('/resource/locales', './routes/resource.locales.tsx'),
route('/fungcaster', './routes/fungcaster/route.tsx'),
...prefix('/blog', [
layout('./routes/blog+/_layout.tsx', [
index('./routes/blog+/index.tsx'),
- ...prefix('/posts', [route('/food-guide', './routes/blog+/posts+/food-guide/route.tsx')]),
+ ...prefix('/posts', [
+ route('food-guide', './routes/blog+/posts+/food-guide/route.tsx'),
+ route('metagame', './routes/blog+/posts+/metagame.mdx'),
+ ]),
]),
]),
...prefix('/privacy', [index('./routes/privacy+/index.tsx'), route('/policy', './routes/privacy+/policy.tsx')]),
diff --git a/app/routes/blog+/posts+/individual-empiricism+/post.md b/app/routes/blog+/posts+/individual-empiricism+/post.md
deleted file mode 100644
index a6afd4b..0000000
--- a/app/routes/blog+/posts+/individual-empiricism+/post.md
+++ /dev/null
@@ -1,117 +0,0 @@
-# Framework for Individual Health Empiricism
-
-Author: [micksabox](https://x.com/micksabox)
-
-Some medical mysteries can be difficult to debug. The modern information landscape is littered with advice and anecdotes, making it tough to sort through. As a programmer, I'm faced with finding creative solutions to solve hard technical problems on a daily basis. I often need to develop customized strategies to "debug" a problem. This article explains a framework for thinking about debugging medical problems when relating to personal health, and how the NAC protocol fits in.
-
-## Empiricism
-
-The idea that "trials" are needed is not new in medical literature. Dr. William Crook, the author of _The Yeast Connection: A Medical Breakthrough_ (published in 1983) called this a "therapeutic trial". This is especially useful when it comes to debugging _candida_(a yeast in the human microbiome), which is difficult to detect and is considered "commensal" (meaning everyone has it, so tests are skewed). Trying something and seeing if it works sometimes is the only way.
-
-### Don't Trust. Verify
-
-The blockchain and web3 movement has enshrined a well known mantra of "don't trust, verify" when discussing how to evaluate the source of truth for network level data. The network architecture of blockchains makes it easy to verify things like total currency supply, hash rate and other publicly broadcast metrics. Note the radical departure from the idea of "truth middlemen" which interpret and curate the primary information for you. My time exploring blockchain technology has instilled this mantra in me and setup my journey into the NAC supplement protocol, which I'll explain below.
-
-### Individual Empiricism
-
-Recently a [discussion on X by @levelsio](https://twitter.com/levelsio/status/1767023421676769708) regarding the elimination of bread from diet sparked my interest. One of the replies was:
-
-> There also just needs to be more respect for “individual empiricism”. We all have incredibly unique bodies. The only way to know what works and doesn’t is to experiment on yourself [Scott Stevenson](https://twitter.com/scottastevenson/status/1767025859884728732)
-
-So, what is empiricism? According to GPT4:
-
-> Empiricism is a philosophical perspective that emphasizes the role of experience and evidence in the acquisition of knowledge. In simpler terms, it's the idea that all knowledge comes from and is tested by our experiences and sensory observations. This approach values empirical evidence, which is information acquired by observation or experimentation, over theoretical or abstract reasoning.
-
-In the context of individual empiricism, it refers to a personal application of this principle, relying on one's own experiences and observations to understand and make decisions about the world. Like Scott mentioned above, there are decisions about our bodies and health that fall under this category.
-
-## Evaluation Framework
-
-When trying out any protocol to debug the body, I've thought of a way to evaluate the intervention according to certain dimensions and the principles of individual empiricism. Visually, try to picture a 3-dimensional chart:
-
-1. X axis is verification difficulty
-2. Y axis is safety
-3. Z axis is benefits
-
-With this 3D structure in mind, it might be easy to see where wellness interventions are preferred. The ideal should be to minimize the difficulty in verification and maximize safety and benefits. In simpler terms, getting more bang for buck while lowering risk and being able to verify yourself without relying on truth middlemen interpreters (as much as reasonably possible).
-
-Within this evaluation framework I think the **NAC protocol is singular** and can demonstrate its appeal compared to other interventions. I go into more detail evaluating NAC protocol along each of these dimensions in the sections that follow.
-
-## Dimension: Verification Difficulty
-
-NAC protocol is relatively simple to self-verify in my opinion. This includes the ability to acquire the supplements (available over the counter) and the amount of time before results are observable. The NAC protocol regimen uses 3 over the counter supplements: Oregano oil, black seed oil, and NAC. Results are frequently seen in first week. My personal breakthroughs (less brain fog, fatigue, energy levels) were apparent within the first week with other similar anecdotes seen in the community. My mom reported better sleep, my wife reported "edge taken off” and both reported increased energy levels. These results are self-motivating to continue and complete the first phase (2-4 months typically).
-
-Side note: this fast observability on NAC protocol can make it “sticky”, to borrow a term from the tech startup growth world. Once started, it makes sense that churning (stopping) is less likely if the person feels and sees positive benefits.
-
-The full technical verification is a different matter which likely includes bio marker and other lab testing. This is part of verification that takes longer and requires patience, especially since the protocol is designed to work gently over time. Within the individual empiricism framework, “I’ve made these changes and started to feel better” is the verification that I’m referring to. There may be health conditions that disappear, taking verification to a personalized level.
-
-## Dimension: Safety
-
-**Obligatory disclaimer: make sure to consult your primary care provider before starting and nothing in this post should be considered medical advice.**
-
-Individual empiricism requires safety considerations. The 3 supplements in NAC protocol are well studied in medical literature and in the case of black seed oil are have a long history. Oregano oil is plant based herbal medicine. NAC is a widely studied and popular amino acid supplement. So the amount of previous scientific literature, upon review, can contribute to evaluating how safe an intervention is, in my opinion.
-
-In the spirit of individual empiricism, before I started NAC protocol I made sure to cross-reference the supplements with ChatGPT and articles I could find online. There’s lots of studies published in medical journals, and anecdotal evidence is littered across social media of people trying and seeing results with these supplements (both on their own and at combined level).
-
-The NAC protocol whitepaper describes in more details some of the things to look out for. The protocol suggests probiotics to help complement gut health. Of course, the topic of safety is complex and so is the human body, so these few paragraphs on safety are by no means definitive.
-
-There is a large and growing community that has been on the NAC protocol, in some cases for multiple years (on maintenance variations of it). The element of safety will be more understood over time. The NAC protocol was designed by a team of serious biomedical researchers including Seth Peribsen and M. MacLir. You can read the NAC protocol whitepaper [here](https://cognactive.net/files/NAC_Protocol.pdf).
-
-## Dimension: Benefits
-
-NAC protocol targets fungal pathogens in the human biome. Previously, these have been considered commensal (a natural, inseparable part of us) but recent research is changing this view. There are both first-order and second-order systemic effects at play, so this dimension is the most interesting and most technical.
-
-Fungus species in biome like candida, cryptococcus and aspergillus contribute to a huge surface area of maladies. This is a well supported fact found in the medical literature. Long term colonization and overgrowth can remain undetected for years and I believe decades. There are large benefits to cutting the fung, it’s actually hard to believe (more on that below). That’s not to mention the symbiotic interactions with other bacteria and viruses (hint: they hide in biofilm substrate that fungus produces). Fungis role as part of overall immune system compromise is also a major component.
-
-I can really only scratch the surface on benefits here to be honest. What I found helped my curriculum is previous literature on candida as a base layer, and then moving upwards to understanding systemic effects. It also was comforting for me to understand that candida has been identified in literature at least as far back as 1983 (41 years ago!). This is a sample of some of the books and the medical journal literature is easily discoverable.
-
-1. **Candida: Conquering an Invisible Disease** by _David A. Lopez_ (2018)
-
- - This book provides a comprehensive overview of candida overgrowth, its symptoms, and a holistic approach to treatment.
-
-2. **The Yeast Connection: A Medical Breakthrough** by _William G. Crook_ (1983)
-
- - A pioneering book that linked chronic health problems to yeast overgrowth and became a foundational text in the field.
-
-3. **Living Candida-Free: 100 Recipes and a 3-Stage Program to Restore Your Health and Vitality** by _Ricki Heller_ and _Andrea Nakayama_ (2015)
-
- - A guide to living a candida-free life through diet and lifestyle changes, including recipes and meal plans.
-
-4. **The Candida Cure: Yeast, Fungus & Your Health - The 90-Day Program to Beat Candida & Restore Vibrant Health** by _Ann Boroch_ (2009)
-
- - This book offers a comprehensive plan to eliminate candida and restore health, with a focus on diet, herbal supplements, and detoxifying the body.
-
-5. **Candida and Your Health: Beyond the Basics** by _Julia Davies_ (2021)
-
- - An insightful exploration into the deeper impacts of candida on overall health, offering advanced strategies for managing its effects.
-
-6. **The Candida Control Cookbook: What You Should Know and What You Should Eat to Manage Yeast Infections** by _Gail Burton_ (1996)
-
- - A practical guide to controlling candida through diet, including recipes and meal plans tailored to reduce yeast infections.
-
-7. **Healing Candida with Food: The Ultimate Guide to Tackling Yeast Infections Through Diet** by _Sandra Boehner_ (2014)
-
- - This book focuses on dietary solutions to candida overgrowth, providing readers with a comprehensive plan to heal their bodies naturally.
-
-8. **Candida: The Silent Epidemic: Vital Information to Detect, Combat, and Prevent Yeast Infections** by _Jeanne Marie Martin_ (2003)
-
- - A detailed account of the candida epidemic, offering vital information on detection, prevention, and combat strategies.
-
-## Note on Heuristics
-
-It's important to note there exists cultured heuristics (rules of thumb) to be wary of miracle cures which I've encountered. One of them is to be cautious against miracle cures, to which I say:
-
-**Miracle is relative.** Modern medicine would seem miraculous to people of the past lacking a sufficient explanation of mechanism of action. To someone knowledgeable about how a solution works mechanistically, it is less of a miracle and more within the realm of scientific explanation. Just because something seems like a miracle doesn't make it actually so.
-
-Same goes for _"if it's too good to be true, it probably is"_. This heuristic is a double-edge sword, traditionally passed on as common sense caution to guard against charlatans and risky scenarios. Fair enough. But it can also prevent the earnest exploration of things that do seem like miracles to the uninitiated.
-
-In either case, the framework of individual empiricism rejects the notion of truth middlemen towards self-verification, so the use of heuristics may not be as relevant in this paradigm.
-
-## Bonus Dimension: Simplicity / Complexity
-
-This dimension deserves special mention. The heavy lifting behind the NAC protocol occurs in the theory of fungal pathogens at a systemic level. Full credit to the NAC protocol creators and more power to them. The supplement and diet protocols do the work, but the systemic interactions behind the theory are where elegant simplicity shines through.
-
-## **🌎🧑🏼🚀🔫🧑🏼🚀 Wait, it's the fungi? Always has been.**
-
-Compare the simple few-step supplement NAC protocol against Bryan Johnsons Blueprint, which involves a laundry list of supplements and techniques to decrease aging. In my opinion, this is a more difficult road as each supplement may increase the dimensions of verification difficulty, safety and benefits towards undesirable equilibria.
-
-I hope this framework of individual health empiricism is useful to somebody who is struggling with the challenge of navigating the waters of personal health, either for themselves or their loved ones.
diff --git a/app/routes/blog+/posts+/individual-empiricism+/route.tsx b/app/routes/blog+/posts+/individual-empiricism+/route.tsx
deleted file mode 100644
index 9228004..0000000
--- a/app/routes/blog+/posts+/individual-empiricism+/route.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import Component from './post.md'
-
-import { MetaFunction } from 'react-router'
-import { redirectToImageGenerator, openGraphImageMeta } from 'src/utils/misc'
-
-export const meta: MetaFunction = () => {
- const title = 'Individual Health Empiricism'
- const subtitle = 'Evaluation Framework for Self-Debugging'
- const body =
- 'Unveiling the power of self-experimentation in the quest for personal health breakthroughs, using my NAC protocol journey as example.'
- const imageUrl = redirectToImageGenerator({ title, subtitle })
-
- return [
- { name: 'title', content: title },
- { property: 'og:title', content: title },
- { property: 'og:description', content: body },
- ...openGraphImageMeta(imageUrl),
- ]
-}
-
-export default Component
diff --git a/app/routes/memery+/_layout.tsx b/app/routes/memery+/_layout.tsx
index 3d00d68..1617122 100644
--- a/app/routes/memery+/_layout.tsx
+++ b/app/routes/memery+/_layout.tsx
@@ -1,5 +1,6 @@
import { Link, Outlet, useLoaderData, LoaderFunctionArgs } from 'react-router'
import React from 'react'
+import { useTranslation } from 'react-i18next'
import { getMemeByKey } from './utils.server'
import { invariantResponse } from 'src/utils/misc'
import { Carousel, CarouselContent, CarouselItem } from 'src/components/ui/carousel'
@@ -21,12 +22,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
}
const MemeLayout: React.FC = () => {
+ const { t } = useTranslation()
const { meme, memeKey } = useLoaderData()
return (
diff --git a/app/routes/memery+/platos-cave/readme.mdx b/app/routes/memery+/platos-cave/en/readme.mdx
similarity index 89%
rename from app/routes/memery+/platos-cave/readme.mdx
rename to app/routes/memery+/platos-cave/en/readme.mdx
index a32615e..860b0a6 100644
--- a/app/routes/memery+/platos-cave/readme.mdx
+++ b/app/routes/memery+/platos-cave/en/readme.mdx
@@ -10,7 +10,7 @@ Trojan horse mechanism: Fungi can also use immune cells like monocytes to "hitch
References:
-1.
[It’s all in your head: antifungal immunity in the brain](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7438209/)
+1.
[It's all in your head: antifungal immunity in the brain](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7438209/)
2.
[Blood–brain barrier invasion by Cryptococcus neoformans is enhanced by functional interactions with plasmin](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3352358/)
3.
[Cryptococcus neoformans Infection in the Central Nervous System: The Battle between Host and Pathogen](https://www.mdpi.com/2309-608X/8/10/1069)
diff --git a/app/routes/memery+/platos-cave/es/readme.mdx b/app/routes/memery+/platos-cave/es/readme.mdx
new file mode 100644
index 0000000..92a885f
--- /dev/null
+++ b/app/routes/memery+/platos-cave/es/readme.mdx
@@ -0,0 +1,17 @@
+## Cruzando la barrera hematoencefálica
+
+Los hongos pueden acceder al sistema nervioso central (SNC) cruzando la barrera hematoencefálica (BHE) a través de algunos mecanismos clave:
+
+Penetración transcelular: Hongos como Cryptococcus neoformans pueden invadir activamente y penetrar a través de las células endoteliales cerebrales para cruzar la BHE. Esto implica interacciones entre las proteínas de superficie del hongo y los receptores en las células endoteliales. [2](#2),[3](#3).
+
+Migración paracelular: Hongos como Candida albicans también pueden ser capaces de cruzar la BHE a través de los espacios entre las células endoteliales, degradando proteínas de las uniones estrechas como la E-cadherina. [1](#1)
+
+Mecanismo del caballo de Troya: Los hongos también pueden utilizar células inmunitarias como los monocitos para "viajar" a través de la BHE, un proceso conocido como el mecanismo del caballo de Troya. [2](#2)
+
+Referencias:
+
+1.
[Todo está en tu cabeza: inmunidad antifúngica en el cerebro](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7438209/)
+2.
[La invasión de la barrera hematoencefálica por Cryptococcus neoformans se ve potenciada por interacciones funcionales con la plasmina](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3352358/)
+3.
+ [Infección por Cryptococcus neoformans en el sistema nervioso central: La batalla entre el huésped y el patógeno](https://www.mdpi.com/2309-608X/8/10/1069)
+
\ No newline at end of file
diff --git a/app/routes/memery+/platos-cave/fr/readme.mdx b/app/routes/memery+/platos-cave/fr/readme.mdx
new file mode 100644
index 0000000..cc4e0e3
--- /dev/null
+++ b/app/routes/memery+/platos-cave/fr/readme.mdx
@@ -0,0 +1,17 @@
+## Traversée de la barrière hémato-encéphalique
+
+Les champignons peuvent accéder au système nerveux central (SNC) en traversant la barrière hémato-encéphalique (BHE) par quelques mécanismes clés :
+
+Pénétration transcellulaire : Les champignons comme Cryptococcus neoformans peuvent envahir activement et pénétrer à travers les cellules endothéliales cérébrales pour traverser la BHE. Cela implique des interactions entre les protéines de surface fongiques et les récepteurs sur les cellules endothéliales. [2](#2),[3](#3).
+
+Migration paracellulaire : Les champignons comme Candida albicans peuvent également être capables de traverser la BHE à travers les espaces entre les cellules endothéliales, en dégradant des protéines de jonction serrée comme l'E-cadhérine. [1](#1)
+
+Mécanisme du cheval de Troie : Les champignons peuvent également utiliser des cellules immunitaires comme les monocytes pour "faire du stop" à travers la BHE, un processus connu sous le nom de mécanisme du cheval de Troie. [2](#2)
+
+Références :
+
+1.
[Tout est dans votre tête : l'immunité antifongique dans le cerveau](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7438209/)
+2.
[L'invasion de la barrière hémato-encéphalique par Cryptococcus neoformans est renforcée par des interactions fonctionnelles avec la plasmine](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3352358/)
+3.
+ [Infection par Cryptococcus neoformans dans le système nerveux central : La bataille entre l'hôte et le pathogène](https://www.mdpi.com/2309-608X/8/10/1069)
+
\ No newline at end of file
diff --git a/app/routes/memery+/platos-cave/route.tsx b/app/routes/memery+/platos-cave/route.tsx
index db3e918..c02ab04 100644
--- a/app/routes/memery+/platos-cave/route.tsx
+++ b/app/routes/memery+/platos-cave/route.tsx
@@ -1,7 +1,6 @@
import { MetaFunction } from 'react-router'
-import { getMemeByKey } from '../utils.server'
-
-import Readme from './readme.mdx'
+import { lazy, Suspense } from 'react'
+import type { Route } from './+types/route'
import { openGraphImageMeta, redirectToImageGenerator } from 'src/utils/misc'
export const meta: MetaFunction = () => {
@@ -20,15 +19,20 @@ export const meta: MetaFunction = () => {
]
}
-export const loader = async () => {
- const meme = await getMemeByKey('platos-cave')
- return { meme }
+export const loader = async ({ context }: Route.LoaderArgs) => {
+ const { lang } = context
+ return { lang }
}
-export default function PlatosCaveRoute() {
+export default function PlatosCaveRoute({ loaderData }: Route.ComponentProps) {
+ const { lang } = loaderData
+ const ReadmeComponent = lazy(() => import(`./${lang}/readme.mdx`))
+
return (
-
+ Loading...
}>
+
+
)
}
diff --git a/app/routes/privacy+/en/privacy.mdx b/app/routes/privacy+/en/privacy.mdx
new file mode 100644
index 0000000..508a4ee
--- /dev/null
+++ b/app/routes/privacy+/en/privacy.mdx
@@ -0,0 +1,33 @@
+# Cognactive Privacy & Data Usage Policy
+
+Last Updated: 2025-01-27
+
+Sovilon Software Inc ("we," "us," or "our"), registered in Canada, operates the Cognactive application ("Service") globally. This policy outlines how we handle personal and non-personal data when you use our Service, regardless of your location, and explains your rights regarding this data.
+
+## Information Collection And Use
+
+We do not collect any personally identifying information (PII) from users of our Service, in compliance with international privacy regulations including GDPR (European Union), CCPA (California, USA), PIPEDA (Canada), and other applicable laws.
+
+## Data Collected:
+
+**Regimen Tracking Information:** We collect data related to important dates for regimen habit tracking activities (start date, phase transition dates), regimen activities like supplements, notes, and observed experiences. This data is stored locally on your device using browser local storage technology and is not transmitted to or stored on any server owned or operated by us or third parties. This approach ensures data privacy regardless of your geographical location.
+
+**Camera Photos:** We relay photos for features related to computer vision assisted workflows, including but not limited to ingredient scanning. The photos are transmitted from the Cognactive application through servers located in Canada and the United States, then forwarded directly to upstream computer vision API providers without persistent storage on our infrastructure. For detailed information about data handling practices, please refer to the privacy policies of our API providers.
+
+Our primary computer vision API provider is OpenAI (United States), whose policy explicitly states they do not save or train their models on data submitted via their API.
+
+## Use of Data
+
+The data collected by the Cognactive app is used exclusively to enable your regimen tracking activities within the app. We maintain a strict no-sharing policy - your data is not shared with third parties for any purpose, regardless of jurisdiction.
+
+## Data Management
+
+Users worldwide can exercise their right to data control by resetting their activity tracker within the Cognactive app, which permanently removes all locally saved data.
+
+## Changes To This Privacy Policy
+
+We may update our Privacy Policy periodically to reflect changes in global privacy regulations or our practices. We will notify you of any changes by posting an updated policy at this webpage. We recommend reviewing this Privacy Policy regularly to stay informed of any updates.
+
+## Contact Us
+
+For privacy-related inquiries, you can reach us through our [contact page](https://sovilon.com/contact).
diff --git a/app/routes/privacy+/es/privacy.mdx b/app/routes/privacy+/es/privacy.mdx
new file mode 100644
index 0000000..4987e12
--- /dev/null
+++ b/app/routes/privacy+/es/privacy.mdx
@@ -0,0 +1,33 @@
+# Política de Privacidad y Uso de Datos de Cognactive
+
+Última Actualización: 2025-06-15
+
+Sovilon Software Inc ("nosotros," "nos," o "nuestro") opera la aplicación Cognactive ("Servicio"). Esta página le informa sobre nuestras políticas con respecto a la recopilación, uso y divulgación de datos personales y no personales cuando utiliza nuestro Servicio y las opciones que tiene asociadas con esos datos.
+
+## Recopilación y Uso de Información
+
+No recopilamos ninguna información de identificación personal (PII) de los usuarios de nuestro Servicio.
+
+## Datos Recopilados:
+
+**Información de Seguimiento de Régimen:** Recopilamos datos relacionados con fechas importantes para actividades de seguimiento de hábitos de régimen (fecha de inicio, fechas de transición de fase), actividades de régimen como suplementos, notas y experiencias observadas. Estos datos se almacenan localmente en su dispositivo utilizando la tecnología de almacenamiento local del navegador y no se transmiten ni almacenan en ningún servidor de nuestra propiedad o operado por nosotros o terceros.
+
+**Fotos de Cámara:** Transmitimos fotos para funciones relacionadas con flujos de trabajo asistidos por visión por computadora, incluyendo pero no limitado a escaneo de ingredientes. Las fotos se transmiten desde la aplicación Cognactive, pasando por nuestros servidores y se reenvían directamente a los proveedores de API de visión por computadora sin guardarlas por separado en nuestros servidores. Consulte la política de privacidad de los proveedores de API para obtener más información.
+
+El proveedor de API de visión por fotos de cámara es OpenAI, cuya política indica que no se guarda ni se entrena con los datos enviados a través de su API.
+
+## Uso de Datos
+
+Los datos recopilados por la aplicación Cognactive son únicamente para permitirle rastrear sus actividades de régimen dentro de la aplicación. No se comparte ningún dato con terceros para ningún propósito.
+
+## Gestión de Datos
+
+Los usuarios pueden restablecer su rastreador de actividades dentro de la aplicación Cognactive, lo que elimina todos los datos guardados localmente.
+
+## Cambios en Esta Política de Privacidad
+
+Podemos actualizar nuestra Política de Privacidad de vez en cuando. Le notificaremos sobre cualquier cambio publicando una política actualizada en esta página web. Se le aconseja revisar esta Política de Privacidad periódicamente para cualquier cambio.
+
+## Contáctenos
+
+Si tiene alguna pregunta sobre esta Política de Privacidad, [contacte aquí](https://sovilon.com/contact).
\ No newline at end of file
diff --git a/app/routes/privacy+/fr/privacy.mdx b/app/routes/privacy+/fr/privacy.mdx
new file mode 100644
index 0000000..beb92c9
--- /dev/null
+++ b/app/routes/privacy+/fr/privacy.mdx
@@ -0,0 +1,33 @@
+# Politique de Confidentialité et d'Utilisation des Données de Cognactive
+
+Dernière mise à jour : 2025-06-15
+
+Sovilon Software Inc ("nous," "notre" ou "nos") exploite l'application Cognactive ("Service"). Cette page vous informe de nos politiques concernant la collecte, l'utilisation et la divulgation des données personnelles et non personnelles lorsque vous utilisez notre Service et des choix que vous avez associés à ces données.
+
+## Collecte et Utilisation des Informations
+
+Nous ne collectons aucune information personnellement identifiable (PII) des utilisateurs de notre Service.
+
+## Données Collectées :
+
+**Informations de Suivi de Régime :** Nous collectons des données relatives aux dates importantes pour les activités de suivi des habitudes de régime (date de début, dates de transition de phase), les activités de régime comme les suppléments, les notes et les expériences observées. Ces données sont stockées localement sur votre appareil en utilisant la technologie de stockage local du navigateur et ne sont pas transmises ou stockées sur un serveur détenu ou exploité par nous ou par des tiers.
+
+**Photos de Caméra :** Nous relayons des photos pour des fonctionnalités liées aux flux de travail assistés par vision par ordinateur, y compris mais sans s'y limiter, la numérisation d'ingrédients. Les photos sont relayées depuis l'application Cognactive, passant par nos serveurs et transmises directement aux fournisseurs d'API de vision par ordinateur en amont sans les enregistrer séparément sur nos serveurs. Veuillez vous référer à la politique de confidentialité des fournisseurs d'API pour plus d'informations.
+
+Le fournisseur d'API de vision par photo de caméra est OpenAI, dont la politique indique qu'il n'y a pas de sauvegarde ou d'entraînement sur les données soumises via leur API.
+
+## Utilisation des Données
+
+Les données collectées par l'application Cognactive sont uniquement destinées à vous permettre de suivre vos activités de régime au sein de l'application. Aucune donnée n'est partagée avec des tiers à quelque fin que ce soit.
+
+## Gestion des Données
+
+Les utilisateurs peuvent réinitialiser leur traqueur d'activité dans l'application Cognactive, ce qui supprime toutes les données enregistrées localement.
+
+## Modifications de cette Politique de Confidentialité
+
+Nous pouvons mettre à jour notre Politique de Confidentialité de temps à autre. Nous vous informerons de tout changement en publiant une politique mise à jour sur cette page web. Il est conseillé de consulter périodiquement cette Politique de Confidentialité pour tout changement.
+
+## Contactez-nous
+
+Si vous avez des questions concernant cette Politique de Confidentialité, [contactez-nous ici](https://sovilon.com/contact).
\ No newline at end of file
diff --git a/app/routes/privacy+/index.tsx b/app/routes/privacy+/index.tsx
index f35b1a3..a5cc109 100644
--- a/app/routes/privacy+/index.tsx
+++ b/app/routes/privacy+/index.tsx
@@ -1,32 +1,34 @@
import { useState } from 'react'
import { CheckCircle, ExternalLink, XCircle } from 'lucide-react'
import { Link } from 'react-router'
+import { useTranslation } from 'react-i18next'
const PrivacyDimensions: React.FC = () => {
+ const { t } = useTranslation()
const [selectedDimension, setSelectedDimension] = useState('localDataStorage')
const dimensions = [
{
id: 'localDataStorage',
- name: 'Local Data Privacy',
- description: 'Regimen activity data is stored only on your device and never transmitted to servers.',
+ name: t('privacy.dimensions.local-storage.title'),
+ description: t('privacy.dimensions.local-storage.description'),
icon: CheckCircle,
},
{
id: 'thirdPartySharing',
- name: 'No Third-Party Sharing',
- description: 'Regimen activity data is not shared with third parties.',
+ name: t('privacy.dimensions.third-party.title'),
+ description: t('privacy.dimensions.third-party.description'),
icon: XCircle,
},
]
return (
-
Privacy & Data Usage
+
{t('privacy.title')}
- Read full privacy policy
+ {t('privacy.policy-link')}
-
)
}
-
-export default PrivacyPolicy
diff --git a/app/routes/privacy+/privacy.md b/app/routes/privacy+/privacy.md
deleted file mode 100644
index 4bbba5e..0000000
--- a/app/routes/privacy+/privacy.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Cognactive Privacy & Data Usage Policy
-
-Last Updated: 2024-06-15
-
-Sovilon Software Inc ("we," "us," or "our") operates the Cognactive application ("Service"). This page informs you of our policies regarding the collection, use, and disclosure of personal and non-personal data when you use our Service and the choices you have associated with that data.
-
-## Information Collection And Use
-
-We do not collect any personally identifying information (PII) from users of our Service.
-
-## Data Collected:
-
-**Regimen Tracking Information:** We collect data related to important dates for regimen habit tracking activities (start date, phase transition dates), regimen activities like supplements, notes, and observed experiences. This data is stored locally on your device using browser local storage technology and is not transmitted to or stored on any server owned or operated by us or third parties.
-
-**Camera Photos:** We relay photos for features related to computer vision assisted workflows, including but not limited to ingredient scanning. The photos are relayed from the Cognactive application, passing through our servers and forwarded directly to upstream computer vision API providers without saving them separately on our servers. Refer to the privacy policy of the API providers for more information.
-
-Camera photo vision API provider is OpenAI, whose policy indicates there is no saving or training on data submitted via their API.
-
-## Use of Data
-
-The data collected by the Cognactive app is solely for enabling you to track your regimen activities within the app. No data is shared with third parties for any purpose.
-
-## Data Management
-
-Users can reset their activity tracker within the Cognactive app, which removes all locally saved data.
-
-## Changes To This Privacy Policy
-
-We may update our Privacy Policy from time to time. We will notify you of any changes by posting an updated policy at this webpage. You are advised to review this Privacy Policy periodically for any changes.
-
-## Contact Us
-
-If you have any questions about this Privacy Policy, [contact here](https://sovilon.com/contact).
diff --git a/app/routes/resource.locales.tsx b/app/routes/resource.locales.tsx
new file mode 100644
index 0000000..5e32e69
--- /dev/null
+++ b/app/routes/resource.locales.tsx
@@ -0,0 +1,56 @@
+import { z } from 'zod'
+import { resources } from '@/localization/resource'
+import type { Route } from './+types/resource.locales'
+import { getEnv } from '@/utils/env.server'
+import { cacheHeader } from 'pretty-cache-header'
+
+export async function loader({ request }: Route.LoaderArgs) {
+ const url = new URL(request.url)
+
+ const lng = z
+ .string()
+ .refine((lng): lng is keyof typeof resources => Object.keys(resources).includes(lng))
+ .safeParse(url.searchParams.get('lng'))
+
+ const namespaces = resources[lng.success ? lng.data : 'en']
+
+ const ns = z
+ .string()
+ .refine((ns): ns is keyof typeof namespaces => {
+ return Object.keys(namespaces).includes(ns)
+ })
+ .safeParse(url.searchParams.get('ns'))
+
+ const headers = new Headers()
+
+ // On production, we want to add cache headers to the response
+ if (getEnv().MODE === 'production') {
+ headers.set(
+ 'Cache-Control',
+ cacheHeader({
+ maxAge: '5m',
+ sMaxage: '1d',
+ staleWhileRevalidate: '7d',
+ staleIfError: '7d',
+ }),
+ )
+ }
+
+ return Response.json(namespaces[ns.success ? ns.data : 'translation'], { headers })
+}
+
+export const action = async ({ request }: Route.ActionArgs) => {
+ const formData = await request.formData()
+ const lng = formData.get('lng')
+
+ const referrer = request.headers.get('Referer') || '/'
+
+ const headers = new Headers()
+ headers.append('Set-Cookie', `lng=${lng}; Path=/; SameSite=Lax`)
+ headers.append('Location', referrer)
+
+ return new Response('Ok', {
+ headers,
+ status: 302,
+ })
+}
diff --git a/app/routes/trends.tsx b/app/routes/trends.tsx
index 6821397..a18e96e 100644
--- a/app/routes/trends.tsx
+++ b/app/routes/trends.tsx
@@ -4,6 +4,7 @@ import ContentHeader from 'src/components/content-header.tsx'
import { Button } from 'src/components/ui/button'
import db, { ISymptom } from 'src/pages/tracker/db'
import type { Route } from './+types/trends'
+import { useTranslation } from 'react-i18next'
export const clientLoader = async () => {
const symptoms = await db.symptoms.toArray()
@@ -15,10 +16,12 @@ export const clientLoader = async () => {
clientLoader.hydrate = true
export function HydrateFallback() {
- return