-
Notifications
You must be signed in to change notification settings - Fork 420
fix(examples): devinxi Tanstack Start example #2267
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?
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
size-limit report 📦
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2267 +/- ##
==========================================
- Coverage 77.05% 76.27% -0.78%
==========================================
Files 84 99 +15
Lines 2157 2651 +494
Branches 555 692 +137
==========================================
+ Hits 1662 2022 +360
- Misses 382 505 +123
- Partials 113 124 +11 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
I briefly checked the integration, and came across that global i18n instance used in some places eq The global i18n should be never ever used in server side rendered applications because all requests to the server would share the same context and using one global instance will cause a mess |
|
@timofei-iatsenko thanks for taking a look. What would you use instead of the global i18n instance? I'm not in production yet, but have not seen any particular issue so far in local env. |
|
@depsimon you will not see it in local development because you don't have a concurent access to your server-rendered app. You need to have a users which will access app at the same time and then you will get issues. The basic rule - you should create one i18n instance per request and share this instance everywhere. I'm not particulare familiar with tanstack start, so i could not help you with an example. |
|
I'm briefly checking the docs for tanstack start + tanstack router and probably Router Context + You might want to create i18n instance in before load hook and then pass it down using a RouterContext. |
|
Thanks for the pointers, I'll try to take a look at this before merging. It should be trivial to simply add the i18n instance to the router context in the It might be a bit trickier to pass it around in server routes (APIs). |
|
I pushed a commit which is getting rid of all global i18n instances usages. I think that the integration could be improved even more, instead of loading a catalogs using asynchronous import ( This will ensure that the loading of the dynamic catalog would follow the same invalidation / awaiting rules as other async recourses and you will not need handle loading state for catalogs separately. I think that the starting point might be Please check the code i pushed, because i didn't test all possible cases. |
Thanks @timofei-iatsenko I definitely missed the I updated the PR to remove unused imports & add a couple missing translations. The middleware is great, I used it in a project, the only downside is that this code is run twice for each API request that use the middleware: const locale = getLocaleFromRequest()
const i18n = setupI18n({})
await dynamicActivate(i18n, locale)You can attest this by adding a log in the
Interesting, we might achieve this in the |
that's fine, once es module loaded next time it will be taken from cache. it will not affect performance.
Yes i also was curios what is the recomended way to pass a hydration state from server to client. And was a bit disapointed when discovered that they are not showing how to do that and hide all logic in the |
|
@timofei-iatsenko I managed to make a simple router wrapper that handle the catalog hydration. I'm not sure how I did as I never played with that, but it seems to work. We could also delete the export function createRouter({ i18n: serverI18n }: { i18n?: I18n } = {}) {
const i18n = serverI18n ?? setupI18n({})
const router = routerWithLingui(createTanStackRouter({
routeTree,
context: {
i18n,
},
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
scrollRestoration: true
}), i18n)
return router
}This way, only the server sets the locale & loads the initial catalog. Then it's loaded in the client. WDYT? |
So hydrate/dehydrate is the magic behind the I also don't like the fact that when you switch the language the catalog will be loaded in a different way. So imagine the situation:
I'm actually a big fun of the "stateless" as much as possible approach for the clientside applications. That means when you switch the langauge, i would assume that the whole page will reload from scratch. Because approach with switching language "on the fly" is requires many assumptions/testing and generally effort which is not really worth it. You need not only switch the translation of messages which is in the codebase, but also keep track that all resources will be reload on the correct language, if user in the middle of filling the form, all fields will be populated for correct language and so on. |
|
I've added an example page that uses a
Are you sure? In TSS, the loader is called client-side except for the first (SSR) request. Only the server functions are called in the server, but that wouldn't let us manipulate the client's i18n instance.
Do you think the In such a case, the |
|
@depsimon here is the code what i had in mind: // $lang/route.tsx
import { createFileRoute, notFound, Outlet } from "@tanstack/react-router"
import { updateLocale } from "~/functions/locale"
import { loadCatalog, locales } from "~/modules/lingui/i18n"
import { I18nProvider } from "@lingui/react"
import { setupI18n } from "@lingui/core"
export const Route = createFileRoute("/$lang")({
component: Page,
async loader({ context, params }) {
if (!Object.keys(locales).includes(params.lang)) {
throw notFound()
}
const messages = loadCatalog(params.lang)
await updateLocale({ data: params.lang }) // Persist the locale in the cookies
return messages
},
})
function Page() {
const messages = Route.useLoaderData()
const { lang } = Route.useParams()
const i18n = setupI18n({ messages: { [lang]: messages }, locale: lang })
return (
<I18nProvider i18n={i18n}>
<Outlet />
</I18nProvider>
)
}This route should be a parent of all routes. The loader indeed is isomorphic, so first time it's requesting data from SSR next time on client navigations it's executed on the client side. But this is OK, what i'm trying to achieve here is that initializtion and langauge switching is uniform. So instead of switching language in runtime and causing all components to re-render, we switch page and re-create i18n instance. In the current example not all routes is behind Also lingui has deps extrator feature, which allows to split catalogs by route. Than using loader will be the only one way to load these catalogs: |
|
I think that if you setup a new instance in the Page component, it'll be different from the context.i18n which is the one you'd use in the loaders/head functions for instance. I'm not sure I understand why it's better to re-create a I18n instance rather than loading a catalog in the existing instance, is there a particular reason to that?
I don't want that for this example TBH. I just think there are many different strategies when it comes to how to structure your app in a i18n context, this is one valid way of doing it but I don't think it should be imposed on all users. That's why it's available in a simple form as a part of the example template (cfr the $lang/content route). If there is a demand, maybe it'll be worth to create two/multiple examples with different strategies (multi domain, subdomain, directory, ...). |
This
From my experience with meta frameworks like Next.js — which TanStack Start is quite similar to — pages should be treated as stateless. In SPAs, it's common to share instances across the app, but that pattern doesn’t hold up in MPA or isomorphic scenarios. Relying on shared runtime state (like a global i18n instance) can lead to subtle bugs that are difficult to reproduce. For example: “I opened page A via direct link, then navigated to page B — everything worked. Then I refreshed and it broke.” These issues usually stem from mismatches between client-side navigation and server-side rendering. They’re especially problematic when the app is hydrated with assumptions based on prior state. That's why I strongly prefer initializing i18n explicitly per page. It removes hidden dependencies and makes the system more predictable and easier to debug — especially at scale.
I think it’s risky to include approaches that are known to lead to architectural dead-ends — even as optional examples. If users see them in an official template, they will often assume it's a recommended or future-proof approach. Yes, different strategies exist (subfolders, subdomains, multi-domain, etc.), and showing them in isolated, scoped examples could be helpful. But we should avoid including patterns that break down after just a few iterations — for example, when trying to add static generation, proper SEO. If the example encourages a structure that later turns out to be incompatible with these features, users will understandably be frustrated. Even worse, they may blame the library itself: “I followed the official example and now it doesn’t work.” That’s what we should avoid. |
I don't think you can avoid the PS: pinged you on Discord if you want to discuss things more easily than in a Github Discussion |
|
any recommendations on how to get this would be helpful, now that devinxi got released. Also on topic of route path, my 2c is that it's the webSITES that do a path based approach, web APPs I think rarely does that which would be majority of Start users will be (I guess) |
|
any progress? |
|
Hey team! Great progress so far! Please let me know if you need any help with testing or anything else to move this PR forward. Idea: it would be nice to have a recommended approach on how to access translation utils in the head function to translate meta tags like title and description. FYI, this is what im currently doing but you don't seem to like/recommend this approach right? @timofei-iatsenko |
|
@jsefiani If you have feedback about this PR it's more than welcome :) I don't think there is a better way to translate metatags in the head function than what you're doing right now. It's also how you'd do it with content that comes from the DB. |
Have you tried using the updated example from the PR in your project? It should work fine with devinxi.
Thanks for your input. That's also my feeling. Though it's not a bad idea to show users how to do it with Start & Lingui, but maybe in a separate example that focuses more around a content-website? |
|
RC is out now and I don't have ideas to update for lingui |
Let's wait for the RC to stabilize a bit, a lot of fixes are still going through. |
|
@depsimon is attempting to deploy a commit to the Crowdin Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR migrates the TanStack Start example from vinxi to vite-based architecture and updates to Tailwind CSS v4, aligning with TanStack Start's latest version changes.
Key changes:
- Replaced vinxi build tooling with vite configuration
- Updated to Tailwind CSS v4 with new @import syntax
- Refactored i18n setup to work with new TanStack Start patterns using middleware and router context
Reviewed Changes
Copilot reviewed 32 out of 33 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | New vite configuration replacing vinxi setup |
| tailwind.config.mjs | Removed legacy Tailwind v3 configuration |
| src/styles/app.css | Updated to Tailwind v4 @import syntax |
| src/server.ts | New server entry point replacing ssr.tsx |
| src/client.tsx | Updated hydration setup with StrictMode |
| src/router.tsx | Added AppContext type and routerWithLingui plugin integration |
| src/modules/lingui/router-plugin.tsx | New router plugin for i18n provider wrapping |
| src/modules/lingui/lingui-middleware.ts | New middleware for server-side i18n setup |
| src/modules/lingui/i18n.ts | Refactored dynamicActivate to accept i18n instance |
| src/modules/lingui/i18n.server.ts | Simplified locale detection logic |
| src/routes/api/users.ts | Migrated from createAPIFileRoute to createServerFileRoute |
| src/routes/api/users.$id.ts | Added linguiMiddleware and updated to new API patterns |
| src/routes/__root.tsx | Updated to createRootRouteWithContext and new locale update flow |
| src/routes/$lang/*.tsx | New localized route files demonstrating i18n routing |
| package.json | Updated dependencies to latest versions |
| app.config.ts | Removed, configuration moved to vite.config.ts |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Had to update to RC at work, so I finally had the time to work on it. I have pushed the changes to make it work with Tanstack Start RC. Feedback welcome |
|
@depsimon there is no definition of initI18n in modules/lingui/i18n.ts |
Fixed, thanks for reporting |


Description
This PR updates the tanstack-start example to use the latest version which relies on vite instead of vinxi. It also updates to tailwind 4
Types of changes
Could be fixing TanStack/router#4279 & TanStack/router#4409
Checklist