create-fate 1.4.0 → 1.5.0

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.
Files changed (89) hide show
  1. package/bin/create-fate.mjs +30 -1
  2. package/package.json +1 -1
  3. package/templates/fate/cloudflare/README.md +11 -0
  4. package/templates/fate/cloudflare/client/package.json +10 -5
  5. package/templates/fate/cloudflare/client/pages/layout.tsx +2 -16
  6. package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +3 -3
  7. package/templates/fate/cloudflare/client/src/lib/LocaleContext.tsx +61 -0
  8. package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +26 -44
  9. package/templates/fate/cloudflare/client/start.mjs +11 -0
  10. package/templates/fate/cloudflare/client/translations/{de_DE.json → de-DE.json} +56 -113
  11. package/templates/fate/{graphql/client/translations/ja_JP.json → cloudflare/client/translations/ja-JP.json} +56 -113
  12. package/templates/fate/cloudflare/client/vite.config.ts +1 -1
  13. package/templates/fate/cloudflare/server/src/lib/auth.ts +0 -2
  14. package/templates/fate/cloudflare/server/tsconfig.json +1 -1
  15. package/templates/fate/cloudflare/tsconfig.json +1 -1
  16. package/templates/fate/drizzle/README.md +11 -0
  17. package/templates/fate/drizzle/client/package.json +9 -5
  18. package/templates/fate/drizzle/client/pages/layout.tsx +2 -16
  19. package/templates/fate/drizzle/client/src/lib/AvailableLanguages.tsx +3 -3
  20. package/templates/fate/drizzle/client/src/lib/LocaleContext.tsx +61 -0
  21. package/templates/fate/drizzle/client/src/ui/LocaleSwitcher.tsx +26 -44
  22. package/templates/fate/drizzle/client/start.mjs +11 -0
  23. package/templates/fate/{graphql/client/translations/de_DE.json → drizzle/client/translations/de-DE.json} +56 -113
  24. package/templates/fate/{cloudflare/client/translations/ja_JP.json → drizzle/client/translations/ja-JP.json} +56 -113
  25. package/templates/fate/drizzle/client/vite.config.ts +1 -1
  26. package/templates/fate/graphql/README.md +11 -0
  27. package/templates/fate/graphql/client/package.json +9 -5
  28. package/templates/fate/graphql/client/pages/layout.tsx +2 -16
  29. package/templates/fate/graphql/client/src/lib/AvailableLanguages.tsx +3 -3
  30. package/templates/fate/graphql/client/src/lib/LocaleContext.tsx +61 -0
  31. package/templates/fate/graphql/client/src/ui/LocaleSwitcher.tsx +26 -44
  32. package/templates/fate/graphql/client/start.mjs +11 -0
  33. package/templates/fate/{drizzle/client/translations/de_DE.json → graphql/client/translations/de-DE.json} +56 -113
  34. package/templates/fate/{http/client/translations/ja_JP.json → graphql/client/translations/ja-JP.json} +56 -113
  35. package/templates/fate/graphql/client/vite.config.ts +1 -1
  36. package/templates/fate/graphql-client/README.md +9 -0
  37. package/templates/fate/graphql-client/_gitignore +3 -0
  38. package/templates/fate/graphql-client/package.json +9 -2
  39. package/templates/fate/graphql-client/pages/index.tsx +13 -5
  40. package/templates/fate/graphql-client/pages/layout.tsx +22 -15
  41. package/templates/fate/graphql-client/src/lib/AvailableLanguages.tsx +5 -0
  42. package/templates/fate/graphql-client/src/lib/LocaleContext.tsx +61 -0
  43. package/templates/fate/graphql-client/src/ui/LocaleSwitcher.tsx +34 -0
  44. package/templates/fate/graphql-client/translations/de-DE.json +225 -0
  45. package/templates/fate/graphql-client/translations/ja-JP.json +225 -0
  46. package/templates/fate/graphql-client/vite.config.ts +2 -0
  47. package/templates/fate/http/README.md +11 -0
  48. package/templates/fate/http/client/package.json +9 -5
  49. package/templates/fate/http/client/pages/layout.tsx +2 -16
  50. package/templates/fate/http/client/src/lib/AvailableLanguages.tsx +3 -3
  51. package/templates/fate/http/client/src/lib/LocaleContext.tsx +61 -0
  52. package/templates/fate/http/client/src/ui/LocaleSwitcher.tsx +26 -44
  53. package/templates/fate/http/client/start.mjs +11 -0
  54. package/templates/fate/http/client/translations/{de_DE.json → de-DE.json} +56 -113
  55. package/templates/fate/{drizzle/client/translations/ja_JP.json → http/client/translations/ja-JP.json} +56 -113
  56. package/templates/fate/http/client/vite.config.ts +1 -1
  57. package/templates/fate/prisma/README.md +11 -0
  58. package/templates/fate/prisma/client/package.json +9 -5
  59. package/templates/fate/prisma/client/pages/layout.tsx +2 -16
  60. package/templates/fate/prisma/client/src/lib/AvailableLanguages.tsx +3 -3
  61. package/templates/fate/prisma/client/src/lib/LocaleContext.tsx +61 -0
  62. package/templates/fate/prisma/client/src/ui/LocaleSwitcher.tsx +26 -44
  63. package/templates/fate/prisma/client/start.mjs +11 -0
  64. package/templates/fate/prisma/client/translations/de-DE.json +632 -0
  65. package/templates/fate/prisma/client/translations/ja-JP.json +632 -0
  66. package/templates/fate/prisma/client/vite.config.ts +1 -1
  67. package/templates/fate/void/README.md +9 -0
  68. package/templates/fate/void/_gitignore +3 -0
  69. package/templates/fate/void/middleware/01.shared.ts +3 -0
  70. package/templates/fate/void/package.json +9 -2
  71. package/templates/fate/void/pages/index.tsx +22 -10
  72. package/templates/fate/void/pages/layout.tsx +22 -19
  73. package/templates/fate/void/pages/login.tsx +22 -18
  74. package/templates/fate/void/pages/search.tsx +18 -4
  75. package/templates/fate/void/src/lib/AvailableLanguages.tsx +5 -0
  76. package/templates/fate/void/src/lib/LocaleContext.tsx +61 -0
  77. package/templates/fate/void/src/ui/CategoryCard.tsx +22 -4
  78. package/templates/fate/void/src/ui/CreatePost.tsx +12 -7
  79. package/templates/fate/void/src/ui/Error.tsx +4 -2
  80. package/templates/fate/void/src/ui/EventCard.tsx +18 -5
  81. package/templates/fate/void/src/ui/Header.tsx +10 -4
  82. package/templates/fate/void/src/ui/LocaleSwitcher.tsx +34 -0
  83. package/templates/fate/void/src/ui/PostCard.tsx +29 -14
  84. package/templates/fate/void/src/ui/UserCard.tsx +15 -8
  85. package/templates/fate/void/translations/de-DE.json +863 -0
  86. package/templates/fate/void/translations/ja-JP.json +863 -0
  87. package/templates/fate/void/vite.config.ts +2 -0
  88. package/templates/fate/prisma/client/translations/de_DE.json +0 -689
  89. package/templates/fate/prisma/client/translations/ja_JP.json +0 -689
@@ -591,7 +591,13 @@ const removeIfExists = (entryPath) => {
591
591
 
592
592
  const removeReactFrontend = (frontendRoot, selectedVariant) => {
593
593
  if (selectedVariant === 'void' || selectedVariant === 'graphql-client') {
594
- for (const entry of ['pages', 'src/ui']) {
594
+ for (const entry of [
595
+ 'pages',
596
+ 'src/ui',
597
+ 'translations',
598
+ 'src/lib/LocaleContext.tsx',
599
+ 'src/lib/AvailableLanguages.tsx',
600
+ ]) {
595
601
  removeIfExists(path.join(frontendRoot, entry));
596
602
  }
597
603
  removeIfExists(path.join(frontendRoot, 'src', 'App.css'));
@@ -617,9 +623,22 @@ const configureVuePackageJson = (frontendRoot, selectedVariant, originalPackageJ
617
623
  ...originalPackageJson.devDependencies,
618
624
  ...vuePackageJson.devDependencies,
619
625
  },
626
+ scripts: {
627
+ ...originalPackageJson.scripts,
628
+ build: vuePackageJson.scripts.build,
629
+ 'dev:setup': originalPackageJson.scripts['dev:setup'].replace('vp run fbtee:all && ', ''),
630
+ },
620
631
  }
621
632
  : vuePackageJson;
622
633
 
634
+ if (packageJson.scripts) {
635
+ for (const name of Object.keys(packageJson.scripts)) {
636
+ if (name.startsWith('fbtee:')) {
637
+ delete packageJson.scripts[name];
638
+ }
639
+ }
640
+ }
641
+
623
642
  removePackageEntries(packageJson.dependencies, reactPackageEntries);
624
643
  removePackageEntries(packageJson.devDependencies, reactPackageEntries);
625
644
 
@@ -709,6 +728,11 @@ const configureVueReadme = (targetPath, selectedVariant) => {
709
728
  fs.writeFileSync(
710
729
  readmePath,
711
730
  readme
731
+ .replace(/\n## Translations\n[\s\S]*$/, '')
732
+ .replaceAll(/^- \[fbtee\].*\n/gm, '')
733
+ .replaceAll('runs fbtee setup, and ', '')
734
+ .replaceAll('fbtee setup, and ', '')
735
+ .replaceAll(', fbtee setup,', ',')
712
736
  .replaceAll(
713
737
  `vp create fate my-app --template ${selectedVariant}`,
714
738
  `vp create fate my-app --template ${selectedVariant} --framework vue`,
@@ -879,6 +903,11 @@ const runCommand = (command, args, cwd) => {
879
903
  const setupProject = (targetPath, selectedVariant) => {
880
904
  runCommand('vp', ['install'], targetPath);
881
905
 
906
+ const frontendRoot = frontendRootForVariant(targetPath, selectedVariant);
907
+ if (readPackageJson(path.join(frontendRoot, 'package.json')).scripts?.['fbtee:all']) {
908
+ runCommand('vp', ['run', 'fbtee:all'], frontendRoot);
909
+ }
910
+
882
911
  switch (selectedVariant) {
883
912
  case 'cloudflare':
884
913
  runCommand('vp', ['run', '--filter', '@app/server', 'dev:setup'], targetPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fate",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Create a new fate app.",
5
5
  "homepage": "https://github.com/nkzw-tech/fate",
6
6
  "license": "MIT",
@@ -124,3 +124,14 @@ VITE_SERVER_URL="https://<your-worker>.<your-subdomain>.workers.dev" pnpm run de
124
124
  ```
125
125
 
126
126
  If you deploy the client to a hosted origin, add that origin to `server/src/lib/origins.ts`.
127
+
128
+ ## Translations
129
+
130
+ The React app uses fbtee 4 with its native Oxc Vite plugin and CLI. Setup and builds generate the runtime translation files automatically.
131
+
132
+ - Wrap UI text in `<fbt desc="Translator context">Text</fbt>`; use `fbs('Text', 'Translator context')` from `fbtee` for string attributes such as placeholders.
133
+ - Run `vp run fbtee:collect` in `client/` to extract strings, then `vp run fbtee:prepare` to update the editable German and Japanese files in `translations/`.
134
+ - Translate entries marked `"status": "new"`, remove that status when finished, and run `vp run fbtee:translate` (or `vp run fbtee:all`). Commit `translations/`; runtime files in `src/translations/` are generated and ignored.
135
+ - Add locales to `src/lib/AvailableLanguages.tsx` and the `fbtee:prepare` script. The language selector remembers the choice. Server rendering starts in English and restores the browser preference after hydration.
136
+
137
+ Run `vp run start` in `client/` to serve the built frontend. Set `PORT` to change its default port of 3000, and run the backend separately.
@@ -9,14 +9,19 @@
9
9
  },
10
10
  "type": "module",
11
11
  "scripts": {
12
- "build": "vp build",
12
+ "build": "vp run fbtee:all && vp build",
13
13
  "dev": "vp dev",
14
14
  "dev:setup": "vp run fbtee:all",
15
15
  "fate:generate": "vp exec fate generate",
16
- "fbtee:all": "vpx fbtee collect && vpx fbtee translate"
16
+ "fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
17
+ "fbtee:collect": "vp exec fbtee collect",
18
+ "fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
19
+ "fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
20
+ "start": "node start.mjs"
17
21
  },
18
22
  "dependencies": {
19
23
  "@app/server": "workspace:*",
24
+ "@hono/node-server": "^2.1.1",
20
25
  "@nkzw/core": "^1.3.1",
21
26
  "@nkzw/define-env": "^1.1.0",
22
27
  "@nkzw/stack": "^2.3.2",
@@ -26,7 +31,7 @@
26
31
  "cf-fate": "latest",
27
32
  "class-variance-authority": "^0.7.1",
28
33
  "clsx": "^2.1.1",
29
- "fbtee": "^3.1.0",
34
+ "fbtee": "^4.0.0",
30
35
  "lucide-react": "^1.34.0",
31
36
  "react": "^19.2.8",
32
37
  "react-dom": "^19.2.8",
@@ -36,8 +41,8 @@
36
41
  "void": "^0.10.12"
37
42
  },
38
43
  "devDependencies": {
39
- "@nkzw/fbtee-cli": "^3.1.0",
40
- "@nkzw/vite-plugin-fbtee": "^3.1.0",
44
+ "@nkzw/fbtee-cli": "^4.0.0",
45
+ "@nkzw/vite-plugin-fbtee": "^4.0.0",
41
46
  "@tailwindcss/vite": "^4.3.3",
42
47
  "@types/node": "^26.4.0",
43
48
  "@types/react": "^19.2.18",
@@ -1,14 +1,12 @@
1
+ import Stack from '@nkzw/stack';
1
2
  /// <reference types="fbtee/ReactTypes.d.ts" />
2
-
3
3
  import '../src/App.css';
4
- import Stack from '@nkzw/stack';
5
- import { createLocaleContext } from 'fbtee';
6
4
  import { ReactNode, Suspense, useMemo } from 'react';
7
5
  import { ErrorBoundary } from 'react-error-boundary';
8
6
  import { FateClient } from 'react-fate';
9
7
  import { createFateClient } from 'react-fate/client';
10
- import AvailableLanguages from '../src/lib/AvailableLanguages.tsx';
11
8
  import env from '../src/lib/env.tsx';
9
+ import LocaleContext from '../src/lib/LocaleContext.tsx';
12
10
  import Card from '../src/ui/Card.tsx';
13
11
  import Error from '../src/ui/Error.tsx';
14
12
  import Header from '../src/ui/Header.tsx';
@@ -23,18 +21,6 @@ const Thinking = () => (
23
21
  </Section>
24
22
  );
25
23
 
26
- const LocaleContext = createLocaleContext({
27
- availableLanguages: AvailableLanguages,
28
- clientLocales: [navigator.language, ...navigator.languages],
29
- loadLocale: async (locale: string) => {
30
- if (locale !== 'en_US' && AvailableLanguages.has(locale)) {
31
- return (await import(`../src/translations/${locale}.json`)).default[locale];
32
- }
33
-
34
- return {};
35
- },
36
- });
37
-
38
24
  export default function Layout({ children }: { children: ReactNode }) {
39
25
  const { data: session } = AuthClient.useSession();
40
26
  const userId = session?.user.id;
@@ -1,5 +1,5 @@
1
1
  export default new Map([
2
- ['en_US', 'English'],
3
- ['de_DE', 'Deutsch (German)'],
4
- ['ja_JP', '日本語 (Japanese)'],
2
+ ['en-US', 'English'],
3
+ ['de-DE', 'Deutsch (German)'],
4
+ ['ja-JP', '日本語 (Japanese)'],
5
5
  ]);
@@ -0,0 +1,61 @@
1
+ /// <reference types="fbtee/ReactTypes.d.ts" />
2
+
3
+ import { createLocaleContext, useLocaleContext } from 'fbtee';
4
+ import { ReactNode, startTransition, useEffect, useState } from 'react';
5
+ import AvailableLanguages from './AvailableLanguages.tsx';
6
+
7
+ const Context = createLocaleContext({
8
+ availableLanguages: AvailableLanguages,
9
+ // Keep the server and first client render identical; restore preferences after hydration.
10
+ clientLocales: ['en-US'],
11
+ fallbackLocale: 'en-US',
12
+ loadLocale: async (locale) => {
13
+ if (locale !== 'en-US' && AvailableLanguages.has(locale)) {
14
+ return (await import(`../translations/${locale}.json`)).default[locale];
15
+ }
16
+ return {};
17
+ },
18
+ });
19
+
20
+ function RestoreLocale({ restored, onRestore }: { restored: boolean; onRestore: () => void }) {
21
+ const { locale, setLocale } = useLocaleContext();
22
+ useEffect(() => {
23
+ if (restored) return;
24
+ onRestore();
25
+ let savedLocale: string | null = null;
26
+ try {
27
+ savedLocale = localStorage.getItem('fbtee:locale');
28
+ } catch {
29
+ // Browser storage can be unavailable.
30
+ }
31
+ const preferred = [savedLocale, ...navigator.languages, navigator.language]
32
+ .filter((value): value is string => !!value)
33
+ .map((value) => value.replaceAll('_', '-'));
34
+ const nextLocale = preferred
35
+ .map((value) =>
36
+ AvailableLanguages.has(value)
37
+ ? value
38
+ : [...AvailableLanguages.keys()].find(
39
+ (available) => available.split('-')[0] === value.split('-')[0],
40
+ ),
41
+ )
42
+ .find(Boolean);
43
+ if (nextLocale && nextLocale !== locale) {
44
+ startTransition(() => setLocale(nextLocale));
45
+ }
46
+ }, [locale, setLocale, restored, onRestore]);
47
+ useEffect(() => {
48
+ document.documentElement.lang = locale;
49
+ }, [locale]);
50
+ return null;
51
+ }
52
+
53
+ export default function LocaleContext({ children }: { children: ReactNode }) {
54
+ const [restored, setRestored] = useState(false);
55
+ return (
56
+ <Context>
57
+ <RestoreLocale restored={restored} onRestore={() => setRestored(true)} />
58
+ {children}
59
+ </Context>
60
+ );
61
+ }
@@ -1,52 +1,34 @@
1
- import { VStack } from '@nkzw/stack';
2
- import { cx } from 'class-variance-authority';
3
1
  import { useLocaleContext } from 'fbtee';
4
- import { Languages } from 'lucide-react';
2
+ import { startTransition } from 'react';
5
3
  import AvailableLanguages from '../lib/AvailableLanguages.tsx';
6
- import { Button } from './Button.tsx';
7
- import Card from './Card.tsx';
8
4
 
9
5
  export default function LocaleSwitcher() {
10
- const { locale: currentLocale, setLocale } = useLocaleContext();
11
-
6
+ const { locale, localeChangeIsPending, setLocale } = useLocaleContext();
12
7
  return (
13
- <>
14
- <Button asChild size="sm" variant="ghost">
15
- <button popoverTarget="locale-switcher" popoverTargetAction="show" type="button">
16
- <Languages className="h-4 w-4" />
17
- <span className="hidden sm:inline">
18
- <fbt desc="Locale switcher button">Change Language</fbt>
19
- </span>
20
- </button>
21
- </Button>
22
- <Card
23
- className="translate-y-50 justify-self-center border-slate-100 bg-white shadow transition-all dark:bg-black"
24
- id="locale-switcher"
25
- popover="auto"
8
+ <label className="inline-flex items-center gap-2 text-sm">
9
+ <span className="sr-only">
10
+ <fbt desc="Locale switcher button">Change Language</fbt>
11
+ </span>
12
+ <select
13
+ className="rounded-md border border-gray-300 bg-background px-2 py-1 text-foreground"
14
+ disabled={localeChangeIsPending}
15
+ onChange={(event) => {
16
+ const nextLocale = event.target.value;
17
+ try {
18
+ localStorage.setItem('fbtee:locale', nextLocale);
19
+ } catch {
20
+ // Changing the language still works without browser storage.
21
+ }
22
+ startTransition(() => setLocale(nextLocale));
23
+ }}
24
+ value={locale}
26
25
  >
27
- <VStack alignCenter center className="w-64" gap={12} padding={16}>
28
- {[...AvailableLanguages.entries()].map(([locale, name]) => (
29
- <Button
30
- action={() => {
31
- setLocale(locale);
32
- localStorage.setItem('fbtee:locale', locale);
33
- }}
34
- asChild
35
- className={cx(
36
- 'transition-background border-slate-200 bg-transparent duration-150 hover:border-slate-300 hover:bg-slate-200 dark:hover:bg-slate-900 dark:text-white active:py-2',
37
- {
38
- 'bg-slate-100 dark:bg-slate-900': currentLocale === locale,
39
- },
40
- )}
41
- key={locale}
42
- size="sm"
43
- variant="outline"
44
- >
45
- <a className="w-full flex-1 cursor-pointer p-2">{name}</a>
46
- </Button>
47
- ))}
48
- </VStack>
49
- </Card>
50
- </>
26
+ {[...AvailableLanguages].map(([value, name]) => (
27
+ <option key={value} value={value}>
28
+ {name}
29
+ </option>
30
+ ))}
31
+ </select>
32
+ </label>
51
33
  );
52
34
  }
@@ -0,0 +1,11 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import process from 'node:process';
3
+ import { fileURLToPath, URL } from 'node:url';
4
+
5
+ const dist = new URL('./dist/', import.meta.url);
6
+ // Void's Node renderer reads this manifest to emit client scripts and styles.
7
+ globalThis.__VITE_MANIFEST__ = JSON.parse(
8
+ readFileSync(new URL('client/.vite/manifest.json', dist), 'utf8'),
9
+ );
10
+ process.chdir(fileURLToPath(dist));
11
+ await import(new URL('ssr/index.js', dist).href);