@meith/theme-default 0.6.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.
Files changed (41) hide show
  1. package/package.json +3 -3
  2. package/src/copy.ts +316 -0
  3. package/src/index.ts +6 -6
  4. package/src/messages/en.json +189 -0
  5. package/src/messages/index.ts +5 -0
  6. package/src/panel-icons.tsx +1 -2
  7. package/src/shared.tsx +4 -6
  8. package/src/slots/announcement.tsx +15 -9
  9. package/src/slots/auth-page.tsx +3 -1
  10. package/src/slots/board-index.tsx +11 -4
  11. package/src/slots/board-stats.tsx +14 -12
  12. package/src/slots/category-block.tsx +1 -1
  13. package/src/slots/discovery-view.tsx +16 -15
  14. package/src/slots/error-notice.tsx +17 -9
  15. package/src/slots/footer.tsx +15 -4
  16. package/src/slots/forum-display.tsx +25 -20
  17. package/src/slots/forum-jump.tsx +1 -1
  18. package/src/slots/forum-row.tsx +16 -15
  19. package/src/slots/header.tsx +13 -3
  20. package/src/slots/latest-posts.tsx +21 -12
  21. package/src/slots/latest-threads.tsx +27 -16
  22. package/src/slots/member-profile.tsx +19 -15
  23. package/src/slots/navigation.tsx +7 -4
  24. package/src/slots/notice.tsx +12 -9
  25. package/src/slots/pagination.tsx +16 -10
  26. package/src/slots/panel-nav.tsx +37 -16
  27. package/src/slots/panel-page.tsx +15 -3
  28. package/src/slots/panel-shell.tsx +1 -7
  29. package/src/slots/post-actions.tsx +18 -11
  30. package/src/slots/post-bit.tsx +42 -20
  31. package/src/slots/post-form.tsx +7 -3
  32. package/src/slots/redirect-notice.tsx +16 -7
  33. package/src/slots/search-form.tsx +13 -9
  34. package/src/slots/search-results.tsx +27 -26
  35. package/src/slots/shell.tsx +4 -3
  36. package/src/slots/subforum-list.tsx +12 -9
  37. package/src/slots/thread-row.tsx +19 -18
  38. package/src/slots/thread-view.tsx +23 -13
  39. package/src/slots/user-panel.tsx +19 -14
  40. package/src/slots/who-is-online.tsx +35 -19
  41. package/src/theme.ts +90 -19
@@ -1,5 +1,6 @@
1
- import { Avatar, Card, CardContent, CardHeader, CardTitle, buttonVariants, cn } from '@meith/ui'
2
- import type { MemberProfileModel } from '@meith/theme-kit'
1
+ import type { MemberProfileModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+ import { Avatar, buttonVariants, Card, CardContent, CardHeader, CardTitle, cn } from '@meith/ui'
3
4
 
4
5
  import { NUMERIC, PAGE_BODY, Stamp } from '../shared'
5
6
 
@@ -14,7 +15,10 @@ export function MemberProfile({
14
15
  fields,
15
16
  actions,
16
17
  regions,
17
- }: MemberProfileModel) {
18
+ copy,
19
+ }: MemberProfileModel & { copy: SlotCopy }) {
20
+ const c = (key: string) => fromSlotCopy(copy, `default.memberProfile.${key}`)
21
+
18
22
  return (
19
23
  <div className={PAGE_BODY}>
20
24
  <Card>
@@ -24,10 +28,7 @@ export function MemberProfile({
24
28
 
25
29
  <div className="min-w-0">
26
30
  <h1
27
- className={cn(
28
- 'text-2xl font-semibold tracking-tight break-words',
29
- user.nameClass,
30
- )}
31
+ className={cn('text-2xl font-semibold tracking-tight break-words', user.nameClass)}
31
32
  >
32
33
  {user.username}
33
34
  </h1>
@@ -36,7 +37,10 @@ export function MemberProfile({
36
37
  </div>
37
38
 
38
39
  {actions.length > 0 && (
39
- <nav aria-label="Member actions" className="flex shrink-0 flex-wrap items-center gap-2">
40
+ <nav
41
+ aria-label={c('memberActions')}
42
+ className="flex shrink-0 flex-wrap items-center gap-2"
43
+ >
40
44
  {actions.map((action, index) => (
41
45
  <a
42
46
  key={action.href}
@@ -56,21 +60,21 @@ export function MemberProfile({
56
60
  <dl className="grid grid-cols-3 gap-3 border-t border-border px-5 py-4">
57
61
  <div>
58
62
  <dd className={`text-lg font-semibold text-foreground ${NUMERIC}`}>
59
- {postCount.toLocaleString('en')}
63
+ {postCount.label}
60
64
  </dd>
61
- <dt className="text-xs text-muted-foreground">Posts</dt>
65
+ <dt className="text-xs text-muted-foreground">{c('postsLabel')}</dt>
62
66
  </div>
63
67
  <div>
64
68
  <dd className="text-sm font-medium text-foreground">
65
69
  <Stamp at={joinedAt} />
66
70
  </dd>
67
- <dt className="text-xs text-muted-foreground">Joined</dt>
71
+ <dt className="text-xs text-muted-foreground">{c('joinedLabel')}</dt>
68
72
  </div>
69
73
  <div>
70
74
  <dd className="text-sm font-medium text-foreground">
71
- {lastVisitAt === null ? 'Never' : <Stamp at={lastVisitAt} />}
75
+ {lastVisitAt === null ? c('never') : <Stamp at={lastVisitAt} />}
72
76
  </dd>
73
- <dt className="text-xs text-muted-foreground">Last visit</dt>
77
+ <dt className="text-xs text-muted-foreground">{c('lastVisitLabel')}</dt>
74
78
  </div>
75
79
  </dl>
76
80
  </Card>
@@ -79,7 +83,7 @@ export function MemberProfile({
79
83
  <Card aria-labelledby="profile-fields-heading">
80
84
  <CardHeader>
81
85
  <CardTitle id="profile-fields-heading" className="text-sm">
82
- About
86
+ {c('about')}
83
87
  </CardTitle>
84
88
  </CardHeader>
85
89
  <CardContent>
@@ -99,7 +103,7 @@ export function MemberProfile({
99
103
  <Card aria-labelledby="profile-signature-heading">
100
104
  <CardHeader>
101
105
  <CardTitle id="profile-signature-heading" className="text-sm">
102
- Signature
106
+ {c('signature')}
103
107
  </CardTitle>
104
108
  </CardHeader>
105
109
  <CardContent>
@@ -1,12 +1,15 @@
1
- import type { NavigationModel } from '@meith/theme-kit'
1
+ import type { NavigationModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
2
3
 
3
4
  import { MUTED_LINK, PAGE } from '../shared'
4
5
 
5
- export function Navigation({ items }: NavigationModel) {
6
+ export function Navigation({ items, copy }: NavigationModel & { copy: SlotCopy }) {
6
7
  if (items.length === 0) return null
7
8
 
9
+ const c = (key: string) => fromSlotCopy(copy, `default.navigation.${key}`)
10
+
8
11
  return (
9
- <nav aria-label="Breadcrumb" className="border-b border-border bg-card/40">
12
+ <nav aria-label={c('breadcrumb')} className="border-b border-border bg-card/40">
10
13
  <ol
11
14
  className={`${PAGE} flex items-center gap-1.5 overflow-x-auto py-2 text-xs whitespace-nowrap text-muted-foreground`}
12
15
  >
@@ -16,7 +19,7 @@ export function Navigation({ items }: NavigationModel) {
16
19
  <li key={item.href} className="flex shrink-0 items-center gap-1.5">
17
20
  {index > 0 && (
18
21
  <span aria-hidden="true" className="text-border select-none">
19
- /
22
+ {c('separator')}
20
23
  </span>
21
24
  )}
22
25
  {isLast ? (
@@ -1,25 +1,28 @@
1
+ import type { NoticeModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
1
3
  import { Alert, AlertDescription, AlertTitle } from '@meith/ui'
2
- import type { NoticeModel } from '@meith/theme-kit'
3
4
 
4
5
  import { MUTED_LINK } from '../shared'
5
6
 
6
- const KIND_LABELS: Record<NoticeModel['kind'], string> = {
7
- info: 'Notice:',
8
- success: 'Done:',
9
- warning: 'Warning:',
10
- error: 'Error:',
7
+ const KIND_KEYS: Record<NoticeModel['kind'], string> = {
8
+ info: 'info',
9
+ success: 'success',
10
+ warning: 'warning',
11
+ error: 'error',
11
12
  }
12
13
 
13
- export function Notice({ kind, message, dismissHref }: NoticeModel) {
14
+ export function Notice({ kind, message, dismissHref, copy }: NoticeModel & { copy: SlotCopy }) {
15
+ const c = (key: string) => fromSlotCopy(copy, `default.notice.${key}`)
16
+
14
17
  return (
15
18
  <Alert tone={kind}>
16
19
  <AlertDescription>
17
- <AlertTitle>{KIND_LABELS[kind]}</AlertTitle> {message}
20
+ <AlertTitle>{c(KIND_KEYS[kind])}</AlertTitle> {message}
18
21
  </AlertDescription>
19
22
 
20
23
  {dismissHref !== null && (
21
24
  <a href={dismissHref} className={`shrink-0 text-xs ${MUTED_LINK}`}>
22
- Dismiss
25
+ {c('dismiss')}
23
26
  </a>
24
27
  )}
25
28
  </Alert>
@@ -1,5 +1,6 @@
1
+ import type { PaginationModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
1
3
  import { buttonVariants, cn } from '@meith/ui'
2
- import type { PaginationModel } from '@meith/theme-kit'
3
4
 
4
5
  import { NUMERIC } from '../shared'
5
6
 
@@ -10,21 +11,24 @@ export function Pagination({
10
11
  pages,
11
12
  previousHref,
12
13
  nextHref,
13
- }: PaginationModel) {
14
+ copy,
15
+ }: PaginationModel & { copy: SlotCopy }) {
14
16
  if (pageCount <= 1 && previousHref === null && nextHref === null) return null
15
17
 
18
+ const c = (key: string) => fromSlotCopy(copy, `default.pagination.${key}`)
19
+
16
20
  const step = cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'min-w-20')
17
21
  const stepDisabled = cn(step, 'pointer-events-none opacity-40')
18
22
 
19
23
  return (
20
- <nav aria-label="Pagination" className="flex flex-wrap items-center justify-between gap-3">
24
+ <nav aria-label={c('nav')} className="flex flex-wrap items-center justify-between gap-3">
21
25
  {previousHref === null ? (
22
26
  <span className={stepDisabled} aria-hidden="true">
23
- Previous
27
+ {c('previous')}
24
28
  </span>
25
29
  ) : (
26
30
  <a href={previousHref} rel="prev" className={step}>
27
- Previous
31
+ {c('previous')}
28
32
  </a>
29
33
  )}
30
34
 
@@ -37,13 +41,13 @@ export function Pagination({
37
41
  <li key={entry.page} className="flex items-center gap-1">
38
42
  {gap && (
39
43
  <span aria-hidden="true" className="px-1 text-sm text-muted-foreground">
40
-
44
+ {c('ellipsis')}
41
45
  </span>
42
46
  )}
43
47
  <a
44
48
  href={entry.href}
45
49
  aria-current={entry.isCurrent ? 'page' : undefined}
46
- aria-label={`Page ${entry.page}`}
50
+ aria-label={`${c('page')} ${entry.page}`}
47
51
  className={cn(
48
52
  buttonVariants({
49
53
  variant: entry.isCurrent ? 'primary' : 'ghost',
@@ -62,18 +66,20 @@ export function Pagination({
62
66
 
63
67
  <li className="ml-1 text-xs whitespace-nowrap text-muted-foreground">
64
68
  <span className={NUMERIC}>
65
- {pageCountIsExact ? `Page ${page} of ${pageCount}` : `Page ${page}`}
69
+ {pageCountIsExact
70
+ ? `${c('page')} ${page} ${c('of')} ${pageCount}`
71
+ : `${c('page')} ${page}`}
66
72
  </span>
67
73
  </li>
68
74
  </ol>
69
75
 
70
76
  {nextHref === null ? (
71
77
  <span className={stepDisabled} aria-hidden="true">
72
- Next
78
+ {c('next')}
73
79
  </span>
74
80
  ) : (
75
81
  <a href={nextHref} rel="next" className={step}>
76
- Next
82
+ {c('next')}
77
83
  </a>
78
84
  )}
79
85
  </nav>
@@ -1,5 +1,6 @@
1
- import { Disclosure, cn } from '@meith/ui'
2
- import type { PanelNavItemModel, PanelNavModel } from '@meith/theme-kit'
1
+ import type { PanelNavItemModel, PanelNavModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+ import { cn, Disclosure } from '@meith/ui'
3
4
 
4
5
  import { PanelIcon } from '../panel-icons'
5
6
 
@@ -11,21 +12,33 @@ const HERE =
11
12
  const OPEN = 'font-semibold text-foreground hover:bg-accent'
12
13
  const ELSEWHERE = 'text-muted-foreground hover:bg-accent hover:text-foreground'
13
14
 
14
- function Count({ count }: { count: number }) {
15
+ function Count({ count, copy }: { count: number; copy: SlotCopy }) {
16
+ const c = (key: string) => fromSlotCopy(copy, `default.panelNav.${key}`)
17
+
15
18
  return (
16
19
  <>
17
20
  <span
18
21
  aria-hidden="true"
19
22
  className="ml-auto min-w-5 rounded-full bg-primary px-1.5 py-0.5 text-center text-xs font-semibold tabular-nums text-primary-foreground"
20
23
  >
21
- {count > 99 ? '99+' : count}
24
+ {count > 99 ? c('countCap') : count}
25
+ </span>
26
+ <span className="sr-only">
27
+ ({count} {c('waiting')})
22
28
  </span>
23
- <span className="sr-only">({count} waiting)</span>
24
29
  </>
25
30
  )
26
31
  }
27
32
 
28
- function Item({ item, className }: { item: PanelNavItemModel; className: string }) {
33
+ function Item({
34
+ item,
35
+ className,
36
+ copy,
37
+ }: {
38
+ item: PanelNavItemModel
39
+ className: string
40
+ copy: SlotCopy
41
+ }) {
29
42
  const body = (
30
43
  <>
31
44
  {item.icon !== null && (
@@ -34,7 +47,7 @@ function Item({ item, className }: { item: PanelNavItemModel; className: string
34
47
  </span>
35
48
  )}
36
49
  <span className="min-w-0 flex-1 truncate">{item.title}</span>
37
- {item.count !== null && <Count count={item.count} />}
50
+ {item.count !== null && <Count count={item.count} copy={copy} />}
38
51
  </>
39
52
  )
40
53
 
@@ -53,8 +66,7 @@ function Item({ item, className }: { item: PanelNavItemModel; className: string
53
66
  {...(item.current === null
54
67
  ? {}
55
68
  : {
56
- 'aria-current':
57
- item.current === 'here' ? ('page' as const) : ('true' as const),
69
+ 'aria-current': item.current === 'here' ? ('page' as const) : ('true' as const),
58
70
  })}
59
71
  >
60
72
  {body}
@@ -62,7 +74,11 @@ function Item({ item, className }: { item: PanelNavItemModel; className: string
62
74
  )
63
75
  }
64
76
 
65
- function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>) {
77
+ function Sections({
78
+ label,
79
+ sections,
80
+ copy,
81
+ }: Pick<PanelNavModel, 'label' | 'sections'> & { copy: SlotCopy }) {
66
82
  return (
67
83
  <nav aria-label={label} className="text-sm">
68
84
  <ul className="flex flex-col gap-0.5 pl-2.5">
@@ -73,9 +89,8 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
73
89
  >
74
90
  <Item
75
91
  item={section}
76
- className={
77
- section.current === 'here' ? HERE : section.isOpen ? OPEN : ELSEWHERE
78
- }
92
+ className={section.current === 'here' ? HERE : section.isOpen ? OPEN : ELSEWHERE}
93
+ copy={copy}
79
94
  />
80
95
 
81
96
  {section.isOpen && section.children.length > 0 && (
@@ -85,6 +100,7 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
85
100
  <Item
86
101
  item={child}
87
102
  className={child.current === 'here' ? HERE : ELSEWHERE}
103
+ copy={copy}
88
104
  />
89
105
  </li>
90
106
  ))}
@@ -97,7 +113,12 @@ function Sections({ label, sections }: Pick<PanelNavModel, 'label' | 'sections'>
97
113
  )
98
114
  }
99
115
 
100
- export function PanelNav({ label, sections, currentTitle }: PanelNavModel) {
116
+ export function PanelNav({
117
+ label,
118
+ sections,
119
+ currentTitle,
120
+ copy,
121
+ }: PanelNavModel & { copy: SlotCopy }) {
101
122
  return (
102
123
  <>
103
124
  <div className="sticky top-0 z-20 -mx-4 bg-background/95 px-4 py-2 backdrop-blur-sm sm:-mx-6 sm:px-6 lg:hidden">
@@ -106,12 +127,12 @@ export function PanelNav({ label, sections, currentTitle }: PanelNavModel) {
106
127
  contentClassName="p-2"
107
128
  {...(currentTitle === null ? {} : { aside: currentTitle })}
108
129
  >
109
- <Sections label={label} sections={sections} />
130
+ <Sections label={label} sections={sections} copy={copy} />
110
131
  </Disclosure>
111
132
  </div>
112
133
 
113
134
  <div className="hidden rounded-xl border border-border bg-card p-2 shadow-elevation lg:block">
114
- <Sections label={label} sections={sections} />
135
+ <Sections label={label} sections={sections} copy={copy} />
115
136
  </div>
116
137
  </>
117
138
  )
@@ -1,9 +1,21 @@
1
+ import type { PanelPageModel, PanelSectionModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
1
3
  import { cn } from '@meith/ui'
2
- import type { PanelPageModel, PanelSectionModel } from '@meith/theme-kit'
3
4
 
4
5
  import { MUTED_LINK, PAGE, pageAt } from '../shared'
5
6
 
6
- export function PanelPage({ title, back, frame, width, gap, regions, children }: PanelPageModel) {
7
+ export function PanelPage({
8
+ title,
9
+ back,
10
+ frame,
11
+ width,
12
+ gap,
13
+ regions,
14
+ children,
15
+ copy,
16
+ }: PanelPageModel & { copy: SlotCopy }) {
17
+ const c = (key: string) => fromSlotCopy(copy, `default.panelPage.${key}`)
18
+
7
19
  return (
8
20
  <main
9
21
  id="board-content"
@@ -20,7 +32,7 @@ export function PanelPage({ title, back, frame, width, gap, regions, children }:
20
32
  <div className="flex min-w-0 flex-col gap-1">
21
33
  {back !== null && (
22
34
  <a href={back.href} className={`text-sm font-medium ${MUTED_LINK}`}>
23
- {back.label}
35
+ {c('back')} {back.label}
24
36
  </a>
25
37
  )}
26
38
 
@@ -5,13 +5,7 @@ import { MUTED_LINK } from '../shared'
5
5
  const RAIL =
6
6
  'flex flex-col gap-4 px-4 pt-4 sm:px-6 lg:sticky lg:w-60 lg:shrink-0 lg:self-start lg:py-8 lg:pr-0'
7
7
 
8
- export function PanelShell({
9
- panel,
10
- links,
11
- linksLabel,
12
- regions,
13
- children,
14
- }: PanelShellModel) {
8
+ export function PanelShell({ panel, links, linksLabel, regions, children }: PanelShellModel) {
15
9
  return (
16
10
  <div className="mx-auto flex w-full max-w-6xl flex-1 flex-col lg:flex-row">
17
11
  <aside className={`${RAIL} ${panel === 'admincp' ? 'lg:top-14' : 'lg:top-6'}`}>
@@ -1,29 +1,36 @@
1
- import { Separator, buttonVariants } from '@meith/ui'
2
- import type { PostActionsSlotModel } from '@meith/theme-kit'
1
+ import type { PostActionsSlotModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
3
+ import { buttonVariants, Separator } from '@meith/ui'
3
4
 
4
5
  interface Action {
5
6
  readonly href: string
6
7
  readonly label: string
7
8
  }
8
9
 
9
- export function PostActions({ actions, children }: PostActionsSlotModel) {
10
+ export function PostActions({
11
+ actions,
12
+ children,
13
+ copy,
14
+ }: PostActionsSlotModel & { copy: SlotCopy }) {
15
+ const c = (key: string) => fromSlotCopy(copy, `default.postActions.${key}`)
16
+
10
17
  const reader: Action[] = [
11
- actions.quoteHref === null ? null : { href: actions.quoteHref, label: 'Quote' },
12
- actions.editHref === null ? null : { href: actions.editHref, label: 'Edit' },
13
- actions.rateHref === null ? null : { href: actions.rateHref, label: 'Rate' },
14
- actions.reportHref === null ? null : { href: actions.reportHref, label: 'Report' },
18
+ actions.quoteHref === null ? null : { href: actions.quoteHref, label: c('quote') },
19
+ actions.editHref === null ? null : { href: actions.editHref, label: c('edit') },
20
+ actions.rateHref === null ? null : { href: actions.rateHref, label: c('rate') },
21
+ actions.reportHref === null ? null : { href: actions.reportHref, label: c('report') },
15
22
  ].filter((action): action is Action => action !== null)
16
23
 
17
24
  const staff: Action[] = [
18
- actions.restoreHref === null ? null : { href: actions.restoreHref, label: 'Restore' },
19
- actions.warnHref === null ? null : { href: actions.warnHref, label: 'Warn' },
20
- actions.moderateHref === null ? null : { href: actions.moderateHref, label: 'Moderate' },
25
+ actions.restoreHref === null ? null : { href: actions.restoreHref, label: c('restore') },
26
+ actions.warnHref === null ? null : { href: actions.warnHref, label: c('warn') },
27
+ actions.moderateHref === null ? null : { href: actions.moderateHref, label: c('moderate') },
21
28
  ].filter((action): action is Action => action !== null)
22
29
 
23
30
  if (reader.length === 0 && staff.length === 0 && children === undefined) return null
24
31
 
25
32
  return (
26
- <nav aria-label="Post actions" className="flex flex-wrap items-center gap-x-1 gap-y-1">
33
+ <nav aria-label={c('nav')} className="flex flex-wrap items-center gap-x-1 gap-y-1">
27
34
  {reader.map((action) => (
28
35
  <a
29
36
  key={action.href}
@@ -1,5 +1,6 @@
1
+ import type { PostBitSlotModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
1
3
  import { Alert, AlertDescription, AlertTitle, Avatar, Card } from '@meith/ui'
2
- import type { PostBitSlotModel } from '@meith/theme-kit'
3
4
 
4
5
  import { LINK, MUTED_LINK, NUMERIC, Stamp, UserRef } from '../shared'
5
6
 
@@ -9,9 +10,17 @@ const VISIBILITY_TINT = {
9
10
  deleted: 'border-thread-deleted/50 bg-destructive/5',
10
11
  } as const
11
12
 
12
- function StatusBanner({ visibility }: { visibility: PostBitSlotModel['post']['visibility'] }) {
13
+ function StatusBanner({
14
+ visibility,
15
+ copy,
16
+ }: {
17
+ visibility: PostBitSlotModel['post']['visibility']
18
+ copy: SlotCopy
19
+ }) {
13
20
  if (visibility === 'visible') return null
14
21
 
22
+ const c = (key: string) => fromSlotCopy(copy, `default.postBit.${key}`)
23
+
15
24
  return (
16
25
  <Alert
17
26
  tone={visibility === 'deleted' ? 'error' : 'warning'}
@@ -19,15 +28,19 @@ function StatusBanner({ visibility }: { visibility: PostBitSlotModel['post']['vi
19
28
  >
20
29
  <AlertDescription>
21
30
  <AlertTitle>
22
- {visibility === 'deleted' ? 'Deleted post.' : 'Waiting for approval.'}
31
+ {visibility === 'deleted' ? c('deletedPost') : c('waitingApproval')}
23
32
  </AlertTitle>{' '}
24
- Only moderators can see this.
33
+ {c('staffOnly')}
25
34
  </AlertDescription>
26
35
  </Alert>
27
36
  )
28
37
  }
29
38
 
30
- function GroupBadge({ badge }: { badge: NonNullable<PostBitSlotModel['post']['author']['badge']> }) {
39
+ function GroupBadge({
40
+ badge,
41
+ }: {
42
+ badge: NonNullable<PostBitSlotModel['post']['author']['badge']>
43
+ }) {
31
44
  const image = (
32
45
  <img
33
46
  src={badge.src}
@@ -52,10 +65,14 @@ function GroupBadge({ badge }: { badge: NonNullable<PostBitSlotModel['post']['au
52
65
  function AuthorBlock({
53
66
  author,
54
67
  badges,
68
+ copy,
55
69
  }: {
56
70
  author: PostBitSlotModel['post']['author']
57
71
  badges: React.ReactNode
72
+ copy: SlotCopy
58
73
  }) {
74
+ const c = (key: string) => fromSlotCopy(copy, `default.postBit.${key}`)
75
+
59
76
  return (
60
77
  <div className="flex gap-3 sm:flex-col sm:gap-2 sm:text-center">
61
78
  <Avatar src={author.avatarUrl} name={author.username} size={48} className="sm:self-center" />
@@ -78,7 +95,7 @@ function AuthorBlock({
78
95
  {author.isOnline && (
79
96
  <p className="mt-1 flex items-center gap-1.5 text-xs text-moderation-approved sm:justify-center">
80
97
  <span aria-hidden="true" className="size-1.5 rounded-full bg-moderation-approved" />
81
- Online
98
+ {c('online')}
82
99
  </p>
83
100
  )}
84
101
 
@@ -86,23 +103,26 @@ function AuthorBlock({
86
103
 
87
104
  <dl className={`mt-1.5 text-xs text-muted-foreground ${NUMERIC}`}>
88
105
  <div className="flex gap-1 sm:justify-center">
89
- <dt className="sr-only">Posts</dt>
106
+ <dt className="sr-only">{c('postsLabel')}</dt>
90
107
  <dd>
91
- {author.postCount.toLocaleString('en')} {author.postCount === 1 ? 'post' : 'posts'}
108
+ {author.postCount.label}{' '}
109
+ {author.postCount.value === 1 ? c('post.one') : c('post.other')}
92
110
  </dd>
93
111
  </div>
94
112
  {author.joinedAt !== null && (
95
113
  <div className="flex gap-1 sm:justify-center">
96
- <dt className="sr-only">Joined</dt>
114
+ <dt className="sr-only">{c('joined')}</dt>
97
115
  <dd>
98
- Joined <Stamp at={author.joinedAt} />
116
+ {c('joined')} <Stamp at={author.joinedAt} />
99
117
  </dd>
100
118
  </div>
101
119
  )}
102
120
  {author.reputation != null && (
103
121
  <div className="flex gap-1 sm:justify-center">
104
- <dt className="sr-only">Reputation</dt>
105
- <dd>{author.reputation.toLocaleString('en')} reputation</dd>
122
+ <dt className="sr-only">{c('reputationLabel')}</dt>
123
+ <dd>
124
+ {author.reputation.label} {c('reputation')}
125
+ </dd>
106
126
  </div>
107
127
  )}
108
128
  </dl>
@@ -122,7 +142,9 @@ function AuthorBlock({
122
142
  )
123
143
  }
124
144
 
125
- export function PostBit({ post, select, regions }: PostBitSlotModel) {
145
+ export function PostBit({ post, select, regions, copy }: PostBitSlotModel & { copy: SlotCopy }) {
146
+ const c = (key: string) => fromSlotCopy(copy, `default.postBit.${key}`)
147
+
126
148
  return (
127
149
  <Card
128
150
  as="article"
@@ -131,11 +153,11 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
131
153
  data-visibility={post.visibility}
132
154
  className={VISIBILITY_TINT[post.visibility]}
133
155
  >
134
- <StatusBanner visibility={post.visibility} />
156
+ <StatusBanner visibility={post.visibility} copy={copy} />
135
157
 
136
158
  <div className="grid grid-cols-1 sm:grid-cols-[11rem_minmax(0,1fr)]">
137
159
  <div className="border-b border-border px-4 py-3 sm:border-r sm:border-b-0 sm:bg-muted/40">
138
- <AuthorBlock author={post.author} badges={regions.pluginBadges} />
160
+ <AuthorBlock author={post.author} badges={regions.pluginBadges} copy={copy} />
139
161
  </div>
140
162
 
141
163
  <div className="min-w-0">
@@ -163,11 +185,11 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
163
185
 
164
186
  {post.ignored !== null ? (
165
187
  <div className="px-4 py-5 text-sm text-muted-foreground">
166
- You are ignoring{' '}
167
- <span className="font-medium text-foreground">{post.ignored.authorUsername}</span>.
168
- This post is hidden.{' '}
188
+ {c('ignoringPrefix')}{' '}
189
+ <span className="font-medium text-foreground">{post.ignored.authorUsername}</span>.{' '}
190
+ {c('hiddenNotice')}{' '}
169
191
  <a href={post.ignored.revealHref} className={`font-medium text-foreground ${LINK}`}>
170
- Show it anyway
192
+ {c('showAnyway')}
171
193
  </a>
172
194
  </div>
173
195
  ) : (
@@ -189,7 +211,7 @@ export function PostBit({ post, select, regions }: PostBitSlotModel) {
189
211
  <div className="border-t border-border px-4 py-3">
190
212
  <h4 className="mb-2 text-xs font-semibold tracking-wide text-muted-foreground uppercase">
191
213
  {post.attachments.length}{' '}
192
- {post.attachments.length === 1 ? 'attachment' : 'attachments'}
214
+ {post.attachments.length === 1 ? c('attachment.one') : c('attachment.other')}
193
215
  </h4>
194
216
  <ul className="flex flex-wrap gap-3">
195
217
  {post.attachments.map((file) => (
@@ -1,5 +1,6 @@
1
+ import type { PostFormModel, SlotCopy } from '@meith/theme-kit'
2
+ import { fromSlotCopy } from '@meith/theme-kit'
1
3
  import { Alert, AlertDescription, AlertTitle, Card } from '@meith/ui'
2
- import type { PostFormModel } from '@meith/theme-kit'
3
4
 
4
5
  import { MUTED_LINK, pageAt } from '../shared'
5
6
 
@@ -9,7 +10,10 @@ export function PostForm({
9
10
  cancelLabel,
10
11
  errorMessage,
11
12
  regions,
12
- }: PostFormModel) {
13
+ copy,
14
+ }: PostFormModel & { copy: SlotCopy }) {
15
+ const c = (key: string) => fromSlotCopy(copy, `default.postForm.${key}`)
16
+
13
17
  return (
14
18
  <div className={`${pageAt('max-w-3xl')} flex w-full flex-col gap-5 py-6 sm:py-8`}>
15
19
  <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-2">
@@ -22,7 +26,7 @@ export function PostForm({
22
26
  {errorMessage !== null && (
23
27
  <Alert tone="error">
24
28
  <AlertDescription>
25
- <AlertTitle>Cannot post.</AlertTitle> {errorMessage}
29
+ <AlertTitle>{c('cannotPost')}</AlertTitle> {errorMessage}
26
30
  </AlertDescription>
27
31
  </Alert>
28
32
  )}