mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-23 02:16:08 +00:00
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:
parent
e40264ea5e
commit
891257c1e2
69
README.md
69
README.md
@ -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.
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
17
app/locales/en.ts
Normal 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
19
app/locales/tr.ts
Normal 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
40
app/middleware/i18next.ts
Normal 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
|
||||
});
|
53
app/root.tsx
53
app/root.tsx
@ -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 />;
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
160
pnpm-lock.yaml
160
pnpm-lock.yaml
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user