-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/translation #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 18 commits
04e5d23
2a673b3
2786767
5f328ed
f1c85cf
4187312
bbc3ec2
ad319a0
1473f35
ebab202
c95d07f
e1262f8
be393c6
40d1d26
fa4e331
670d442
42f63a3
3455b3a
3f1de81
7ced2ca
5377a75
49904ac
82e7850
b2a8522
1ccee57
42ff3a8
9b8faf6
583a556
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,47 @@ | ||||||
| import { Link } from 'react-router' | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect import path for The import path should be -import { Link } from 'react-router'
+import { Link } from '@remix-run/react'📝 Committable suggestion
Suggested change
|
||||||
| 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')} | ||||||
| <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 | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| 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) => { | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>
|
||||||||||||||||||||||||||||||||||||
| </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 } | ||||||||||||||||||||||||||||||||||||
| 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) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.