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.
- package/bin/create-fate.mjs +30 -1
- package/package.json +1 -1
- package/templates/fate/cloudflare/README.md +11 -0
- package/templates/fate/cloudflare/client/package.json +10 -5
- package/templates/fate/cloudflare/client/pages/layout.tsx +2 -16
- package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +3 -3
- package/templates/fate/cloudflare/client/src/lib/LocaleContext.tsx +61 -0
- package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +26 -44
- package/templates/fate/cloudflare/client/start.mjs +11 -0
- package/templates/fate/cloudflare/client/translations/{de_DE.json → de-DE.json} +56 -113
- package/templates/fate/{graphql/client/translations/ja_JP.json → cloudflare/client/translations/ja-JP.json} +56 -113
- package/templates/fate/cloudflare/client/vite.config.ts +1 -1
- package/templates/fate/cloudflare/server/src/lib/auth.ts +0 -2
- package/templates/fate/cloudflare/server/tsconfig.json +1 -1
- package/templates/fate/cloudflare/tsconfig.json +1 -1
- package/templates/fate/drizzle/README.md +11 -0
- package/templates/fate/drizzle/client/package.json +9 -5
- package/templates/fate/drizzle/client/pages/layout.tsx +2 -16
- package/templates/fate/drizzle/client/src/lib/AvailableLanguages.tsx +3 -3
- package/templates/fate/drizzle/client/src/lib/LocaleContext.tsx +61 -0
- package/templates/fate/drizzle/client/src/ui/LocaleSwitcher.tsx +26 -44
- package/templates/fate/drizzle/client/start.mjs +11 -0
- package/templates/fate/{graphql/client/translations/de_DE.json → drizzle/client/translations/de-DE.json} +56 -113
- package/templates/fate/{cloudflare/client/translations/ja_JP.json → drizzle/client/translations/ja-JP.json} +56 -113
- package/templates/fate/drizzle/client/vite.config.ts +1 -1
- package/templates/fate/graphql/README.md +11 -0
- package/templates/fate/graphql/client/package.json +9 -5
- package/templates/fate/graphql/client/pages/layout.tsx +2 -16
- package/templates/fate/graphql/client/src/lib/AvailableLanguages.tsx +3 -3
- package/templates/fate/graphql/client/src/lib/LocaleContext.tsx +61 -0
- package/templates/fate/graphql/client/src/ui/LocaleSwitcher.tsx +26 -44
- package/templates/fate/graphql/client/start.mjs +11 -0
- package/templates/fate/{drizzle/client/translations/de_DE.json → graphql/client/translations/de-DE.json} +56 -113
- package/templates/fate/{http/client/translations/ja_JP.json → graphql/client/translations/ja-JP.json} +56 -113
- package/templates/fate/graphql/client/vite.config.ts +1 -1
- package/templates/fate/graphql-client/README.md +9 -0
- package/templates/fate/graphql-client/_gitignore +3 -0
- package/templates/fate/graphql-client/package.json +9 -2
- package/templates/fate/graphql-client/pages/index.tsx +13 -5
- package/templates/fate/graphql-client/pages/layout.tsx +22 -15
- package/templates/fate/graphql-client/src/lib/AvailableLanguages.tsx +5 -0
- package/templates/fate/graphql-client/src/lib/LocaleContext.tsx +61 -0
- package/templates/fate/graphql-client/src/ui/LocaleSwitcher.tsx +34 -0
- package/templates/fate/graphql-client/translations/de-DE.json +225 -0
- package/templates/fate/graphql-client/translations/ja-JP.json +225 -0
- package/templates/fate/graphql-client/vite.config.ts +2 -0
- package/templates/fate/http/README.md +11 -0
- package/templates/fate/http/client/package.json +9 -5
- package/templates/fate/http/client/pages/layout.tsx +2 -16
- package/templates/fate/http/client/src/lib/AvailableLanguages.tsx +3 -3
- package/templates/fate/http/client/src/lib/LocaleContext.tsx +61 -0
- package/templates/fate/http/client/src/ui/LocaleSwitcher.tsx +26 -44
- package/templates/fate/http/client/start.mjs +11 -0
- package/templates/fate/http/client/translations/{de_DE.json → de-DE.json} +56 -113
- package/templates/fate/{drizzle/client/translations/ja_JP.json → http/client/translations/ja-JP.json} +56 -113
- package/templates/fate/http/client/vite.config.ts +1 -1
- package/templates/fate/prisma/README.md +11 -0
- package/templates/fate/prisma/client/package.json +9 -5
- package/templates/fate/prisma/client/pages/layout.tsx +2 -16
- package/templates/fate/prisma/client/src/lib/AvailableLanguages.tsx +3 -3
- package/templates/fate/prisma/client/src/lib/LocaleContext.tsx +61 -0
- package/templates/fate/prisma/client/src/ui/LocaleSwitcher.tsx +26 -44
- package/templates/fate/prisma/client/start.mjs +11 -0
- package/templates/fate/prisma/client/translations/de-DE.json +632 -0
- package/templates/fate/prisma/client/translations/ja-JP.json +632 -0
- package/templates/fate/prisma/client/vite.config.ts +1 -1
- package/templates/fate/void/README.md +9 -0
- package/templates/fate/void/_gitignore +3 -0
- package/templates/fate/void/middleware/01.shared.ts +3 -0
- package/templates/fate/void/package.json +9 -2
- package/templates/fate/void/pages/index.tsx +22 -10
- package/templates/fate/void/pages/layout.tsx +22 -19
- package/templates/fate/void/pages/login.tsx +22 -18
- package/templates/fate/void/pages/search.tsx +18 -4
- package/templates/fate/void/src/lib/AvailableLanguages.tsx +5 -0
- package/templates/fate/void/src/lib/LocaleContext.tsx +61 -0
- package/templates/fate/void/src/ui/CategoryCard.tsx +22 -4
- package/templates/fate/void/src/ui/CreatePost.tsx +12 -7
- package/templates/fate/void/src/ui/Error.tsx +4 -2
- package/templates/fate/void/src/ui/EventCard.tsx +18 -5
- package/templates/fate/void/src/ui/Header.tsx +10 -4
- package/templates/fate/void/src/ui/LocaleSwitcher.tsx +34 -0
- package/templates/fate/void/src/ui/PostCard.tsx +29 -14
- package/templates/fate/void/src/ui/UserCard.tsx +15 -8
- package/templates/fate/void/translations/de-DE.json +863 -0
- package/templates/fate/void/translations/ja-JP.json +863 -0
- package/templates/fate/void/vite.config.ts +2 -0
- package/templates/fate/prisma/client/translations/de_DE.json +0 -689
- package/templates/fate/prisma/client/translations/ja_JP.json +0 -689
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "vp build",
|
|
13
|
+
"build": "vp run fbtee:all && vp build",
|
|
14
14
|
"db:generate": "void db generate",
|
|
15
15
|
"db:migrate": "void db migrate",
|
|
16
16
|
"db:seed": "void db seed --file db/seed.ts",
|
|
17
17
|
"dev": "vp dev",
|
|
18
|
-
"dev:setup": "vp run prepare:void && vp run db:seed && vp run fate:generate",
|
|
18
|
+
"dev:setup": "vp run fbtee:all && vp run prepare:void && vp run db:seed && vp run fate:generate",
|
|
19
19
|
"fate:generate": "vp exec fate generate",
|
|
20
|
+
"fbtee:all": "vp run fbtee:collect && vp run fbtee:translate",
|
|
21
|
+
"fbtee:collect": "vp exec fbtee collect",
|
|
22
|
+
"fbtee:prepare": "vp run fbtee:collect && vp exec fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE ja-JP",
|
|
23
|
+
"fbtee:translate": "vp exec fbtee translate --source-strings source_strings.json --output-dir src/translations --output-locale-style=bcp47",
|
|
20
24
|
"prepare:void": "void prepare",
|
|
21
25
|
"test:all": "vp check && vp test"
|
|
22
26
|
},
|
|
@@ -30,6 +34,7 @@
|
|
|
30
34
|
"class-variance-authority": "^0.7.1",
|
|
31
35
|
"clsx": "^2.1.1",
|
|
32
36
|
"drizzle-orm": "^0.45.2",
|
|
37
|
+
"fbtee": "^4.0.0",
|
|
33
38
|
"lucide-react": "^1.34.0",
|
|
34
39
|
"react": "^19.2.8",
|
|
35
40
|
"react-dom": "^19.2.8",
|
|
@@ -43,7 +48,9 @@
|
|
|
43
48
|
"devDependencies": {
|
|
44
49
|
"@cloudflare/workers-types": "4.20260702.1",
|
|
45
50
|
"@nkzw/eslint-plugin": "^2.0.0",
|
|
51
|
+
"@nkzw/fbtee-cli": "^4.0.0",
|
|
46
52
|
"@nkzw/oxlint-config": "^2.0.1",
|
|
53
|
+
"@nkzw/vite-plugin-fbtee": "^4.0.0",
|
|
47
54
|
"@oxc-node/core": "^0.1.0",
|
|
48
55
|
"@tailwindcss/vite": "^4.3.3",
|
|
49
56
|
"@types/node": "^26.4.0",
|
|
@@ -38,7 +38,9 @@ const PostFeed = ({
|
|
|
38
38
|
return posts.length ? (
|
|
39
39
|
<VStack gap={16}>
|
|
40
40
|
<Stack alignCenter between gap={16}>
|
|
41
|
-
<H2 className="pl-5">
|
|
41
|
+
<H2 className="pl-5">
|
|
42
|
+
<fbt desc="Latest posts header">Latest posts</fbt>
|
|
43
|
+
</H2>
|
|
42
44
|
<div>
|
|
43
45
|
<button
|
|
44
46
|
className={cx(
|
|
@@ -66,7 +68,7 @@ const PostFeed = ({
|
|
|
66
68
|
{loadNext ? (
|
|
67
69
|
<Stack center>
|
|
68
70
|
<Button onClick={loadNext} variant="ghost">
|
|
69
|
-
Load more posts
|
|
71
|
+
<fbt desc="Load more posts button">Load more posts</fbt>{' '}
|
|
70
72
|
</Button>
|
|
71
73
|
</Stack>
|
|
72
74
|
) : null}
|
|
@@ -78,7 +80,9 @@ const PostFeed = ({
|
|
|
78
80
|
const CategoryFeed = ({ categories }: { categories: Array<ViewRef<'Category'>> }) =>
|
|
79
81
|
categories.length ? (
|
|
80
82
|
<VStack gap={16}>
|
|
81
|
-
<H2 className="pl-5">
|
|
83
|
+
<H2 className="pl-5">
|
|
84
|
+
<fbt desc="index: Explore by theme">Explore by theme</fbt>
|
|
85
|
+
</H2>
|
|
82
86
|
<VStack gap={24}>
|
|
83
87
|
{categories.map((category) => (
|
|
84
88
|
<CategoryCard category={category} key={category.id} />
|
|
@@ -90,7 +94,9 @@ const CategoryFeed = ({ categories }: { categories: Array<ViewRef<'Category'>> }
|
|
|
90
94
|
const EventFeed = ({ events }: { events: Array<ViewRef<'Event'>> }) =>
|
|
91
95
|
events.length ? (
|
|
92
96
|
<VStack gap={16}>
|
|
93
|
-
<H2 className="pl-5">
|
|
97
|
+
<H2 className="pl-5">
|
|
98
|
+
<fbt desc="index: Events">Events</fbt>
|
|
99
|
+
</H2>
|
|
94
100
|
<VStack gap={24}>
|
|
95
101
|
{events.map((event) => (
|
|
96
102
|
<EventCard event={event} key={event.id} />
|
|
@@ -115,16 +121,20 @@ export default function HomePage() {
|
|
|
115
121
|
<Card className="border border-white/20 bg-linear-to-r from-blue-500 to-sky-500 text-white dark:from-blue-600 dark:to-sky-600">
|
|
116
122
|
<Stack alignCenter gap={12} wrap>
|
|
117
123
|
<span className="squircle bg-white/20 px-2 py-1 text-xs font-semibold tracking-widest uppercase">
|
|
118
|
-
<span className="lowercase italic">fate</span> demo
|
|
124
|
+
<span className="lowercase italic">fate</span> <fbt desc="index: demo">demo</fbt>{' '}
|
|
119
125
|
</span>
|
|
120
126
|
</Stack>
|
|
121
127
|
<div className="space-y-3">
|
|
122
128
|
<h1 className="text-3xl leading-tight font-semibold text-balance lg:text-4xl">
|
|
123
|
-
|
|
129
|
+
<fbt desc="Description of fate">
|
|
130
|
+
fate is a modern data client for React inspired by Relay and GraphQL.
|
|
131
|
+
</fbt>{' '}
|
|
124
132
|
</h1>
|
|
125
133
|
<p className="text-sm text-white/80 lg:text-base">
|
|
126
|
-
fate combines view composition, normalized caching, data masking, Async React
|
|
127
|
-
|
|
134
|
+
<fbt desc="index: fate combines view composition, normalized caching, data masking, Async React features, and type-safe data fetching.">
|
|
135
|
+
fate combines view composition, normalized caching, data masking, Async React
|
|
136
|
+
features, and type-safe data fetching.
|
|
137
|
+
</fbt>{' '}
|
|
128
138
|
</p>
|
|
129
139
|
</div>
|
|
130
140
|
<Stack alignCenter gap={12} wrap>
|
|
@@ -132,10 +142,12 @@ export default function HomePage() {
|
|
|
132
142
|
<>
|
|
133
143
|
<Button asChild size="sm" variant="secondary">
|
|
134
144
|
<Link className="squircle px-4 py-2 text-sm font-semibold" href="/login">
|
|
135
|
-
Login
|
|
145
|
+
<fbt desc="Login button">Login</fbt>{' '}
|
|
136
146
|
</Link>
|
|
137
147
|
</Button>
|
|
138
|
-
<span className="text-sm text-white/80">
|
|
148
|
+
<span className="text-sm text-white/80">
|
|
149
|
+
<fbt desc="Login button label">Sign in to post comments.</fbt>
|
|
150
|
+
</span>
|
|
139
151
|
</>
|
|
140
152
|
)}
|
|
141
153
|
</Stack>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import '../src/App.css';
|
|
2
1
|
import Stack from '@nkzw/stack';
|
|
2
|
+
import '../src/App.css';
|
|
3
3
|
import { useShared } from '@void/react';
|
|
4
4
|
import { ReactNode, Suspense } from 'react';
|
|
5
5
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
6
6
|
import { VoidFateClient } from 'void-fate/react';
|
|
7
|
+
import LocaleContext from '../src/lib/LocaleContext.tsx';
|
|
7
8
|
import type { SharedData } from '../src/lib/shared.ts';
|
|
8
9
|
import Card from '../src/ui/Card.tsx';
|
|
9
10
|
import Error from '../src/ui/Error.tsx';
|
|
@@ -13,7 +14,7 @@ import Section from '../src/ui/Section.tsx';
|
|
|
13
14
|
const Thinking = () => (
|
|
14
15
|
<Section>
|
|
15
16
|
<Stack center className="animate-pulse text-gray-500 italic" verticalPadding={48}>
|
|
16
|
-
Thinking
|
|
17
|
+
<fbt desc="Text for thinking/loading screen">Thinking...</fbt>
|
|
17
18
|
</Stack>
|
|
18
19
|
</Section>
|
|
19
20
|
);
|
|
@@ -24,23 +25,25 @@ export default function Layout({ children }: { children: ReactNode }) {
|
|
|
24
25
|
const origin = typeof window === 'undefined' ? shared.origin : window.location.origin;
|
|
25
26
|
|
|
26
27
|
return (
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
<div className="min-h-screen bg-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
<LocaleContext>
|
|
29
|
+
<VoidFateClient origin={origin} userId={userId}>
|
|
30
|
+
<div className="min-h-screen bg-background text-foreground">
|
|
31
|
+
<div className="min-h-screen bg-[radial-gradient(circle_at_20%_20%,rgba(59,130,246,0.08),transparent_35%),radial-gradient(circle_at_80%_0,rgba(99,102,241,0.08),transparent_28%)]">
|
|
32
|
+
<Header />
|
|
33
|
+
<ErrorBoundary
|
|
34
|
+
fallbackRender={({ error }) => (
|
|
35
|
+
<Section>
|
|
36
|
+
<Card>
|
|
37
|
+
<Error error={error} />
|
|
38
|
+
</Card>
|
|
39
|
+
</Section>
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
<Suspense fallback={<Thinking />}>{children}</Suspense>
|
|
43
|
+
</ErrorBoundary>
|
|
44
|
+
</div>
|
|
42
45
|
</div>
|
|
43
|
-
</
|
|
44
|
-
</
|
|
46
|
+
</VoidFateClient>
|
|
47
|
+
</LocaleContext>
|
|
45
48
|
);
|
|
46
49
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Stack, { VStack } from '@nkzw/stack';
|
|
2
2
|
import { useRouter, useShared } from '@void/react';
|
|
3
|
+
import { fbs } from 'fbtee';
|
|
3
4
|
import { ExternalLinkIcon } from 'lucide-react';
|
|
4
5
|
import { useActionState, useEffect, useState } from 'react';
|
|
5
6
|
import type { SharedData } from '../src/lib/shared.ts';
|
|
@@ -49,7 +50,9 @@ export default function LoginPage() {
|
|
|
49
50
|
return (
|
|
50
51
|
<Section>
|
|
51
52
|
<VStack center gap={16}>
|
|
52
|
-
<H2 className="pl-5">
|
|
53
|
+
<H2 className="pl-5">
|
|
54
|
+
<fbt desc="Sign in button">Sign In</fbt>
|
|
55
|
+
</H2>
|
|
53
56
|
<Stack gap={32} wrap>
|
|
54
57
|
<Card className="w-84">
|
|
55
58
|
<Stack gap vertical>
|
|
@@ -58,7 +61,7 @@ export default function LoginPage() {
|
|
|
58
61
|
className="w-48"
|
|
59
62
|
name="email"
|
|
60
63
|
onChange={(e) => setEmail(e.target.value)}
|
|
61
|
-
placeholder=
|
|
64
|
+
placeholder={fbs('email', 'login: placeholder')}
|
|
62
65
|
type="email"
|
|
63
66
|
value={email}
|
|
64
67
|
/>
|
|
@@ -66,13 +69,13 @@ export default function LoginPage() {
|
|
|
66
69
|
className="w-48"
|
|
67
70
|
name="password"
|
|
68
71
|
onChange={(e) => setPassword(e.target.value)}
|
|
69
|
-
placeholder=
|
|
72
|
+
placeholder={fbs('password', 'login: placeholder')}
|
|
70
73
|
type="password"
|
|
71
74
|
value={password}
|
|
72
75
|
/>
|
|
73
76
|
<div>
|
|
74
77
|
<AsyncButton type="submit" variant="outline">
|
|
75
|
-
Sign In
|
|
78
|
+
<fbt desc="Sign in button">Sign In</fbt>{' '}
|
|
76
79
|
</AsyncButton>
|
|
77
80
|
</div>
|
|
78
81
|
</VStack>
|
|
@@ -80,20 +83,21 @@ export default function LoginPage() {
|
|
|
80
83
|
</Card>
|
|
81
84
|
<Card className="w-84">
|
|
82
85
|
<p>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
<fbt desc="Example account instructions">
|
|
87
|
+
Try one of the{' '}
|
|
88
|
+
<Stack
|
|
89
|
+
alignCenter
|
|
90
|
+
as="a"
|
|
91
|
+
className="inline-flex! px-1 underline hover:no-underline"
|
|
92
|
+
gap={4}
|
|
93
|
+
href="https://github.com/nkzw-tech/fate/blob/main/packages/create-fate/templates/fate/void/seedData.ts#L1"
|
|
94
|
+
rel="noreferrer"
|
|
95
|
+
target="_blank"
|
|
96
|
+
>
|
|
97
|
+
Example Accounts <ExternalLinkIcon className="h-4 w-4" />
|
|
98
|
+
</Stack>{' '}
|
|
99
|
+
in the seed data.
|
|
100
|
+
</fbt>{' '}
|
|
97
101
|
</p>
|
|
98
102
|
</Card>
|
|
99
103
|
</Stack>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Stack, { VStack } from '@nkzw/stack';
|
|
2
|
+
import { fbs } from 'fbtee';
|
|
2
3
|
import { Suspense, useDeferredValue, useState } from 'react';
|
|
3
4
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
4
5
|
import { useRequest, useView, view, ViewRef } from 'react-fate';
|
|
@@ -39,7 +40,12 @@ const SearchResults = ({ isStale, query }: { isStale: boolean; query: string })
|
|
|
39
40
|
if (commentSearch.length === 0) {
|
|
40
41
|
return (
|
|
41
42
|
<p>
|
|
42
|
-
|
|
43
|
+
<fbt desc="Empty comment search results">
|
|
44
|
+
No matches for{' '}
|
|
45
|
+
<i>
|
|
46
|
+
"<fbt:param name="query">{query}</fbt:param>"
|
|
47
|
+
</i>
|
|
48
|
+
</fbt>
|
|
43
49
|
</p>
|
|
44
50
|
);
|
|
45
51
|
}
|
|
@@ -65,17 +71,25 @@ export default function SearchPage() {
|
|
|
65
71
|
<Input
|
|
66
72
|
className="w-64"
|
|
67
73
|
onChange={(e) => setQuery(e.target.value)}
|
|
68
|
-
placeholder=
|
|
74
|
+
placeholder={fbs('Search comments...', 'search: placeholder')}
|
|
69
75
|
ref={(ref) => ref?.focus()}
|
|
70
76
|
value={query}
|
|
71
77
|
/>
|
|
72
78
|
{isDevelopment ? (
|
|
73
|
-
<div className="text-xs text-muted-foreground">
|
|
79
|
+
<div className="text-xs text-muted-foreground">
|
|
80
|
+
<fbt desc="Slowdown label explanation">500ms artificial slowdown</fbt>
|
|
81
|
+
</div>
|
|
74
82
|
) : null}
|
|
75
83
|
</Stack>
|
|
76
84
|
|
|
77
85
|
<ErrorBoundary FallbackComponent={Error}>
|
|
78
|
-
<Suspense
|
|
86
|
+
<Suspense
|
|
87
|
+
fallback={
|
|
88
|
+
<h2>
|
|
89
|
+
<fbt desc="Text for thinking/loading screen">Thinking…</fbt>
|
|
90
|
+
</h2>
|
|
91
|
+
}
|
|
92
|
+
>
|
|
79
93
|
{query.trim().length > 0 ? <SearchResults isStale={isStale} query={query} /> : null}
|
|
80
94
|
</Suspense>
|
|
81
95
|
</ErrorBoundary>
|
|
@@ -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
|
+
}
|
|
@@ -33,11 +33,23 @@ const CategoryPost = ({ post: postRef }: { post: ViewRef<'Post'> }) => {
|
|
|
33
33
|
{post.title}
|
|
34
34
|
</span>
|
|
35
35
|
</Link>
|
|
36
|
-
<span className="text-xs text-muted-foreground">
|
|
36
|
+
<span className="text-xs text-muted-foreground">
|
|
37
|
+
<fbt desc="Post like count">
|
|
38
|
+
<fbt:plural count={post.likes} many="likes" showCount="yes">
|
|
39
|
+
like
|
|
40
|
+
</fbt:plural>
|
|
41
|
+
</fbt>
|
|
42
|
+
</span>
|
|
37
43
|
</Stack>
|
|
38
44
|
<Stack alignCenter gap wrap>
|
|
39
45
|
<span className="text-xs text-muted-foreground">
|
|
40
|
-
{author?.name ?
|
|
46
|
+
{author?.name ? (
|
|
47
|
+
<fbt desc="Post author">
|
|
48
|
+
by <fbt:param name="author">{author.name}</fbt:param>
|
|
49
|
+
</fbt>
|
|
50
|
+
) : (
|
|
51
|
+
<fbt desc="Anonymous post author">By an anonymous collaborator</fbt>
|
|
52
|
+
)}
|
|
41
53
|
</span>
|
|
42
54
|
{tags.length ? (
|
|
43
55
|
<Stack gap wrap>
|
|
@@ -82,7 +94,11 @@ export default function CategoryCard({ category: categoryRef }: { category: View
|
|
|
82
94
|
<p className="text-sm text-muted-foreground">{category.description}</p>
|
|
83
95
|
</div>
|
|
84
96
|
<Badge className="text-nowrap" variant="outline">
|
|
85
|
-
|
|
97
|
+
<fbt desc="Category post count">
|
|
98
|
+
<fbt:plural count={category.postCount} many="posts" showCount="yes">
|
|
99
|
+
post
|
|
100
|
+
</fbt:plural>
|
|
101
|
+
</fbt>{' '}
|
|
86
102
|
</Badge>
|
|
87
103
|
</Stack>
|
|
88
104
|
<VStack gap={12}>
|
|
@@ -92,7 +108,9 @@ export default function CategoryCard({ category: categoryRef }: { category: View
|
|
|
92
108
|
</VStack>
|
|
93
109
|
{category.posts?.pagination?.hasNext ? (
|
|
94
110
|
<span className="text-sm text-muted-foreground">
|
|
95
|
-
More posts available in this category...
|
|
111
|
+
<fbt desc="CategoryCard: More posts available in this category...">
|
|
112
|
+
More posts available in this category...
|
|
113
|
+
</fbt>{' '}
|
|
96
114
|
</span>
|
|
97
115
|
) : null}
|
|
98
116
|
</VStack>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Stack, { VStack } from '@nkzw/stack';
|
|
2
|
+
import { fbs } from 'fbtee';
|
|
2
3
|
import { KeyboardEvent, startTransition, useActionState, useState } from 'react';
|
|
3
4
|
import { useFateClient, useView, ViewRef } from 'react-fate';
|
|
4
5
|
import { Button } from '../ui/Button.tsx';
|
|
@@ -64,13 +65,15 @@ export default function CreatePost({ user: userRef }: { user: ViewRef<'User'> |
|
|
|
64
65
|
return (
|
|
65
66
|
<Card>
|
|
66
67
|
<VStack action={createPost} as="form" gap={16}>
|
|
67
|
-
<H3>
|
|
68
|
+
<H3>
|
|
69
|
+
<fbt desc="CreatePost: Create a Post">Create a Post</fbt>
|
|
70
|
+
</H3>
|
|
68
71
|
<Input
|
|
69
72
|
className="w-full"
|
|
70
73
|
disabled={isPending}
|
|
71
74
|
onChange={(event) => setTitleValue(event.target.value)}
|
|
72
75
|
onKeyDown={maybeSubmitPost}
|
|
73
|
-
placeholder=
|
|
76
|
+
placeholder={fbs('Post Title', 'CreatePost: placeholder')}
|
|
74
77
|
value={titleValue}
|
|
75
78
|
/>
|
|
76
79
|
<textarea
|
|
@@ -78,18 +81,20 @@ export default function CreatePost({ user: userRef }: { user: ViewRef<'User'> |
|
|
|
78
81
|
disabled={isPending}
|
|
79
82
|
onChange={(event) => setContentValue(event.target.value)}
|
|
80
83
|
onKeyDown={maybeSubmitPost}
|
|
81
|
-
placeholder={'Share your thoughts about fate...'}
|
|
84
|
+
placeholder={fbs('Share your thoughts about fate...', 'CreatePost: placeholder')}
|
|
82
85
|
value={contentValue}
|
|
83
86
|
/>
|
|
84
87
|
<Stack alignCenter between className="text-sm" gap={16}>
|
|
85
|
-
<span className="font-bold">
|
|
88
|
+
<span className="font-bold">
|
|
89
|
+
<fbt desc="CreatePost: Mutation Debug Options">Mutation Debug Options</fbt>
|
|
90
|
+
</span>
|
|
86
91
|
<Stack alignCenter as="label" gap>
|
|
87
92
|
<CheckBox
|
|
88
93
|
checked={missingMutationSelection}
|
|
89
94
|
disabled={isPending}
|
|
90
95
|
onChange={(event) => setMissingMutationSelection(event.target.checked)}
|
|
91
96
|
/>
|
|
92
|
-
Missing mutation selection
|
|
97
|
+
<fbt desc="CreatePost: Missing mutation selection">Missing mutation selection</fbt>{' '}
|
|
93
98
|
</Stack>
|
|
94
99
|
<Stack alignCenter as="label" gap>
|
|
95
100
|
<CheckBox
|
|
@@ -97,12 +102,12 @@ export default function CreatePost({ user: userRef }: { user: ViewRef<'User'> |
|
|
|
97
102
|
disabled={isPending}
|
|
98
103
|
onChange={(event) => setMissingOptimisticContent(event.target.checked)}
|
|
99
104
|
/>
|
|
100
|
-
Missing optimistic content
|
|
105
|
+
<fbt desc="CreatePost: Missing optimistic content">Missing optimistic content</fbt>{' '}
|
|
101
106
|
</Stack>
|
|
102
107
|
</Stack>
|
|
103
108
|
<Stack alignCenter end gap={16}>
|
|
104
109
|
<Button disabled={postingIsDisabled} size="sm" type="submit" variant="secondary">
|
|
105
|
-
Post comment
|
|
110
|
+
<fbt desc="Post comment button">Post comment</fbt>{' '}
|
|
106
111
|
</Button>
|
|
107
112
|
</Stack>
|
|
108
113
|
</VStack>
|
|
@@ -4,14 +4,16 @@ export default function Error({ error: initialError }: { error: unknown }) {
|
|
|
4
4
|
const error = initialError instanceof Error ? (initialError as Error) : null;
|
|
5
5
|
return (
|
|
6
6
|
<VStack gap={12}>
|
|
7
|
-
<h3 className="text-xl font-semibold text-red-700">
|
|
7
|
+
<h3 className="text-xl font-semibold text-red-700">
|
|
8
|
+
<fbt desc="Error: Error">Error</fbt>
|
|
9
|
+
</h3>
|
|
8
10
|
<code>{error ? error.stack || `fate Error: ${error.message}` : String(initialError)}</code>
|
|
9
11
|
<div>
|
|
10
12
|
<a
|
|
11
13
|
className="cursor-pointer underline decoration-gray-300 hover:no-underline"
|
|
12
14
|
onClick={() => window.location.reload()}
|
|
13
15
|
>
|
|
14
|
-
Try again
|
|
16
|
+
<fbt desc="Try again button">Try again</fbt>{' '}
|
|
15
17
|
</a>
|
|
16
18
|
</div>
|
|
17
19
|
</VStack>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Stack, { VStack } from '@nkzw/stack';
|
|
2
|
+
import { fbs } from 'fbtee';
|
|
2
3
|
import { ArrowUpRight, CalendarDays, MapPin, Users } from 'lucide-react';
|
|
3
4
|
import { useEffect, useState } from 'react';
|
|
4
5
|
import { useView, view, ViewRef } from 'react-fate';
|
|
@@ -41,7 +42,7 @@ const EventAttendeeChip = ({ attendee: attendeeRef }: { attendee: ViewRef<'Event
|
|
|
41
42
|
|
|
42
43
|
return (
|
|
43
44
|
<Badge className="text-nowrap" key={attendee.id} variant="outline">
|
|
44
|
-
{user?.name ?? 'Guest'} · {formatLabel(attendee.status)}
|
|
45
|
+
{user?.name ?? fbs('Guest', 'Unnamed event attendee')} · {formatLabel(attendee.status)}
|
|
45
46
|
</Badge>
|
|
46
47
|
);
|
|
47
48
|
};
|
|
@@ -110,12 +111,22 @@ export default function EventCard({ event: eventRef }: { event: ViewRef<'Event'>
|
|
|
110
111
|
<Stack alignCenter gap>
|
|
111
112
|
<Users className="text-muted-foreground" size={14} />
|
|
112
113
|
<span className="text-sm text-foreground/80">
|
|
113
|
-
|
|
114
|
+
<fbt desc="Event attendance and capacity">
|
|
115
|
+
<fbt:param name="attending">{event.attendingCount ?? attendees.length}</fbt:param>{' '}
|
|
116
|
+
attending · capacity <fbt:param name="capacity">{event.capacity}</fbt:param>
|
|
117
|
+
</fbt>
|
|
114
118
|
</span>
|
|
115
119
|
</Stack>
|
|
116
120
|
<Stack alignCenter gap>
|
|
117
121
|
<ArrowUpRight className="text-muted-foreground" size={14} />
|
|
118
|
-
<span className="text-sm text-foreground/80">
|
|
122
|
+
<span className="text-sm text-foreground/80">
|
|
123
|
+
<fbt desc="Event host">
|
|
124
|
+
Hosted by{' '}
|
|
125
|
+
<fbt:param name="host">
|
|
126
|
+
{host?.name ?? fbs('Unknown', 'Unknown event host')}
|
|
127
|
+
</fbt:param>
|
|
128
|
+
</fbt>
|
|
129
|
+
</span>
|
|
119
130
|
</Stack>
|
|
120
131
|
{topics.length ? (
|
|
121
132
|
<Stack gap wrap>
|
|
@@ -128,7 +139,9 @@ export default function EventCard({ event: eventRef }: { event: ViewRef<'Event'>
|
|
|
128
139
|
) : null}
|
|
129
140
|
{attendees.length ? (
|
|
130
141
|
<VStack gap>
|
|
131
|
-
<span className="text-xs text-muted-foreground">
|
|
142
|
+
<span className="text-xs text-muted-foreground">
|
|
143
|
+
<fbt desc="EventCard: Community RSVPs">Community RSVPs</fbt>
|
|
144
|
+
</span>
|
|
132
145
|
<Stack gap wrap>
|
|
133
146
|
{attendees.slice(0, 4).map(({ node }) => (
|
|
134
147
|
<EventAttendeeChip attendee={node} key={node.id} />
|
|
@@ -143,7 +156,7 @@ export default function EventCard({ event: eventRef }: { event: ViewRef<'Event'>
|
|
|
143
156
|
rel="noreferrer"
|
|
144
157
|
target="_blank"
|
|
145
158
|
>
|
|
146
|
-
Join livestream
|
|
159
|
+
<fbt desc="EventCard: Join livestream">Join livestream</fbt>{' '}
|
|
147
160
|
</a>
|
|
148
161
|
) : null}
|
|
149
162
|
</VStack>
|
|
@@ -5,6 +5,7 @@ import type { SharedData } from '../lib/shared.ts';
|
|
|
5
5
|
import AuthClient from '../user/AuthClient.tsx';
|
|
6
6
|
import { Button } from './Button.tsx';
|
|
7
7
|
import Link from './Link.tsx';
|
|
8
|
+
import LocaleSwitcher from './LocaleSwitcher.tsx';
|
|
8
9
|
|
|
9
10
|
export default function Header() {
|
|
10
11
|
const router = useRouter();
|
|
@@ -51,16 +52,19 @@ export default function Header() {
|
|
|
51
52
|
<span className="italic">fate</span>
|
|
52
53
|
</span>
|
|
53
54
|
<p className="hidden text-xs text-muted-foreground sm:block">
|
|
54
|
-
A modern data client for React
|
|
55
|
+
<fbt desc="fate library tagline">A modern data client for React.</fbt>{' '}
|
|
55
56
|
</p>
|
|
56
57
|
</div>
|
|
57
58
|
</Stack>
|
|
58
59
|
</Link>
|
|
59
60
|
<Stack alignCenter>
|
|
61
|
+
<LocaleSwitcher />
|
|
60
62
|
<Button asChild size="sm" variant="ghost">
|
|
61
63
|
<Link href="/search">
|
|
62
64
|
<Search className="h-4 w-4" />
|
|
63
|
-
<span className="hidden sm:inline">
|
|
65
|
+
<span className="hidden sm:inline">
|
|
66
|
+
<fbt desc="Search button">Search</fbt>
|
|
67
|
+
</span>
|
|
64
68
|
</Link>
|
|
65
69
|
</Button>
|
|
66
70
|
<div className="flex w-9 justify-end sm:w-24">
|
|
@@ -78,13 +82,15 @@ export default function Header() {
|
|
|
78
82
|
}}
|
|
79
83
|
>
|
|
80
84
|
<LogOut className="h-4 w-4" />
|
|
81
|
-
<span className="hidden sm:inline">
|
|
85
|
+
<span className="hidden sm:inline">
|
|
86
|
+
<fbt desc="Logout button">Logout</fbt>
|
|
87
|
+
</span>
|
|
82
88
|
</Stack>
|
|
83
89
|
</Button>
|
|
84
90
|
) : (
|
|
85
91
|
<Button asChild className="w-full" size="sm" variant="ghost">
|
|
86
92
|
<Link href="/login">
|
|
87
|
-
<LogIn className="h-4 w-4" /> Login
|
|
93
|
+
<LogIn className="h-4 w-4" /> <fbt desc="Login button">Login</fbt>{' '}
|
|
88
94
|
</Link>
|
|
89
95
|
</Button>
|
|
90
96
|
)}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useLocaleContext } from 'fbtee';
|
|
2
|
+
import { startTransition } from 'react';
|
|
3
|
+
import AvailableLanguages from '../lib/AvailableLanguages.tsx';
|
|
4
|
+
|
|
5
|
+
export default function LocaleSwitcher() {
|
|
6
|
+
const { locale, localeChangeIsPending, setLocale } = useLocaleContext();
|
|
7
|
+
return (
|
|
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}
|
|
25
|
+
>
|
|
26
|
+
{[...AvailableLanguages].map(([value, name]) => (
|
|
27
|
+
<option key={value} value={value}>
|
|
28
|
+
{name}
|
|
29
|
+
</option>
|
|
30
|
+
))}
|
|
31
|
+
</select>
|
|
32
|
+
</label>
|
|
33
|
+
);
|
|
34
|
+
}
|