@kernhq/module-quire 0.10.1 → 0.10.2
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 +1 -1
- package/src/client/components/CommentsPanel.svelte +73 -12
- package/src/client/components/NewSpaceDialog.svelte +1 -0
- package/src/client/components/PageTreeRow.svelte +55 -27
- package/src/client/components/SidebarSpaces.svelte +5 -0
- package/src/client/database/BoardView.svelte +6 -2
- package/src/client/database/PropertyDialog.svelte +6 -0
- package/src/client/database/ViewDialog.svelte +12 -1
- package/src/client/i18n.ts +35 -15
package/package.json
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Avatar,
|
|
4
|
+
Button,
|
|
5
|
+
coreApi,
|
|
6
|
+
EmptyState,
|
|
7
|
+
Icon,
|
|
8
|
+
IconButton,
|
|
9
|
+
keys,
|
|
10
|
+
relativeTime,
|
|
11
|
+
Skeleton,
|
|
12
|
+
session,
|
|
13
|
+
} from '@kernhq/ui'
|
|
3
14
|
import { RichTextEditor } from '@kernhq/ui/editor'
|
|
4
15
|
import { createQuery, useQueryClient } from '@tanstack/svelte-query'
|
|
5
16
|
import { getQuireApi } from '../api-instance.js'
|
|
17
|
+
import { type CoreApi, toPerson } from '../core-api.js'
|
|
6
18
|
import { t } from '../i18n.js'
|
|
7
19
|
import type { CommentThread } from '../index.js'
|
|
8
20
|
import { canQuire } from '../permissions.js'
|
|
@@ -33,6 +45,7 @@ interface Props {
|
|
|
33
45
|
const { workspaceId, pageId, activeId, orphaned, onFocus, pending, onPendingHandled }: Props = $props()
|
|
34
46
|
|
|
35
47
|
const api = getQuireApi()
|
|
48
|
+
const core = coreApi<CoreApi>()
|
|
36
49
|
const client = useQueryClient()
|
|
37
50
|
|
|
38
51
|
const query = createQuery(() => ({
|
|
@@ -42,6 +55,26 @@ const query = createQuery(() => ({
|
|
|
42
55
|
}))
|
|
43
56
|
const threads = $derived(query.data ?? [])
|
|
44
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Who said it.
|
|
60
|
+
*
|
|
61
|
+
* A comment carries an author id and nothing else, so without this the margin reads "3h ago" over
|
|
62
|
+
* an unlettered square — a conversation where nobody can tell who is speaking, and an avatar a
|
|
63
|
+
* screen reader passes over in silence. The same member list the database's person cells use.
|
|
64
|
+
*/
|
|
65
|
+
const membersQuery = createQuery(() => ({
|
|
66
|
+
queryKey: keys.members(workspaceId),
|
|
67
|
+
enabled: Boolean(workspaceId),
|
|
68
|
+
queryFn: () => core.workspaces.members.list({ workspaceId, limit: 200 }),
|
|
69
|
+
}))
|
|
70
|
+
const nameOf = $derived.by(() => {
|
|
71
|
+
const byId = new Map((membersQuery.data?.items ?? []).map(toPerson).map((p) => [p.id, p]))
|
|
72
|
+
// `authorId` is nullable — a comment left by somebody since removed from the workspace — and an
|
|
73
|
+
// unresolved id reads the same way, so both land on the same honest placeholder.
|
|
74
|
+
return (id: string | null) =>
|
|
75
|
+
(id ? byId.get(id) : undefined) ?? { id: id ?? '', name: t('comment_someone'), avatarUrl: null }
|
|
76
|
+
})
|
|
77
|
+
|
|
45
78
|
let draft = $state<unknown>(undefined)
|
|
46
79
|
let replyTo = $state<string | null>(null)
|
|
47
80
|
let replyDraft = $state<unknown>(undefined)
|
|
@@ -112,7 +145,17 @@ async function remove(commentId: string) {
|
|
|
112
145
|
{#if pending.quotedText}
|
|
113
146
|
<p class="quoted">“{pending.quotedText}”</p>
|
|
114
147
|
{/if}
|
|
115
|
-
|
|
148
|
+
<!--
|
|
149
|
+
`label`, because a rich-text field is a `contenteditable` div: without one a screen reader
|
|
150
|
+
announces "edit text" and nothing about what is being written. The placeholder is a prompt,
|
|
151
|
+
not a name — it disappears the moment somebody types.
|
|
152
|
+
-->
|
|
153
|
+
<RichTextEditor
|
|
154
|
+
bind:value={draft}
|
|
155
|
+
label={t('comment_field')}
|
|
156
|
+
placeholder={t('comment_placeholder')}
|
|
157
|
+
minRows={2}
|
|
158
|
+
/>
|
|
116
159
|
<div class="actions">
|
|
117
160
|
<Button size="sm" variant="secondary" onclick={() => onPendingHandled?.()}>{t('cancel')}</Button>
|
|
118
161
|
<Button size="sm" disabled={busy || empty(draft)} onclick={() => submit(null, draft)}>
|
|
@@ -136,18 +179,22 @@ async function remove(commentId: string) {
|
|
|
136
179
|
/>
|
|
137
180
|
{:else}
|
|
138
181
|
{#each threads as thread (thread.id)}
|
|
182
|
+
<!--
|
|
183
|
+
Not a `role="button"`, though it was one.
|
|
184
|
+
A thread holds a delete, a reply and a resolve, and a button's accessible name is its
|
|
185
|
+
contents — so a screen reader announced the whole conversation, its timestamps and the
|
|
186
|
+
labels of the three controls inside it, as the name of one button, and offered those
|
|
187
|
+
controls inside it anyway. Highlighting the anchored text is a *side effect* of arriving
|
|
188
|
+
here, so it happens on `focusin` for the keyboard and on click for the pointer, and the
|
|
189
|
+
thread stays a plain container with nothing to announce.
|
|
190
|
+
-->
|
|
191
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
192
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
139
193
|
<div
|
|
140
194
|
class="thread"
|
|
141
195
|
class:active={thread.id === activeId}
|
|
142
|
-
role="button"
|
|
143
|
-
tabindex="0"
|
|
144
196
|
onclick={() => onFocus?.(thread.id)}
|
|
145
|
-
|
|
146
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
147
|
-
e.preventDefault()
|
|
148
|
-
onFocus?.(thread.id)
|
|
149
|
-
}
|
|
150
|
-
}}
|
|
197
|
+
onfocusin={() => onFocus?.(thread.id)}
|
|
151
198
|
>
|
|
152
199
|
{#if thread.root.quotedText}
|
|
153
200
|
<p class="quoted" class:orphan={orphaned.has(thread.id)}>“{thread.root.quotedText}”</p>
|
|
@@ -157,10 +204,12 @@ async function remove(commentId: string) {
|
|
|
157
204
|
{/if}
|
|
158
205
|
|
|
159
206
|
{#each [thread.root, ...thread.replies] as comment (comment.id)}
|
|
207
|
+
{@const author = nameOf(comment.authorId)}
|
|
160
208
|
<div class="comment">
|
|
161
|
-
<Avatar id={
|
|
209
|
+
<Avatar id={author.id} name={author.name} src={author.avatarUrl} size={22} />
|
|
162
210
|
<div class="bubble">
|
|
163
211
|
<div class="who">
|
|
212
|
+
<span class="author">{author.name}</span>
|
|
164
213
|
<span class="time">{relativeTime(comment.createdAt)}</span>
|
|
165
214
|
{#if comment.editedAt}<span class="edited">{t('comment_edited')}</span>{/if}
|
|
166
215
|
{#if comment.authorId === session.user?.id}
|
|
@@ -181,7 +230,12 @@ async function remove(commentId: string) {
|
|
|
181
230
|
|
|
182
231
|
<div class="thread-actions">
|
|
183
232
|
{#if replyTo === thread.id}
|
|
184
|
-
<RichTextEditor
|
|
233
|
+
<RichTextEditor
|
|
234
|
+
bind:value={replyDraft}
|
|
235
|
+
label={t('comment_reply_field')}
|
|
236
|
+
placeholder={t('comment_reply')}
|
|
237
|
+
minRows={1}
|
|
238
|
+
/>
|
|
185
239
|
<div class="actions">
|
|
186
240
|
<Button size="sm" variant="secondary" onclick={() => (replyTo = null)}>{t('cancel')}</Button>
|
|
187
241
|
<Button
|
|
@@ -271,6 +325,13 @@ async function remove(commentId: string) {
|
|
|
271
325
|
font-size: 12px;
|
|
272
326
|
color: var(--kern-ink-400);
|
|
273
327
|
}
|
|
328
|
+
.author {
|
|
329
|
+
font-weight: 600;
|
|
330
|
+
color: var(--kern-ink-700);
|
|
331
|
+
overflow: hidden;
|
|
332
|
+
text-overflow: ellipsis;
|
|
333
|
+
white-space: nowrap;
|
|
334
|
+
}
|
|
274
335
|
.spacer {
|
|
275
336
|
flex: 1;
|
|
276
337
|
}
|
|
@@ -32,6 +32,24 @@ const indent = $derived(Math.min(depth, 2) as 0 | 1 | 2)
|
|
|
32
32
|
</script>
|
|
33
33
|
|
|
34
34
|
<div class="row">
|
|
35
|
+
<!--
|
|
36
|
+
Disclosure first, row second, add last — DOM order *is* tab order, and both of the other two are
|
|
37
|
+
positioned over the row rather than laid out in it. Written the other way round the keyboard
|
|
38
|
+
reached a page's chevron only after passing the page itself, which is not where it is drawn.
|
|
39
|
+
-->
|
|
40
|
+
{#if node.hasChildren}
|
|
41
|
+
<button
|
|
42
|
+
class="twisty"
|
|
43
|
+
type="button"
|
|
44
|
+
style:inset-inline-start="{indent * 22 - 4}px"
|
|
45
|
+
aria-label={isOpen ? t('collapse', { title }) : t('expand', { title })}
|
|
46
|
+
aria-expanded={isOpen}
|
|
47
|
+
onclick={() => onToggle(node.id)}
|
|
48
|
+
>
|
|
49
|
+
<span class:open={isOpen}><Icon name="chevron-right" size={11} strokeWidth={2} /></span>
|
|
50
|
+
</button>
|
|
51
|
+
{/if}
|
|
52
|
+
|
|
35
53
|
<SidebarItem
|
|
36
54
|
label={title}
|
|
37
55
|
icon={node.kind === 'live' ? 'square-pen' : node.kind === 'database' ? 'database' : 'file-text'}
|
|
@@ -43,34 +61,27 @@ const indent = $derived(Math.min(depth, 2) as 0 | 1 | 2)
|
|
|
43
61
|
{#if node.archivedAt}
|
|
44
62
|
<span class="flag" title={t('archived')}><Icon name="archive" size={12} /></span>
|
|
45
63
|
{/if}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
e.stopPropagation()
|
|
55
|
-
onCreateChild(node.id)
|
|
56
|
-
}}
|
|
57
|
-
/>
|
|
58
|
-
</span>
|
|
59
|
-
{/if}
|
|
64
|
+
<!--
|
|
65
|
+
Room for the add button, which is not in here. `SidebarItem` is a `<button>`, so anything
|
|
66
|
+
interactive inside it is a button inside a button: invalid, and the row's accessible name
|
|
67
|
+
becomes "Welcome New page inside Welcome" because the name of a button is its contents.
|
|
68
|
+
The control is a sibling laid over the row's trailing edge instead, and this reserves the
|
|
69
|
+
width so a long title still ellipsises before it reaches the button.
|
|
70
|
+
-->
|
|
71
|
+
{#if canCreate}<span class="add-well"></span>{/if}
|
|
60
72
|
{/snippet}
|
|
61
73
|
</SidebarItem>
|
|
62
74
|
|
|
63
|
-
{#if
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
</button>
|
|
75
|
+
{#if canCreate}
|
|
76
|
+
<span class="add">
|
|
77
|
+
<IconButton
|
|
78
|
+
icon="plus"
|
|
79
|
+
size={22}
|
|
80
|
+
variant="ghost"
|
|
81
|
+
label={t('new_child_page', { title })}
|
|
82
|
+
onclick={() => onCreateChild(node.id)}
|
|
83
|
+
/>
|
|
84
|
+
</span>
|
|
74
85
|
{/if}
|
|
75
86
|
</div>
|
|
76
87
|
|
|
@@ -124,20 +135,37 @@ const indent = $derived(Math.min(depth, 2) as 0 | 1 | 2)
|
|
|
124
135
|
.twisty span.open {
|
|
125
136
|
transform: rotate(90deg);
|
|
126
137
|
}
|
|
127
|
-
/*
|
|
138
|
+
/*
|
|
139
|
+
* A closed chevron points along the reading direction, so it mirrors with the document. An open
|
|
140
|
+
* one points *down*, at the children it revealed — which is the same direction in both, and not
|
|
141
|
+
* something mirroring produces: rotating the other way turned it upside down in Persian.
|
|
142
|
+
*/
|
|
128
143
|
:global([dir='rtl']) .twisty span {
|
|
129
144
|
transform: scaleX(-1);
|
|
130
145
|
}
|
|
131
146
|
:global([dir='rtl']) .twisty span.open {
|
|
132
|
-
transform: rotate(
|
|
147
|
+
transform: rotate(90deg);
|
|
133
148
|
}
|
|
134
149
|
.flag {
|
|
135
150
|
display: inline-flex;
|
|
136
151
|
color: var(--kern-ink-400);
|
|
137
152
|
flex: none;
|
|
138
153
|
}
|
|
154
|
+
/*
|
|
155
|
+
* Laid over the row's trailing edge rather than inside it, for the reason in the markup above.
|
|
156
|
+
* `opacity: 0` rather than `display: none` is what keeps it focusable, and `:focus-within` on the
|
|
157
|
+
* row is what makes it visible once the keyboard arrives.
|
|
158
|
+
*/
|
|
139
159
|
.add {
|
|
160
|
+
position: absolute;
|
|
161
|
+
inset-block-start: 50%;
|
|
162
|
+
inset-inline-end: 8px;
|
|
163
|
+
transform: translateY(-50%);
|
|
164
|
+
display: inline-flex;
|
|
140
165
|
opacity: 0;
|
|
166
|
+
}
|
|
167
|
+
.add-well {
|
|
168
|
+
width: 22px;
|
|
141
169
|
flex: none;
|
|
142
170
|
}
|
|
143
171
|
.row:hover .add,
|
|
@@ -134,8 +134,13 @@ async function createPage(parentId: string | null) {
|
|
|
134
134
|
<div class="wrap">
|
|
135
135
|
{#if spaceList.length > 1}
|
|
136
136
|
<div class="switcher">
|
|
137
|
+
<!--
|
|
138
|
+
`ariaLabel`, because the trigger otherwise falls back to the placeholder and announces
|
|
139
|
+
itself as "Select…" — which names nothing. The chosen space is read after the label.
|
|
140
|
+
-->
|
|
137
141
|
<Select
|
|
138
142
|
value={activeSpace?.key ?? ''}
|
|
143
|
+
ariaLabel={t('space_choose')}
|
|
139
144
|
options={spaceList.map((space) => ({ value: space.key, label: space.name }))}
|
|
140
145
|
onValueChange={(v: string) => switchSpace(v)}
|
|
141
146
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { DropdownMenu, EmptyState, Icon, IconButton, type MenuItem } from '@kernhq/ui'
|
|
2
|
+
import { DropdownMenu, EmptyState, formatCount, Icon, IconButton, type MenuItem } from '@kernhq/ui'
|
|
3
3
|
import { untrack } from 'svelte'
|
|
4
4
|
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action'
|
|
5
5
|
import type { Database, Property, Row, View } from '../../contract/index.js'
|
|
@@ -152,7 +152,11 @@ const laneRule = (lane: Lane) =>
|
|
|
152
152
|
{:else}
|
|
153
153
|
<span class="none">{t('db_board_uncategorised')}</span>
|
|
154
154
|
{/if}
|
|
155
|
-
|
|
155
|
+
<!--
|
|
156
|
+
Through `formatCount`, not interpolated raw: a bare number is the one untranslated thing
|
|
157
|
+
left on a Persian board, where every other digit on the screen is ۰-۹.
|
|
158
|
+
-->
|
|
159
|
+
<span class="count">{formatCount(lane.rows.length, 999)}</span>
|
|
156
160
|
<span class="sp"></span>
|
|
157
161
|
{#if canCreate}
|
|
158
162
|
<IconButton
|
|
@@ -171,6 +171,7 @@ $effect(() => {
|
|
|
171
171
|
<Field label={t('db_property_type')} hint={property ? t('db_property_retype_hint') : undefined}>
|
|
172
172
|
{#snippet children(id: string)}
|
|
173
173
|
<Select
|
|
174
|
+
ariaLabel={t('db_property_type')}
|
|
174
175
|
{id}
|
|
175
176
|
value={type}
|
|
176
177
|
options={CREATABLE_TYPES.map((option) => ({
|
|
@@ -243,6 +244,7 @@ $effect(() => {
|
|
|
243
244
|
<Field label={t('db_number_format')}>
|
|
244
245
|
{#snippet children(id: string)}
|
|
245
246
|
<Select
|
|
247
|
+
ariaLabel={t('db_number_format')}
|
|
246
248
|
{id}
|
|
247
249
|
value={config.format ?? 'plain'}
|
|
248
250
|
options={['plain', 'percent', 'currency'].map((f) => ({ value: f, label: t(`db_format_${f}`) }))}
|
|
@@ -287,6 +289,7 @@ $effect(() => {
|
|
|
287
289
|
<Field label={t('db_relation_database')} hint={t('db_relation_database_hint')}>
|
|
288
290
|
{#snippet children(id: string)}
|
|
289
291
|
<Select
|
|
292
|
+
ariaLabel={t('db_relation_database')}
|
|
290
293
|
{id}
|
|
291
294
|
value={config.relationDatabaseId ?? ''}
|
|
292
295
|
placeholder={t('db_relation_choose')}
|
|
@@ -303,6 +306,7 @@ $effect(() => {
|
|
|
303
306
|
<Field label={t('db_rollup_relation')}>
|
|
304
307
|
{#snippet children(id: string)}
|
|
305
308
|
<Select
|
|
309
|
+
ariaLabel={t('db_rollup_relation')}
|
|
306
310
|
{id}
|
|
307
311
|
value={config.rollupRelationPropertyId ?? ''}
|
|
308
312
|
placeholder={t('db_rollup_choose')}
|
|
@@ -318,6 +322,7 @@ $effect(() => {
|
|
|
318
322
|
<Field label={t('db_rollup_target')} hint={viaDatabaseId ? undefined : t('db_rollup_target_hint')}>
|
|
319
323
|
{#snippet children(id: string)}
|
|
320
324
|
<Select
|
|
325
|
+
ariaLabel={t('db_rollup_target')}
|
|
321
326
|
{id}
|
|
322
327
|
value={config.rollupTargetPropertyId ?? ''}
|
|
323
328
|
placeholder={t('db_rollup_choose')}
|
|
@@ -330,6 +335,7 @@ $effect(() => {
|
|
|
330
335
|
<Field label={t('db_rollup_function')}>
|
|
331
336
|
{#snippet children(id: string)}
|
|
332
337
|
<Select
|
|
338
|
+
ariaLabel={t('db_rollup_function')}
|
|
333
339
|
{id}
|
|
334
340
|
value={config.rollupFunction ?? 'count'}
|
|
335
341
|
options={ROLLUP_FUNCTIONS.map((fn) => ({ value: fn, label: t(`db_rollup_${fn}`) }))}
|
|
@@ -89,6 +89,7 @@ $effect(() => {
|
|
|
89
89
|
<Field label={t('db_view_kind')}>
|
|
90
90
|
{#snippet children(id: string)}
|
|
91
91
|
<Select
|
|
92
|
+
ariaLabel={t('db_view_kind')}
|
|
92
93
|
{id}
|
|
93
94
|
value={kind}
|
|
94
95
|
options={VIEW_KINDS.map((v) => ({ value: v.kind, label: t(`db_kind_${v.kind}`), icon: v.icon }))}
|
|
@@ -101,6 +102,7 @@ $effect(() => {
|
|
|
101
102
|
<Field label={t('db_group_by')} hint={groupable.length === 0 ? t('db_group_needs_column') : undefined}>
|
|
102
103
|
{#snippet children(id: string)}
|
|
103
104
|
<Select
|
|
105
|
+
ariaLabel={t('db_group_by')}
|
|
104
106
|
{id}
|
|
105
107
|
value={config.groupBy ?? ''}
|
|
106
108
|
placeholder={t('db_group_none')}
|
|
@@ -116,6 +118,7 @@ $effect(() => {
|
|
|
116
118
|
<Field label={t('db_date_property')} hint={datable.length === 0 ? t('db_date_needs_column') : undefined}>
|
|
117
119
|
{#snippet children(id: string)}
|
|
118
120
|
<Select
|
|
121
|
+
ariaLabel={t('db_date_property')}
|
|
119
122
|
{id}
|
|
120
123
|
value={config.dateProperty ?? ''}
|
|
121
124
|
placeholder={t('db_date_choose')}
|
|
@@ -130,12 +133,20 @@ $effect(() => {
|
|
|
130
133
|
{#if kind === 'gallery'}
|
|
131
134
|
<Field label={t('db_cover_property')} hint={t('db_cover_unsupported')}>
|
|
132
135
|
{#snippet children(id: string)}
|
|
133
|
-
<Select
|
|
136
|
+
<Select
|
|
137
|
+
ariaLabel={t('db_cover_property')}
|
|
138
|
+
{id}
|
|
139
|
+
value=""
|
|
140
|
+
disabled
|
|
141
|
+
placeholder={t('db_cover_none')}
|
|
142
|
+
options={[]}
|
|
143
|
+
/>
|
|
134
144
|
{/snippet}
|
|
135
145
|
</Field>
|
|
136
146
|
<Field label={t('db_card_size')}>
|
|
137
147
|
{#snippet children(id: string)}
|
|
138
148
|
<Select
|
|
149
|
+
ariaLabel={t('db_card_size')}
|
|
139
150
|
{id}
|
|
140
151
|
value={config.cardSize}
|
|
141
152
|
options={(['small', 'medium', 'large'] as const).map((s) => ({
|
package/src/client/i18n.ts
CHANGED
|
@@ -16,12 +16,15 @@ export const en: Record<string, Message> = {
|
|
|
16
16
|
'quire.archived': 'Archived',
|
|
17
17
|
'quire.cmd_new_space': 'New space',
|
|
18
18
|
'quire.cmd_open': 'Open Quire',
|
|
19
|
-
'quire.collapse': 'Collapse',
|
|
19
|
+
'quire.collapse': 'Collapse {title}',
|
|
20
20
|
'quire.comment_edited': 'edited',
|
|
21
|
+
'quire.comment_field': 'Your comment',
|
|
21
22
|
'quire.comment_orphaned': 'The text this was about has been deleted',
|
|
22
23
|
'quire.comment_placeholder': 'Ask about this…',
|
|
23
24
|
'quire.comment_post': 'Comment',
|
|
24
25
|
'quire.comment_reply': 'Reply',
|
|
26
|
+
'quire.comment_reply_field': 'Your reply',
|
|
27
|
+
'quire.comment_someone': 'Someone',
|
|
25
28
|
'quire.comment_resolve': 'Resolve',
|
|
26
29
|
'quire.comments': 'Comments',
|
|
27
30
|
'quire.comments_empty': 'No comments yet',
|
|
@@ -33,7 +36,7 @@ export const en: Record<string, Message> = {
|
|
|
33
36
|
'quire.editor_no_session': 'You are not signed in',
|
|
34
37
|
'quire.editor_no_session_desc': 'Sign in again to write in this page.',
|
|
35
38
|
'quire.editor_placeholder': 'Write something…',
|
|
36
|
-
'quire.expand': 'Expand',
|
|
39
|
+
'quire.expand': 'Expand {title}',
|
|
37
40
|
'quire.history': 'Version history',
|
|
38
41
|
'quire.history_empty': 'Nothing saved yet',
|
|
39
42
|
'quire.history_empty_desc': 'Versions are kept as the page is written, and whenever it is published.',
|
|
@@ -41,7 +44,7 @@ export const en: Record<string, Message> = {
|
|
|
41
44
|
'quire.kind_live': 'Live doc',
|
|
42
45
|
'quire.move_to_trash': 'Move to trash',
|
|
43
46
|
'quire.nav': 'Quire',
|
|
44
|
-
'quire.new_child_page': 'New page inside
|
|
47
|
+
'quire.new_child_page': 'New page inside {title}',
|
|
45
48
|
'quire.new_page': 'New page',
|
|
46
49
|
'quire.new_space': 'New space',
|
|
47
50
|
'quire.new_space_desc': 'A space holds a tree of pages, with its own members and its own permissions.',
|
|
@@ -65,6 +68,7 @@ export const en: Record<string, Message> = {
|
|
|
65
68
|
'quire.search_none': 'Nothing in this space matches.',
|
|
66
69
|
'quire.search_results': 'Results',
|
|
67
70
|
'quire.search_space': 'Search this space',
|
|
71
|
+
'quire.space_choose': 'Space',
|
|
68
72
|
'quire.space_description': 'Description',
|
|
69
73
|
'quire.space_empty': 'This space has no pages',
|
|
70
74
|
'quire.space_empty_desc': 'Create the first page and start writing.',
|
|
@@ -348,12 +352,15 @@ const ar: Record<string, Message> = {
|
|
|
348
352
|
'quire.archived': 'مؤرشفة',
|
|
349
353
|
'quire.cmd_new_space': 'مساحة جديدة',
|
|
350
354
|
'quire.cmd_open': 'فتح Quire',
|
|
351
|
-
'quire.collapse': 'طي',
|
|
355
|
+
'quire.collapse': 'طي {title}',
|
|
352
356
|
'quire.comment_edited': 'مُعدّل',
|
|
357
|
+
'quire.comment_field': 'تعليقك',
|
|
353
358
|
'quire.comment_orphaned': 'حُذف النص الذي كان هذا بشأنه',
|
|
354
359
|
'quire.comment_placeholder': 'اسأل عن هذا…',
|
|
355
360
|
'quire.comment_post': 'تعليق',
|
|
356
361
|
'quire.comment_reply': 'رد',
|
|
362
|
+
'quire.comment_reply_field': 'ردّك',
|
|
363
|
+
'quire.comment_someone': 'شخص ما',
|
|
357
364
|
'quire.comment_resolve': 'إغلاق',
|
|
358
365
|
'quire.comments': 'التعليقات',
|
|
359
366
|
'quire.comments_empty': 'لا توجد تعليقات بعد',
|
|
@@ -364,7 +371,7 @@ const ar: Record<string, Message> = {
|
|
|
364
371
|
'quire.editor_no_session': 'لم تسجّل الدخول',
|
|
365
372
|
'quire.editor_no_session_desc': 'سجّل الدخول مرة أخرى للكتابة في هذه الصفحة.',
|
|
366
373
|
'quire.editor_placeholder': 'اكتب شيئًا…',
|
|
367
|
-
'quire.expand': 'توسيع',
|
|
374
|
+
'quire.expand': 'توسيع {title}',
|
|
368
375
|
'quire.history': 'سجل النسخ',
|
|
369
376
|
'quire.history_empty': 'لم يُحفظ شيء بعد',
|
|
370
377
|
'quire.history_empty_desc': 'تُحفظ النسخ أثناء كتابة الصفحة، وعند كل نشر.',
|
|
@@ -372,7 +379,7 @@ const ar: Record<string, Message> = {
|
|
|
372
379
|
'quire.kind_live': 'مستند حي',
|
|
373
380
|
'quire.move_to_trash': 'نقل إلى المهملات',
|
|
374
381
|
'quire.nav': 'Quire',
|
|
375
|
-
'quire.new_child_page': 'صفحة جديدة داخل
|
|
382
|
+
'quire.new_child_page': 'صفحة جديدة داخل {title}',
|
|
376
383
|
'quire.new_page': 'صفحة جديدة',
|
|
377
384
|
'quire.new_space': 'مساحة جديدة',
|
|
378
385
|
'quire.new_space_desc': 'تحتوي كل مساحة على شجرة من الصفحات، بأعضائها وصلاحياتها الخاصة.',
|
|
@@ -400,6 +407,7 @@ const ar: Record<string, Message> = {
|
|
|
400
407
|
'quire.search_none': 'لا شيء في هذه المساحة يطابق البحث.',
|
|
401
408
|
'quire.search_results': 'النتائج',
|
|
402
409
|
'quire.search_space': 'ابحث في هذه المساحة',
|
|
410
|
+
'quire.space_choose': 'المساحة',
|
|
403
411
|
'quire.space_description': 'الوصف',
|
|
404
412
|
'quire.space_empty': 'لا تحتوي هذه المساحة على صفحات',
|
|
405
413
|
'quire.space_empty_desc': 'أنشئ الصفحة الأولى وابدأ الكتابة.',
|
|
@@ -691,12 +699,15 @@ const de: Record<string, Message> = {
|
|
|
691
699
|
'quire.archived': 'Archiviert',
|
|
692
700
|
'quire.cmd_new_space': 'Neuer Bereich',
|
|
693
701
|
'quire.cmd_open': 'Quire öffnen',
|
|
694
|
-
'quire.collapse': '
|
|
702
|
+
'quire.collapse': '{title} zuklappen',
|
|
695
703
|
'quire.comment_edited': 'bearbeitet',
|
|
704
|
+
'quire.comment_field': 'Dein Kommentar',
|
|
696
705
|
'quire.comment_orphaned': 'Der Text, um den es hier ging, wurde gelöscht',
|
|
697
706
|
'quire.comment_placeholder': 'Frag dazu etwas…',
|
|
698
707
|
'quire.comment_post': 'Kommentieren',
|
|
699
708
|
'quire.comment_reply': 'Antworten',
|
|
709
|
+
'quire.comment_reply_field': 'Deine Antwort',
|
|
710
|
+
'quire.comment_someone': 'Jemand',
|
|
700
711
|
'quire.comment_resolve': 'Erledigen',
|
|
701
712
|
'quire.comments': 'Kommentare',
|
|
702
713
|
'quire.comments_empty': 'Noch keine Kommentare',
|
|
@@ -708,7 +719,7 @@ const de: Record<string, Message> = {
|
|
|
708
719
|
'quire.editor_no_session': 'Du bist nicht angemeldet',
|
|
709
720
|
'quire.editor_no_session_desc': 'Melde dich erneut an, um in dieser Seite zu schreiben.',
|
|
710
721
|
'quire.editor_placeholder': 'Schreib etwas…',
|
|
711
|
-
'quire.expand': '
|
|
722
|
+
'quire.expand': '{title} aufklappen',
|
|
712
723
|
'quire.history': 'Versionsverlauf',
|
|
713
724
|
'quire.history_empty': 'Noch nichts gespeichert',
|
|
714
725
|
'quire.history_empty_desc': 'Versionen entstehen beim Schreiben und bei jeder Veröffentlichung.',
|
|
@@ -716,7 +727,7 @@ const de: Record<string, Message> = {
|
|
|
716
727
|
'quire.kind_live': 'Live-Dokument',
|
|
717
728
|
'quire.move_to_trash': 'In den Papierkorb',
|
|
718
729
|
'quire.nav': 'Quire',
|
|
719
|
-
'quire.new_child_page': 'Neue Seite
|
|
730
|
+
'quire.new_child_page': 'Neue Seite in {title}',
|
|
720
731
|
'quire.new_page': 'Neue Seite',
|
|
721
732
|
'quire.new_space': 'Neuer Bereich',
|
|
722
733
|
'quire.new_space_desc':
|
|
@@ -741,6 +752,7 @@ const de: Record<string, Message> = {
|
|
|
741
752
|
'quire.search_none': 'Nichts in diesem Bereich passt.',
|
|
742
753
|
'quire.search_results': 'Ergebnisse',
|
|
743
754
|
'quire.search_space': 'In diesem Bereich suchen',
|
|
755
|
+
'quire.space_choose': 'Bereich',
|
|
744
756
|
'quire.space_description': 'Beschreibung',
|
|
745
757
|
'quire.space_empty': 'Dieser Bereich hat keine Seiten',
|
|
746
758
|
'quire.space_empty_desc': 'Lege die erste Seite an und schreib los.',
|
|
@@ -1026,12 +1038,15 @@ const fa: Record<string, Message> = {
|
|
|
1026
1038
|
'quire.archived': 'بایگانیشده',
|
|
1027
1039
|
'quire.cmd_new_space': 'فضای تازه',
|
|
1028
1040
|
'quire.cmd_open': 'باز کردن Quire',
|
|
1029
|
-
'quire.collapse': 'بستن',
|
|
1041
|
+
'quire.collapse': 'بستن {title}',
|
|
1030
1042
|
'quire.comment_edited': 'ویرایششده',
|
|
1043
|
+
'quire.comment_field': 'دیدگاه شما',
|
|
1031
1044
|
'quire.comment_orphaned': 'متنی که این دربارهٔ آن بود حذف شده است',
|
|
1032
1045
|
'quire.comment_placeholder': 'دربارهٔ این بپرسید…',
|
|
1033
1046
|
'quire.comment_post': 'ثبت دیدگاه',
|
|
1034
1047
|
'quire.comment_reply': 'پاسخ',
|
|
1048
|
+
'quire.comment_reply_field': 'پاسخ شما',
|
|
1049
|
+
'quire.comment_someone': 'کسی',
|
|
1035
1050
|
'quire.comment_resolve': 'بستن',
|
|
1036
1051
|
'quire.comments': 'دیدگاهها',
|
|
1037
1052
|
'quire.comments_empty': 'هنوز دیدگاهی نیست',
|
|
@@ -1042,7 +1057,7 @@ const fa: Record<string, Message> = {
|
|
|
1042
1057
|
'quire.editor_no_session': 'وارد نشدهاید',
|
|
1043
1058
|
'quire.editor_no_session_desc': 'برای نوشتن در این صفحه دوباره وارد شوید.',
|
|
1044
1059
|
'quire.editor_placeholder': 'چیزی بنویسید…',
|
|
1045
|
-
'quire.expand': 'باز کردن',
|
|
1060
|
+
'quire.expand': 'باز کردن {title}',
|
|
1046
1061
|
'quire.history': 'تاریخچهٔ نسخهها',
|
|
1047
1062
|
'quire.history_empty': 'هنوز چیزی ذخیره نشده',
|
|
1048
1063
|
'quire.history_empty_desc': 'نسخهها همانطور که صفحه نوشته میشود و هر بار که منتشر شود نگه داشته میشوند.',
|
|
@@ -1050,7 +1065,7 @@ const fa: Record<string, Message> = {
|
|
|
1050
1065
|
'quire.kind_live': 'سند زنده',
|
|
1051
1066
|
'quire.move_to_trash': 'انتقال به زبالهدان',
|
|
1052
1067
|
'quire.nav': 'Quire',
|
|
1053
|
-
'quire.new_child_page': 'صفحهٔ تازه درون
|
|
1068
|
+
'quire.new_child_page': 'صفحهٔ تازه درون {title}',
|
|
1054
1069
|
'quire.new_page': 'صفحهٔ تازه',
|
|
1055
1070
|
'quire.new_space': 'فضای تازه',
|
|
1056
1071
|
'quire.new_space_desc': 'هر فضا درختی از صفحهها را نگه میدارد، با اعضا و دسترسیهای خودش.',
|
|
@@ -1074,6 +1089,7 @@ const fa: Record<string, Message> = {
|
|
|
1074
1089
|
'quire.search_none': 'چیزی در این فضا پیدا نشد.',
|
|
1075
1090
|
'quire.search_results': 'نتیجهها',
|
|
1076
1091
|
'quire.search_space': 'جستوجو در این فضا',
|
|
1092
|
+
'quire.space_choose': 'فضا',
|
|
1077
1093
|
'quire.space_description': 'توضیح',
|
|
1078
1094
|
'quire.space_empty': 'این فضا هیچ صفحهای ندارد',
|
|
1079
1095
|
'quire.space_empty_desc': 'نخستین صفحه را بسازید و بنویسید.',
|
|
@@ -1351,12 +1367,15 @@ const tr: Record<string, Message> = {
|
|
|
1351
1367
|
'quire.archived': 'Arşivlendi',
|
|
1352
1368
|
'quire.cmd_new_space': 'Yeni alan',
|
|
1353
1369
|
'quire.cmd_open': 'Quire’ı aç',
|
|
1354
|
-
'quire.collapse': '
|
|
1370
|
+
'quire.collapse': '{title} ögesini daralt',
|
|
1355
1371
|
'quire.comment_edited': 'düzenlendi',
|
|
1372
|
+
'quire.comment_field': 'Yorumunuz',
|
|
1356
1373
|
'quire.comment_orphaned': 'Bunun konusu olan metin silinmiş',
|
|
1357
1374
|
'quire.comment_placeholder': 'Bunun hakkında sorun…',
|
|
1358
1375
|
'quire.comment_post': 'Yorumla',
|
|
1359
1376
|
'quire.comment_reply': 'Yanıtla',
|
|
1377
|
+
'quire.comment_reply_field': 'Yanıtınız',
|
|
1378
|
+
'quire.comment_someone': 'Biri',
|
|
1360
1379
|
'quire.comment_resolve': 'Kapat',
|
|
1361
1380
|
'quire.comments': 'Yorumlar',
|
|
1362
1381
|
'quire.comments_empty': 'Henüz yorum yok',
|
|
@@ -1367,7 +1386,7 @@ const tr: Record<string, Message> = {
|
|
|
1367
1386
|
'quire.editor_no_session': 'Oturum açmadınız',
|
|
1368
1387
|
'quire.editor_no_session_desc': 'Bu sayfada yazmak için yeniden oturum açın.',
|
|
1369
1388
|
'quire.editor_placeholder': 'Bir şeyler yazın…',
|
|
1370
|
-
'quire.expand': '
|
|
1389
|
+
'quire.expand': '{title} ögesini genişlet',
|
|
1371
1390
|
'quire.history': 'Sürüm geçmişi',
|
|
1372
1391
|
'quire.history_empty': 'Henüz kayıt yok',
|
|
1373
1392
|
'quire.history_empty_desc': 'Sürümler sayfa yazılırken ve her yayımlamada saklanır.',
|
|
@@ -1375,7 +1394,7 @@ const tr: Record<string, Message> = {
|
|
|
1375
1394
|
'quire.kind_live': 'Canlı belge',
|
|
1376
1395
|
'quire.move_to_trash': 'Çöp kutusuna taşı',
|
|
1377
1396
|
'quire.nav': 'Quire',
|
|
1378
|
-
'quire.new_child_page': '
|
|
1397
|
+
'quire.new_child_page': '{title} içinde yeni sayfa',
|
|
1379
1398
|
'quire.new_page': 'Yeni sayfa',
|
|
1380
1399
|
'quire.new_space': 'Yeni alan',
|
|
1381
1400
|
'quire.new_space_desc': 'Bir alan, kendi üyeleri ve kendi izinleriyle bir sayfa ağacı tutar.',
|
|
@@ -1399,6 +1418,7 @@ const tr: Record<string, Message> = {
|
|
|
1399
1418
|
'quire.search_none': 'Bu alanda eşleşen bir şey yok.',
|
|
1400
1419
|
'quire.search_results': 'Sonuçlar',
|
|
1401
1420
|
'quire.search_space': 'Bu alanda ara',
|
|
1421
|
+
'quire.space_choose': 'Alan',
|
|
1402
1422
|
'quire.space_description': 'Açıklama',
|
|
1403
1423
|
'quire.space_empty': 'Bu alanda sayfa yok',
|
|
1404
1424
|
'quire.space_empty_desc': 'İlk sayfayı oluşturun ve yazmaya başlayın.',
|