feat: Implement internationalization and add Turkish language support

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.
This commit is contained in:
google-labs-jules[bot] 2025-06-07 16:45:43 +00:00
parent e40264ea5e
commit 891257c1e2
11 changed files with 467 additions and 56 deletions

View File

@ -345,6 +345,75 @@ Remember to always commit your local changes or stash them before pulling update
---
## Internationalization (i18n)
This project uses `remix-i18next` (which builds on `i18next` and `react-i18next`) to support multiple languages.
### Current Supported Languages
* English (en) - Fallback language
* Turkish (tr)
### Adding or Modifying Translations
1. **Locate Locale Files**: Translation files are located in the `app/locales/` directory. Each language has its own file (e.g., `en.ts` for English, `tr.ts` for Turkish).
2. **Structure of Locale Files**: These are TypeScript files exporting a default object where keys are strings (often nested for organization, though currently flat) and values are the translated strings.
```typescript
// app/locales/en.ts
export default {
greeting: "Hello",
pageTitle: "Welcome to our App",
// ... more keys
};
```
3. **Using Translations in Components**:
* Import the `useTranslation` hook from `react-i18next`.
* Get the translation function `t` and the `i18n` instance: `const { t, i18n } = useTranslation();`
* Use the `t` function with the desired key: `<h1>{t('pageTitle')}</h1>`.
4. **Interpolation**: For dynamic values in translations:
* In the locale file: `userWelcome: "Welcome, {{name}}!"`
* In the component: `t('userWelcome', { name: userName })`
5. **Plurals**: `i18next` supports pluralization. Refer to the `i18next` documentation for detailed usage (e.g., `key_zero`, `key_one`, `key_many`).
### Adding a New Language
1. **Create Locale File**: Duplicate an existing locale file (e.g., `app/locales/en.ts`) and rename it for the new language (e.g., `app/locales/fr.ts` for French). Translate all the string values in this new file. Ensure the `satisfies typeof en;` type assertion (if used) is updated or handled correctly if the new language file is also TypeScript.
2. **Update Middleware**:
* Open `app/middleware/i18next.ts`.
* Import the new locale file: `import fr from "~/locales/fr";`
* Add the new language code to `supportedLanguages` array in the `detection` options.
* Add the new language resources to the `resources` object in the `i18next` options:
```typescript
resources: {
en: { translation: en },
tr: { translation: tr },
fr: { translation: fr }, // Add new language here
},
```
* Also add the new language code to `supportedLngs` array in `i18next` options.
3. **Update Language Switcher**:
* Open `app/components/header/Header.tsx` (or wherever the language switcher component is located).
* Add an `<option>` for the new language in the language selection dropdown.
```html
<option value="fr">Français</option>
```
4. **Testing**: Thoroughly test the application with the new language selected to ensure all translations appear correctly and the layout is not broken.
### Key Libraries
* [remix-i18next](https://github.com/sergiodxa/remix-i18next): Integrates i18next with Remix.
* [react-i18next](https://react.i18next.com/): React bindings for i18next.
* [i18next](https://www.i18next.com/): The core internationalization framework.
---
## Contributing
We welcome contributions! Check out our [Contributing Guide](CONTRIBUTING.md) to get started.

View File

@ -1,5 +1,6 @@
import { useStore } from '@nanostores/react';
import { ClientOnly } from 'remix-utils/client-only';
import { useTranslation } from 'react-i18next';
import { chatStore } from '~/lib/stores/chat';
import { classNames } from '~/utils/classNames';
import { HeaderActionButtons } from './HeaderActionButtons.client';
@ -7,6 +8,11 @@ import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
export function Header() {
const chat = useStore(chatStore);
const { i18n, t } = useTranslation();
const handleChangeLanguage = (lang: string) => {
i18n.changeLanguage(lang);
};
return (
<header
@ -19,23 +25,40 @@ export function Header() {
<div className="i-ph:sidebar-simple-duotone text-xl" />
<a href="/" className="text-2xl font-semibold text-accent flex items-center">
{/* <span className="i-bolt:logo-text?mask w-[46px] inline-block" /> */}
<img src="/logo-light-styled.png" alt="logo" className="w-[90px] inline-block dark:hidden" />
<img src="/logo-dark-styled.png" alt="logo" className="w-[90px] inline-block hidden dark:block" />
<img src="/logo-light-styled.png" alt={t('logoAlt')} className="w-[90px] inline-block dark:hidden" />
<img src="/logo-dark-styled.png" alt={t('logoAlt')} className="w-[90px] inline-block hidden dark:block" />
</a>
</div>
{chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
<>
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
</span>
<ClientOnly>
{() => (
<div className="mr-1">
<HeaderActionButtons />
</div>
)}
</ClientOnly>
</>
{/* Spacer when chat has not started to push language switcher to the right */}
{!chat.started && <div className="flex-grow"></div>}
{chat.started && (
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
</span>
)}
<div className={classNames("flex items-center", { "ml-auto": !chat.started, "ml-2": chat.started })}>
<select
value={i18n.language.split('-')[0]} // Use base language (e.g., 'en' from 'en-US')
onChange={(e) => handleChangeLanguage(e.target.value)}
className="bg-bolt-elements-backgroundDefault text-bolt-elements-textPrimary border border-bolt-elements-borderColor rounded-md p-1 text-sm focus:ring-accent focus:border-accent"
aria-label={t('languageSelectorLabel')}
>
<option value="en">English</option>
<option value="tr">Türkçe</option>
</select>
</div>
{chat.started && (
<ClientOnly>
{() => (
<div className="ml-2"> {/* Adjusted margin for spacing */}
<HeaderActionButtons />
</div>
)}
</ClientOnly>
)}
</header>
);

View File

@ -1,4 +1,5 @@
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';
@ -15,6 +16,7 @@ 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);
@ -77,7 +79,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="px-4 hover:bg-bolt-elements-item-backgroundActive flex items-center gap-2"
>
{isDeploying ? `Deploying to ${deployingTo}...` : 'Deploy'}
{isDeploying ? t('deployingToPlatform', { platform: deployingTo }) : t('deploy')}
<div
className={classNames('i-ph:caret-down w-4 h-4 transition-transform', isDropdownOpen ? 'rotate-180' : '')}
/>
@ -103,7 +105,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
src="https://cdn.simpleicons.org/netlify"
/>
<span className="mx-auto">
{!netlifyConn.user ? 'No Netlify Account Connected' : 'Deploy to Netlify'}
{!netlifyConn.user ? t('noNetlifyAccount') : t('deployToNetlify')}
</span>
{netlifyConn.user && <NetlifyDeploymentLink />}
</Button>
@ -122,9 +124,9 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/vercel/white"
alt="vercel"
alt={t('vercelAlt')}
/>
<span className="mx-auto">{!vercelConn.user ? 'No Vercel Account Connected' : 'Deploy to Vercel'}</span>
<span className="mx-auto">{!vercelConn.user ? t('noVercelAccount') : t('deployToVercel')}</span>
{vercelConn.user && <VercelDeploymentLink />}
</Button>
<Button
@ -132,16 +134,16 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
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">Coming Soon</span>
<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="cloudflare"
alt={t('cloudflareAlt')}
/>
<span className="mx-auto">Deploy to Cloudflare (Coming Soon)</span>
<span className="mx-auto">{t('deployToCloudflareComingSoon')}</span>
</Button>
</div>
)}

View File

@ -1,7 +1,46 @@
import { RemixBrowser } from '@remix-run/react';
import { startTransition } from 'react';
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';
import i18next from 'i18next';
import { I18nextProvider, initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { getInitialNamespaces } from 'remix-i18next/client';
startTransition(() => {
hydrateRoot(document.getElementById('root')!, <RemixBrowser />);
});
async function hydrate() {
await i18next
.use(initReactI18next) // passes i18n down to react-i18next
.use(LanguageDetector) // detects user language
.init({
supportedLngs: ['en', 'tr'], // ensure this matches middleware
fallbackLng: 'en',
react: { useSuspense: false }, // Optional: useSuspense
ns: getInitialNamespaces(), // gets the namespaces used on the server
detection: {
// order and from where user language should be detected
order: ['htmlTag', 'cookie', 'localStorage', 'path', 'subdomain'],
caches: ['cookie'], // cache found language in cookie
cookieSameSite: 'strict',
},
// resources will be passed from the server via remix-i18next/react's useChangeLanguage and root loader
// No need for backend if resources are fully passed.
});
startTransition(() => {
hydrateRoot(
document, // Use document instead of document.getElementById('root')! for full page hydration
<I18nextProvider i18n={i18next}>
<StrictMode>
<RemixBrowser />
</StrictMode>
</I18nextProvider>
);
});
}
if (window.requestIdleCallback) {
window.requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
window.setTimeout(hydrate, 1);
}

View File

@ -4,20 +4,28 @@ import { isbot } from 'isbot';
import { renderToReadableStream } from 'react-dom/server';
import { renderHeadToString } from 'remix-island';
import { Head } from './root';
import { themeStore } from '~/lib/stores/theme';
// import { themeStore } from '~/lib/stores/theme'; // themeStore.value will be replaced by i18n instance logic for lang
import { I18nextProvider } from 'react-i18next';
import { getInstance } from '~/middleware/i18next';
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: any,
_loadContext: AppLoadContext,
loadContext: AppLoadContext, // Use loadContext
) {
// await initializeModelList({});
const i18nInstance = await getInstance(loadContext);
// Ensure i18next is initialized. The middleware should handle this.
// await i18nInstance.init(); // Not typically needed if middleware does its job
const readable = await renderToReadableStream(<RemixServer context={remixContext} url={request.url} />, {
signal: request.signal,
onError(error: unknown) {
const readable = await renderToReadableStream(
<I18nextProvider i18n={i18nInstance}>
<RemixServer context={remixContext} url={request.url} />
</I18nextProvider>,
{
signal: request.signal,
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
@ -26,11 +34,28 @@ export default async function handleRequest(
const body = new ReadableStream({
start(controller) {
const head = renderHeadToString({ request, remixContext, Head });
// The theme can be obtained from the i18n context or another store if needed here.
// For now, root.tsx's Layout handles theme on client side and initial data-theme.
// The lang and dir will be set by root.tsx's Layout. The initial HTML here can omit them
// or try to sync, but root.tsx is the source of truth after hydration.
// For simplicity, we'll let root.tsx handle lang, dir, and data-theme attributes on the html tag.
// The Head component itself is rendered to string here.
// The `html` tag attributes `lang` and `dir` are set in `root.tsx`'s `Layout`.
// The `data-theme` is also set there. The `entry.server.tsx` only provides the initial structure.
// The `root.tsx` Layout will overwrite these on the client if necessary.
// However, it's better to have the server output the correct lang from the start.
const lang = i18nInstance.language || 'en';
const dir = i18nInstance.dir(lang);
// Theme value: if you have a way to get it on server for initial paint, use it. Otherwise, client will set it.
// For now, let's remove direct themeStore.value as it might not be in sync with what root.tsx loader provides.
// root.tsx's loader and Layout are now responsible for theme, lang, dir.
// This initial HTML structure is minimal.
controller.enqueue(
new Uint8Array(
new TextEncoder().encode(
`<!DOCTYPE html><html lang="en" data-theme="${themeStore.value}"><head>${head}</head><body><div id="root" class="w-full h-full">`,
// `<!DOCTYPE html><html lang="${lang}" dir="${dir}" data-theme="${themeStore.value}"><head>${head}</head><body><div id="root" class="w-full h-full">`
// Let root.tsx handle these attributes via loader and Layout for consistency
`<!DOCTYPE html><html><head>${head}</head><body><div id="root" class="w-full h-full">`
),
),
);

17
app/locales/en.ts Normal file
View File

@ -0,0 +1,17 @@
// app/locales/en.ts
export default {
greeting: "Hello",
description: "Welcome to Bolt.diy",
logoAlt: "Logo",
deployingToPlatform: "Deploying to {{platform}}...",
deploy: "Deploy",
noNetlifyAccount: "No Netlify Account Connected",
deployToNetlify: "Deploy to Netlify",
noVercelAccount: "No Vercel Account Connected",
deployToVercel: "Deploy to Vercel",
comingSoonSR: "Coming Soon",
deployToCloudflareComingSoon: "Deploy to Cloudflare (Coming Soon)",
vercelAlt: "Vercel logo",
cloudflareAlt: "Cloudflare logo",
languageSelectorLabel: "Select language"
};

19
app/locales/tr.ts Normal file
View File

@ -0,0 +1,19 @@
// app/locales/tr.ts
import type en from "./en";
export default {
greeting: "Merhaba",
description: "Bolt.diy'e Hoş Geldiniz",
logoAlt: "Logo", // Turkish translation can be the same if "Logo" is standard
deployingToPlatform: "{{platform}} platformuna dağıtılıyor...",
deploy: "Dağıt",
noNetlifyAccount: "Netlify Hesabı Bağlı Değil",
deployToNetlify: "Netlify'a Dağıt",
noVercelAccount: "Vercel Hesabı Bağlı Değil",
deployToVercel: "Vercel'e Dağıt",
comingSoonSR: "Yakında",
deployToCloudflareComingSoon: "Cloudflare'e Dağıt (Yakında)",
vercelAlt: "Vercel logosu",
cloudflareAlt: "Cloudflare logosu",
languageSelectorLabel: "Dil seçin"
} satisfies typeof en;

40
app/middleware/i18next.ts Normal file
View File

@ -0,0 +1,40 @@
// app/middleware/i18next.ts
import { createCookie } from "@remix-run/node"; // Or "@remix-run/cloudflare" if deployed on Cloudflare
import { unstable_createI18nextMiddleware } from "remix-i18next/middleware";
import Backend from "i18next-http-backend";
import en from "~/locales/en";
import tr from "~/locales/tr";
// Create a cookie object to store the user's language preference
export const localeCookie = createCookie("lng", {
path: "/",
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
});
export const [i18nextMiddleware, getLocale, getInstance] =
unstable_createI18nextMiddleware({
detection: {
supportedLanguages: ["en", "tr"],
fallbackLanguage: "en",
cookie: localeCookie, // Use the cookie to detect language
order: ["cookie", "header"], // Order of detection: cookie, then Accept-Language header
},
i18next: {
supportedLngs: ["en", "tr"],
fallbackLng: "en",
defaultNS: "translation", // Default namespace
resources: {
en: { translation: en },
tr: { translation: tr },
},
// If you want to load translations from a backend (e.g., /locales/{{lng}}.json)
// backend: {
// loadPath: "/locales/{{lng}}.json", // Path to your translation files
// },
// initImmediate: false, // Important for SSR to prevent issues
},
// backend: Backend, // Uncomment if using i18next-http-backend to load files
});

View File

@ -1,6 +1,10 @@
import { useStore } from '@nanostores/react';
import type { LinksFunction } from '@remix-run/cloudflare';
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
import { json, Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { unstable_LoaderFunctionArgs as LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare
import { useChangeLanguage } from "remix-i18next/react";
import { i18nextMiddleware, getLocale, localeCookie } from "~/middleware/i18next"; // Adjust path if necessary
import { useTranslation } from "react-i18next";
import tailwindReset from '@unocss/reset/tailwind-compat.css?url';
import { themeStore } from './lib/stores/theme';
import { stripIndents } from './utils/stripIndent';
@ -16,6 +20,19 @@ import xtermStyles from '@xterm/xterm/css/xterm.css?url';
import 'virtual:uno.css';
export const unstable_middleware = [i18nextMiddleware];
export async function loader({ request, context }: LoaderFunctionArgs) {
// Note: getLocale now takes context if using the latest remix-i18next middleware style
// For older versions or direct RemixI18Next class, it might take request directly.
// Assuming context is passed correctly by the middleware setup.
let locale = await getLocale(context); // Or simply getLocale(request) with older remix-i18next
return json(
{ locale },
{ headers: { "Set-Cookie": await localeCookie.serialize(locale) } }
);
}
export const links: LinksFunction = () => [
{
rel: 'icon',
@ -67,24 +84,32 @@ export const Head = createHead(() => (
export function Layout({ children }: { children: React.ReactNode }) {
const theme = useStore(themeStore);
let { i18n } = useTranslation();
let { locale } = useLoaderData<typeof loader>();
useChangeLanguage(locale);
useEffect(() => {
document.querySelector('html')?.setAttribute('data-theme', theme);
// Theme setting logic remains, but now it's inside the html tag rendered by this Layout
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return (
<>
<ClientOnly>{() => <DndProvider backend={HTML5Backend}>{children}</DndProvider>}</ClientOnly>
<ScrollRestoration />
<Scripts />
</>
<html lang={i18n.language} dir={i18n.dir(i18n.language)} data-theme={theme}>
<Head />
<body>
<ClientOnly>{() => <DndProvider backend={HTML5Backend}>{children}</DndProvider>}</ClientOnly>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
import { logStore } from './lib/stores/logs';
export default function App() {
const theme = useStore(themeStore);
const theme = useStore(themeStore); // This theme is for the log, Layout handles the html attribute
useEffect(() => {
logStore.logSystem('Application initialized', {
@ -93,11 +118,11 @@ export default function App() {
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
});
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Keep theme dependency if log needs to re-run on theme change, otherwise remove. For init, empty array is fine.
return (
<Layout>
<Outlet />
</Layout>
);
// useChangeLanguage was moved to Layout as it needs loaderData.
// The main Outlet is rendered within the Layout defined above.
return <Outlet />;
}

View File

@ -114,6 +114,9 @@
"electron-updater": "^6.3.9",
"file-saver": "^2.0.5",
"framer-motion": "^11.12.0",
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"ignore": "^6.0.2",
"isbot": "^4.4.0",
"isomorphic-git": "^1.27.2",
@ -134,6 +137,7 @@
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
"react-hotkeys-hook": "^4.6.1",
"react-i18next": "^15.5.2",
"react-icons": "^5.4.0",
"react-markdown": "^9.0.1",
"react-qrcode-logo": "^3.0.0",
@ -143,6 +147,7 @@
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.0",
"remix-i18next": "^7.2.1",
"remix-island": "^0.2.0",
"remix-utils": "^7.7.0",
"rollup-plugin-node-polyfills": "^0.2.1",
@ -203,6 +208,5 @@
},
"resolutions": {
"@typescript-eslint/utils": "^8.0.0-alpha.30"
},
"packageManager": "pnpm@9.4.0"
}
}

View File

@ -221,6 +221,15 @@ importers:
framer-motion:
specifier: ^11.12.0
version: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
i18next:
specifier: ^25.2.1
version: 25.2.1(typescript@5.8.2)
i18next-browser-languagedetector:
specifier: ^8.1.0
version: 8.1.0
i18next-http-backend:
specifier: ^3.0.2
version: 3.0.2(encoding@0.1.13)
ignore:
specifier: ^6.0.2
version: 6.0.2
@ -281,6 +290,9 @@ importers:
react-hotkeys-hook:
specifier: ^4.6.1
version: 4.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-i18next:
specifier: ^15.5.2
version: 15.5.2(i18next@25.2.1(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)
react-icons:
specifier: ^5.4.0
version: 5.5.0(react@18.3.1)
@ -308,6 +320,9 @@ importers:
remark-gfm:
specifier: ^4.0.0
version: 4.0.1
remix-i18next:
specifier: ^7.2.1
version: 7.2.1(i18next@25.2.1(typescript@5.8.2))(react-i18next@15.5.2(i18next@25.2.1(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2))(react-router@6.30.0(react@18.3.1))(react@18.3.1)
remix-island:
specifier: ^0.2.0
version: 0.2.0(@remix-run/react@2.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2))(@remix-run/server-runtime@2.16.3(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -410,7 +425,7 @@ importers:
version: 33.4.8
electron-builder:
specifier: ^25.1.8
version: 25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
version: 25.1.8(electron-builder-squirrel-windows@25.1.8)
eslint-config-prettier:
specifier: ^10.1.1
version: 10.1.1(eslint@9.23.0(jiti@1.21.7))
@ -890,6 +905,10 @@ packages:
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.27.6':
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.0':
resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==}
engines: {node: '>=6.9.0'}
@ -4189,6 +4208,9 @@ packages:
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
hasBin: true
cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -5114,6 +5136,9 @@ packages:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@ -5166,6 +5191,20 @@ packages:
engines: {node: '>=18'}
hasBin: true
i18next-browser-languagedetector@8.1.0:
resolution: {integrity: sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==}
i18next-http-backend@3.0.2:
resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==}
i18next@25.2.1:
resolution: {integrity: sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==}
peerDependencies:
typescript: ^5
peerDependenciesMeta:
typescript:
optional: true
iconv-corefoundation@1.1.7:
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
engines: {node: ^8.11.2 || >=10}
@ -6127,6 +6166,15 @@ packages:
node-fetch-native@1.6.6:
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -6649,6 +6697,22 @@ packages:
react: '>=16.8.1'
react-dom: '>=16.8.1'
react-i18next@15.5.2:
resolution: {integrity: sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
typescript: ^5
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
typescript:
optional: true
react-icons@5.5.0:
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
peerDependencies:
@ -6828,6 +6892,15 @@ packages:
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
remix-i18next@7.2.1:
resolution: {integrity: sha512-zjyDEOtSw2yF31tNHP6RLr7LZl1kEdZ3j3EpzB8905JNg4HsUk+6bNVrWhq8fFfacT6sPK0eH//NrYbpAI7bzw==}
engines: {node: '>=20.0.0'}
peerDependencies:
i18next: ^24.0.0 || ^25.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-i18next: ^13.0.0 || ^14.0.0 || ^15.0.0
react-router: ^7.0.0
remix-island@0.2.0:
resolution: {integrity: sha512-NujWtmulgupxNOMiWKAj8lg56eYsy09aV/2pML8rov8N8LmY1UnSml4XYad+KHLy/pgZ1D9UxAmjI6GBJydTUg==}
peerDependencies:
@ -7529,6 +7602,9 @@ packages:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@5.1.0:
resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==}
engines: {node: '>=18'}
@ -7938,6 +8014,10 @@ packages:
vm-browserify@1.1.2:
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
@ -7958,6 +8038,9 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@ -7974,6 +8057,9 @@ packages:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
when-exit@2.1.4:
resolution: {integrity: sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==}
@ -8858,6 +8944,8 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.27.6': {}
'@babel/template@7.27.0':
dependencies:
'@babel/code-frame': 7.26.2
@ -11766,7 +11854,7 @@ snapshots:
app-builder-bin@5.0.0-alpha.10: {}
app-builder-lib@25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)):
app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8):
dependencies:
'@develar/schema-utils': 2.6.5
'@electron/notarize': 2.5.0
@ -12459,6 +12547,12 @@ snapshots:
dependencies:
cross-spawn: 7.0.6
cross-fetch@4.0.0(encoding@0.1.13):
dependencies:
node-fetch: 2.7.0(encoding@0.1.13)
transitivePeerDependencies:
- encoding
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -12623,7 +12717,7 @@ snapshots:
dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8):
dependencies:
app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)
builder-util: 25.1.7
builder-util-runtime: 9.2.10
fs-extra: 10.1.0
@ -12702,7 +12796,7 @@ snapshots:
electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8):
dependencies:
app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)
archiver: 5.3.2
builder-util: 25.1.7
fs-extra: 10.1.0
@ -12711,9 +12805,9 @@ snapshots:
- dmg-builder
- supports-color
electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)):
electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8):
dependencies:
app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)
builder-util: 25.1.7
builder-util-runtime: 9.2.10
chalk: 4.1.2
@ -13711,6 +13805,10 @@ snapshots:
dependencies:
whatwg-encoding: 3.1.1
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
@ -13775,6 +13873,22 @@ snapshots:
husky@9.1.7: {}
i18next-browser-languagedetector@8.1.0:
dependencies:
'@babel/runtime': 7.27.0
i18next-http-backend@3.0.2(encoding@0.1.13):
dependencies:
cross-fetch: 4.0.0(encoding@0.1.13)
transitivePeerDependencies:
- encoding
i18next@25.2.1(typescript@5.8.2):
dependencies:
'@babel/runtime': 7.27.6
optionalDependencies:
typescript: 5.8.2
iconv-corefoundation@1.1.7:
dependencies:
cli-truncate: 2.1.0
@ -15102,6 +15216,12 @@ snapshots:
node-fetch-native@1.6.6: {}
node-fetch@2.7.0(encoding@0.1.13):
dependencies:
whatwg-url: 5.0.0
optionalDependencies:
encoding: 0.1.13
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
@ -15665,6 +15785,16 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-i18next@15.5.2(i18next@25.2.1(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2):
dependencies:
'@babel/runtime': 7.27.0
html-parse-stringify: 3.0.1
i18next: 25.2.1(typescript@5.8.2)
react: 18.3.1
optionalDependencies:
react-dom: 18.3.1(react@18.3.1)
typescript: 5.8.2
react-icons@5.5.0(react@18.3.1):
dependencies:
react: 18.3.1
@ -15909,6 +16039,13 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
remix-i18next@7.2.1(i18next@25.2.1(typescript@5.8.2))(react-i18next@15.5.2(i18next@25.2.1(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2))(react-router@6.30.0(react@18.3.1))(react@18.3.1):
dependencies:
i18next: 25.2.1(typescript@5.8.2)
react: 18.3.1
react-i18next: 15.5.2(i18next@25.2.1(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)
react-router: 6.30.0(react@18.3.1)
remix-island@0.2.0(@remix-run/react@2.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2))(@remix-run/server-runtime@2.16.3(typescript@5.8.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@remix-run/react': 2.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.2)
@ -16624,6 +16761,8 @@ snapshots:
dependencies:
tldts: 6.1.85
tr46@0.0.3: {}
tr46@5.1.0:
dependencies:
punycode: 2.3.1
@ -17101,6 +17240,8 @@ snapshots:
vm-browserify@1.1.2: {}
void-elements@3.1.0: {}
w3c-keyname@2.2.8: {}
w3c-xmlserializer@5.0.0:
@ -17121,6 +17262,8 @@ snapshots:
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}
whatwg-encoding@3.1.1:
@ -17134,6 +17277,11 @@ snapshots:
tr46: 5.1.0
webidl-conversions: 7.0.0
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
when-exit@2.1.4: {}
which-typed-array@1.1.19: