-
Notifications
You must be signed in to change notification settings - Fork 25
feat: add TanStack Start example app with gt-tanstack-start #1080
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <p align="center"> | ||
| <a href="https://generaltranslation.com" target="_blank"> | ||
| <img src="https://generaltranslation.com/gt-logo-light.svg" alt="General Translation" width="100" height="100"> | ||
| </a> | ||
| </p> | ||
|
|
||
| # gt-tanstack-start + TanStack Start Example | ||
|
|
||
| A multilingual TanStack Start app using `gt-tanstack-start` for internationalization with local translations. | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ### Clone and install | ||
|
|
||
| ```bash | ||
| git clone https://github.com/generaltranslation/gt.git | ||
| cd gt/examples/tanstack-start-basic | ||
| npm install | ||
| ``` | ||
|
|
||
| ### Generate translations | ||
|
|
||
| ```bash | ||
| npx gt translate | ||
| ``` | ||
|
|
||
| ### Run the dev server | ||
|
|
||
| ```bash | ||
| npm run dev | ||
| ``` | ||
|
|
||
| Visit [localhost:3000](http://localhost:3000) and use the locale selector to switch languages. | ||
|
|
||
| ## How it works | ||
|
|
||
| 1. **`initializeGT()`** in `__root.tsx` sets up the i18n manager with local translations | ||
| 2. **`getTranslations()`** loads the right translation file in the root loader | ||
| 3. **`<GTProvider>`** provides translations to all child components | ||
| 4. **`<T>`** wraps JSX content for automatic translation | ||
| 5. **`<LocaleSelector>`** lets users switch languages | ||
|
|
||
| ## Key files | ||
|
|
||
| - `src/routes/__root.tsx` — GT initialization, root loader, and provider setup | ||
| - `src/routes/index.tsx` — Home page with translated content | ||
| - `src/routes/about.tsx` — About page with translated content | ||
| - `loadTranslations.ts` — Dynamic import loader for translation JSON files | ||
| - `gt.config.json` — GT configuration (locales, output path) | ||
| - `src/_gt/*.json` — Generated translation files | ||
|
|
||
| ## Notes | ||
|
|
||
| - Import `<T>` from `gt-react` (not `gt-tanstack-start`) so the `gt` CLI can detect translatable content | ||
| - Translation files must be in `src/` (not `public/`) for Vite's dynamic imports to work | ||
| - The `gt` CLI requires `gt-react` as a direct dependency to scan for `<T>` components |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "defaultLocale": "en", | ||
| "locales": [ | ||
| "es", | ||
| "ja" | ||
| ], | ||
| "files": { | ||
| "gt": { | ||
| "output": "src/_gt/[locale].json" | ||
| } | ||
| }, | ||
| "_versionId": "2bc75b832dcf1f026203cefd5b81c72cf0e146b20bff75d661b597479294cad0" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export default async function loadTranslations(locale: string) { | ||
| const translations = await import(`./src/_gt/${locale}.json`); | ||
| return translations.default; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,30 @@ | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| "name": "tanstack-start-basic", | ||||||||||||||||||||||||
| "version": "0.1.0", | ||||||||||||||||||||||||
| "private": true, | ||||||||||||||||||||||||
| "type": "module", | ||||||||||||||||||||||||
| "scripts": { | ||||||||||||||||||||||||
| "dev": "vite dev --port 3000", | ||||||||||||||||||||||||
| "build": "vite build", | ||||||||||||||||||||||||
| "preview": "vite preview" | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| "dependencies": { | ||||||||||||||||||||||||
| "@tailwindcss/vite": "^4.1.18", | ||||||||||||||||||||||||
| "@tanstack/react-router": "latest", | ||||||||||||||||||||||||
| "@tanstack/react-start": "latest", | ||||||||||||||||||||||||
| "@tanstack/router-plugin": "^1.132.0", | ||||||||||||||||||||||||
| "gt-tanstack-start": "workspace:*", | ||||||||||||||||||||||||
| "gt-react": "workspace:*", | ||||||||||||||||||||||||
|
Comment on lines
+9
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unpinned Two concerns here:
Suggested change
And move Prompt To Fix With AIThis is a comment left during a code review.
Path: examples/tanstack-start-basic/package.json
Line: 9-17
Comment:
**Unpinned `latest` tags and build tool in production dependencies**
Two concerns here:
1. `@tanstack/react-router` and `@tanstack/react-start` are pinned to `latest`. For an example app that is expected to remain runnable over time, `latest` can silently break as new major versions ship. Pinning to a specific version (e.g., `^1.132.0`) is more reliable.
2. `@tanstack/router-plugin` is a build-time Vite/Rollup plugin and should live in `devDependencies`, not `dependencies`.
```suggestion
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-start": "^1.132.0",
```
And move `@tanstack/router-plugin` to `devDependencies`.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||
| "react": "^19.2.0", | ||||||||||||||||||||||||
| "react-dom": "^19.2.0", | ||||||||||||||||||||||||
| "tailwindcss": "^4.1.18" | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| "devDependencies": { | ||||||||||||||||||||||||
| "@types/react": "^19.2.0", | ||||||||||||||||||||||||
| "@types/react-dom": "^19.2.0", | ||||||||||||||||||||||||
| "@vitejs/plugin-react": "^5.1.4", | ||||||||||||||||||||||||
| "typescript": "^5.7.2", | ||||||||||||||||||||||||
| "vite": "^7.3.1", | ||||||||||||||||||||||||
| "vite-tsconfig-paths": "^5.1.4" | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| { | ||
| "7be0c393d34092d8": "Acerca de", | ||
| "7f9e1070fd437434": [ | ||
| { | ||
| "c": [ | ||
| "Envuelve con <T>" | ||
| ], | ||
| "i": 1, | ||
| "t": "h2" | ||
| }, | ||
| { | ||
| "c": [ | ||
| "Envuelve cualquier contenido JSX en un componente <T> y se traducirá automáticamente." | ||
| ], | ||
| "i": 2, | ||
| "t": "p" | ||
| } | ||
| ], | ||
| "93505deebff4e9d6": [ | ||
| { | ||
| "c": "Renderizado del lado del servidor", | ||
| "i": 1, | ||
| "t": "h2" | ||
| }, | ||
| { | ||
| "c": "Las traducciones se cargan en el cargador raíz, por lo que el renderizado inicial ya aparece en el idioma correcto.", | ||
| "i": 2, | ||
| "t": "p" | ||
| } | ||
| ], | ||
| "985ef45524728a6f": "Documentación", | ||
| "c3bedf341bc197c1": [ | ||
| { | ||
| "c": "TanStack Start + General Translation", | ||
| "i": 1, | ||
| "t": "p" | ||
| }, | ||
| { | ||
| "c": "Internacionaliza tu aplicación de TanStack Start.", | ||
| "i": 2, | ||
| "t": "h1" | ||
| }, | ||
| { | ||
| "c": "Este ejemplo muestra gt-tanstack-start con traducciones locales. Usa el selector de idioma en la esquina superior derecha para cambiar de idioma.", | ||
| "i": 3, | ||
| "t": "p" | ||
| } | ||
| ], | ||
| "e64b93dc7849f179": [ | ||
| { | ||
|
Comment on lines
+1
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Translation files out of sync with source components The content hashes in these translation files do not match the JSX structure currently in the source routes, so translations will silently fall back to English at runtime for several key components. Mismatches found:
These files were likely generated from a previous iteration of the UI. Re-running Prompt To Fix With AIThis is a comment left during a code review.
Path: examples/tanstack-start-basic/src/_gt/es.json
Line: 1-50
Comment:
**Translation files out of sync with source components**
The content hashes in these translation files do not match the JSX structure currently in the source routes, so translations will silently fall back to English at runtime for several key components.
Mismatches found:
1. **`index.tsx` – first `<T>` block** (`c3bedf341bc197c1`): The JSON translation has a `p → h1 → p` (3-element) structure, but the current source has `h1 → p` (2 elements). The h1 text also differs: the translation was generated from `"TanStack Start + General Translation"`, not the current `"Welcome to TanStack Start + GT"`.
2. **`about.tsx` – `<T>` block** (`e64b93dc7849f179`): Same structural mismatch — translation has `p → h1 → p` (3 elements) but the component has `h1 → p` (2 elements).
3. **Orphaned translation keys** present in the JSON but with no matching source content:
- `93505deebff4e9d6` ("SSR" card) — no corresponding `<T>` block exists in either route file.
- `985ef45524728a6f` ("Documentation") — no matching content found in any route.
- `7be0c393d34092d8` ("About") — the nav `<a>About</a>` in `__root.tsx` has no `<T>` wrapper.
These files were likely generated from a previous iteration of the UI. Re-running `npx gt translate` against the current source files should regenerate the correct keys. Both `es.json` and `ja.json` are affected identically.
How can I resolve this? If you propose a fix, please make it concise. |
||
| "c": "Acerca de", | ||
| "i": 1, | ||
| "t": "p" | ||
| }, | ||
| { | ||
| "c": "Un punto de partida sencillo con margen para crecer.", | ||
| "i": 2, | ||
| "t": "h1" | ||
| }, | ||
| { | ||
| "c": "Este ejemplo muestra gt-tanstack-start, la integración de General Translation con TanStack Start. Ofrece i18n automática y compatibilidad con renderizado del lado del servidor.", | ||
| "i": 3, | ||
| "t": "p" | ||
| } | ||
| ], | ||
| "f54afcdf2fcf87b0": [ | ||
| { | ||
| "c": "Traducciones locales", | ||
| "i": 1, | ||
| "t": "h2" | ||
| }, | ||
| { | ||
| "c": "Incluye las traducciones como archivos JSON en la compilación. No requiere CDN ni llamadas a la API en tiempo de ejecución.", | ||
| "i": 2, | ||
| "t": "p" | ||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| { | ||
| "7be0c393d34092d8": "概要", | ||
| "7f9e1070fd437434": [ | ||
| { | ||
| "c": [ | ||
| "<T> で囲む" | ||
| ], | ||
| "i": 1, | ||
| "t": "h2" | ||
| }, | ||
| { | ||
| "c": [ | ||
| "任意の JSX コンテンツを <T> コンポーネントで囲むと、自動的に翻訳されます。" | ||
| ], | ||
| "i": 2, | ||
| "t": "p" | ||
| } | ||
| ], | ||
| "93505deebff4e9d6": [ | ||
| { | ||
| "c": "サーバーサイドレンダリング", | ||
| "i": 1, | ||
| "t": "h2" | ||
| }, | ||
| { | ||
| "c": "翻訳はルートローダーで読み込まれるため、初回レンダリング時から正しい言語で表示されます。", | ||
| "i": 2, | ||
| "t": "p" | ||
| } | ||
| ], | ||
| "985ef45524728a6f": "ドキュメント", | ||
| "c3bedf341bc197c1": [ | ||
| { | ||
| "c": "TanStack Start + General Translation", | ||
| "i": 1, | ||
| "t": "p" | ||
| }, | ||
| { | ||
| "c": "TanStack Start アプリを国際化します。", | ||
| "i": 2, | ||
| "t": "h1" | ||
| }, | ||
| { | ||
| "c": "この例では、ローカル翻訳を使用する gt-tanstack-start を紹介します。右上のロケールセレクターで言語を切り替えます。", | ||
| "i": 3, | ||
| "t": "p" | ||
| } | ||
| ], | ||
| "e64b93dc7849f179": [ | ||
| { | ||
| "c": "概要", | ||
| "i": 1, | ||
| "t": "p" | ||
| }, | ||
| { | ||
| "c": "小さく始めて、拡張に備える。", | ||
| "i": 2, | ||
| "t": "h1" | ||
| }, | ||
| { | ||
| "c": "この例では、TanStack Start 向けの General Translation 統合である gt-tanstack-start を紹介します。サーバーサイドレンダリング対応の自動 i18n を提供します。", | ||
| "i": 3, | ||
| "t": "p" | ||
| } | ||
| ], | ||
| "f54afcdf2fcf87b0": [ | ||
| { | ||
| "c": "ローカル翻訳", | ||
| "i": 1, | ||
| "t": "h2" | ||
| }, | ||
| { | ||
| "c": "翻訳を JSON ファイルとしてビルドに含めます。CDN や実行時の API 呼び出しは不要です。", | ||
| "i": 2, | ||
| "t": "p" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /* eslint-disable */ | ||
|
|
||
| // @ts-nocheck | ||
|
|
||
| // noinspection JSUnusedGlobalSymbols | ||
|
|
||
| // This file was automatically generated by TanStack Router. | ||
| // You should NOT make any changes in this file as it will be overwritten. | ||
| // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. | ||
|
|
||
| import { Route as rootRouteImport } from './routes/__root' | ||
| import { Route as AboutRouteImport } from './routes/about' | ||
| import { Route as IndexRouteImport } from './routes/index' | ||
|
|
||
| const AboutRoute = AboutRouteImport.update({ | ||
| id: '/about', | ||
| path: '/about', | ||
| getParentRoute: () => rootRouteImport, | ||
| } as any) | ||
| const IndexRoute = IndexRouteImport.update({ | ||
| id: '/', | ||
| path: '/', | ||
| getParentRoute: () => rootRouteImport, | ||
| } as any) | ||
|
|
||
| export interface FileRoutesByFullPath { | ||
| '/': typeof IndexRoute | ||
| '/about': typeof AboutRoute | ||
| } | ||
| export interface FileRoutesByTo { | ||
| '/': typeof IndexRoute | ||
| '/about': typeof AboutRoute | ||
| } | ||
| export interface FileRoutesById { | ||
| __root__: typeof rootRouteImport | ||
| '/': typeof IndexRoute | ||
| '/about': typeof AboutRoute | ||
| } | ||
| export interface FileRouteTypes { | ||
| fileRoutesByFullPath: FileRoutesByFullPath | ||
| fullPaths: '/' | '/about' | ||
| fileRoutesByTo: FileRoutesByTo | ||
| to: '/' | '/about' | ||
| id: '__root__' | '/' | '/about' | ||
| fileRoutesById: FileRoutesById | ||
| } | ||
| export interface RootRouteChildren { | ||
| IndexRoute: typeof IndexRoute | ||
| AboutRoute: typeof AboutRoute | ||
| } | ||
|
|
||
| declare module '@tanstack/react-router' { | ||
| interface FileRoutesByPath { | ||
| '/about': { | ||
| id: '/about' | ||
| path: '/about' | ||
| fullPath: '/about' | ||
| preLoaderRoute: typeof AboutRouteImport | ||
| parentRoute: typeof rootRouteImport | ||
| } | ||
| '/': { | ||
| id: '/' | ||
| path: '/' | ||
| fullPath: '/' | ||
| preLoaderRoute: typeof IndexRouteImport | ||
| parentRoute: typeof rootRouteImport | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const rootRouteChildren: RootRouteChildren = { | ||
| IndexRoute: IndexRoute, | ||
| AboutRoute: AboutRoute, | ||
| } | ||
| export const routeTree = rootRouteImport | ||
| ._addFileChildren(rootRouteChildren) | ||
| ._addFileTypes<FileRouteTypes>() | ||
|
|
||
| import type { getRouter } from './router.tsx' | ||
| import type { createStart } from '@tanstack/react-start' | ||
| declare module '@tanstack/react-start' { | ||
| interface Register { | ||
| ssr: true | ||
| router: Awaited<ReturnType<typeof getRouter>> | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { createRouter as createTanStackRouter } from '@tanstack/react-router' | ||
| import { routeTree } from './routeTree.gen' | ||
|
|
||
| export function getRouter() { | ||
| const router = createTanStackRouter({ | ||
| routeTree, | ||
|
|
||
| scrollRestoration: true, | ||
| defaultPreload: 'intent', | ||
| defaultPreloadStaleTime: 0, | ||
| }) | ||
|
|
||
| return router | ||
| } | ||
|
|
||
| declare module '@tanstack/react-router' { | ||
| interface Register { | ||
| router: ReturnType<typeof getRouter> | ||
| } | ||
| } |
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.
Unsanitized locale used in dynamic import path
The
localestring from the caller is interpolated directly into the dynamic import path with no validation. In an SSR context (which TanStack Start uses), a crafted locale value such as../../../vite.configcould potentially cause unintended module resolution. Since this function is called server-side in the root loader, the attack surface is real.Consider validating the locale against a known allowlist before using it in the import path:
Alternatively, use a static mapping to eliminate the dynamic path entirely.
Prompt To Fix With AI