mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-25 17:56:12 +00:00
This commit introduces internationalization (i18n) to your application using the `remix-i18next` framework, along with `i18next` and `react-i18next`. It also includes Turkish as the first additional language. Key changes include: 1. **Framework Integration:** * Installed necessary dependencies: `remix-i18next`, `i18next`, `react-i18next`, `i18next-browser-languagedetector`, and `i18next-http-backend`. * Configured `remix-i18next` middleware (`app/middleware/i18next.ts`) with language detection (cookie-based) and resource loading. * Updated `app/root.tsx` to incorporate the i18n middleware, manage locale state via a loader, and set appropriate HTML attributes. * Modified `app/entry.client.tsx` and `app/entry.server.tsx` to initialize i18next and wrap the application with `I18nextProvider` for both client-side rendering and SSR. 2. **Localization Files:** * Created `app/locales/en.ts` for English (fallback) translations. * Created `app/locales/tr.ts` for Turkish translations. * Populated these files with initial strings for UI elements in the header. 3. **Component Internationalization:** * Modified `app/components/header/Header.tsx` and `app/components/header/HeaderActionButtons.client.tsx` to use the `useTranslation` hook and `t()` function for displaying translated strings. This includes static text, dynamic text with interpolation, and alt attributes for images. 4. **Language Switching:** * Implemented a language switcher dropdown component within `app/components/header/Header.tsx`. * The switcher allows you to select between English and Turkish, with the selection persisted via a cookie. 5. **Documentation:** * Added a new "Internationalization (i18n)" section to `README.md`, detailing how to add/modify translations and support new languages. This work completes Part 1 of the issue, laying the foundation for a multilingual application.
209 lines
7.8 KiB
TypeScript
209 lines
7.8 KiB
TypeScript
import { useStore } from '@nanostores/react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import useViewport from '~/lib/hooks';
|
|
import { chatStore } from '~/lib/stores/chat';
|
|
import { netlifyConnection } from '~/lib/stores/netlify';
|
|
import { vercelConnection } from '~/lib/stores/vercel';
|
|
import { workbenchStore } from '~/lib/stores/workbench';
|
|
import { classNames } from '~/utils/classNames';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { streamingState } from '~/lib/stores/streaming';
|
|
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
|
|
import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
|
|
import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
|
|
import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
|
|
|
|
interface HeaderActionButtonsProps {}
|
|
|
|
export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|
const { t } = useTranslation();
|
|
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
|
const { showChat } = useStore(chatStore);
|
|
const netlifyConn = useStore(netlifyConnection);
|
|
const vercelConn = useStore(vercelConnection);
|
|
const [activePreviewIndex] = useState(0);
|
|
const previews = useStore(workbenchStore.previews);
|
|
const activePreview = previews[activePreviewIndex];
|
|
const [isDeploying, setIsDeploying] = useState(false);
|
|
const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | null>(null);
|
|
const isSmallViewport = useViewport(1024);
|
|
const canHideChat = showWorkbench || !showChat;
|
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
const isStreaming = useStore(streamingState);
|
|
const { handleVercelDeploy } = useVercelDeploy();
|
|
const { handleNetlifyDeploy } = useNetlifyDeploy();
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
setIsDropdownOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
const onVercelDeploy = async () => {
|
|
setIsDeploying(true);
|
|
setDeployingTo('vercel');
|
|
|
|
try {
|
|
await handleVercelDeploy();
|
|
} finally {
|
|
setIsDeploying(false);
|
|
setDeployingTo(null);
|
|
}
|
|
};
|
|
|
|
const onNetlifyDeploy = async () => {
|
|
setIsDeploying(true);
|
|
setDeployingTo('netlify');
|
|
|
|
try {
|
|
await handleNetlifyDeploy();
|
|
} finally {
|
|
setIsDeploying(false);
|
|
setDeployingTo(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex">
|
|
<div className="relative" ref={dropdownRef}>
|
|
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
|
|
<Button
|
|
active
|
|
disabled={isDeploying || !activePreview || isStreaming}
|
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
className="px-4 hover:bg-bolt-elements-item-backgroundActive flex items-center gap-2"
|
|
>
|
|
{isDeploying ? t('deployingToPlatform', { platform: deployingTo }) : t('deploy')}
|
|
<div
|
|
className={classNames('i-ph:caret-down w-4 h-4 transition-transform', isDropdownOpen ? 'rotate-180' : '')}
|
|
/>
|
|
</Button>
|
|
</div>
|
|
|
|
{isDropdownOpen && (
|
|
<div className="absolute right-2 flex flex-col gap-1 z-50 p-1 mt-1 min-w-[13.5rem] bg-bolt-elements-background-depth-2 rounded-md shadow-lg bg-bolt-elements-backgroundDefault border border-bolt-elements-borderColor">
|
|
<Button
|
|
active
|
|
onClick={() => {
|
|
onNetlifyDeploy();
|
|
setIsDropdownOpen(false);
|
|
}}
|
|
disabled={isDeploying || !activePreview || !netlifyConn.user}
|
|
className="flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative"
|
|
>
|
|
<img
|
|
className="w-5 h-5"
|
|
height="24"
|
|
width="24"
|
|
crossOrigin="anonymous"
|
|
src="https://cdn.simpleicons.org/netlify"
|
|
/>
|
|
<span className="mx-auto">
|
|
{!netlifyConn.user ? t('noNetlifyAccount') : t('deployToNetlify')}
|
|
</span>
|
|
{netlifyConn.user && <NetlifyDeploymentLink />}
|
|
</Button>
|
|
<Button
|
|
active
|
|
onClick={() => {
|
|
onVercelDeploy();
|
|
setIsDropdownOpen(false);
|
|
}}
|
|
disabled={isDeploying || !activePreview || !vercelConn.user}
|
|
className="flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative"
|
|
>
|
|
<img
|
|
className="w-5 h-5 bg-black p-1 rounded"
|
|
height="24"
|
|
width="24"
|
|
crossOrigin="anonymous"
|
|
src="https://cdn.simpleicons.org/vercel/white"
|
|
alt={t('vercelAlt')}
|
|
/>
|
|
<span className="mx-auto">{!vercelConn.user ? t('noVercelAccount') : t('deployToVercel')}</span>
|
|
{vercelConn.user && <VercelDeploymentLink />}
|
|
</Button>
|
|
<Button
|
|
active={false}
|
|
disabled
|
|
className="flex items-center w-full rounded-md px-4 py-2 text-sm text-bolt-elements-textTertiary gap-2"
|
|
>
|
|
<span className="sr-only">{t('comingSoonSR')}</span>
|
|
<img
|
|
className="w-5 h-5"
|
|
height="24"
|
|
width="24"
|
|
crossOrigin="anonymous"
|
|
src="https://cdn.simpleicons.org/cloudflare"
|
|
alt={t('cloudflareAlt')}
|
|
/>
|
|
<span className="mx-auto">{t('deployToCloudflareComingSoon')}</span>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
|
<Button
|
|
active={showChat}
|
|
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's not needed
|
|
onClick={() => {
|
|
if (canHideChat) {
|
|
chatStore.setKey('showChat', !showChat);
|
|
}
|
|
}}
|
|
>
|
|
<div className="i-bolt:chat text-sm" />
|
|
</Button>
|
|
<div className="w-[1px] bg-bolt-elements-borderColor" />
|
|
<Button
|
|
active={showWorkbench}
|
|
onClick={() => {
|
|
if (showWorkbench && !showChat) {
|
|
chatStore.setKey('showChat', true);
|
|
}
|
|
|
|
workbenchStore.showWorkbench.set(!showWorkbench);
|
|
}}
|
|
>
|
|
<div className="i-ph:code-bold" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ButtonProps {
|
|
active?: boolean;
|
|
disabled?: boolean;
|
|
children?: any;
|
|
onClick?: VoidFunction;
|
|
className?: string;
|
|
}
|
|
|
|
function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) {
|
|
return (
|
|
<button
|
|
className={classNames(
|
|
'flex items-center p-1.5',
|
|
{
|
|
'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
|
|
!active,
|
|
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
|
|
'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
|
|
disabled,
|
|
},
|
|
className,
|
|
)}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|