Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions examples/tanstack-start-basic/README.md
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
13 changes: 13 additions & 0 deletions examples/tanstack-start-basic/gt.config.json
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"
}
4 changes: 4 additions & 0 deletions examples/tanstack-start-basic/loadTranslations.ts
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;
Comment on lines +1 to +3
Copy link
Copy Markdown
Contributor

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 locale string 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.config could 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:

Suggested change
export default async function loadTranslations(locale: string) {
const translations = await import(`./src/_gt/${locale}.json`);
return translations.default;
export default async function loadTranslations(locale: string) {
const allowed = ['es', 'ja'] // keep in sync with gt.config.json locales
if (!allowed.includes(locale)) return {}
const translations = await import(`./src/_gt/${locale}.json`)
return translations.default
}

Alternatively, use a static mapping to eliminate the dynamic path entirely.

Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/tanstack-start-basic/loadTranslations.ts
Line: 1-3

Comment:
**Unsanitized locale used in dynamic import path**

The `locale` string 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.config` could 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:

```suggestion
export default async function loadTranslations(locale: string) {
  const allowed = ['es', 'ja'] // keep in sync with gt.config.json locales
  if (!allowed.includes(locale)) return {}
  const translations = await import(`./src/_gt/${locale}.json`)
  return translations.default
}
```

Alternatively, use a static mapping to eliminate the dynamic path entirely.

How can I resolve this? If you propose a fix, please make it concise.

}
30 changes: 30 additions & 0 deletions examples/tanstack-start-basic/package.json
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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:*",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-start": "^1.132.0",

And move @tanstack/router-plugin to devDependencies.

Prompt To Fix With AI
This 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"
}
}
78 changes: 78 additions & 0 deletions examples/tanstack-start-basic/src/_gt/es.json
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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:

  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.

Prompt To Fix With AI
This 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"
}
]
}
78 changes: 78 additions & 0 deletions examples/tanstack-start-basic/src/_gt/ja.json
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"
}
]
}
86 changes: 86 additions & 0 deletions examples/tanstack-start-basic/src/routeTree.gen.ts
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>>
}
}
20 changes: 20 additions & 0 deletions examples/tanstack-start-basic/src/router.tsx
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>
}
}
Loading