create-fate 1.4.0 → 1.5.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.
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
@@ -0,0 +1,5 @@
1
+ export default new Map([
2
+ ['en-US', 'English'],
3
+ ['de-DE', 'Deutsch (German)'],
4
+ ['ja-JP', '日本語 (Japanese)'],
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
+ }
@@ -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
+ }
@@ -0,0 +1,225 @@
1
+ {
2
+ "fb-locale": "de-DE",
3
+ "translations": {
4
+ "IYUmtYxQtGdt1MucjXrsVA==": {
5
+ "description": "Label with meaning 'at least number'",
6
+ "tokens": [],
7
+ "translations": [
8
+ {
9
+ "translation": "{number}+",
10
+ "variations": {}
11
+ }
12
+ ],
13
+ "types": []
14
+ },
15
+ "ZIF1WhVQwt7WZJvFTEbMWw==": {
16
+ "description": "Label with meaning 'less than number'",
17
+ "tokens": [],
18
+ "translations": [
19
+ {
20
+ "translation": "<{number}",
21
+ "variations": {}
22
+ }
23
+ ],
24
+ "types": []
25
+ },
26
+ "Oc/9PrbjfyLHS60BXceD1A==": {
27
+ "description": "A list of items of various types, for example: \"San Francisco; London; Tokyo\". {previous items} and {following items} are themselves lists that contain one or more items.",
28
+ "tokens": [],
29
+ "translations": [
30
+ {
31
+ "translation": "{previous items}; {following items}",
32
+ "variations": {}
33
+ }
34
+ ],
35
+ "types": []
36
+ },
37
+ "AXXbKEUZuc0ji1IClRrOOw==": {
38
+ "description": "A list of items of various types separated by bullets, for example: \"San Francisco \\u2022 London \\u2022 Tokyo\". {previous items} and {following items} are themselves lists that contain one or more items.",
39
+ "tokens": [],
40
+ "translations": [
41
+ {
42
+ "translation": "{previous items} • {following items}",
43
+ "variations": {}
44
+ }
45
+ ],
46
+ "types": []
47
+ },
48
+ "89ACgPMtnwlLDSeqhLBx2Q==": {
49
+ "description": "A list of items of various types separated by commas, for example: \"San Francisco, London, Tokyo\". {previous items} and {following items} are themselves lists that contain one or more items.",
50
+ "tokens": [],
51
+ "translations": [
52
+ {
53
+ "translation": "{previous items}, {following items}",
54
+ "variations": {}
55
+ }
56
+ ],
57
+ "types": []
58
+ },
59
+ "5hL7VXOBVnetm46YAjpX0g==": {
60
+ "description": "A list of items of various types with a serial comma, for example: \"item1, item2, and item3\"",
61
+ "tokens": [],
62
+ "translations": [
63
+ {
64
+ "translation": "{list of items} und {last item}",
65
+ "variations": {}
66
+ }
67
+ ],
68
+ "types": []
69
+ },
70
+ "icNetEL/crQv/PS6yhySVQ==": {
71
+ "description": "A list of items of various types, for example: \"item1, item2, item3 and item4\"",
72
+ "tokens": [],
73
+ "translations": [
74
+ {
75
+ "translation": "{list of items} und {last item}",
76
+ "variations": {}
77
+ }
78
+ ],
79
+ "types": []
80
+ },
81
+ "8kLAQ3ffiiFnQ/bWPqXfuQ==": {
82
+ "description": "A list of items of various types with a serial comma, for example: \"item1, item2, or item3\"",
83
+ "tokens": [],
84
+ "translations": [
85
+ {
86
+ "translation": "{list of items} oder {last item}",
87
+ "variations": {}
88
+ }
89
+ ],
90
+ "types": []
91
+ },
92
+ "8eSaAzrWtAqn9ktNKfkaEg==": {
93
+ "description": "A list of items of various types, for example: \"item1, item2, item3 or item4\"",
94
+ "tokens": [],
95
+ "translations": [
96
+ {
97
+ "translation": "{list of items} oder {last item}",
98
+ "variations": {}
99
+ }
100
+ ],
101
+ "types": []
102
+ },
103
+ "+IBI7kr/mMKdzp+3stDvkw==": {
104
+ "description": "A list of items of various types, for example: \"San Francisco; London; Tokyo\". {previous items} itself contains one or more items.",
105
+ "tokens": [],
106
+ "translations": [
107
+ {
108
+ "translation": "{previous items}; {last item}",
109
+ "variations": {}
110
+ }
111
+ ],
112
+ "types": []
113
+ },
114
+ "55IeE8c0qqtjTU0lo7q9AA==": {
115
+ "description": "A list of items of various types separated by bullets, for example: \"San Francisco \\u2022 London \\u2022 Tokyo\". {previous items} contains one or more items.",
116
+ "tokens": [],
117
+ "translations": [
118
+ {
119
+ "translation": "{list of items} • {last item}",
120
+ "variations": {}
121
+ }
122
+ ],
123
+ "types": []
124
+ },
125
+ "6PRJS0QalZfvaN8CkKCUCw==": {
126
+ "description": "A list of items of various types, for example: \"item1, item2, item3, item4\"",
127
+ "tokens": [],
128
+ "translations": [
129
+ {
130
+ "translation": "{list of items}, {last item}",
131
+ "variations": {}
132
+ }
133
+ ],
134
+ "types": []
135
+ },
136
+ "6/ZbJcEx19aXKls64qknkg==": {
137
+ "description": "Locale switcher button",
138
+ "tokens": [],
139
+ "translations": [
140
+ {
141
+ "translation": "Sprache ändern",
142
+ "variations": {}
143
+ }
144
+ ],
145
+ "types": []
146
+ },
147
+ "wfNswo3mwqgq+8bQUCJAPA==": {
148
+ "description": "index: Viewer",
149
+ "tokens": [],
150
+ "translations": [
151
+ {
152
+ "translation": "Angemeldete Person",
153
+ "variations": {}
154
+ }
155
+ ],
156
+ "types": []
157
+ },
158
+ "mMgxQsJPII8/aJtwXOPsNw==": {
159
+ "description": "index: Posts",
160
+ "tokens": [],
161
+ "translations": [
162
+ {
163
+ "translation": "Beiträge",
164
+ "variations": {}
165
+ }
166
+ ],
167
+ "types": []
168
+ },
169
+ "LyADov8X2C8AsE0BOJ73Mg==": {
170
+ "description": "index: Use fate with an existing GraphQL server",
171
+ "tokens": [],
172
+ "translations": [
173
+ {
174
+ "translation": "fate mit einem bestehenden GraphQL-Server verwenden",
175
+ "variations": {}
176
+ }
177
+ ],
178
+ "types": []
179
+ },
180
+ "wlLz2xe9I3pIUzeg46KuVw==": {
181
+ "description": "layout: Loading...",
182
+ "tokens": [],
183
+ "translations": [
184
+ {
185
+ "translation": "Lädt…",
186
+ "variations": {}
187
+ }
188
+ ],
189
+ "types": []
190
+ },
191
+ "4QjroIHQHerlYYiN65OAqQ==": {
192
+ "description": "GraphQL client setup instructions",
193
+ "tokens": [],
194
+ "translations": [
195
+ {
196
+ "translation": "Ersetze die Beispiel-Views in {=src/fate/graphql.ts} durch deine Schematypen und setze {=VITE_GRAPHQL_URL} auf deine API.",
197
+ "variations": {}
198
+ }
199
+ ],
200
+ "types": []
201
+ },
202
+ "wDeH760jWvob7b0UD/ldjw==": {
203
+ "description": "In the phrase: \"Replace the sample views in {=src/fate/graphql.ts} with your schema types and point {=VITE_GRAPHQL_URL} at your API.\"",
204
+ "tokens": [],
205
+ "translations": [
206
+ {
207
+ "translation": "src/fate/graphql.ts",
208
+ "variations": {}
209
+ }
210
+ ],
211
+ "types": []
212
+ },
213
+ "T4saJreAWC68v2UUSToqoA==": {
214
+ "description": "In the phrase: \"Replace the sample views in {=src/fate/graphql.ts} with your schema types and point {=VITE_GRAPHQL_URL} at your API.\"",
215
+ "tokens": [],
216
+ "translations": [
217
+ {
218
+ "translation": "VITE_GRAPHQL_URL",
219
+ "variations": {}
220
+ }
221
+ ],
222
+ "types": []
223
+ }
224
+ }
225
+ }
@@ -0,0 +1,225 @@
1
+ {
2
+ "fb-locale": "ja-JP",
3
+ "translations": {
4
+ "IYUmtYxQtGdt1MucjXrsVA==": {
5
+ "description": "Label with meaning 'at least number'",
6
+ "tokens": [],
7
+ "translations": [
8
+ {
9
+ "translation": "{number}+",
10
+ "variations": {}
11
+ }
12
+ ],
13
+ "types": []
14
+ },
15
+ "ZIF1WhVQwt7WZJvFTEbMWw==": {
16
+ "description": "Label with meaning 'less than number'",
17
+ "tokens": [],
18
+ "translations": [
19
+ {
20
+ "translation": "<{number}",
21
+ "variations": {}
22
+ }
23
+ ],
24
+ "types": []
25
+ },
26
+ "Oc/9PrbjfyLHS60BXceD1A==": {
27
+ "description": "A list of items of various types, for example: \"San Francisco; London; Tokyo\". {previous items} and {following items} are themselves lists that contain one or more items.",
28
+ "tokens": [],
29
+ "translations": [
30
+ {
31
+ "translation": "{previous items}; {following items}",
32
+ "variations": {}
33
+ }
34
+ ],
35
+ "types": []
36
+ },
37
+ "AXXbKEUZuc0ji1IClRrOOw==": {
38
+ "description": "A list of items of various types separated by bullets, for example: \"San Francisco \\u2022 London \\u2022 Tokyo\". {previous items} and {following items} are themselves lists that contain one or more items.",
39
+ "tokens": [],
40
+ "translations": [
41
+ {
42
+ "translation": "{previous items} • {following items}",
43
+ "variations": {}
44
+ }
45
+ ],
46
+ "types": []
47
+ },
48
+ "89ACgPMtnwlLDSeqhLBx2Q==": {
49
+ "description": "A list of items of various types separated by commas, for example: \"San Francisco, London, Tokyo\". {previous items} and {following items} are themselves lists that contain one or more items.",
50
+ "tokens": [],
51
+ "translations": [
52
+ {
53
+ "translation": "{previous items}, {following items}",
54
+ "variations": {}
55
+ }
56
+ ],
57
+ "types": []
58
+ },
59
+ "5hL7VXOBVnetm46YAjpX0g==": {
60
+ "description": "A list of items of various types with a serial comma, for example: \"item1, item2, and item3\"",
61
+ "tokens": [],
62
+ "translations": [
63
+ {
64
+ "translation": "{list of items}、そして {last item}",
65
+ "variations": {}
66
+ }
67
+ ],
68
+ "types": []
69
+ },
70
+ "icNetEL/crQv/PS6yhySVQ==": {
71
+ "description": "A list of items of various types, for example: \"item1, item2, item3 and item4\"",
72
+ "tokens": [],
73
+ "translations": [
74
+ {
75
+ "translation": "{list of items} と {last item}",
76
+ "variations": {}
77
+ }
78
+ ],
79
+ "types": []
80
+ },
81
+ "8kLAQ3ffiiFnQ/bWPqXfuQ==": {
82
+ "description": "A list of items of various types with a serial comma, for example: \"item1, item2, or item3\"",
83
+ "tokens": [],
84
+ "translations": [
85
+ {
86
+ "translation": "{list of items}、または {last item}",
87
+ "variations": {}
88
+ }
89
+ ],
90
+ "types": []
91
+ },
92
+ "8eSaAzrWtAqn9ktNKfkaEg==": {
93
+ "description": "A list of items of various types, for example: \"item1, item2, item3 or item4\"",
94
+ "tokens": [],
95
+ "translations": [
96
+ {
97
+ "translation": "{list of items} または {last item}",
98
+ "variations": {}
99
+ }
100
+ ],
101
+ "types": []
102
+ },
103
+ "+IBI7kr/mMKdzp+3stDvkw==": {
104
+ "description": "A list of items of various types, for example: \"San Francisco; London; Tokyo\". {previous items} itself contains one or more items.",
105
+ "tokens": [],
106
+ "translations": [
107
+ {
108
+ "translation": "{previous items}; {last item}",
109
+ "variations": {}
110
+ }
111
+ ],
112
+ "types": []
113
+ },
114
+ "55IeE8c0qqtjTU0lo7q9AA==": {
115
+ "description": "A list of items of various types separated by bullets, for example: \"San Francisco \\u2022 London \\u2022 Tokyo\". {previous items} contains one or more items.",
116
+ "tokens": [],
117
+ "translations": [
118
+ {
119
+ "translation": "{list of items} • {last item}",
120
+ "variations": {}
121
+ }
122
+ ],
123
+ "types": []
124
+ },
125
+ "6PRJS0QalZfvaN8CkKCUCw==": {
126
+ "description": "A list of items of various types, for example: \"item1, item2, item3, item4\"",
127
+ "tokens": [],
128
+ "translations": [
129
+ {
130
+ "translation": "{list of items}、{last item}",
131
+ "variations": {}
132
+ }
133
+ ],
134
+ "types": []
135
+ },
136
+ "6/ZbJcEx19aXKls64qknkg==": {
137
+ "description": "Locale switcher button",
138
+ "tokens": [],
139
+ "translations": [
140
+ {
141
+ "translation": "言語を変更",
142
+ "variations": {}
143
+ }
144
+ ],
145
+ "types": []
146
+ },
147
+ "wfNswo3mwqgq+8bQUCJAPA==": {
148
+ "description": "index: Viewer",
149
+ "tokens": [],
150
+ "translations": [
151
+ {
152
+ "translation": "ログインユーザー",
153
+ "variations": {}
154
+ }
155
+ ],
156
+ "types": []
157
+ },
158
+ "mMgxQsJPII8/aJtwXOPsNw==": {
159
+ "description": "index: Posts",
160
+ "tokens": [],
161
+ "translations": [
162
+ {
163
+ "translation": "投稿",
164
+ "variations": {}
165
+ }
166
+ ],
167
+ "types": []
168
+ },
169
+ "LyADov8X2C8AsE0BOJ73Mg==": {
170
+ "description": "index: Use fate with an existing GraphQL server",
171
+ "tokens": [],
172
+ "translations": [
173
+ {
174
+ "translation": "既存の GraphQL サーバーで fate を使う",
175
+ "variations": {}
176
+ }
177
+ ],
178
+ "types": []
179
+ },
180
+ "wlLz2xe9I3pIUzeg46KuVw==": {
181
+ "description": "layout: Loading...",
182
+ "tokens": [],
183
+ "translations": [
184
+ {
185
+ "translation": "読み込み中…",
186
+ "variations": {}
187
+ }
188
+ ],
189
+ "types": []
190
+ },
191
+ "4QjroIHQHerlYYiN65OAqQ==": {
192
+ "description": "GraphQL client setup instructions",
193
+ "tokens": [],
194
+ "translations": [
195
+ {
196
+ "translation": "{=src/fate/graphql.ts} のサンプルビューを自分のスキーマ型に置き換え、{=VITE_GRAPHQL_URL} を自分の API に設定してください。",
197
+ "variations": {}
198
+ }
199
+ ],
200
+ "types": []
201
+ },
202
+ "wDeH760jWvob7b0UD/ldjw==": {
203
+ "description": "In the phrase: \"Replace the sample views in {=src/fate/graphql.ts} with your schema types and point {=VITE_GRAPHQL_URL} at your API.\"",
204
+ "tokens": [],
205
+ "translations": [
206
+ {
207
+ "translation": "src/fate/graphql.ts",
208
+ "variations": {}
209
+ }
210
+ ],
211
+ "types": []
212
+ },
213
+ "T4saJreAWC68v2UUSToqoA==": {
214
+ "description": "In the phrase: \"Replace the sample views in {=src/fate/graphql.ts} with your schema types and point {=VITE_GRAPHQL_URL} at your API.\"",
215
+ "tokens": [],
216
+ "translations": [
217
+ {
218
+ "translation": "VITE_GRAPHQL_URL",
219
+ "variations": {}
220
+ }
221
+ ],
222
+ "types": []
223
+ }
224
+ }
225
+ }
@@ -1,4 +1,5 @@
1
1
  import nkzw from '@nkzw/oxlint-config';
2
+ import fbtee from '@nkzw/vite-plugin-fbtee';
2
3
  import tailwindcss from '@tailwindcss/vite';
3
4
  import react from '@vitejs/plugin-react';
4
5
  import { voidReact } from '@void/react/plugin';
@@ -60,6 +61,7 @@ export default defineConfig({
60
61
  },
61
62
  plugins: [
62
63
  ...(lazyPlugins(() => [
64
+ fbtee(),
63
65
  tailwindcss(),
64
66
  ...(isTest
65
67
  ? [react({ compiler: true })]
@@ -104,3 +104,14 @@ Common commands from the project root:
104
104
  - `vp check --fix` formats, lints, and type-checks the workspace.
105
105
  - `vp test` runs the test suite.
106
106
  - `vp run build` builds the client and server.
107
+
108
+ ## Translations
109
+
110
+ The React app uses fbtee 4 with its native Oxc Vite plugin and CLI. Setup and builds generate the runtime translation files automatically.
111
+
112
+ - Wrap UI text in `<fbt desc="Translator context">Text</fbt>`; use `fbs('Text', 'Translator context')` from `fbtee` for string attributes such as placeholders.
113
+ - 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/`.
114
+ - 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.
115
+ - 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.
116
+
117
+ 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,11 +9,15 @@
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:*",
@@ -26,7 +30,7 @@
26
30
  "better-auth": "1.7.2",
27
31
  "class-variance-authority": "^0.7.1",
28
32
  "clsx": "^2.1.1",
29
- "fbtee": "^3.1.0",
33
+ "fbtee": "^4.0.0",
30
34
  "lucide-react": "^1.34.0",
31
35
  "react": "^19.2.8",
32
36
  "react-dom": "^19.2.8",
@@ -36,8 +40,8 @@
36
40
  "void": "^0.10.12"
37
41
  },
38
42
  "devDependencies": {
39
- "@nkzw/fbtee-cli": "^3.1.0",
40
- "@nkzw/vite-plugin-fbtee": "^3.1.0",
43
+ "@nkzw/fbtee-cli": "^4.0.0",
44
+ "@nkzw/vite-plugin-fbtee": "^4.0.0",
41
45
  "@tailwindcss/vite": "^4.3.3",
42
46
  "@types/node": "^26.4.0",
43
47
  "@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
  ]);