create-mikstack 0.1.22 → 0.1.24

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/dist/index.js CHANGED
@@ -145,11 +145,13 @@ async function runPrompts(projectName, packageManager) {
145
145
 
146
146
  //#endregion
147
147
  //#region src/template-engine.ts
148
- const CONDITIONAL_BLOCK_RE = /^[^\S\n]*(?:\/\/|#|<!--)[^\S\n]*\{\{#if:(\w+)\}\}[^\S\n]*(?:-->)?[^\S\n]*\n([\s\S]*?)^[^\S\n]*(?:\/\/|#|<!--)[^\S\n]*\{\{\/if:\1\}\}[^\S\n]*(?:-->)?[^\S\n]*\n?/gm;
148
+ const CONDITIONAL_BLOCK_RE = /^[^\S\n]*(?:\/\/|#|<!--)[^\S\n]*\{\{#if:(!?\w+)\}\}[^\S\n]*(?:-->)?[^\S\n]*\n([\s\S]*?)^[^\S\n]*(?:\/\/|#|<!--)[^\S\n]*\{\{\/if:\1\}\}[^\S\n]*(?:-->)?[^\S\n]*\n?/gm;
149
149
  const VARIABLE_RE = /\{\{(\w+)\}\}/g;
150
150
  function renderTemplate(content, context) {
151
151
  let result = content.replace(CONDITIONAL_BLOCK_RE, (_match, name, body) => {
152
- if (context[name]) return body;
152
+ const negated = name.startsWith("!");
153
+ const value = context[negated ? name.slice(1) : name];
154
+ if (negated ? !value : value) return body;
153
155
  return "";
154
156
  });
155
157
  result = result.replace(VARIABLE_RE, (_match, name) => {
@@ -368,10 +370,13 @@ async function cli() {
368
370
  "pipe"
369
371
  ]
370
372
  });
373
+ const cols = process.stdout.columns || 80;
371
374
  const handleData = (data) => {
372
375
  if (!onLine) return;
373
376
  const line = data.toString().trim().split("\n").pop();
374
- if (line) onLine(line);
377
+ if (!line) return;
378
+ const maxLen = cols - 5;
379
+ onLine(line.length > maxLen ? line.slice(0, maxLen - 1) + "…" : line);
375
380
  };
376
381
  child.stdout.on("data", handleData);
377
382
  child.stderr.on("data", handleData);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mikstack",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,6 +10,26 @@ services:
10
10
  volumes:
11
11
  - pgdata:/var/lib/postgresql/data
12
12
 
13
+ zero-cache:
14
+ image: rocicorp/zero:${ZERO_VERSION}
15
+ restart: always
16
+ ports:
17
+ - 4848:4848
18
+ environment:
19
+ ZERO_UPSTREAM_DB: postgres://root:${POSTGRES_PASSWORD}@db:5432/{{projectName}}
20
+ ZERO_CVR_DB: postgres://root:${POSTGRES_PASSWORD}@db:5432/{{projectName}}_cvr
21
+ ZERO_CHANGE_DB: postgres://root:${POSTGRES_PASSWORD}@db:5432/{{projectName}}_cdb
22
+ ZERO_REPLICA_FILE: /data/sync-replica.db
23
+ ZERO_PUSH_URL: http://app:3000/api/zero/mutate
24
+ ZERO_QUERY_URL: http://app:3000/api/zero/get-queries
25
+ ZERO_QUERY_FORWARD_COOKIES: "true"
26
+ ZERO_MUTATE_FORWARD_COOKIES: "true"
27
+ ZERO_AUTH_SECRET: ${BETTER_AUTH_SECRET}
28
+ volumes:
29
+ - zero-data:/data
30
+ depends_on:
31
+ - db
32
+
13
33
  app:
14
34
  build: .
15
35
  restart: always
@@ -20,8 +40,11 @@ services:
20
40
  BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
21
41
  BETTER_AUTH_URL: ${BETTER_AUTH_URL}
22
42
  ORIGIN: ${BETTER_AUTH_URL}
43
+ PUBLIC_SERVER: http://zero-cache:4848
23
44
  depends_on:
24
45
  - db
46
+ - zero-cache
25
47
 
26
48
  volumes:
27
49
  pgdata:
50
+ zero-data:
@@ -2,114 +2,114 @@ import { boolean, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/p
2
2
 
3
3
  // better-auth tables (managed by better-auth, do not insert/update directly)
4
4
  export const user = pgTable("user", {
5
- id: text("id").primaryKey(),
6
- name: text("name").notNull(),
7
- email: text("email").notNull().unique(),
8
- emailVerified: boolean("email_verified").notNull().default(false),
9
- image: text("image"),
10
- createdAt: timestamp("created_at").notNull().defaultNow(),
11
- updatedAt: timestamp("updated_at").notNull().defaultNow(),
5
+ id: text().primaryKey(),
6
+ name: text().notNull(),
7
+ email: text().notNull().unique(),
8
+ emailVerified: boolean().notNull().default(false),
9
+ image: text(),
10
+ createdAt: timestamp().notNull().defaultNow(),
11
+ updatedAt: timestamp().notNull().defaultNow(),
12
12
  });
13
13
 
14
14
  export const session = pgTable("session", {
15
- id: text("id").primaryKey(),
16
- expiresAt: timestamp("expires_at").notNull(),
17
- token: text("token").notNull().unique(),
18
- ipAddress: text("ip_address"),
19
- userAgent: text("user_agent"),
20
- userId: text("user_id")
15
+ id: text().primaryKey(),
16
+ expiresAt: timestamp().notNull(),
17
+ token: text().notNull().unique(),
18
+ ipAddress: text(),
19
+ userAgent: text(),
20
+ userId: text()
21
21
  .notNull()
22
22
  .references(() => user.id),
23
- createdAt: timestamp("created_at").notNull().defaultNow(),
24
- updatedAt: timestamp("updated_at").notNull().defaultNow(),
23
+ createdAt: timestamp().notNull().defaultNow(),
24
+ updatedAt: timestamp().notNull().defaultNow(),
25
25
  });
26
26
 
27
27
  export const account = pgTable("account", {
28
- id: text("id").primaryKey(),
29
- accountId: text("account_id").notNull(),
30
- providerId: text("provider_id").notNull(),
31
- userId: text("user_id")
28
+ id: text().primaryKey(),
29
+ accountId: text().notNull(),
30
+ providerId: text().notNull(),
31
+ userId: text()
32
32
  .notNull()
33
33
  .references(() => user.id),
34
- accessToken: text("access_token"),
35
- refreshToken: text("refresh_token"),
36
- idToken: text("id_token"),
37
- accessTokenExpiresAt: timestamp("access_token_expires_at"),
38
- refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
39
- scope: text("scope"),
40
- password: text("password"),
41
- createdAt: timestamp("created_at").notNull().defaultNow(),
42
- updatedAt: timestamp("updated_at").notNull().defaultNow(),
34
+ accessToken: text(),
35
+ refreshToken: text(),
36
+ idToken: text(),
37
+ accessTokenExpiresAt: timestamp(),
38
+ refreshTokenExpiresAt: timestamp(),
39
+ scope: text(),
40
+ password: text(),
41
+ createdAt: timestamp().notNull().defaultNow(),
42
+ updatedAt: timestamp().notNull().defaultNow(),
43
43
  });
44
44
 
45
45
  export const verification = pgTable("verification", {
46
- id: text("id").primaryKey(),
47
- identifier: text("identifier").notNull(),
48
- value: text("value").notNull(),
49
- expiresAt: timestamp("expires_at").notNull(),
50
- createdAt: timestamp("created_at").notNull().defaultNow(),
51
- updatedAt: timestamp("updated_at").notNull().defaultNow(),
46
+ id: text().primaryKey(),
47
+ identifier: text().notNull(),
48
+ value: text().notNull(),
49
+ expiresAt: timestamp().notNull(),
50
+ createdAt: timestamp().notNull().defaultNow(),
51
+ updatedAt: timestamp().notNull().defaultNow(),
52
52
  });
53
53
 
54
54
  // Notification tables (managed by @mikstack/notifications)
55
55
  export const notificationDelivery = pgTable("notification_delivery", {
56
- id: text("id")
56
+ id: text()
57
57
  .primaryKey()
58
58
  .$defaultFn(() => crypto.randomUUID()),
59
- userId: text("user_id").references(() => user.id),
60
- type: text("type").notNull(),
61
- channel: text("channel").notNull(),
62
- status: text("status", { enum: ["pending", "sent", "delivered", "failed"] })
59
+ userId: text().references(() => user.id),
60
+ type: text().notNull(),
61
+ channel: text().notNull(),
62
+ status: text({ enum: ["pending", "sent", "delivered", "failed"] })
63
63
  .notNull()
64
64
  .default("pending"),
65
- content: jsonb("content"),
66
- error: text("error"),
67
- retryOf: text("retry_of"),
68
- retriesLeft: integer("retries_left").notNull().default(0),
69
- recipientEmail: text("recipient_email"),
70
- externalId: text("external_id"),
71
- createdAt: timestamp("created_at").notNull().defaultNow(),
72
- updatedAt: timestamp("updated_at").notNull().defaultNow(),
65
+ content: jsonb(),
66
+ error: text(),
67
+ retryOf: text(),
68
+ retriesLeft: integer().notNull().default(0),
69
+ recipientEmail: text(),
70
+ externalId: text(),
71
+ createdAt: timestamp().notNull().defaultNow(),
72
+ updatedAt: timestamp().notNull().defaultNow(),
73
73
  });
74
74
 
75
75
  export const inAppNotification = pgTable("in_app_notification", {
76
- id: text("id")
76
+ id: text()
77
77
  .primaryKey()
78
78
  .$defaultFn(() => crypto.randomUUID()),
79
- userId: text("user_id")
79
+ userId: text()
80
80
  .notNull()
81
81
  .references(() => user.id),
82
- type: text("type").notNull(),
83
- title: text("title").notNull(),
84
- body: text("body"),
85
- url: text("url"),
86
- icon: text("icon"),
87
- read: boolean("read").notNull().default(false),
88
- createdAt: timestamp("created_at").notNull().defaultNow(),
82
+ type: text().notNull(),
83
+ title: text().notNull(),
84
+ body: text(),
85
+ url: text(),
86
+ icon: text(),
87
+ read: boolean().notNull().default(false),
88
+ createdAt: timestamp().notNull().defaultNow(),
89
89
  });
90
90
 
91
91
  export const notificationPreference = pgTable("notification_preference", {
92
- id: text("id")
92
+ id: text()
93
93
  .primaryKey()
94
94
  .$defaultFn(() => crypto.randomUUID()),
95
- userId: text("user_id")
95
+ userId: text()
96
96
  .notNull()
97
97
  .references(() => user.id),
98
- notificationType: text("notification_type").notNull(),
99
- channel: text("channel").notNull(),
100
- enabled: boolean("enabled").notNull(),
101
- updatedAt: timestamp("updated_at").notNull().defaultNow(),
98
+ notificationType: text().notNull(),
99
+ channel: text().notNull(),
100
+ enabled: boolean().notNull(),
101
+ updatedAt: timestamp().notNull().defaultNow(),
102
102
  });
103
103
 
104
104
  // Application tables
105
105
 
106
106
  export const note = pgTable("note", {
107
- id: text("id").primaryKey(),
108
- title: text("title").notNull(),
109
- content: text("content").notNull().default(""),
110
- userId: text("user_id")
107
+ id: text().primaryKey(),
108
+ title: text().notNull(),
109
+ content: text().notNull().default(""),
110
+ userId: text()
111
111
  .notNull()
112
112
  .references(() => user.id),
113
- createdAt: timestamp("created_at").notNull(),
114
- updatedAt: timestamp("updated_at").notNull(),
113
+ createdAt: timestamp().notNull(),
114
+ updatedAt: timestamp().notNull(),
115
115
  });
@@ -1,10 +1,10 @@
1
1
  <script lang="ts">
2
- import NotePencil from "phosphor-svelte/lib/NotePencil";
3
- import PencilSimple from "phosphor-svelte/lib/PencilSimple";
4
- import Plus from "phosphor-svelte/lib/Plus";
5
- import SignOut from "phosphor-svelte/lib/SignOut";
6
- import Trash from "phosphor-svelte/lib/Trash";
7
- import X from "phosphor-svelte/lib/X";
2
+ import NotePencilIcon from "phosphor-svelte/lib/NotePencilIcon";
3
+ import PencilSimpleIcon from "phosphor-svelte/lib/PencilSimpleIcon";
4
+ import PlusIcon from "phosphor-svelte/lib/PlusIcon";
5
+ import SignOutIcon from "phosphor-svelte/lib/SignOutIcon";
6
+ import TrashIcon from "phosphor-svelte/lib/TrashIcon";
7
+ import XIcon from "phosphor-svelte/lib/XIcon";
8
8
  import Button from "{{uiPrefix}}/Button";
9
9
  import FormField from "{{uiPrefix}}/FormField";
10
10
  import Input from "{{uiPrefix}}/Input";
@@ -18,6 +18,11 @@
18
18
  import { get_z } from "$lib/z.svelte";
19
19
  import { queries } from "$lib/zero/queries";
20
20
  import { mutators } from "$lib/zero/mutators";
21
+ // {{#if:i18n}}
22
+ import { useLingui } from "@mikstack/svelte-lingui";
23
+ import LocaleSwitcher from "$lib/LocaleSwitcher.svelte";
24
+ const { t } = useLingui();
25
+ // {{/if:i18n}}
21
26
 
22
27
  let { data } = $props();
23
28
 
@@ -39,7 +44,12 @@
39
44
  let editingId = $state<string | null>(null);
40
45
 
41
46
  const noteSchema = v.object({
47
+ // {{#if:i18n}}
48
+ title: v.pipe(v.string(), v.minLength(1, t`Title is required`)),
49
+ // {{/if:i18n}}
50
+ // {{#if:!i18n}}
42
51
  title: v.pipe(v.string(), v.minLength(1, "Title is required")),
52
+ // {{/if:!i18n}}
43
53
  content: v.string(),
44
54
  });
45
55
 
@@ -103,13 +113,26 @@
103
113
  <div class="container">
104
114
  <header class="header">
105
115
  <div class="header-title">
106
- <NotePencil size={24} weight="duotone" />
116
+ <NotePencilIcon size={24} weight="duotone" />
117
+ <!-- {{#if:i18n}} -->
118
+ <h1>{t`Notes`}</h1>
119
+ <!-- {{/if:i18n}} -->
120
+ <!-- {{#if:!i18n}} -->
107
121
  <h1>Notes</h1>
122
+ <!-- {{/if:!i18n}} -->
108
123
  </div>
109
124
  <div class="header-actions">
125
+ <!-- {{#if:i18n}} -->
126
+ <LocaleSwitcher />
127
+ <!-- {{/if:i18n}} -->
110
128
  <span class="email">{data.user.email}</span>
111
129
  <Button variant="ghost" onclick={signOut}>
112
- <SignOut size={16} weight="bold" /> Sign out
130
+ <!-- {{#if:i18n}} -->
131
+ <SignOutIcon size={16} weight="bold" /> {t`Sign out`}
132
+ <!-- {{/if:i18n}} -->
133
+ <!-- {{#if:!i18n}} -->
134
+ <SignOutIcon size={16} weight="bold" /> Sign out
135
+ <!-- {{/if:!i18n}} -->
113
136
  </Button>
114
137
  </div>
115
138
  </header>
@@ -117,13 +140,28 @@
117
140
  <Separator />
118
141
 
119
142
  <section>
143
+ <!-- {{#if:i18n}} -->
144
+ <h2>{t`New note`}</h2>
145
+ <!-- {{/if:i18n}} -->
146
+ <!-- {{#if:!i18n}} -->
120
147
  <h2>New note</h2>
148
+ <!-- {{/if:!i18n}} -->
121
149
  <form id={createNoteForm.id} onsubmit={createNoteForm.onsubmit} onkeydown={submitOnModEnter} class="note-form">
122
150
  <FormField for={createNoteForm.fields.title.as("text").id}>
123
151
  {#snippet label(attrs)}
152
+ <!-- {{#if:i18n}} -->
153
+ <label {...attrs}>{t`Title`}</label>
154
+ <!-- {{/if:i18n}} -->
155
+ <!-- {{#if:!i18n}} -->
124
156
  <label {...attrs}>Title</label>
157
+ <!-- {{/if:!i18n}} -->
125
158
  {/snippet}
159
+ <!-- {{#if:i18n}} -->
160
+ <Input {...createNoteForm.fields.title.as("text")} placeholder={t`Note title`} />
161
+ <!-- {{/if:i18n}} -->
162
+ <!-- {{#if:!i18n}} -->
126
163
  <Input {...createNoteForm.fields.title.as("text")} placeholder="Note title" />
164
+ <!-- {{/if:!i18n}} -->
127
165
  {#snippet error(attrs)}
128
166
  {#each createNoteForm.fields.title.issues() as issue (issue.message)}
129
167
  <p {...attrs}>{issue.message}</p>
@@ -133,8 +171,24 @@
133
171
 
134
172
  <FormField for={`${createNoteForm.id}-content`}>
135
173
  {#snippet label(attrs)}
174
+ <!-- {{#if:i18n}} -->
175
+ <label {...attrs}>{t`Content`}</label>
176
+ <!-- {{/if:i18n}} -->
177
+ <!-- {{#if:!i18n}} -->
136
178
  <label {...attrs}>Content</label>
179
+ <!-- {{/if:!i18n}} -->
137
180
  {/snippet}
181
+ <!-- {{#if:i18n}} -->
182
+ <Textarea
183
+ name={createNoteForm.fields.content.name()}
184
+ id="{createNoteForm.id}-content"
185
+ oninput={(e) =>
186
+ createNoteForm.fields.content.set((e.target as HTMLTextAreaElement).value)}
187
+ value={createNoteForm.fields.content.value() as string}
188
+ placeholder={t`Write something...`}
189
+ />
190
+ <!-- {{/if:i18n}} -->
191
+ <!-- {{#if:!i18n}} -->
138
192
  <Textarea
139
193
  name={createNoteForm.fields.content.name()}
140
194
  id="{createNoteForm.id}-content"
@@ -143,6 +197,7 @@
143
197
  value={createNoteForm.fields.content.value() as string}
144
198
  placeholder="Write something..."
145
199
  />
200
+ <!-- {{/if:!i18n}} -->
146
201
  </FormField>
147
202
 
148
203
  {#if createNoteForm.error}
@@ -150,8 +205,13 @@
150
205
  {/if}
151
206
 
152
207
  <Button type="submit" disabled={createNoteForm.pending}>
153
- <Plus size={16} weight="bold" />
208
+ <PlusIcon size={16} weight="bold" />
209
+ <!-- {{#if:i18n}} -->
210
+ {createNoteForm.pending ? t`Creating...` : t`Create note`}
211
+ <!-- {{/if:i18n}} -->
212
+ <!-- {{#if:!i18n}} -->
154
213
  {createNoteForm.pending ? "Creating..." : "Create note"}
214
+ <!-- {{/if:!i18n}} -->
155
215
  {#if !isMobile}<kbd>{modLabel}+Enter</kbd>{/if}
156
216
  </Button>
157
217
  </form>
@@ -160,9 +220,19 @@
160
220
  <Separator />
161
221
 
162
222
  <section>
223
+ <!-- {{#if:i18n}} -->
224
+ <h2>{t`Your notes`}</h2>
225
+ <!-- {{/if:i18n}} -->
226
+ <!-- {{#if:!i18n}} -->
163
227
  <h2>Your notes</h2>
228
+ <!-- {{/if:!i18n}} -->
164
229
  {#if notes.length === 0}
230
+ <!-- {{#if:i18n}} -->
231
+ <p class="empty">{t`No notes yet. Create one above!`}</p>
232
+ <!-- {{/if:i18n}} -->
233
+ <!-- {{#if:!i18n}} -->
165
234
  <p class="empty">No notes yet. Create one above!</p>
235
+ <!-- {{/if:!i18n}} -->
166
236
  {:else}
167
237
  <ul class="note-list">
168
238
  {#each notes as note (note.id)}
@@ -171,7 +241,12 @@
171
241
  <form id={editForm.id} onsubmit={editForm.onsubmit} onkeydown={submitOnModEnter} class="note-form">
172
242
  <FormField for={editForm.fields.title.as("text").id}>
173
243
  {#snippet label(attrs)}
244
+ <!-- {{#if:i18n}} -->
245
+ <label {...attrs}>{t`Title`}</label>
246
+ <!-- {{/if:i18n}} -->
247
+ <!-- {{#if:!i18n}} -->
174
248
  <label {...attrs}>Title</label>
249
+ <!-- {{/if:!i18n}} -->
175
250
  {/snippet}
176
251
  <Input {...editForm.fields.title.as("text")} />
177
252
  {#snippet error(attrs)}
@@ -183,7 +258,12 @@
183
258
 
184
259
  <FormField for={`${editForm.id}-content`}>
185
260
  {#snippet label(attrs)}
261
+ <!-- {{#if:i18n}} -->
262
+ <label {...attrs}>{t`Content`}</label>
263
+ <!-- {{/if:i18n}} -->
264
+ <!-- {{#if:!i18n}} -->
186
265
  <label {...attrs}>Content</label>
266
+ <!-- {{/if:!i18n}} -->
187
267
  {/snippet}
188
268
  <Textarea
189
269
  name={editForm.fields.content.name()}
@@ -201,11 +281,21 @@
201
281
 
202
282
  <div class="actions">
203
283
  <Button type="submit" disabled={editForm.pending}>
284
+ <!-- {{#if:i18n}} -->
285
+ {editForm.pending ? t`Saving...` : t`Save`}
286
+ <!-- {{/if:i18n}} -->
287
+ <!-- {{#if:!i18n}} -->
204
288
  {editForm.pending ? "Saving..." : "Save"}
289
+ <!-- {{/if:!i18n}} -->
205
290
  {#if !isMobile}<kbd>{modLabel}+Enter</kbd>{/if}
206
291
  </Button>
207
292
  <Button variant="ghost" type="button" onclick={cancelEdit}>
208
- <X size={16} weight="bold" /> Cancel
293
+ <!-- {{#if:i18n}} -->
294
+ <XIcon size={16} weight="bold" /> {t`Cancel`}
295
+ <!-- {{/if:i18n}} -->
296
+ <!-- {{#if:!i18n}} -->
297
+ <XIcon size={16} weight="bold" /> Cancel
298
+ <!-- {{/if:!i18n}} -->
209
299
  </Button>
210
300
  </div>
211
301
  </form>
@@ -218,10 +308,20 @@
218
308
  </div>
219
309
  <div class="actions">
220
310
  <Button variant="ghost" onclick={() => startEdit(note)}>
221
- <PencilSimple size={16} /> Edit
311
+ <!-- {{#if:i18n}} -->
312
+ <PencilSimpleIcon size={16} /> {t`Edit`}
313
+ <!-- {{/if:i18n}} -->
314
+ <!-- {{#if:!i18n}} -->
315
+ <PencilSimpleIcon size={16} /> Edit
316
+ <!-- {{/if:!i18n}} -->
222
317
  </Button>
223
318
  <Button variant="danger" onclick={() => deleteNote(note.id)}>
224
- <Trash size={16} /> Delete
319
+ <!-- {{#if:i18n}} -->
320
+ <TrashIcon size={16} /> {t`Delete`}
321
+ <!-- {{/if:i18n}} -->
322
+ <!-- {{#if:!i18n}} -->
323
+ <TrashIcon size={16} /> Delete
324
+ <!-- {{/if:!i18n}} -->
225
325
  </Button>
226
326
  </div>
227
327
  {/if}
@@ -1,16 +1,26 @@
1
1
  <script lang="ts">
2
- import EnvelopeSimple from "phosphor-svelte/lib/EnvelopeSimple";
3
- import CheckCircle from "phosphor-svelte/lib/CheckCircle";
2
+ import EnvelopeSimpleIcon from "phosphor-svelte/lib/EnvelopeSimpleIcon";
3
+ import CheckCircleIcon from "phosphor-svelte/lib/CheckCircleIcon";
4
4
  import Button from "{{uiPrefix}}/Button";
5
5
  import FormField from "{{uiPrefix}}/FormField";
6
6
  import Input from "{{uiPrefix}}/Input";
7
7
  import { createForm } from "@mikstack/form";
8
8
  import * as v from "valibot";
9
9
  import { authClient } from "$lib/auth-client";
10
+ // {{#if:i18n}}
11
+ import { useLingui } from "@mikstack/svelte-lingui";
12
+ import LocaleSwitcher from "$lib/LocaleSwitcher.svelte";
13
+ const { t } = useLingui();
14
+ // {{/if:i18n}}
10
15
 
11
16
  const form = createForm({
12
17
  schema: v.object({
18
+ // {{#if:i18n}}
19
+ email: v.pipe(v.string(), v.email(t`Please enter a valid email address`)),
20
+ // {{/if:i18n}}
21
+ // {{#if:!i18n}}
13
22
  email: v.pipe(v.string(), v.email("Please enter a valid email address")),
23
+ // {{/if:!i18n}}
14
24
  }),
15
25
  initialValues: { email: "" },
16
26
  async onSubmit(data) {
@@ -27,18 +37,36 @@
27
37
  </script>
28
38
 
29
39
  <div class="sign-in">
40
+ <!-- {{#if:i18n}} -->
41
+ <div class="locale-bar">
42
+ <LocaleSwitcher />
43
+ </div>
44
+ <h1>{t`Sign in to {{projectName}}`}</h1>
45
+ <!-- {{/if:i18n}} -->
46
+ <!-- {{#if:!i18n}} -->
30
47
  <h1>Sign in to {{projectName}}</h1>
48
+ <!-- {{/if:!i18n}} -->
31
49
 
32
50
  {#if form.result}
33
51
  <div class="success">
34
- <CheckCircle size={24} weight="duotone" />
52
+ <CheckCircleIcon size={24} weight="duotone" />
53
+ <!-- {{#if:i18n}} -->
54
+ <p>{t`Check your email for a magic link to sign in.`}</p>
55
+ <!-- {{/if:i18n}} -->
56
+ <!-- {{#if:!i18n}} -->
35
57
  <p>Check your email for a magic link to sign in.</p>
58
+ <!-- {{/if:!i18n}} -->
36
59
  </div>
37
60
  {:else}
38
61
  <form id={form.id} onsubmit={form.onsubmit} class="sign-in-form">
39
62
  <FormField for={emailField.as("email").id}>
40
63
  {#snippet label(attrs)}
64
+ <!-- {{#if:i18n}} -->
65
+ <label {...attrs}>{t`Email`}</label>
66
+ <!-- {{/if:i18n}} -->
67
+ <!-- {{#if:!i18n}} -->
41
68
  <label {...attrs}>Email</label>
69
+ <!-- {{/if:!i18n}} -->
42
70
  {/snippet}
43
71
  <Input {...emailField.as("email")} placeholder="you@example.com" />
44
72
  {#snippet error(attrs)}
@@ -53,8 +81,13 @@
53
81
  {/if}
54
82
 
55
83
  <Button type="submit" disabled={form.pending}>
56
- <EnvelopeSimple size={16} weight="bold" />
84
+ <EnvelopeSimpleIcon size={16} weight="bold" />
85
+ <!-- {{#if:i18n}} -->
86
+ {form.pending ? t`Sending magic link...` : t`Sign in with magic link`}
87
+ <!-- {{/if:i18n}} -->
88
+ <!-- {{#if:!i18n}} -->
57
89
  {form.pending ? "Sending magic link..." : "Sign in with magic link"}
90
+ <!-- {{/if:!i18n}} -->
58
91
  </Button>
59
92
  </form>
60
93
  {/if}
@@ -70,6 +103,11 @@
70
103
  gap: var(--space-5);
71
104
  }
72
105
 
106
+ .locale-bar {
107
+ display: flex;
108
+ justify-content: flex-end;
109
+ }
110
+
73
111
  h1 {
74
112
  font-size: var(--text-2xl);
75
113
  }
@@ -2,7 +2,7 @@ import { extractor } from "@mikstack/svelte-lingui/extractor";
2
2
  import type { LinguiConfig } from "@lingui/conf";
3
3
 
4
4
  const config: LinguiConfig = {
5
- locales: ["en"],
5
+ locales: ["en", "fi"],
6
6
  sourceLocale: "en",
7
7
  catalogs: [
8
8
  {
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import { locales, getLocale, setLocale } from "$lib/i18n";
3
+
4
+ let currentLocale = $state(getLocale());
5
+
6
+ function switchLocale(locale: string) {
7
+ setLocale(locale);
8
+ currentLocale = locale;
9
+ }
10
+ </script>
11
+
12
+ <div class="locale-switcher">
13
+ {#each locales as locale (locale)}
14
+ <button
15
+ class:active={locale === currentLocale}
16
+ disabled={locale === currentLocale}
17
+ onclick={() => switchLocale(locale)}
18
+ >
19
+ {locale.toUpperCase()}
20
+ </button>
21
+ {/each}
22
+ </div>
23
+
24
+ <style>
25
+ .locale-switcher {
26
+ display: flex;
27
+ gap: var(--space-1);
28
+ }
29
+
30
+ button {
31
+ padding: var(--space-1) var(--space-2);
32
+ font-size: var(--text-xs);
33
+ font-weight: 500;
34
+ border: 1px solid transparent;
35
+ border-radius: var(--radius-sm);
36
+ background: none;
37
+ color: var(--text-2);
38
+ cursor: pointer;
39
+ }
40
+
41
+ button:hover:not(:disabled) {
42
+ background-color: var(--surface-2);
43
+ }
44
+
45
+ button.active {
46
+ color: var(--text-1);
47
+ font-weight: 700;
48
+ cursor: default;
49
+ }
50
+ </style>
@@ -1,10 +1,29 @@
1
1
  import { setupI18n } from "@lingui/core";
2
2
  import { setI18n } from "@mikstack/svelte-lingui";
3
- import { messages } from "../locales/en.po";
3
+ import { messages as enMessages } from "../locales/en.po";
4
+ import { messages as fiMessages } from "../locales/fi.po";
5
+
6
+ const allMessages: Record<string, typeof enMessages> = {
7
+ en: enMessages,
8
+ fi: fiMessages,
9
+ };
4
10
 
5
11
  const i18n = setupI18n();
6
12
 
7
- export function initI18n(): void {
8
- i18n.loadAndActivate({ locale: "en", messages });
13
+ export function initI18n(locale = "en"): void {
14
+ i18n.loadAndActivate({ locale, messages: allMessages[locale] });
9
15
  setI18n(i18n);
10
16
  }
17
+
18
+ export function setLocale(locale: string): void {
19
+ const messages = allMessages[locale];
20
+ if (messages) {
21
+ i18n.loadAndActivate({ locale, messages });
22
+ }
23
+ }
24
+
25
+ export function getLocale(): string {
26
+ return i18n.locale;
27
+ }
28
+
29
+ export const locales = Object.keys(allMessages);
@@ -4,3 +4,95 @@ msgstr ""
4
4
  "MIME-Version: 1.0\n"
5
5
  "Content-Type: text/plain; charset=UTF-8\n"
6
6
  "Content-Transfer-Encoding: 8bit\n"
7
+
8
+ #: src/routes/(app)/+page.svelte
9
+ msgid "Cancel"
10
+ msgstr "Cancel"
11
+
12
+ #: src/routes/(public)/sign-in/+page.svelte
13
+ msgid "Check your email for a magic link to sign in."
14
+ msgstr "Check your email for a magic link to sign in."
15
+
16
+ #: src/routes/(app)/+page.svelte
17
+ msgid "Content"
18
+ msgstr "Content"
19
+
20
+ #: src/routes/(app)/+page.svelte
21
+ msgid "Create note"
22
+ msgstr "Create note"
23
+
24
+ #: src/routes/(app)/+page.svelte
25
+ msgid "Creating..."
26
+ msgstr "Creating..."
27
+
28
+ #: src/routes/(app)/+page.svelte
29
+ msgid "Delete"
30
+ msgstr "Delete"
31
+
32
+ #: src/routes/(app)/+page.svelte
33
+ msgid "Edit"
34
+ msgstr "Edit"
35
+
36
+ #: src/routes/(public)/sign-in/+page.svelte
37
+ msgid "Email"
38
+ msgstr "Email"
39
+
40
+ #: src/routes/(app)/+page.svelte
41
+ msgid "New note"
42
+ msgstr "New note"
43
+
44
+ #: src/routes/(app)/+page.svelte
45
+ msgid "No notes yet. Create one above!"
46
+ msgstr "No notes yet. Create one above!"
47
+
48
+ #: src/routes/(app)/+page.svelte
49
+ msgid "Note title"
50
+ msgstr "Note title"
51
+
52
+ #: src/routes/(app)/+page.svelte
53
+ msgid "Notes"
54
+ msgstr "Notes"
55
+
56
+ #: src/routes/(public)/sign-in/+page.svelte
57
+ msgid "Please enter a valid email address"
58
+ msgstr "Please enter a valid email address"
59
+
60
+ #: src/routes/(app)/+page.svelte
61
+ msgid "Save"
62
+ msgstr "Save"
63
+
64
+ #: src/routes/(app)/+page.svelte
65
+ msgid "Saving..."
66
+ msgstr "Saving..."
67
+
68
+ #: src/routes/(public)/sign-in/+page.svelte
69
+ msgid "Sending magic link..."
70
+ msgstr "Sending magic link..."
71
+
72
+ #: src/routes/(app)/+page.svelte
73
+ msgid "Sign out"
74
+ msgstr "Sign out"
75
+
76
+ #: src/routes/(public)/sign-in/+page.svelte
77
+ msgid "Sign in to {{projectName}}"
78
+ msgstr "Sign in to {{projectName}}"
79
+
80
+ #: src/routes/(public)/sign-in/+page.svelte
81
+ msgid "Sign in with magic link"
82
+ msgstr "Sign in with magic link"
83
+
84
+ #: src/routes/(app)/+page.svelte
85
+ msgid "Title"
86
+ msgstr "Title"
87
+
88
+ #: src/routes/(app)/+page.svelte
89
+ msgid "Title is required"
90
+ msgstr "Title is required"
91
+
92
+ #: src/routes/(app)/+page.svelte
93
+ msgid "Write something..."
94
+ msgstr "Write something..."
95
+
96
+ #: src/routes/(app)/+page.svelte
97
+ msgid "Your notes"
98
+ msgstr "Your notes"
@@ -0,0 +1,98 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Language: fi\n"
4
+ "MIME-Version: 1.0\n"
5
+ "Content-Type: text/plain; charset=UTF-8\n"
6
+ "Content-Transfer-Encoding: 8bit\n"
7
+
8
+ #: src/routes/(app)/+page.svelte
9
+ msgid "Cancel"
10
+ msgstr "Peruuta"
11
+
12
+ #: src/routes/(public)/sign-in/+page.svelte
13
+ msgid "Check your email for a magic link to sign in."
14
+ msgstr "Tarkista sähköpostisi kirjautumislinkin varalta."
15
+
16
+ #: src/routes/(app)/+page.svelte
17
+ msgid "Content"
18
+ msgstr "Sisältö"
19
+
20
+ #: src/routes/(app)/+page.svelte
21
+ msgid "Create note"
22
+ msgstr "Luo muistiinpano"
23
+
24
+ #: src/routes/(app)/+page.svelte
25
+ msgid "Creating..."
26
+ msgstr "Luodaan..."
27
+
28
+ #: src/routes/(app)/+page.svelte
29
+ msgid "Delete"
30
+ msgstr "Poista"
31
+
32
+ #: src/routes/(app)/+page.svelte
33
+ msgid "Edit"
34
+ msgstr "Muokkaa"
35
+
36
+ #: src/routes/(public)/sign-in/+page.svelte
37
+ msgid "Email"
38
+ msgstr "Sähköposti"
39
+
40
+ #: src/routes/(app)/+page.svelte
41
+ msgid "New note"
42
+ msgstr "Uusi muistiinpano"
43
+
44
+ #: src/routes/(app)/+page.svelte
45
+ msgid "No notes yet. Create one above!"
46
+ msgstr "Ei vielä muistiinpanoja. Luo ensimmäinen yllä!"
47
+
48
+ #: src/routes/(app)/+page.svelte
49
+ msgid "Note title"
50
+ msgstr "Muistiinpanon otsikko"
51
+
52
+ #: src/routes/(app)/+page.svelte
53
+ msgid "Notes"
54
+ msgstr "Muistiinpanot"
55
+
56
+ #: src/routes/(public)/sign-in/+page.svelte
57
+ msgid "Please enter a valid email address"
58
+ msgstr "Syötä kelvollinen sähköpostiosoite"
59
+
60
+ #: src/routes/(app)/+page.svelte
61
+ msgid "Save"
62
+ msgstr "Tallenna"
63
+
64
+ #: src/routes/(app)/+page.svelte
65
+ msgid "Saving..."
66
+ msgstr "Tallennetaan..."
67
+
68
+ #: src/routes/(public)/sign-in/+page.svelte
69
+ msgid "Sending magic link..."
70
+ msgstr "Lähetetään kirjautumislinkkiä..."
71
+
72
+ #: src/routes/(app)/+page.svelte
73
+ msgid "Sign out"
74
+ msgstr "Kirjaudu ulos"
75
+
76
+ #: src/routes/(public)/sign-in/+page.svelte
77
+ msgid "Sign in to {{projectName}}"
78
+ msgstr "Kirjaudu palveluun {{projectName}}"
79
+
80
+ #: src/routes/(public)/sign-in/+page.svelte
81
+ msgid "Sign in with magic link"
82
+ msgstr "Kirjaudu kirjautumislinkillä"
83
+
84
+ #: src/routes/(app)/+page.svelte
85
+ msgid "Title"
86
+ msgstr "Otsikko"
87
+
88
+ #: src/routes/(app)/+page.svelte
89
+ msgid "Title is required"
90
+ msgstr "Otsikko on pakollinen"
91
+
92
+ #: src/routes/(app)/+page.svelte
93
+ msgid "Write something..."
94
+ msgstr "Kirjoita jotain..."
95
+
96
+ #: src/routes/(app)/+page.svelte
97
+ msgid "Your notes"
98
+ msgstr "Muistiinpanosi"