Skip to content
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
04e5d23
feat: internationalization fullstack setup
micksabox Dec 29, 2024
2a673b3
chore: internationalization configuration
micksabox Dec 29, 2024
2786767
chore: configure settings
micksabox Dec 29, 2024
5f328ed
Merge branch 'feat/translation' into dev
micksabox Dec 29, 2024
f1c85cf
fix: server start and stream
micksabox Dec 29, 2024
4187312
feat: forward client env to browser context
micksabox Dec 29, 2024
bbc3ec2
fix: vite dev server issues by moving /src into /app and setup tsconf…
micksabox Dec 29, 2024
ad319a0
feat: language switcher in footer
micksabox Dec 29, 2024
1473f35
fix: type resolution and parse error for resource.locales
micksabox Dec 30, 2024
ebab202
feat: configure languine.ai
micksabox Dec 30, 2024
c95d07f
feat: footer, header and french language localization
micksabox Dec 30, 2024
e1262f8
fix: tailwind and try transmart
micksabox Dec 30, 2024
be393c6
feat: language select persistence & detection via cookie
micksabox Dec 30, 2024
40d1d26
fix: default locale detection
micksabox Dec 30, 2024
fa4e331
chore: remove unused individual empiricism post
micksabox Jan 2, 2025
670d442
chore: more localization config w/ languine.ai
micksabox Jan 2, 2025
42f63a3
fix: metagame post
micksabox Jan 2, 2025
3455b3a
fix: order of locale detection
micksabox Jan 2, 2025
3f1de81
chore: remove transmart
micksabox Jan 16, 2025
7ced2ca
docs: localization readme
micksabox Jan 16, 2025
5377a75
refactor: translation keys and homepage translated
micksabox Jan 16, 2025
49904ac
feat: more homepage translations keyed
micksabox Jan 16, 2025
82e7850
chore: more translations throughout app
micksabox Jan 18, 2025
b2a8522
chore: localize date picker
micksabox Jan 18, 2025
1ccee57
feat: memery translation
micksabox Jan 28, 2025
42ff3a8
feat: update privacy translations
micksabox Jan 28, 2025
9b8faf6
chore: privacy localized
micksabox Jan 28, 2025
583a556
feat: privacy translations
micksabox Jan 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# npm run typecheck
npx lint-staged
# npx lint-staged
6 changes: 4 additions & 2 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@
"className",
"ngClass",
"tw"
]
],
"i18n-ally.localesPaths": [
"src/i18n/locales",
],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.annotations": true
}
File renamed without changes
47 changes: 47 additions & 0 deletions app/components/footer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Link } from 'react-router'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect import path for react-router.

The import path should be @remix-run/react instead of react-router.

-import { Link } from 'react-router'
+import { Link } from '@remix-run/react'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { Link } from 'react-router'
import { Link } from '@remix-run/react'

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('translation')

return (
<footer className="w-full bg-foreground text-xs text-slate-500">
<div className="container p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<span className="inline-block w-16">
<CognactiveIcon className="fill-slate-500 transition-colors hover:fill-white" darkMode />
</span>
<Button size={'sm'} asChild>
<Link to={TELEGRAM_CHAT_LINK}>
{t('chat-nac-link-title')} <ArrowUpRightSquare className="ml-2 inline-block w-4" />
</Link>
</Button>
<Button size={'sm'} asChild>
<a href={GITHUB_REPO_BASE} target="_blank" rel="noreferrer">
{t('github-link-title')} &nbsp;
<Github className="w-4" />
</a>
</Button>
</div>
<LanguageSelector triggerClassName="text-white" />
</div>
<p className="my-2">
<Link className="underline underline-offset-auto" to={'/privacy'}>
{t('data-usage-title')}
</Link>
</p>
<p className="bottom-inset">
<span className="font-semibold">{t('medical-disclaimer')}:</span> {t('medical-disclaimer-desc')}
</p>
</div>
</footer>
)
}

export default Footer
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -21,6 +21,8 @@ export function Header(props: IProps) {
minDuration: 300,
})

const { t } = useTranslation('translation')

return (
<div
className={cn(
Expand All @@ -36,11 +38,10 @@ export function Header(props: IProps) {
cognactive
</Link>
<div className="flex items-center gap-4">
{/* <LanguageSelector /> */}
{isHomepage && (
<Button asChild>
<a href="https://github.com/micksabox/cognactive" target="_blank" rel="noreferrer">
View Code &nbsp;
{t('github-link-title')} &nbsp;
<Github className="w-4" />
</a>
</Button>
Expand Down
22 changes: 11 additions & 11 deletions src/components/hero/index.tsx → app/components/hero/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ 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
Expand Down Expand Up @@ -46,6 +45,8 @@ const HeroFeature: React.FC<FeatureProps> = ({ icon: Icon, title, description, l
}

export const Hero = () => {
const { t } = useTranslation('translation')

return (
<div className="flex bg-gradient-to-b from-slate-600 to-slate-900">
<section className="w-full py-8 text-white">
Expand All @@ -57,15 +58,14 @@ export const Hero = () => {
<CognactiveIcon className="fill-white" darkMode />
</div>
<h1 className="my-4 text-3xl font-bold tracking-tighter text-transparent text-white sm:text-5xl xl:text-6xl/none">
{t['hero-title']}
{t('hero-title')}
</h1>
<p className="mb-4">Open source tools and tech for the anti-fungal NAC protocol.</p>
<div className="mx-auto flex max-w-sm flex-col gap-2">
<Button className="bg-cyan py-6 text-lg font-semibold" size={'lg'} asChild>
<Link to="/tracker" viewTransition>
<ListTodo className="mr-2" />
Regimen Tracker
{/* {t('get-started')} */}
{t('get-started')}
</Link>
</Button>
<Button className="py-6 text-lg font-semibold" size={'lg'} asChild>
Expand Down Expand Up @@ -107,18 +107,18 @@ export const Hero = () => {
<div className="grid grid-cols-1 gap-2">
<HeroFeature
icon={() => <ActivitySquare size={16} />}
title={t['tracking']}
description={t['tracking-desc']}
title={t('tracking')}
description={t('tracking-desc')}
/>
<HeroFeature
icon={() => <LayoutGridIcon size={16} />}
title={t['hero-appexperience']}
description={t['hero-appexperience-desc']}
title={t('hero-appexperience')}
description={t('hero-appexperience-desc')}
/>
<HeroFeature
icon={() => <HeartHandshakeIcon size={16} />}
title={t['hero-opensource']}
description={t['hero-opensource-desc']}
title={t('hero-opensource')}
description={t('hero-opensource-desc')}
link={GITHUB_REPO_BASE}
linkText="Source code available here."
/>
Expand Down
81 changes: 81 additions & 0 deletions app/components/language-selector/index.tsx
Original file line number Diff line number Diff line change
@@ -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
}
Comment on lines +10 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for Intl.DisplayNames.

The current implementation might throw errors in environments where Intl.DisplayNames is not supported.

 const getLocaleDisplayName = (locale: string, displayLocale?: string) => {
+  try {
   const displayName = new Intl.DisplayNames([displayLocale || locale], {
     type: 'language',
   }).of(locale)
   return displayName ? displayName.charAt(0).toLocaleUpperCase() + displayName.slice(1) : locale
+  } catch (error) {
+    console.warn('Intl.DisplayNames not supported:', error)
+    return locale
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
}
const getLocaleDisplayName = (locale: string, displayLocale?: string) => {
try {
const displayName = new Intl.DisplayNames([displayLocale || locale], {
type: 'language',
}).of(locale)
return displayName ? displayName.charAt(0).toLocaleUpperCase() + displayName.slice(1) : locale
} catch (error) {
console.warn('Intl.DisplayNames not supported:', error)
return 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) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace 'any' type with proper type definition.

Using 'any' type reduces type safety. Consider using a more specific type.

-  const languageChanged = useCallback(async (locale: any) => {
+  const languageChanged = useCallback(async (locale: string) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const languageChanged = useCallback(async (locale: any) => {
const languageChanged = useCallback(async (locale: string) => {

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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant={'outline'} className={cn('flex items-center gap-1', triggerClassName)}>
<Languages size={18} />
{currentLanguage && getLocaleDisplayName(currentLanguage)}
<ChevronDown size={12} />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Select Language</DialogTitle>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Internationalize dialog title.

The dialog title should be translated using the translation hook.

-          <DialogTitle>Select Language</DialogTitle>
+          <DialogTitle>{t('language-selector.title')}</DialogTitle>

Committable suggestion skipped: line range outside the PR's diff.

</DialogHeader>
<div className="mt-4 space-y-2">
{localesAndNames.map(({ locale, name }) => {
const isSelected = currentLanguage === locale
return (
<form key={locale} action="/resource/locales" method="post">
<input type="hidden" name="lng" value={locale} />
<button
className={cn(
'relative w-full cursor-pointer select-none rounded-md px-4 py-3 hover:bg-zinc-200',
isSelected && 'bg-zinc-100',
)}
type="submit"
>
<div key={locale}>
<span className={cn('block truncate', isSelected && 'font-bold text-primary')}>{name}</span>
</div>
</button>
</form>
)
})}
</div>
</DialogContent>
</Dialog>
)
}

export { LanguageSelector }
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
59 changes: 51 additions & 8 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -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,
<StrictMode>
<HydratedRouter />
</StrictMode>,
)
})
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 `<html lang>` 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,
<I18nextProvider i18n={i18next}>
<StrictMode>
<HydratedRouter />
</StrictMode>
</I18nextProvider>,
)
})
}

if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate)
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
window.setTimeout(hydrate, 1)
}
Loading