@meith/theme-midnight 0.7.0 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/theme-midnight",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "A dark theme for Meith, built on the default theme's slots.",
5
5
  "license": "LGPL-3.0-or-later",
6
6
  "repository": {
@@ -20,8 +20,8 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@meith/theme-default": "^0.7.0",
24
- "@meith/theme-kit": "^0.7.0"
23
+ "@meith/theme-default": "^0.8.0",
24
+ "@meith/theme-kit": "^0.8.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": "^19.2.0"
package/src/copy.ts ADDED
@@ -0,0 +1,186 @@
1
+ import type { SlotCopy, Translator } from '@meith/theme-kit'
2
+
3
+ function copyFor(t: Translator, keys: readonly string[]): SlotCopy {
4
+ const copy: Record<string, string> = {}
5
+ for (const key of keys) copy[key] = t.t(key)
6
+ return copy
7
+ }
8
+
9
+ export function announcementCopy(t: Translator): SlotCopy {
10
+ return copyFor(t, ['midnight.announcement.posted', 'midnight.announcement.postedBy'])
11
+ }
12
+
13
+ export function boardIndexCopy(t: Translator): SlotCopy {
14
+ return copyFor(t, ['midnight.boardIndex.activityLabel', 'midnight.boardIndex.markAllRead'])
15
+ }
16
+
17
+ export function boardStatsCopy(t: Translator): SlotCopy {
18
+ return copyFor(t, [
19
+ 'midnight.boardStats.heading',
20
+ 'midnight.boardStats.threads',
21
+ 'midnight.boardStats.posts',
22
+ 'midnight.boardStats.members',
23
+ 'midnight.boardStats.newest',
24
+ 'midnight.boardStats.notCounted',
25
+ 'midnight.boardStats.counted',
26
+ ])
27
+ }
28
+
29
+ export function categoryBlockCopy(t: Translator): SlotCopy {
30
+ return copyFor(t, [
31
+ 'midnight.categoryBlock.forumHeader',
32
+ 'midnight.categoryBlock.threadsHeader',
33
+ 'midnight.categoryBlock.postsHeader',
34
+ 'midnight.categoryBlock.lastPostHeader',
35
+ ])
36
+ }
37
+
38
+ export function footerCopy(t: Translator): SlotCopy {
39
+ return copyFor(t, ['midnight.footer.allTimes'])
40
+ }
41
+
42
+ export function forumDisplayCopy(t: Translator): SlotCopy {
43
+ return copyFor(t, [
44
+ 'midnight.forumDisplay.newThread',
45
+ 'midnight.forumDisplay.threadHeader',
46
+ 'midnight.forumDisplay.repliesHeader',
47
+ 'midnight.forumDisplay.viewsHeader',
48
+ 'midnight.forumDisplay.lastPostHeader',
49
+ 'midnight.forumDisplay.markForumRead',
50
+ ])
51
+ }
52
+
53
+ export function forumRowCopy(t: Translator): SlotCopy {
54
+ return copyFor(t, ['midnight.forumRow.newPosts', 'midnight.forumRow.noPosts'])
55
+ }
56
+
57
+ export function headerCopy(t: Translator): SlotCopy {
58
+ return copyFor(t, ['midnight.header.sectionsLabel'])
59
+ }
60
+
61
+ export function latestPostsCopy(t: Translator): SlotCopy {
62
+ return copyFor(t, [
63
+ 'midnight.latestPosts.heading',
64
+ 'midnight.latestPosts.empty',
65
+ 'midnight.latestPosts.asOf',
66
+ ])
67
+ }
68
+
69
+ export function latestThreadsCopy(t: Translator): SlotCopy {
70
+ return copyFor(t, [
71
+ 'midnight.latestThreads.heading',
72
+ 'midnight.latestThreads.empty',
73
+ 'midnight.latestThreads.asOf',
74
+ ])
75
+ }
76
+
77
+ export function memberProfileCopy(t: Translator): SlotCopy {
78
+ return copyFor(t, [
79
+ 'midnight.memberProfile.joined',
80
+ 'midnight.memberProfile.lastVisit',
81
+ 'midnight.memberProfile.never',
82
+ 'midnight.memberProfile.posts',
83
+ 'midnight.memberProfile.signatureLabel',
84
+ 'midnight.memberProfile.actionsLabel',
85
+ ])
86
+ }
87
+
88
+ export function navigationCopy(t: Translator): SlotCopy {
89
+ return copyFor(t, ['midnight.navigation.breadcrumbLabel'])
90
+ }
91
+
92
+ export function noticeCopy(t: Translator): SlotCopy {
93
+ return copyFor(t, [
94
+ 'midnight.notice.dismiss',
95
+ 'midnight.notice.kind.info',
96
+ 'midnight.notice.kind.success',
97
+ 'midnight.notice.kind.warning',
98
+ 'midnight.notice.kind.error',
99
+ ])
100
+ }
101
+
102
+ export function paginationCopy(t: Translator): SlotCopy {
103
+ return copyFor(t, [
104
+ 'midnight.pagination.ariaLabel',
105
+ 'midnight.pagination.prev',
106
+ 'midnight.pagination.next',
107
+ 'midnight.pagination.page',
108
+ 'midnight.pagination.pageOf',
109
+ ])
110
+ }
111
+
112
+ export function postActionsCopy(t: Translator): SlotCopy {
113
+ return copyFor(t, [
114
+ 'midnight.postActions.ariaLabel',
115
+ 'midnight.postActions.quote',
116
+ 'midnight.postActions.edit',
117
+ 'midnight.postActions.restore',
118
+ 'midnight.postActions.rate',
119
+ 'midnight.postActions.report',
120
+ 'midnight.postActions.warn',
121
+ 'midnight.postActions.moderate',
122
+ ])
123
+ }
124
+
125
+ export function postBitCopy(t: Translator): SlotCopy {
126
+ return copyFor(t, [
127
+ 'midnight.postBit.deleted',
128
+ 'midnight.postBit.unapproved',
129
+ 'midnight.postBit.posts',
130
+ 'midnight.postBit.joined',
131
+ 'midnight.postBit.rep',
132
+ 'midnight.postBit.online',
133
+ 'midnight.postBit.ignoredPrefix',
134
+ 'midnight.postBit.ignoredSuffix',
135
+ 'midnight.postBit.showIt',
136
+ ])
137
+ }
138
+
139
+ export function shellCopy(t: Translator): SlotCopy {
140
+ return copyFor(t, ['midnight.shell.skipToContent'])
141
+ }
142
+
143
+ export function subforumListCopy(t: Translator): SlotCopy {
144
+ return copyFor(t, ['midnight.subforumList.ariaLabel'])
145
+ }
146
+
147
+ export function threadRowCopy(t: Translator): SlotCopy {
148
+ return copyFor(t, [
149
+ 'midnight.threadRow.newPosts',
150
+ 'midnight.threadRow.pinned',
151
+ 'midnight.threadRow.locked',
152
+ 'midnight.threadRow.moved',
153
+ 'midnight.threadRow.by',
154
+ ])
155
+ }
156
+
157
+ export function threadViewCopy(t: Translator): SlotCopy {
158
+ return copyFor(t, [
159
+ 'midnight.threadView.in',
160
+ 'midnight.threadView.reply',
161
+ 'midnight.threadView.markRead',
162
+ ])
163
+ }
164
+
165
+ export function userPanelCopy(t: Translator): SlotCopy {
166
+ return copyFor(t, [
167
+ 'midnight.userPanel.guest',
168
+ 'midnight.userPanel.signedIn',
169
+ 'midnight.userPanel.new',
170
+ 'midnight.userPanel.notificationsSrOnly',
171
+ 'midnight.userPanel.pm',
172
+ 'midnight.userPanel.messagesSrOnly',
173
+ ])
174
+ }
175
+
176
+ export function whoIsOnlineCopy(t: Translator): SlotCopy {
177
+ return copyFor(t, [
178
+ 'midnight.whoIsOnline.onlineNow',
179
+ 'midnight.whoIsOnline.noMembers',
180
+ 'midnight.whoIsOnline.hidden',
181
+ 'midnight.whoIsOnline.guests',
182
+ 'midnight.whoIsOnline.fullList',
183
+ 'midnight.whoIsOnline.record',
184
+ 'midnight.whoIsOnline.on',
185
+ ])
186
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
+ export { midnightMessages } from './messages'
2
+ export { midnightTheme } from './theme'
1
3
  export {
2
- LIGHT_TOKENS,
3
- DARK_TOKENS,
4
4
  BROWSER_THEME_COLOR,
5
+ DARK_TOKENS,
6
+ LIGHT_TOKENS,
5
7
  } from './tokens'
6
-
7
- export { midnightTheme } from './theme'
@@ -0,0 +1,90 @@
1
+ {
2
+ "midnight.announcement.posted": "Posted",
3
+ "midnight.announcement.postedBy": "Posted by",
4
+ "midnight.boardIndex.activityLabel": "Board activity",
5
+ "midnight.boardIndex.markAllRead": "mark all forums read",
6
+ "midnight.boardStats.counted": "counted",
7
+ "midnight.boardStats.heading": "Board statistics",
8
+ "midnight.boardStats.members": "members",
9
+ "midnight.boardStats.newest": "newest",
10
+ "midnight.boardStats.notCounted": "not counted yet",
11
+ "midnight.boardStats.posts": "posts",
12
+ "midnight.boardStats.threads": "threads",
13
+ "midnight.categoryBlock.forumHeader": "Forum",
14
+ "midnight.categoryBlock.lastPostHeader": "Last post",
15
+ "midnight.categoryBlock.postsHeader": "Posts",
16
+ "midnight.categoryBlock.threadsHeader": "Threads",
17
+ "midnight.footer.allTimes": "all times",
18
+ "midnight.forumDisplay.lastPostHeader": "Last post",
19
+ "midnight.forumDisplay.markForumRead": "mark this forum read",
20
+ "midnight.forumDisplay.newThread": "new thread",
21
+ "midnight.forumDisplay.repliesHeader": "Replies",
22
+ "midnight.forumDisplay.threadHeader": "Thread",
23
+ "midnight.forumDisplay.viewsHeader": "Views",
24
+ "midnight.forumRow.newPosts": "(new posts)",
25
+ "midnight.forumRow.noPosts": "no posts yet",
26
+ "midnight.header.sectionsLabel": "Board sections",
27
+ "midnight.latestPosts.asOf": "as of",
28
+ "midnight.latestPosts.empty": "nothing said yet",
29
+ "midnight.latestPosts.heading": "Latest posts",
30
+ "midnight.latestThreads.asOf": "as of",
31
+ "midnight.latestThreads.empty": "nothing started yet",
32
+ "midnight.latestThreads.heading": "Latest threads",
33
+ "midnight.memberProfile.actionsLabel": "Member actions",
34
+ "midnight.memberProfile.joined": "joined",
35
+ "midnight.memberProfile.lastVisit": "last visit",
36
+ "midnight.memberProfile.never": "never",
37
+ "midnight.memberProfile.posts": "posts",
38
+ "midnight.memberProfile.signatureLabel": "Signature",
39
+ "midnight.navigation.breadcrumbLabel": "Breadcrumb",
40
+ "midnight.notice.dismiss": "dismiss",
41
+ "midnight.notice.kind.error": "error",
42
+ "midnight.notice.kind.info": "info",
43
+ "midnight.notice.kind.success": "success",
44
+ "midnight.notice.kind.warning": "warning",
45
+ "midnight.pagination.ariaLabel": "Pagination",
46
+ "midnight.pagination.next": "next",
47
+ "midnight.pagination.page": "page",
48
+ "midnight.pagination.pageOf": "of",
49
+ "midnight.pagination.prev": "prev",
50
+ "midnight.postActions.ariaLabel": "Post actions",
51
+ "midnight.postActions.edit": "edit",
52
+ "midnight.postActions.moderate": "moderate",
53
+ "midnight.postActions.quote": "quote",
54
+ "midnight.postActions.rate": "rate",
55
+ "midnight.postActions.report": "report",
56
+ "midnight.postActions.restore": "restore",
57
+ "midnight.postActions.warn": "warn",
58
+ "midnight.postBit.deleted": "deleted — visible to moderators only",
59
+ "midnight.postBit.ignoredPrefix": "Post from",
60
+ "midnight.postBit.ignoredSuffix": ", who you are ignoring.",
61
+ "midnight.postBit.joined": "joined",
62
+ "midnight.postBit.online": "online",
63
+ "midnight.postBit.posts": "posts",
64
+ "midnight.postBit.rep": "rep",
65
+ "midnight.postBit.showIt": "Show it",
66
+ "midnight.postBit.unapproved": "awaiting approval — visible to moderators only",
67
+ "midnight.shell.skipToContent": "Skip to content",
68
+ "midnight.subforumList.ariaLabel": "Subforums",
69
+ "midnight.threadRow.by": "by",
70
+ "midnight.threadRow.locked": "locked",
71
+ "midnight.threadRow.moved": "moved",
72
+ "midnight.threadRow.newPosts": "(new posts)",
73
+ "midnight.threadRow.pinned": "pinned",
74
+ "midnight.threadView.in": "in",
75
+ "midnight.threadView.markRead": "mark read",
76
+ "midnight.threadView.reply": "reply",
77
+ "midnight.userPanel.guest": "guest",
78
+ "midnight.userPanel.messagesSrOnly": "unread messages",
79
+ "midnight.userPanel.new": "new",
80
+ "midnight.userPanel.notificationsSrOnly": "notifications",
81
+ "midnight.userPanel.pm": "pm",
82
+ "midnight.userPanel.signedIn": "signed in",
83
+ "midnight.whoIsOnline.fullList": "full list",
84
+ "midnight.whoIsOnline.guests": "guests",
85
+ "midnight.whoIsOnline.hidden": "(hidden)",
86
+ "midnight.whoIsOnline.noMembers": "no members",
87
+ "midnight.whoIsOnline.on": "on",
88
+ "midnight.whoIsOnline.onlineNow": "Online now —",
89
+ "midnight.whoIsOnline.record": "record"
90
+ }
@@ -0,0 +1,5 @@
1
+ import type { CatalogBundle } from '@meith/theme-kit'
2
+
3
+ import en from './en.json'
4
+
5
+ export const midnightMessages: CatalogBundle = { en }
@@ -1,8 +1,18 @@
1
- import type { AnnouncementModel } from '@meith/theme-kit'
1
+ import type { AnnouncementModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { UserRef } from '../shared'
4
5
 
5
- export function Announcement({ title, bodyHtml, postedBy, postedAt, forum }: AnnouncementModel) {
6
+ export function Announcement({
7
+ title,
8
+ bodyHtml,
9
+ postedBy,
10
+ postedAt,
11
+ forum,
12
+ copy,
13
+ }: AnnouncementModel & { copy: SlotCopy }) {
14
+ const c = (key: string) => fromSlotCopy(copy, `midnight.announcement.${key}`)
15
+
6
16
  return (
7
17
  <article className="flex flex-col gap-2 rounded-lg border border-border bg-muted/40 p-4">
8
18
  <h2 className="font-heading text-lg font-semibold text-foreground">{title}</h2>
@@ -14,10 +24,10 @@ export function Announcement({ title, bodyHtml, postedBy, postedAt, forum }: Ann
14
24
 
15
25
  <p className="text-xs text-muted-foreground">
16
26
  {postedBy === null ? (
17
- 'Posted'
27
+ c('posted')
18
28
  ) : (
19
29
  <>
20
- Posted by{' '}
30
+ {c('postedBy')}{' '}
21
31
  <UserRef
22
32
  user={postedBy}
23
33
  className={
@@ -1,8 +1,14 @@
1
- import type { BoardIndexModel } from '@meith/theme-kit'
1
+ import type { BoardIndexModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
- export function BoardIndex({ markAllReadAction, regions }: BoardIndexModel) {
4
+ export function BoardIndex({
5
+ markAllReadAction,
6
+ regions,
7
+ copy,
8
+ }: BoardIndexModel & { copy: SlotCopy }) {
4
9
  const latest = regions.latest ?? null
5
10
  const hasRail = latest !== null || regions.online !== null || regions.stats !== null
11
+ const c = (key: string) => fromSlotCopy(copy, `midnight.boardIndex.${key}`)
6
12
 
7
13
  return (
8
14
  <div className="flex flex-col gap-3 p-3">
@@ -20,7 +26,7 @@ export function BoardIndex({ markAllReadAction, regions }: BoardIndexModel) {
20
26
  <div className="flex min-w-0 flex-col gap-3">{regions.categories}</div>
21
27
 
22
28
  {hasRail && (
23
- <aside aria-label="Board activity" className="flex min-w-0 flex-col gap-3">
29
+ <aside aria-label={c('activityLabel')} className="flex min-w-0 flex-col gap-3">
24
30
  {latest}
25
31
  {regions.online}
26
32
  {regions.stats}
@@ -36,7 +42,7 @@ export function BoardIndex({ markAllReadAction, regions }: BoardIndexModel) {
36
42
  type="submit"
37
43
  className="border border-border px-2 py-1 font-mono text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
38
44
  >
39
- mark all forums read
45
+ {c('markAllRead')}
40
46
  </button>
41
47
  </form>
42
48
  )}
@@ -1,4 +1,5 @@
1
- import type { BoardStatsModel } from '@meith/theme-kit'
1
+ import type { BoardStatsModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { UserRef } from '../shared'
4
5
 
@@ -8,30 +9,33 @@ export function BoardStats({
8
9
  memberCount,
9
10
  newestMember,
10
11
  computedAt,
11
- }: BoardStatsModel) {
12
+ copy,
13
+ }: BoardStatsModel & { copy: SlotCopy }) {
14
+ const c = (key: string) => fromSlotCopy(copy, `midnight.boardStats.${key}`)
15
+
12
16
  return (
13
17
  <section aria-labelledby="board-stats-heading" className="border border-border">
14
18
  <h2
15
19
  id="board-stats-heading"
16
20
  className="border-b border-border bg-secondary px-3 py-1 font-mono text-xs uppercase tracking-wide"
17
21
  >
18
- Board statistics
22
+ {c('heading')}
19
23
  </h2>
20
24
  <dl className="grid grid-cols-2 gap-x-4 gap-y-1 px-3 py-2 font-mono text-xs md:grid-cols-4 lg:grid-cols-2">
21
25
  <div>
22
- <dt className="text-muted-foreground">threads</dt>
23
- <dd>{threadCount}</dd>
26
+ <dt className="text-muted-foreground">{c('threads')}</dt>
27
+ <dd>{threadCount.label}</dd>
24
28
  </div>
25
29
  <div>
26
- <dt className="text-muted-foreground">posts</dt>
27
- <dd>{postCount}</dd>
30
+ <dt className="text-muted-foreground">{c('posts')}</dt>
31
+ <dd>{postCount.label}</dd>
28
32
  </div>
29
33
  <div>
30
- <dt className="text-muted-foreground">members</dt>
31
- <dd>{memberCount}</dd>
34
+ <dt className="text-muted-foreground">{c('members')}</dt>
35
+ <dd>{memberCount.label}</dd>
32
36
  </div>
33
37
  <div>
34
- <dt className="text-muted-foreground">newest</dt>
38
+ <dt className="text-muted-foreground">{c('newest')}</dt>
35
39
  <dd>
36
40
  {newestMember === null ? (
37
41
  '—'
@@ -43,10 +47,10 @@ export function BoardStats({
43
47
  </dl>
44
48
  <p className="border-t border-border px-3 py-1 font-mono text-xs text-muted-foreground">
45
49
  {computedAt === null ? (
46
- 'not counted yet'
50
+ c('notCounted')
47
51
  ) : (
48
52
  <>
49
- counted <time dateTime={computedAt.iso}>{computedAt.label}</time>
53
+ {c('counted')} <time dateTime={computedAt.iso}>{computedAt.label}</time>
50
54
  </>
51
55
  )}
52
56
  </p>
@@ -1,6 +1,13 @@
1
- import type { CategoryBlockModel } from '@meith/theme-kit'
1
+ import type { CategoryBlockModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+
4
+ export function CategoryBlock({
5
+ category,
6
+ children,
7
+ copy,
8
+ }: CategoryBlockModel & { copy: SlotCopy }) {
9
+ const c = (key: string) => fromSlotCopy(copy, `midnight.categoryBlock.${key}`)
2
10
 
3
- export function CategoryBlock({ category, children }: CategoryBlockModel) {
4
11
  return (
5
12
  <section className="border border-border">
6
13
  <h2 className="border-b border-border bg-secondary px-3 py-1.5 font-mono text-sm font-semibold uppercase tracking-wide">
@@ -22,10 +29,10 @@ export function CategoryBlock({ category, children }: CategoryBlockModel) {
22
29
  </colgroup>
23
30
  <thead className="sr-only">
24
31
  <tr>
25
- <th scope="col">Forum</th>
26
- <th scope="col">Threads</th>
27
- <th scope="col">Posts</th>
28
- <th scope="col">Last post</th>
32
+ <th scope="col">{c('forumHeader')}</th>
33
+ <th scope="col">{c('threadsHeader')}</th>
34
+ <th scope="col">{c('postsHeader')}</th>
35
+ <th scope="col">{c('lastPostHeader')}</th>
29
36
  </tr>
30
37
  </thead>
31
38
  <tbody>{children}</tbody>
@@ -1,6 +1,15 @@
1
- import type { FooterModel } from '@meith/theme-kit'
1
+ import type { FooterModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+
4
+ export function Footer({
5
+ boardTitle,
6
+ links,
7
+ timezoneLabel,
8
+ poweredBy,
9
+ copy,
10
+ }: FooterModel & { copy: SlotCopy }) {
11
+ const c = (key: string) => fromSlotCopy(copy, `midnight.footer.${key}`)
2
12
 
3
- export function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterModel) {
4
13
  return (
5
14
  <footer className="mt-auto flex flex-wrap items-center justify-between gap-2 border-t border-border bg-secondary px-4 py-2 font-mono text-xs text-muted-foreground">
6
15
  <span>{boardTitle}</span>
@@ -12,7 +21,9 @@ export function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterMo
12
21
  ))}
13
22
  </div>
14
23
  <span className="flex flex-wrap gap-3">
15
- <span>all times {timezoneLabel}</span>
24
+ <span>
25
+ {c('allTimes')} {timezoneLabel}
26
+ </span>
16
27
  {poweredBy && (
17
28
  <a href={poweredBy.href} className="hover:text-foreground">
18
29
  {poweredBy.label}
@@ -1,6 +1,15 @@
1
- import type { ForumDisplayModel } from '@meith/theme-kit'
1
+ import type { ForumDisplayModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+
4
+ export function ForumDisplay({
5
+ forum,
6
+ newThreadHref,
7
+ markReadAction,
8
+ regions,
9
+ copy,
10
+ }: ForumDisplayModel & { copy: SlotCopy }) {
11
+ const c = (key: string) => fromSlotCopy(copy, `midnight.forumDisplay.${key}`)
2
12
 
3
- export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }: ForumDisplayModel) {
4
13
  return (
5
14
  <div className="flex flex-col gap-3 p-3">
6
15
  <div className="flex flex-wrap items-baseline justify-between gap-2">
@@ -10,7 +19,7 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
10
19
  href={newThreadHref}
11
20
  className="border border-primary bg-primary px-3 py-1 font-mono text-xs text-primary-foreground hover:opacity-90"
12
21
  >
13
- new thread
22
+ {c('newThread')}
14
23
  </a>
15
24
  )}
16
25
  </div>
@@ -32,10 +41,18 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
32
41
  <table className="w-full border-collapse border border-border text-sm">
33
42
  <thead>
34
43
  <tr className="bg-secondary text-left font-mono text-xs uppercase tracking-wide text-muted-foreground">
35
- <th scope="col" className="px-3 py-1.5">Thread</th>
36
- <th scope="col" className="w-16 px-2 py-1.5 text-right">Replies</th>
37
- <th scope="col" className="w-16 px-2 py-1.5 text-right">Views</th>
38
- <th scope="col" className="w-56 px-3 py-1.5">Last post</th>
44
+ <th scope="col" className="px-3 py-1.5">
45
+ {c('threadHeader')}
46
+ </th>
47
+ <th scope="col" className="w-16 px-2 py-1.5 text-right">
48
+ {c('repliesHeader')}
49
+ </th>
50
+ <th scope="col" className="w-16 px-2 py-1.5 text-right">
51
+ {c('viewsHeader')}
52
+ </th>
53
+ <th scope="col" className="w-56 px-3 py-1.5">
54
+ {c('lastPostHeader')}
55
+ </th>
39
56
  </tr>
40
57
  </thead>
41
58
  <tbody>{regions.threads}</tbody>
@@ -53,7 +70,7 @@ export function ForumDisplay({ forum, newThreadHref, markReadAction, regions }:
53
70
  type="submit"
54
71
  className="border border-border px-2 py-1 font-mono text-xs text-muted-foreground hover:bg-muted hover:text-foreground"
55
72
  >
56
- mark this forum read
73
+ {c('markForumRead')}
57
74
  </button>
58
75
  </form>
59
76
  )}
@@ -1,9 +1,11 @@
1
- import type { ForumRowSlotModel } from '@meith/theme-kit'
1
+ import type { ForumRowSlotModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { UserRef } from '../shared'
4
5
 
5
- export function ForumRow({ forum }: ForumRowSlotModel) {
6
+ export function ForumRow({ forum, copy }: ForumRowSlotModel & { copy: SlotCopy }) {
6
7
  const isLink = forum.type === 'link'
8
+ const c = (key: string) => fromSlotCopy(copy, `midnight.forumRow.${key}`)
7
9
 
8
10
  return (
9
11
  <tr className="border-t border-border align-top odd:bg-card even:bg-muted/40">
@@ -15,7 +17,7 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
15
17
  <a href={forum.href} className="font-medium hover:text-primary">
16
18
  {forum.title}
17
19
  </a>
18
- {forum.isUnread && <span className="sr-only">(new posts)</span>}
20
+ {forum.isUnread && <span className="sr-only">{c('newPosts')}</span>}
19
21
  </div>
20
22
  {forum.description !== null && (
21
23
  <p className="mt-0.5 text-xs text-muted-foreground">{forum.description}</p>
@@ -32,14 +34,14 @@ export function ForumRow({ forum }: ForumRowSlotModel) {
32
34
  </td>
33
35
 
34
36
  <td className="w-16 px-2 py-2 text-right font-mono text-xs text-muted-foreground">
35
- {isLink ? '' : forum.threadCount}
37
+ {isLink ? '' : forum.threadCount.label}
36
38
  </td>
37
39
  <td className="w-16 px-2 py-2 text-right font-mono text-xs text-muted-foreground">
38
- {isLink ? '' : forum.postCount}
40
+ {isLink ? '' : forum.postCount.label}
39
41
  </td>
40
42
  <td className="w-64 px-3 py-2 text-xs text-muted-foreground">
41
43
  {isLink || forum.lastPost === null ? (
42
- <span className="text-forum-read">{isLink ? '' : 'no posts yet'}</span>
44
+ <span className="text-forum-read">{isLink ? '' : c('noPosts')}</span>
43
45
  ) : (
44
46
  <>
45
47
  <a href={forum.lastPost.href} className="block truncate hover:text-primary">
@@ -1,4 +1,5 @@
1
- import type { HeaderModel } from '@meith/theme-kit'
1
+ import type { HeaderModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'>) {
4
5
  if (logo === undefined) return <>{boardTitle}</>
@@ -22,7 +23,16 @@ function BoardMark({ boardTitle, logo }: Pick<HeaderModel, 'boardTitle' | 'logo'
22
23
  )
23
24
  }
24
25
 
25
- export function Header({ boardTitle, homeHref, navigation, logo, children }: HeaderModel) {
26
+ export function Header({
27
+ boardTitle,
28
+ homeHref,
29
+ navigation,
30
+ logo,
31
+ children,
32
+ copy,
33
+ }: HeaderModel & { copy: SlotCopy }) {
34
+ const c = (key: string) => fromSlotCopy(copy, `midnight.header.${key}`)
35
+
26
36
  return (
27
37
  <header>
28
38
  <div className="flex flex-wrap items-baseline justify-between gap-3 border-b border-border bg-secondary px-4 py-3">
@@ -36,7 +46,7 @@ export function Header({ boardTitle, homeHref, navigation, logo, children }: Hea
36
46
  </div>
37
47
 
38
48
  {navigation.length > 0 && (
39
- <nav aria-label="Board sections" className="flex flex-wrap border-b border-border">
49
+ <nav aria-label={c('sectionsLabel')} className="flex flex-wrap border-b border-border">
40
50
  {navigation.map((item) => (
41
51
  <a
42
52
  key={item.href}