@linguanahq/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.js +42 -15
- package/dist/templates/blank/react/.env.example +15 -0
- package/dist/templates/blank/react/ASSETS.md +1 -0
- package/dist/templates/blank/react/DEPLOY.md +23 -0
- package/dist/templates/blank/react/LICENSE +21 -0
- package/dist/templates/blank/react/README.md +34 -0
- package/dist/templates/blank/react/_gitignore +11 -0
- package/dist/templates/blank/react/index.html +2 -0
- package/dist/templates/blank/react/linguana.json +10 -0
- package/dist/templates/blank/react/package.json +34 -0
- package/dist/templates/blank/react/public/favicon.ico +0 -0
- package/dist/templates/blank/react/public/logo.png +0 -0
- package/dist/templates/blank/react/src/App.tsx +72 -0
- package/dist/templates/blank/react/src/Copy.tsx +36 -0
- package/dist/templates/blank/react/src/demo-messages.ts +30 -0
- package/dist/templates/blank/react/src/i18n.ts +62 -0
- package/dist/templates/blank/react/src/main.tsx +11 -0
- package/dist/templates/blank/react/src/messages.ts +10 -0
- package/dist/templates/blank/react/src/styles.css +93 -0
- package/dist/templates/blank/react/src/vite-env.d.ts +1 -0
- package/dist/templates/blank/react/tsconfig.json +23 -0
- package/dist/templates/blank/react/vite.config.ts +36 -0
- package/dist/templates/blank/react/wrangler.jsonc +8 -0
- package/dist/templates/blank/solidjs/.env.example +15 -0
- package/dist/templates/blank/solidjs/ASSETS.md +1 -0
- package/dist/templates/blank/solidjs/DEPLOY.md +23 -0
- package/dist/templates/blank/solidjs/LICENSE +21 -0
- package/dist/templates/blank/solidjs/README.md +34 -0
- package/dist/templates/blank/solidjs/_gitignore +11 -0
- package/dist/templates/blank/solidjs/index.html +2 -0
- package/dist/templates/blank/solidjs/linguana.json +10 -0
- package/dist/templates/blank/solidjs/package.json +31 -0
- package/dist/templates/blank/solidjs/public/favicon.ico +0 -0
- package/dist/templates/blank/solidjs/public/logo.png +0 -0
- package/dist/templates/blank/solidjs/src/App.tsx +68 -0
- package/dist/templates/blank/solidjs/src/Copy.tsx +36 -0
- package/dist/templates/blank/solidjs/src/demo-messages.ts +30 -0
- package/dist/templates/blank/solidjs/src/i18n.ts +62 -0
- package/dist/templates/blank/solidjs/src/main.tsx +14 -0
- package/dist/templates/blank/solidjs/src/messages.ts +10 -0
- package/dist/templates/blank/solidjs/src/styles.css +93 -0
- package/dist/templates/blank/solidjs/src/vite-env.d.ts +1 -0
- package/dist/templates/blank/solidjs/tsconfig.json +24 -0
- package/dist/templates/blank/solidjs/vite.config.ts +36 -0
- package/dist/templates/blank/solidjs/wrangler.jsonc +8 -0
- package/dist/templates/blank/svelte/.env.example +15 -0
- package/dist/templates/blank/svelte/ASSETS.md +1 -0
- package/dist/templates/blank/svelte/DEPLOY.md +23 -0
- package/dist/templates/blank/svelte/LICENSE +21 -0
- package/dist/templates/blank/svelte/README.md +34 -0
- package/dist/templates/blank/svelte/_gitignore +11 -0
- package/dist/templates/blank/svelte/index.html +2 -0
- package/dist/templates/blank/svelte/linguana.json +10 -0
- package/dist/templates/blank/svelte/package.json +32 -0
- package/dist/templates/blank/svelte/public/favicon.ico +0 -0
- package/dist/templates/blank/svelte/public/logo.png +0 -0
- package/dist/templates/blank/svelte/src/App.svelte +40 -0
- package/dist/templates/blank/svelte/src/Copy.svelte +14 -0
- package/dist/templates/blank/svelte/src/Root.svelte +11 -0
- package/dist/templates/blank/svelte/src/demo-messages.ts +30 -0
- package/dist/templates/blank/svelte/src/i18n.ts +62 -0
- package/dist/templates/blank/svelte/src/main.ts +5 -0
- package/dist/templates/blank/svelte/src/messages.ts +10 -0
- package/dist/templates/blank/svelte/src/styles.css +93 -0
- package/dist/templates/blank/svelte/src/vite-env.d.ts +1 -0
- package/dist/templates/blank/svelte/svelte.config.js +2 -0
- package/dist/templates/blank/svelte/tsconfig.json +23 -0
- package/dist/templates/blank/svelte/vite.config.ts +36 -0
- package/dist/templates/blank/svelte/wrangler.jsonc +8 -0
- package/dist/templates/blank/tanstack-start/.env.example +15 -0
- package/dist/templates/blank/tanstack-start/ASSETS.md +1 -0
- package/dist/templates/blank/tanstack-start/DEPLOY.md +23 -0
- package/dist/templates/blank/tanstack-start/LICENSE +21 -0
- package/dist/templates/blank/tanstack-start/README.md +34 -0
- package/dist/templates/blank/tanstack-start/_gitignore +11 -0
- package/dist/templates/blank/tanstack-start/linguana.json +10 -0
- package/dist/templates/blank/tanstack-start/nitro.config.ts +2 -0
- package/dist/templates/blank/tanstack-start/package.json +42 -0
- package/dist/templates/blank/tanstack-start/public/favicon.ico +0 -0
- package/dist/templates/blank/tanstack-start/public/logo.png +0 -0
- package/dist/templates/blank/tanstack-start/src/App.tsx +72 -0
- package/dist/templates/blank/tanstack-start/src/Copy.tsx +36 -0
- package/dist/templates/blank/tanstack-start/src/demo-messages.ts +30 -0
- package/dist/templates/blank/tanstack-start/src/i18n.ts +62 -0
- package/dist/templates/blank/tanstack-start/src/locale-server.ts +54 -0
- package/dist/templates/blank/tanstack-start/src/messages.ts +10 -0
- package/dist/templates/blank/tanstack-start/src/router.tsx +10 -0
- package/dist/templates/blank/tanstack-start/src/routes/__root.tsx +53 -0
- package/dist/templates/blank/tanstack-start/src/routes/index.tsx +5 -0
- package/dist/templates/blank/tanstack-start/src/styles.css +93 -0
- package/dist/templates/blank/tanstack-start/src/vite-env.d.ts +1 -0
- package/dist/templates/blank/tanstack-start/tsconfig.json +23 -0
- package/dist/templates/blank/tanstack-start/vite.config.ts +50 -0
- package/dist/templates/blank/tanstack-start/wrangler.jsonc +12 -0
- package/dist/templates/blank/vue/.env.example +15 -0
- package/dist/templates/blank/vue/ASSETS.md +1 -0
- package/dist/templates/blank/vue/DEPLOY.md +23 -0
- package/dist/templates/blank/vue/LICENSE +21 -0
- package/dist/templates/blank/vue/README.md +34 -0
- package/dist/templates/blank/vue/_gitignore +11 -0
- package/dist/templates/blank/vue/index.html +2 -0
- package/dist/templates/blank/vue/linguana.json +10 -0
- package/dist/templates/blank/vue/package.json +34 -0
- package/dist/templates/blank/vue/public/favicon.ico +0 -0
- package/dist/templates/blank/vue/public/logo.png +0 -0
- package/dist/templates/blank/vue/src/App.vue +33 -0
- package/dist/templates/blank/vue/src/Copy.vue +13 -0
- package/dist/templates/blank/vue/src/demo-messages.ts +30 -0
- package/dist/templates/blank/vue/src/i18n.ts +62 -0
- package/dist/templates/blank/vue/src/main.ts +7 -0
- package/dist/templates/blank/vue/src/messages.ts +10 -0
- package/dist/templates/blank/vue/src/styles.css +93 -0
- package/dist/templates/blank/vue/src/vite-env.d.ts +1 -0
- package/dist/templates/blank/vue/tsconfig.json +23 -0
- package/dist/templates/blank/vue/vite.config.ts +36 -0
- package/dist/templates/blank/vue/wrangler.jsonc +8 -0
- package/dist/templates/blog/react/.env.example +2 -2
- package/dist/templates/blog/react/linguana.json +1 -1
- package/dist/templates/blog/react/src/App.tsx +1 -1
- package/dist/templates/blog/solidjs/.env.example +2 -2
- package/dist/templates/blog/solidjs/linguana.json +1 -1
- package/dist/templates/blog/solidjs/src/App.tsx +1 -1
- package/dist/templates/blog/svelte/.env.example +2 -2
- package/dist/templates/blog/svelte/linguana.json +1 -1
- package/dist/templates/blog/svelte/src/App.svelte +1 -1
- package/dist/templates/blog/tanstack-start/.env.example +2 -2
- package/dist/templates/blog/tanstack-start/linguana.json +1 -1
- package/dist/templates/blog/tanstack-start/src/App.tsx +1 -1
- package/dist/templates/blog/vue/.env.example +2 -2
- package/dist/templates/blog/vue/linguana.json +1 -1
- package/dist/templates/blog/vue/src/App.vue +1 -1
- package/dist/templates/saas/react/.env.example +2 -2
- package/dist/templates/saas/react/linguana.json +1 -1
- package/dist/templates/saas/react/src/App.tsx +1 -1
- package/dist/templates/saas/solidjs/.env.example +2 -2
- package/dist/templates/saas/solidjs/linguana.json +1 -1
- package/dist/templates/saas/solidjs/src/App.tsx +1 -1
- package/dist/templates/saas/svelte/.env.example +2 -2
- package/dist/templates/saas/svelte/linguana.json +1 -1
- package/dist/templates/saas/svelte/src/App.svelte +1 -1
- package/dist/templates/saas/tanstack-start/.env.example +2 -2
- package/dist/templates/saas/tanstack-start/linguana.json +1 -1
- package/dist/templates/saas/tanstack-start/src/App.tsx +1 -1
- package/dist/templates/saas/vue/.env.example +2 -2
- package/dist/templates/saas/vue/linguana.json +1 -1
- package/dist/templates/saas/vue/src/App.vue +1 -1
- package/dist/templates/storefront/react/.env.example +2 -2
- package/dist/templates/storefront/react/linguana.json +1 -1
- package/dist/templates/storefront/react/src/App.tsx +1 -1
- package/dist/templates/storefront/solidjs/.env.example +2 -2
- package/dist/templates/storefront/solidjs/linguana.json +1 -1
- package/dist/templates/storefront/solidjs/src/App.tsx +1 -1
- package/dist/templates/storefront/svelte/.env.example +2 -2
- package/dist/templates/storefront/svelte/linguana.json +1 -1
- package/dist/templates/storefront/svelte/src/App.svelte +1 -1
- package/dist/templates/storefront/tanstack-start/.env.example +2 -2
- package/dist/templates/storefront/tanstack-start/linguana.json +1 -1
- package/dist/templates/storefront/tanstack-start/src/App.tsx +1 -1
- package/dist/templates/storefront/vue/.env.example +2 -2
- package/dist/templates/storefront/vue/linguana.json +1 -1
- package/dist/templates/storefront/vue/src/App.vue +1 -1
- package/package.json +24 -36
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Blank
|
|
2
|
+
|
|
3
|
+
A minimal starting point: the Linguana logo, a translated hello world, a language picker, and hot reload.
|
|
4
|
+
|
|
5
|
+
A standalone tanstack-start app, generated from Linguana templates v0.1.0. Requires Node 22.12+ (or Bun).
|
|
6
|
+
|
|
7
|
+
## Run
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
You can also use pnpm, Yarn, or Bun. English, French, Spanish, and Arabic catalogs are bundled in demo mode. No Linguana API requests are made. The language cookie persists between visits. Arabic switches the document to RTL.
|
|
15
|
+
|
|
16
|
+
## Connect your own project
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx @linguanahq/cli login
|
|
20
|
+
npx @linguanahq/cli link
|
|
21
|
+
npx @linguanahq/cli doctor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Linking writes nonsecret `linguana.json` and ignored `.env.local`. The account session stays outside this project. Only the public project ID, environment, API URL, and publishable key reach the browser. Build tokens never belong in `VITE_*` variables.
|
|
25
|
+
|
|
26
|
+
Connected mode uses your published project catalogs and English source fallback. It does not load demo translations. Builds can upload the source manifest but do not start paid translations or publish catalogs. Review and publish translations in your dashboard. Existing project languages remain unchanged unless you pass `--locales`.
|
|
27
|
+
|
|
28
|
+
To target another environment, use `linguana env pull --environment staging`, then `npm run build -- --mode staging`. Production uses `.env.production.local` and Vite's default production build mode. Configure matching private and public values on your build host; see .env.example.
|
|
29
|
+
|
|
30
|
+
## Customize
|
|
31
|
+
|
|
32
|
+
Start in src/App.tsx. Vite hot reload applies your edits as you save, without losing the selected language. Add a message by giving it a stable ID in src/Copy.tsx with its English text, a source fallback in src/messages.ts, and demo translations in src/demo-messages.ts.
|
|
33
|
+
|
|
34
|
+
Run `npm run check-types` and `npm run build` before deploying. Deployment instructions are in DEPLOY.md. See LICENSE and ASSETS.md.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blank-tanstack-start",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.12.0"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "vite --host 127.0.0.1",
|
|
12
|
+
"build": "vite build",
|
|
13
|
+
"preview": "vite preview",
|
|
14
|
+
"check-types": "tsr generate && tsc --noEmit",
|
|
15
|
+
"build:cloudflare": "NITRO_PRESET=cloudflare_module vite build",
|
|
16
|
+
"build:node": "NITRO_PRESET=node-server vite build",
|
|
17
|
+
"start": "node .output/server/index.mjs",
|
|
18
|
+
"deploy": "npm run build:cloudflare && wrangler deploy --config .output/server/wrangler.json"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@linguanahq/catalog": "0.1.1",
|
|
22
|
+
"@linguanahq/core": "0.1.1",
|
|
23
|
+
"@linguanahq/react": "0.1.1",
|
|
24
|
+
"react": "^19.2.7",
|
|
25
|
+
"react-dom": "^19.2.7",
|
|
26
|
+
"@tanstack/react-router": "^1.170.17",
|
|
27
|
+
"@tanstack/react-start": "^1.168.27",
|
|
28
|
+
"tslib": "^2.8.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@linguanahq/vite": "0.1.1",
|
|
32
|
+
"typescript": "^6.0.0",
|
|
33
|
+
"vite": "^8.1.3",
|
|
34
|
+
"@types/node": "^24.0.0",
|
|
35
|
+
"wrangler": "^4.127.0",
|
|
36
|
+
"@types/react": "^19.2.17",
|
|
37
|
+
"@types/react-dom": "^19.2.3",
|
|
38
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
39
|
+
"nitro": "^3.0.260610-beta",
|
|
40
|
+
"@tanstack/router-cli": "^1.167.21"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useTranslation } from "@linguanahq/react";
|
|
2
|
+
import { Fragment, useEffect } from "react";
|
|
3
|
+
import Copy from "./Copy.tsx";
|
|
4
|
+
import { demoLocales } from "./i18n";
|
|
5
|
+
import { messages } from "./messages";
|
|
6
|
+
|
|
7
|
+
export default function App() {
|
|
8
|
+
const runtime = useTranslation();
|
|
9
|
+
const locale = () => runtime.locale;
|
|
10
|
+
const locales = () =>
|
|
11
|
+
runtime.availableLocales.length
|
|
12
|
+
? runtime.availableLocales
|
|
13
|
+
: demoLocales.filter((item) => item.code === runtime.sourceLocale);
|
|
14
|
+
const translate = runtime.translate;
|
|
15
|
+
const changeLocale = (value: string) => void runtime.setLocale(value);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
document.documentElement.lang = runtime.locale;
|
|
18
|
+
document.documentElement.dir = runtime.locale.startsWith("ar")
|
|
19
|
+
? "rtl"
|
|
20
|
+
: "ltr";
|
|
21
|
+
}, [runtime.locale]);
|
|
22
|
+
const msg = (id: string, values?: Record<string, string | number>) =>
|
|
23
|
+
String(translate(id, messages[id] ?? id, values));
|
|
24
|
+
const value = (event: Event | { currentTarget: EventTarget | null }) =>
|
|
25
|
+
(event.currentTarget as HTMLSelectElement).value;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<main className="hello">
|
|
29
|
+
<img
|
|
30
|
+
src="/logo.png"
|
|
31
|
+
alt={msg("logoAlt")}
|
|
32
|
+
className="hello-logo"
|
|
33
|
+
width={96}
|
|
34
|
+
height={96}
|
|
35
|
+
/>
|
|
36
|
+
<h1>
|
|
37
|
+
<Copy name="hello" />
|
|
38
|
+
</h1>
|
|
39
|
+
<p className="hello-intro">
|
|
40
|
+
<Copy name="intro" />
|
|
41
|
+
</p>
|
|
42
|
+
<label className="locale-picker">
|
|
43
|
+
<span className="sr-only">
|
|
44
|
+
<Copy name="language" />
|
|
45
|
+
</span>
|
|
46
|
+
<select
|
|
47
|
+
value={locale()}
|
|
48
|
+
aria-label={msg("language")}
|
|
49
|
+
onChange={(event) => {
|
|
50
|
+
changeLocale(value(event));
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{locales().map((language) => (
|
|
54
|
+
<Fragment key={language.code}>
|
|
55
|
+
<option value={language.code}>{language.nativeName}</option>
|
|
56
|
+
</Fragment>
|
|
57
|
+
))}
|
|
58
|
+
</select>
|
|
59
|
+
</label>
|
|
60
|
+
<p className="hello-hint">
|
|
61
|
+
<Copy name="hotReload" values={{ file: "src/App.tsx" }} />
|
|
62
|
+
</p>
|
|
63
|
+
<a
|
|
64
|
+
href="https://docs.linguana.dev"
|
|
65
|
+
className="hello-link"
|
|
66
|
+
rel="noreferrer"
|
|
67
|
+
>
|
|
68
|
+
<Copy name="docs" />
|
|
69
|
+
</a>
|
|
70
|
+
</main>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { T } from "@linguanahq/react";
|
|
2
|
+
export default function Copy(props: {
|
|
3
|
+
name: string;
|
|
4
|
+
values?: Record<string, string | number>;
|
|
5
|
+
}) {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
{props.name === "language" && (
|
|
9
|
+
<T id="language" defaultMessage="Language" values={props.values} />
|
|
10
|
+
)}
|
|
11
|
+
{props.name === "hello" && (
|
|
12
|
+
<T id="hello" defaultMessage="Hello, world!" values={props.values} />
|
|
13
|
+
)}
|
|
14
|
+
{props.name === "intro" && (
|
|
15
|
+
<T
|
|
16
|
+
id="intro"
|
|
17
|
+
defaultMessage="Your multilingual app is ready. Pick a language to see it translated."
|
|
18
|
+
values={props.values}
|
|
19
|
+
/>
|
|
20
|
+
)}
|
|
21
|
+
{props.name === "hotReload" && (
|
|
22
|
+
<T
|
|
23
|
+
id="hotReload"
|
|
24
|
+
defaultMessage="Edit {file} and save. The page updates instantly."
|
|
25
|
+
values={props.values}
|
|
26
|
+
/>
|
|
27
|
+
)}
|
|
28
|
+
{props.name === "docs" && (
|
|
29
|
+
<T id="docs" defaultMessage="Read the docs" values={props.values} />
|
|
30
|
+
)}
|
|
31
|
+
{props.name === "logoAlt" && (
|
|
32
|
+
<T id="logoAlt" defaultMessage="Linguana logo" values={props.values} />
|
|
33
|
+
)}
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// These catalogs are only used when linguana.json selects demo mode.
|
|
2
|
+
export const demoMessages = {
|
|
3
|
+
fr: {
|
|
4
|
+
language: "Langue",
|
|
5
|
+
hello: "Bonjour, le monde !",
|
|
6
|
+
intro:
|
|
7
|
+
"Votre application multilingue est prête. Choisissez une langue pour la voir traduite.",
|
|
8
|
+
hotReload:
|
|
9
|
+
"Modifiez {file} et enregistrez. La page se met à jour instantanément.",
|
|
10
|
+
docs: "Lire la documentation",
|
|
11
|
+
logoAlt: "Logo Linguana",
|
|
12
|
+
},
|
|
13
|
+
es: {
|
|
14
|
+
language: "Idioma",
|
|
15
|
+
hello: "¡Hola, mundo!",
|
|
16
|
+
intro:
|
|
17
|
+
"Tu aplicación multilingüe está lista. Elige un idioma para verla traducida.",
|
|
18
|
+
hotReload: "Edita {file} y guarda. La página se actualiza al instante.",
|
|
19
|
+
docs: "Leer la documentación",
|
|
20
|
+
logoAlt: "Logotipo de Linguana",
|
|
21
|
+
},
|
|
22
|
+
ar: {
|
|
23
|
+
language: "اللغة",
|
|
24
|
+
hello: "مرحبًا بالعالم!",
|
|
25
|
+
intro: "تطبيقك متعدد اللغات جاهز. اختر لغة لتراه مترجمًا.",
|
|
26
|
+
hotReload: "عدّل {file} واحفظه. تتحدّث الصفحة فورًا.",
|
|
27
|
+
docs: "اقرأ التوثيق",
|
|
28
|
+
logoAlt: "شعار Linguana",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createCatalog } from "@linguanahq/catalog";
|
|
2
|
+
import config from "../linguana.json";
|
|
3
|
+
import { demoMessages } from "./demo-messages";
|
|
4
|
+
export const demoLocales = [
|
|
5
|
+
{ code: "en", displayName: "English", nativeName: "English" },
|
|
6
|
+
{ code: "fr", displayName: "French", nativeName: "Français" },
|
|
7
|
+
{ code: "es", displayName: "Spanish", nativeName: "Español" },
|
|
8
|
+
{ code: "ar", displayName: "Arabic", nativeName: "العربية" },
|
|
9
|
+
];
|
|
10
|
+
export const demo = config.mode === "demo";
|
|
11
|
+
export function cookieLocale(
|
|
12
|
+
cookie = typeof document === "undefined" ? "" : document.cookie,
|
|
13
|
+
) {
|
|
14
|
+
try {
|
|
15
|
+
const value = decodeURIComponent(
|
|
16
|
+
cookie
|
|
17
|
+
.split(";")
|
|
18
|
+
.find((item) => item.trim().startsWith("linguana_locale="))
|
|
19
|
+
?.split("=")[1] ?? "en",
|
|
20
|
+
);
|
|
21
|
+
return /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(value) &&
|
|
22
|
+
(!demo || demoLocales.some((item) => item.code === value))
|
|
23
|
+
? value
|
|
24
|
+
: "en";
|
|
25
|
+
} catch {
|
|
26
|
+
return "en";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const demoCatalogs = demo
|
|
30
|
+
? Object.entries(demoMessages).map(([locale, messages]) =>
|
|
31
|
+
createCatalog({
|
|
32
|
+
schemaVersion: "1.0",
|
|
33
|
+
projectId: "demo-blank",
|
|
34
|
+
environment: "development",
|
|
35
|
+
sourceLocale: "en",
|
|
36
|
+
locale,
|
|
37
|
+
manifestHash: "0".repeat(64),
|
|
38
|
+
version: 1,
|
|
39
|
+
messages,
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
: [];
|
|
43
|
+
export function optionsFor(
|
|
44
|
+
locale = cookieLocale(),
|
|
45
|
+
): import("@linguanahq/core").LinguanaClientOptions {
|
|
46
|
+
return {
|
|
47
|
+
projectId: demo ? "demo-blank" : import.meta.env.VITE_LINGUANA_PROJECT_ID,
|
|
48
|
+
environment: demo
|
|
49
|
+
? "development"
|
|
50
|
+
: import.meta.env.VITE_LINGUANA_ENVIRONMENT,
|
|
51
|
+
initialLocale: locale,
|
|
52
|
+
fallbackLocale: config.sourceLocale,
|
|
53
|
+
catalogs: demoCatalogs,
|
|
54
|
+
...(demo
|
|
55
|
+
? { availableLocales: demoLocales }
|
|
56
|
+
: {
|
|
57
|
+
publishableKey: import.meta.env.VITE_LINGUANA_PUBLISHABLE_KEY,
|
|
58
|
+
catalogEndpoint: import.meta.env.VITE_LINGUANA_API_URL,
|
|
59
|
+
configEndpoint: import.meta.env.VITE_LINGUANA_API_URL,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHydrationState,
|
|
3
|
+
loadCatalog,
|
|
4
|
+
loadRuntimeConfig,
|
|
5
|
+
} from "@linguanahq/core";
|
|
6
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
7
|
+
import { getRequest } from "@tanstack/react-start/server";
|
|
8
|
+
import {
|
|
9
|
+
cookieLocale,
|
|
10
|
+
demo,
|
|
11
|
+
demoCatalogs,
|
|
12
|
+
demoLocales,
|
|
13
|
+
optionsFor,
|
|
14
|
+
} from "./i18n";
|
|
15
|
+
export const localeState = createServerFn({ method: "GET" }).handler(
|
|
16
|
+
async () => {
|
|
17
|
+
let locale = cookieLocale(getRequest().headers.get("cookie") ?? "");
|
|
18
|
+
const options = optionsFor(locale);
|
|
19
|
+
let catalogs = demoCatalogs;
|
|
20
|
+
let locales = demoLocales;
|
|
21
|
+
if (!demo) {
|
|
22
|
+
catalogs = [];
|
|
23
|
+
locales = demoLocales.filter(
|
|
24
|
+
(item) => item.code === options.fallbackLocale,
|
|
25
|
+
);
|
|
26
|
+
try {
|
|
27
|
+
const config = await loadRuntimeConfig({
|
|
28
|
+
endpoint: options.configEndpoint!,
|
|
29
|
+
projectId: options.projectId,
|
|
30
|
+
environment: options.environment,
|
|
31
|
+
publishableKey: options.publishableKey!,
|
|
32
|
+
signal: AbortSignal.timeout(5000),
|
|
33
|
+
});
|
|
34
|
+
locales = config.locales;
|
|
35
|
+
if (!locales.some((item) => item.code === locale))
|
|
36
|
+
locale = options.fallbackLocale;
|
|
37
|
+
if (locale !== options.fallbackLocale)
|
|
38
|
+
catalogs = [
|
|
39
|
+
await loadCatalog({
|
|
40
|
+
endpoint: options.catalogEndpoint!,
|
|
41
|
+
projectId: options.projectId,
|
|
42
|
+
environment: options.environment,
|
|
43
|
+
publishableKey: options.publishableKey!,
|
|
44
|
+
locale,
|
|
45
|
+
signal: AbortSignal.timeout(5000),
|
|
46
|
+
}),
|
|
47
|
+
];
|
|
48
|
+
} catch {
|
|
49
|
+
locale = options.fallbackLocale;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { hydrationState: createHydrationState(locale, catalogs), locales };
|
|
53
|
+
},
|
|
54
|
+
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Source fallbacks; matching extractable literals live in Copy.tsx.
|
|
2
|
+
export const messages: Record<string, string> = {
|
|
3
|
+
language: "Language",
|
|
4
|
+
hello: "Hello, world!",
|
|
5
|
+
intro:
|
|
6
|
+
"Your multilingual app is ready. Pick a language to see it translated.",
|
|
7
|
+
hotReload: "Edit {file} and save. The page updates instantly.",
|
|
8
|
+
docs: "Read the docs",
|
|
9
|
+
logoAlt: "Linguana logo",
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createRouter } from "@tanstack/react-router";
|
|
2
|
+
import { routeTree } from "./routeTree.gen";
|
|
3
|
+
export function getRouter() {
|
|
4
|
+
return createRouter({ routeTree, scrollRestoration: true });
|
|
5
|
+
}
|
|
6
|
+
declare module "@tanstack/react-router" {
|
|
7
|
+
interface Register {
|
|
8
|
+
router: ReturnType<typeof getRouter>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { LocaleProvider } from "@linguanahq/react";
|
|
2
|
+
import {
|
|
3
|
+
createRootRoute,
|
|
4
|
+
HeadContent,
|
|
5
|
+
Outlet,
|
|
6
|
+
Scripts,
|
|
7
|
+
} from "@tanstack/react-router";
|
|
8
|
+
import { optionsFor } from "../i18n";
|
|
9
|
+
import { localeState } from "../locale-server";
|
|
10
|
+
import styles from "../styles.css?url";
|
|
11
|
+
export const Route = createRootRoute({
|
|
12
|
+
loader: () => localeState(),
|
|
13
|
+
head: () => ({
|
|
14
|
+
meta: [
|
|
15
|
+
{ charSet: "utf-8" },
|
|
16
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
17
|
+
{ title: "Blank" },
|
|
18
|
+
{
|
|
19
|
+
name: "description",
|
|
20
|
+
content:
|
|
21
|
+
"A minimal starting point: the Linguana logo, a translated hello world, a language picker, and hot reload.",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
links: [
|
|
25
|
+
{ rel: "stylesheet", href: styles },
|
|
26
|
+
{ rel: "icon", href: "/favicon.ico" },
|
|
27
|
+
],
|
|
28
|
+
}),
|
|
29
|
+
component: Root,
|
|
30
|
+
});
|
|
31
|
+
function Root() {
|
|
32
|
+
const { hydrationState, locales } = Route.useLoaderData();
|
|
33
|
+
return (
|
|
34
|
+
<html
|
|
35
|
+
lang={hydrationState.locale}
|
|
36
|
+
dir={hydrationState.locale.startsWith("ar") ? "rtl" : "ltr"}
|
|
37
|
+
>
|
|
38
|
+
<head>
|
|
39
|
+
<HeadContent />
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<LocaleProvider
|
|
43
|
+
{...optionsFor(hydrationState.locale)}
|
|
44
|
+
hydrationState={hydrationState}
|
|
45
|
+
availableLocales={locales}
|
|
46
|
+
>
|
|
47
|
+
<Outlet />
|
|
48
|
+
</LocaleProvider>
|
|
49
|
+
<Scripts />
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
body {
|
|
5
|
+
margin: 0;
|
|
6
|
+
min-height: 100vh;
|
|
7
|
+
display: grid;
|
|
8
|
+
place-items: center;
|
|
9
|
+
background: #f7f8fb;
|
|
10
|
+
color: #14213d;
|
|
11
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
12
|
+
line-height: 1.6;
|
|
13
|
+
}
|
|
14
|
+
button,
|
|
15
|
+
select {
|
|
16
|
+
font: inherit;
|
|
17
|
+
}
|
|
18
|
+
.hello {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: 1rem;
|
|
23
|
+
max-width: 36rem;
|
|
24
|
+
padding: 3rem 1.5rem;
|
|
25
|
+
text-align: center;
|
|
26
|
+
}
|
|
27
|
+
.hello-logo {
|
|
28
|
+
width: 6rem;
|
|
29
|
+
height: 6rem;
|
|
30
|
+
}
|
|
31
|
+
.hello h1 {
|
|
32
|
+
margin: 0;
|
|
33
|
+
font-size: clamp(2.5rem, 8vw, 4rem);
|
|
34
|
+
line-height: 1.1;
|
|
35
|
+
letter-spacing: -0.02em;
|
|
36
|
+
}
|
|
37
|
+
.hello-intro {
|
|
38
|
+
margin: 0;
|
|
39
|
+
color: #46536b;
|
|
40
|
+
font-size: 1.125rem;
|
|
41
|
+
}
|
|
42
|
+
.locale-picker select {
|
|
43
|
+
min-height: 2.75rem;
|
|
44
|
+
padding: 0.5rem 2.5rem 0.5rem 1rem;
|
|
45
|
+
border: 1px solid #c9d1e0;
|
|
46
|
+
border-radius: 0.5rem;
|
|
47
|
+
background: #fff;
|
|
48
|
+
color: inherit;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
}
|
|
51
|
+
.locale-picker select:focus-visible,
|
|
52
|
+
.hello-link:focus-visible {
|
|
53
|
+
outline: 3px solid #2f5bea;
|
|
54
|
+
outline-offset: 2px;
|
|
55
|
+
}
|
|
56
|
+
.hello-hint {
|
|
57
|
+
margin: 0.5rem 0 0;
|
|
58
|
+
color: #46536b;
|
|
59
|
+
font-size: 0.9375rem;
|
|
60
|
+
}
|
|
61
|
+
.hello-link {
|
|
62
|
+
color: #2f5bea;
|
|
63
|
+
font-weight: 600;
|
|
64
|
+
text-underline-offset: 0.2em;
|
|
65
|
+
}
|
|
66
|
+
.sr-only {
|
|
67
|
+
position: absolute;
|
|
68
|
+
width: 1px;
|
|
69
|
+
height: 1px;
|
|
70
|
+
padding: 0;
|
|
71
|
+
margin: -1px;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
clip: rect(0, 0, 0, 0);
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
border: 0;
|
|
76
|
+
}
|
|
77
|
+
@media (prefers-color-scheme: dark) {
|
|
78
|
+
body {
|
|
79
|
+
background: #0f1420;
|
|
80
|
+
color: #eef1f7;
|
|
81
|
+
}
|
|
82
|
+
.hello-intro,
|
|
83
|
+
.hello-hint {
|
|
84
|
+
color: #aab4c8;
|
|
85
|
+
}
|
|
86
|
+
.locale-picker select {
|
|
87
|
+
background: #182033;
|
|
88
|
+
border-color: #33405c;
|
|
89
|
+
}
|
|
90
|
+
.hello-link {
|
|
91
|
+
color: #8ea8ff;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"types": ["vite/client", "node"],
|
|
14
|
+
"jsx": "react-jsx"
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"src/**/*.ts",
|
|
18
|
+
"src/**/*.tsx",
|
|
19
|
+
"src/**/*.vue",
|
|
20
|
+
"src/**/*.svelte",
|
|
21
|
+
"vite.config.ts"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { linguana } from "@linguanahq/vite";
|
|
3
|
+
import { react } from "@linguanahq/vite/react";
|
|
4
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
5
|
+
import native from "@vitejs/plugin-react";
|
|
6
|
+
import { nitro } from "nitro/vite";
|
|
7
|
+
import { defineConfig, loadEnv } from "vite";
|
|
8
|
+
import config from "./linguana.json" with { type: "json" };
|
|
9
|
+
export default defineConfig(({ mode }) => {
|
|
10
|
+
const env = { ...loadEnv(mode, process.cwd(), ""), ...process.env };
|
|
11
|
+
const connected = config.mode === "connected";
|
|
12
|
+
if (
|
|
13
|
+
connected &&
|
|
14
|
+
(!env.LINGUANA_PROJECT_ID ||
|
|
15
|
+
!env.VITE_LINGUANA_PUBLISHABLE_KEY ||
|
|
16
|
+
env.LINGUANA_PROJECT_ID !== env.VITE_LINGUANA_PROJECT_ID ||
|
|
17
|
+
env.LINGUANA_ENVIRONMENT !== env.VITE_LINGUANA_ENVIRONMENT ||
|
|
18
|
+
env.LINGUANA_API_URL !== env.VITE_LINGUANA_API_URL)
|
|
19
|
+
)
|
|
20
|
+
throw new Error(
|
|
21
|
+
"Linguana environment is incomplete or inconsistent. Run linguana env pull for this build mode.",
|
|
22
|
+
);
|
|
23
|
+
return {
|
|
24
|
+
resolve: {
|
|
25
|
+
alias: [
|
|
26
|
+
{
|
|
27
|
+
find: "tslib",
|
|
28
|
+
replacement: fileURLToPath(
|
|
29
|
+
import.meta.resolve("tslib/tslib.es6.mjs"),
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
plugins: [
|
|
35
|
+
linguana({
|
|
36
|
+
adapter: react(),
|
|
37
|
+
projectId: connected ? env.LINGUANA_PROJECT_ID! : "demo-blank",
|
|
38
|
+
environment: connected ? env.LINGUANA_ENVIRONMENT : "development",
|
|
39
|
+
sourceLocale: config.sourceLocale,
|
|
40
|
+
apiUrl: connected ? env.LINGUANA_API_URL : undefined,
|
|
41
|
+
token: connected ? env.LINGUANA_PROJECT_TOKEN : undefined,
|
|
42
|
+
upload: connected && env.LINGUANA_UPLOAD === "true",
|
|
43
|
+
translation: false,
|
|
44
|
+
}),
|
|
45
|
+
tanstackStart(),
|
|
46
|
+
nitro(),
|
|
47
|
+
native(),
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Demo mode works without credentials. Connect with npx @linguanahq/cli link.
|
|
2
|
+
# Copying this file alone does not enable connected mode; use linguana.json.
|
|
3
|
+
# Build/server only. NEVER prefix the project token with VITE_.
|
|
4
|
+
LINGUANA_PROJECT_ID=
|
|
5
|
+
LINGUANA_ENVIRONMENT=development
|
|
6
|
+
LINGUANA_API_URL=https://api.linguana.dev
|
|
7
|
+
LINGUANA_PROJECT_TOKEN=
|
|
8
|
+
LINGUANA_UPLOAD=false
|
|
9
|
+
# Paid translation is disabled; this template does not enable it automatically.
|
|
10
|
+
LINGUANA_TRANSLATE=false
|
|
11
|
+
# Browser-safe values. A publishable key reads published catalogs only.
|
|
12
|
+
VITE_LINGUANA_PROJECT_ID=
|
|
13
|
+
VITE_LINGUANA_ENVIRONMENT=development
|
|
14
|
+
VITE_LINGUANA_API_URL=https://api.linguana.dev
|
|
15
|
+
VITE_LINGUANA_PUBLISHABLE_KEY=
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
The Linguana logo and favicon in public/ are Linguana brand assets, included so the starter renders out of the box. Replace them with your own. Sample translations are MIT. No remote images, tracking pixels, or font services are used.
|