Skip to content

examples: add tanstack-start-basic example app#1081

Open
moss-bryophyta wants to merge 1 commit intogeneraltranslation:mainfrom
moss-bryophyta:examples/tanstack-start-basic
Open

examples: add tanstack-start-basic example app#1081
moss-bryophyta wants to merge 1 commit intogeneraltranslation:mainfrom
moss-bryophyta:examples/tanstack-start-basic

Conversation

@moss-bryophyta
Copy link
Copy Markdown
Contributor

@moss-bryophyta moss-bryophyta commented Mar 6, 2026

Adds a minimal TanStack Start example app with gt-tanstack-start for internationalization.

  • Demonstrates initializeGT, GTProvider, getTranslations, getLocale, LocaleSelector
  • Includes pre-generated translations for es and ja
  • Linked from the tanstack-start docs in generaltranslation/content

Greptile Summary

This PR adds a minimal TanStack Start example app (examples/tanstack-start-basic) demonstrating General Translation's gt-tanstack-start package for SSR-compatible i18n. The overall structure follows the GT/TanStack Start integration patterns well — initializeGT at module level, GTProvider wrapping the shell, and getTranslations/getLocale in the root loader. Pre-generated translations for Spanish and Japanese are included so new developers can run the example immediately.

Issues found:

  • Path traversal risk in loadTranslations.ts: The locale parameter is interpolated directly into a dynamic import() path with no allowlist validation. In an SSR context (Nitro/Vinxi), this could allow a crafted locale value to load unintended files from the server filesystem.
  • <Scripts /> rendered outside GTProvider: The hydration script tag is placed after the closing GTProvider, which can lead to a mismatch between the server-rendered DOM tree and what the client hydrator expects.
  • Unpinned "latest" for core dependencies: All @tanstack/* and GT packages resolve to whatever is newest at install time, making the example non-reproducible and potentially broken by a future breaking release. react and react-dom correctly use ^19.2.0 — the same pattern should apply to the other packages.

Confidence Score: 3/5

  • Mergeable with caution — one security concern and one hydration issue should be addressed before wider promotion
  • The example is well-structured but contains a server-side path traversal risk in loadTranslations.ts and a potential hydration mismatch from Scripts placement. The dependency pinning issue means the example may silently break for future cloners.
  • loadTranslations.ts (path traversal) and src/routes/__root.tsx (Scripts placement) need attention before this example is linked from official docs.

Important Files Changed

Filename Overview
examples/tanstack-start-basic/loadTranslations.ts Dynamic import path built directly from an unvalidated locale string — path traversal risk in SSR context
examples/tanstack-start-basic/src/routes/__root.tsx Root route wires up GTProvider and locale correctly; Scripts is rendered outside GTProvider which may cause hydration mismatches
examples/tanstack-start-basic/package.json All GT and TanStack packages use "latest" with no semver pinning, making the example non-reproducible over time
examples/tanstack-start-basic/src/routes/index.tsx Simple home page component using the T wrapper for i18n — no issues
examples/tanstack-start-basic/gt.config.json Clean GT config defining defaultLocale, supported locales, and output path for translation files
examples/tanstack-start-basic/vite.config.ts Standard Vite config with tsconfigPaths, tanstackStart, and viteReact plugins — no issues
examples/tanstack-start-basic/src/_gt/es.json Pre-generated Spanish translations for the two T blocks in index.tsx — content looks correct, missing trailing newline
examples/tanstack-start-basic/src/_gt/ja.json Pre-generated Japanese translations — content looks correct, missing trailing newline
examples/tanstack-start-basic/src/router.tsx Minimal router setup with scrollRestoration and intent preloading — no issues
examples/tanstack-start-basic/tsconfig.json Strict TypeScript config with bundler module resolution — no issues

Sequence Diagram

sequenceDiagram
    participant Browser
    participant TanStackServer as TanStack Start (Nitro)
    participant GTLib as gt-tanstack-start
    participant FS as Filesystem (_gt/*.json)

    Browser->>TanStackServer: HTTP GET / (Accept-Language: es)
    TanStackServer->>GTLib: initializeGT({ locales, loadTranslations })
    TanStackServer->>GTLib: getLocale()
    GTLib-->>TanStackServer: "es"
    TanStackServer->>GTLib: getTranslations()
    GTLib->>FS: loadTranslations("es") → import("./src/_gt/es.json")
    FS-->>GTLib: { "01ba21e09b6e9f0d": [...], ... }
    GTLib-->>TanStackServer: translations object
    TanStackServer->>TanStackServer: SSR render RootDocument + Home
    Note over TanStackServer: GTProvider injects translations into React tree
    TanStackServer-->>Browser: HTML (lang="es", pre-translated content)
    Browser->>Browser: Hydrate React tree with client bundles (Scripts)
    Browser->>GTLib: LocaleSelector onChange → navigate with new locale
    Browser->>TanStackServer: HTTP GET / (Accept-Language: ja)
Loading

Comments Outside Diff (3)

  1. examples/tanstack-start-basic/loadTranslations.ts, line 2 (link)

    Unvalidated locale in dynamic import path

    The locale string is embedded directly into a dynamic import path without any sanitization. In a TanStack Start app the loadTranslations function runs on the server (SSR/Nitro), and locale is ultimately derived from user-controlled input (e.g., Accept-Language headers or cookies). A crafted locale value such as ../../package could cause the server to resolve and import an unintended file (e.g., ./src/_gt/../../package.json), leaking sensitive information.

    Adding an explicit allowlist check before the import ensures only known-good locales are ever loaded:

    export default async function loadTranslations(locale: string) {
      const allowedLocales = ['es', 'ja']
      if (!allowedLocales.includes(locale)) {
        return {}
      }
      const translations = await import(`./src/_gt/${locale}.json`);
      return translations.default;
    }

    Alternatively, sync this list from gt.config.json so it stays in one place.

  2. examples/tanstack-start-basic/package.json, line 11-15 (link)

    Unpinned "latest" for major dependencies

    Several packages are resolved to "latest" with no semver constraint at all:

    • @tanstack/react-router, @tanstack/react-start, @tanstack/router-plugin — these packages release breaking changes between minor/major versions
    • gt-react, gt-tanstack-start, gt — same concern for the GT packages themselves

    Using "latest" means a fresh npm install on a different day can produce an entirely different (potentially incompatible) dependency tree. This is especially impactful in an example app that others will clone and run as a reference.

    Consider pinning to a semver range (e.g., "^1.0.0") so the example remains reproducible, even if it's updated periodically. react/react-dom already correctly use ^19.2.0 — the same pattern should apply here.

  3. examples/tanstack-start-basic/src/routes/__root.tsx, line 53-54 (link)

    Scripts rendered outside GTProvider

    <Scripts /> is placed after the closing </GTProvider> tag. In TanStack Start, <Scripts /> emits the client-side JavaScript bundles responsible for hydrating the React tree. If the hydration script runs outside the GTProvider boundary at the HTML level, the client bundle may try to reconcile a DOM tree that does not match what the server rendered (GTProvider wrapping the nav and children), potentially producing a hydration mismatch warning or subtle rendering issues.

    The conventional pattern is to keep <Scripts /> inside the same root-level wrapper that was used during SSR:

Last reviewed commit: 5b9eea0

@moss-bryophyta moss-bryophyta requested a review from a team as a code owner March 6, 2026 18:04
@@ -0,0 +1,4 @@
export default async function loadTranslations(locale: string) {
const translations = await import(`./src/_gt/${locale}.json`);
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.

Unvalidated locale in dynamic import path

The locale string is embedded directly into a dynamic import path without any sanitization. In a TanStack Start app the loadTranslations function runs on the server (SSR/Nitro), and locale is ultimately derived from user-controlled input (e.g., Accept-Language headers or cookies). A crafted locale value such as ../../package could cause the server to resolve and import an unintended file (e.g., ./src/_gt/../../package.json), leaking sensitive information.

Adding an explicit allowlist check before the import ensures only known-good locales are ever loaded:

export default async function loadTranslations(locale: string) {
  const allowedLocales = ['es', 'ja']
  if (!allowedLocales.includes(locale)) {
    return {}
  }
  const translations = await import(`./src/_gt/${locale}.json`);
  return translations.default;
}

Alternatively, sync this list from gt.config.json so it stays in one place.

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

Comment:
**Unvalidated locale in dynamic import path**

The `locale` string is embedded directly into a dynamic import path without any sanitization. In a TanStack Start app the `loadTranslations` function runs on the server (SSR/Nitro), and `locale` is ultimately derived from user-controlled input (e.g., `Accept-Language` headers or cookies). A crafted locale value such as `../../package` could cause the server to resolve and import an unintended file (e.g., `./src/_gt/../../package.json`), leaking sensitive information.

Adding an explicit allowlist check before the import ensures only known-good locales are ever loaded:

```typescript
export default async function loadTranslations(locale: string) {
  const allowedLocales = ['es', 'ja']
  if (!allowedLocales.includes(locale)) {
    return {}
  }
  const translations = await import(`./src/_gt/${locale}.json`);
  return translations.default;
}
```

Alternatively, sync this list from `gt.config.json` so it stays in one place.

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

Comment on lines +11 to +15
"@tanstack/react-router": "latest",
"@tanstack/react-start": "latest",
"@tanstack/router-plugin": "latest",
"gt-react": "latest",
"gt-tanstack-start": "latest",
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" for major dependencies

Several packages are resolved to "latest" with no semver constraint at all:

  • @tanstack/react-router, @tanstack/react-start, @tanstack/router-plugin — these packages release breaking changes between minor/major versions
  • gt-react, gt-tanstack-start, gt — same concern for the GT packages themselves

Using "latest" means a fresh npm install on a different day can produce an entirely different (potentially incompatible) dependency tree. This is especially impactful in an example app that others will clone and run as a reference.

Consider pinning to a semver range (e.g., "^1.0.0") so the example remains reproducible, even if it's updated periodically. react/react-dom already correctly use ^19.2.0 — the same pattern should apply here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/tanstack-start-basic/package.json
Line: 11-15

Comment:
**Unpinned `"latest"` for major dependencies**

Several packages are resolved to `"latest"` with no semver constraint at all:

- `@tanstack/react-router`, `@tanstack/react-start`, `@tanstack/router-plugin` — these packages release breaking changes between minor/major versions
- `gt-react`, `gt-tanstack-start`, `gt` — same concern for the GT packages themselves

Using `"latest"` means a fresh `npm install` on a different day can produce an entirely different (potentially incompatible) dependency tree. This is especially impactful in an example app that others will clone and run as a reference.

Consider pinning to a semver range (e.g., `"^1.0.0"`) so the example remains reproducible, even if it's updated periodically. `react`/`react-dom` already correctly use `^19.2.0` — the same pattern should apply here.

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

Comment on lines +53 to +54
<Scripts />
</body>
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.

Scripts rendered outside GTProvider

<Scripts /> is placed after the closing </GTProvider> tag. In TanStack Start, <Scripts /> emits the client-side JavaScript bundles responsible for hydrating the React tree. If the hydration script runs outside the GTProvider boundary at the HTML level, the client bundle may try to reconcile a DOM tree that does not match what the server rendered (GTProvider wrapping the nav and children), potentially producing a hydration mismatch warning or subtle rendering issues.

The conventional pattern is to keep <Scripts /> inside the same root-level wrapper that was used during SSR:

Suggested change
<Scripts />
</body>
<body>
<GTProvider translations={translations}>
<nav style={{ padding: '1rem', borderBottom: '1px solid #eee' }}>
<LocaleSelector />
</nav>
{children}
<Scripts />
</GTProvider>
</body>
Prompt To Fix With AI
This is a comment left during a code review.
Path: examples/tanstack-start-basic/src/routes/__root.tsx
Line: 53-54

Comment:
**`Scripts` rendered outside `GTProvider`**

`<Scripts />` is placed after the closing `</GTProvider>` tag. In TanStack Start, `<Scripts />` emits the client-side JavaScript bundles responsible for hydrating the React tree. If the hydration script runs outside the `GTProvider` boundary at the HTML level, the client bundle may try to reconcile a DOM tree that does not match what the server rendered (GTProvider wrapping the nav and children), potentially producing a hydration mismatch warning or subtle rendering issues.

The conventional pattern is to keep `<Scripts />` inside the same root-level wrapper that was used during SSR:

```suggestion
      <body>
        <GTProvider translations={translations}>
          <nav style={{ padding: '1rem', borderBottom: '1px solid #eee' }}>
            <LocaleSelector />
          </nav>
          {children}
          <Scripts />
        </GTProvider>
      </body>
```

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant