@things-factory/ai-assistant 10.1.100 → 10.1.101
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/client/components/entry-review.ts +2 -0
- package/client/utils/entry-proposal.ts +10 -1
- package/dist-client/components/entry-review.js +2 -0
- package/dist-client/components/entry-review.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/entry-proposal.js +11 -1
- package/dist-client/utils/entry-proposal.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.js +25 -1
- package/dist-server/service/data-entry/entry-apply.js.map +1 -1
- package/dist-server/service/data-entry/entry-target.d.ts +18 -3
- package/dist-server/service/data-entry/entry-target.js +44 -2
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-tools.d.ts +12 -0
- package/dist-server/service/data-entry/entry-tools.js +7 -1
- package/dist-server/service/data-entry/entry-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/data-entry/data-entry-update.test.ts +46 -0
- package/server/service/data-entry/data-entry.test.ts +14 -0
- package/server/service/data-entry/entry-apply.ts +24 -3
- package/server/service/data-entry/entry-target.ts +54 -4
- package/server/service/data-entry/entry-tools.ts +8 -1
- package/test/entry-proposal.test.ts +11 -0
- package/translations/en.json +3 -1
- package/translations/ja.json +3 -1
- package/translations/ko.json +3 -1
- package/translations/ms.json +3 -1
- package/translations/zh.json +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-assistant",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.101",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"copyfiles": "^2.4.1",
|
|
52
52
|
"rimraf": "^5.0.0"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "44abc5cac98642b8028b17bf3e019d083539ccab"
|
|
55
55
|
}
|
|
@@ -265,3 +265,49 @@ describe('a field that points at another record', () => {
|
|
|
265
265
|
})
|
|
266
266
|
})
|
|
267
267
|
|
|
268
|
+
describe('a record with lines', () => {
|
|
269
|
+
const advice: EntryTarget = {
|
|
270
|
+
key: 'warehouse.inbound-advice',
|
|
271
|
+
labelKey: 'x',
|
|
272
|
+
description: 'one inbound advice line.',
|
|
273
|
+
mutation: 'mutation ($advice: InboundAdviceInput!) { receiveInboundAdvice(advice: $advice) { name } }',
|
|
274
|
+
fields: [
|
|
275
|
+
{ name: 'despatchAdvice', type: 'string', required: true },
|
|
276
|
+
{ name: 'epcClass', type: 'string', required: true },
|
|
277
|
+
{ name: 'quantity', type: 'number', required: true }
|
|
278
|
+
],
|
|
279
|
+
group: {
|
|
280
|
+
by: 'despatchAdvice',
|
|
281
|
+
variables: rows => ({ advice: { name: rows[0].values.despatchAdvice, lines: rows.map(row => ({ epcClass: row.values.epcClass, quantity: row.values.quantity })) } }),
|
|
282
|
+
appliedOf: data => ({ id: data?.receiveInboundAdvice?.name })
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const line = (despatchAdvice: string, epcClass: string, quantity: any) => ({ values: { despatchAdvice, epcClass, quantity }, sources: [{ where: 'x' }] })
|
|
286
|
+
|
|
287
|
+
it('★ the lines of one record go in by one call; a record with a line kept out stays out whole', async () => {
|
|
288
|
+
declareEntryTarget(advice)
|
|
289
|
+
const proposed: any = await proposeEntries(advice, { rows: [line('DA-1', 'A', 20), line('DA-1', 'B', 5), line('DA-2', 'C', 'many'), line('DA-2', 'D', 1)] }, { state: {} }, async () => new Map(), run)
|
|
290
|
+
expect(proposed.counts).toMatchObject({ rows: 4, groups: 2, groupsReady: 1 })
|
|
291
|
+
expect(proposed.groupBy).toBe('despatchAdvice')
|
|
292
|
+
const sent: any[] = []
|
|
293
|
+
const outcome = await applyEntryProposal('g1', {
|
|
294
|
+
messages: [{ id: 'm1', proposals: [{ ...proposed, key: 'g1' }] }],
|
|
295
|
+
answered: false,
|
|
296
|
+
passesDoor: async () => true,
|
|
297
|
+
execute: async (_document, variables) => (sent.push(variables), { data: { receiveInboundAdvice: { name: variables.advice.name } } }),
|
|
298
|
+
state: {}
|
|
299
|
+
})
|
|
300
|
+
expect(sent).toEqual([{ advice: { name: 'DA-1', lines: [{ epcClass: 'A', quantity: 20 }, { epcClass: 'B', quantity: 5 }] } }])
|
|
301
|
+
expect(outcome.rows).toEqual([
|
|
302
|
+
{ index: 0, ok: true, id: 'DA-1' },
|
|
303
|
+
{ index: 1, ok: true, id: 'DA-1' },
|
|
304
|
+
{ index: 2, ok: false, error: 'quantity: not-a-number' },
|
|
305
|
+
{ index: 3, ok: false, error: 'group-kept-out (row 3)' }
|
|
306
|
+
])
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('a group by a field that is not declared is refused at boot', () => {
|
|
310
|
+
expect(() => declareEntryTarget({ ...advice, group: { ...advice.group!, by: 'order' } })).toThrow(/group by 'order'/)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
@@ -341,3 +341,17 @@ describe('the model names the total its own way', () => {
|
|
|
341
341
|
})
|
|
342
342
|
})
|
|
343
343
|
|
|
344
|
+
describe('a moment', () => {
|
|
345
|
+
const shift: EntryTarget = { ...expense, fields: [{ name: 'startTime', type: 'datetime', required: true }] }
|
|
346
|
+
it('reads the ways a moment is written - without an offset it stays without one', () => {
|
|
347
|
+
const { rows } = normalizeEntryRows(shift, [
|
|
348
|
+
{ values: { startTime: '2026-10-01 08:30' }, sources: [{ where: 'x' }] },
|
|
349
|
+
{ values: { startTime: '2026.10.1 8:05:09' }, sources: [{ where: 'x' }] },
|
|
350
|
+
{ values: { startTime: '2026-10-01T08:30:00+0900' }, sources: [{ where: 'x' }] },
|
|
351
|
+
{ values: { startTime: '2026-10-01 25:00' }, sources: [{ where: 'x' }] }
|
|
352
|
+
])
|
|
353
|
+
expect(rows.map(row => row.values.startTime)).toEqual(['2026-10-01T08:30:00', '2026-10-01T08:05:09', '2026-10-01T08:30:00+09:00', '2026-10-01 25:00'])
|
|
354
|
+
expect(rows[3].issues).toEqual([{ field: 'startTime', code: 'not-a-datetime', blocking: true }])
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { PROPOSAL_OUTCOME_PREFIX, PROPOSAL_VERDICTS } from '../chat-message/proposal-outcome.js'
|
|
14
14
|
import { readRecords, stillAsProposed, type ChangeRow } from './entry-update.js'
|
|
15
|
-
import { changeToolName, resolveReferences, entryDoorOf, entryTargetOf, entryToolName, rootFieldOf, isBlocked, recordEntryEvent, normalizeEntryRows, withCheckedIssues, type EntryRow, type EntryTarget } from './entry-target.js'
|
|
15
|
+
import { changeToolName, entryGroups, resolveReferences, entryDoorOf, entryTargetOf, entryToolName, rootFieldOf, isBlocked, recordEntryEvent, normalizeEntryRows, withCheckedIssues, type EntryRow, type EntryTarget } from './entry-target.js'
|
|
16
16
|
|
|
17
17
|
export interface StoredProposalMessage {
|
|
18
18
|
id: string
|
|
@@ -97,14 +97,35 @@ export async function applyEntryProposal(key: string, deps: EntryApplyDeps, targ
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const results: EntryRowResult[] = []
|
|
100
|
-
|
|
100
|
+
if (target.group) {
|
|
101
|
+
/* One call per group; a group with a row kept out stays out whole - an order missing a line is not entered. */
|
|
102
|
+
for (const [, members] of entryGroups(rows, target.group.by)) {
|
|
103
|
+
const blocked = members.find(isBlocked)
|
|
104
|
+
if (blocked) {
|
|
105
|
+
const issue = blocked.issues.find(one => one.blocking)!
|
|
106
|
+
for (const row of members)
|
|
107
|
+
results.push({ index: row.index, ok: false, error: row === blocked ? (issue.field ? `${issue.field}: ${issue.code}` : issue.code) : `group-kept-out (row ${blocked.index + 1})` })
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const answer = await deps.execute(target.mutation, target.group.variables(members))
|
|
112
|
+
const error = answer.errors?.length ? answer.errors.map(one => one.message).join('; ') || 'refused' : undefined
|
|
113
|
+
const applied = error ? undefined : target.group.appliedOf?.(answer.data, members)
|
|
114
|
+
for (const row of members) results.push(error ? { index: row.index, ok: false, error } : { index: row.index, ok: true, ...(applied?.id ? { id: applied.id } : {}), ...(applied?.label ? { label: applied.label } : {}) })
|
|
115
|
+
} catch (error: any) {
|
|
116
|
+
for (const row of members) results.push({ index: row.index, ok: false, error: error?.message || 'refused' })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
results.sort((a, b) => a.index - b.index)
|
|
120
|
+
}
|
|
121
|
+
for (const row of target.group ? [] : rows) {
|
|
101
122
|
if (isBlocked(row)) {
|
|
102
123
|
const issue = row.issues.find(one => one.blocking)!
|
|
103
124
|
results.push({ index: row.index, ok: false, error: issue.field ? `${issue.field}: ${issue.code}` : issue.code })
|
|
104
125
|
continue
|
|
105
126
|
}
|
|
106
127
|
try {
|
|
107
|
-
const answer = await deps.execute(target.mutation, target.variables(row))
|
|
128
|
+
const answer = await deps.execute(target.mutation, target.variables!(row))
|
|
108
129
|
if (answer.errors?.length) {
|
|
109
130
|
results.push({ index: row.index, ok: false, error: answer.errors.map(one => one.message).join('; ') || 'refused' })
|
|
110
131
|
continue
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { parse } from 'graphql'
|
|
16
16
|
|
|
17
|
-
export type EntryFieldType = 'string' | 'text' | 'number' | 'integer' | 'date' | 'boolean' | 'enum' | 'reference'
|
|
17
|
+
export type EntryFieldType = 'string' | 'text' | 'number' | 'integer' | 'date' | 'datetime' | 'boolean' | 'enum' | 'reference'
|
|
18
18
|
|
|
19
19
|
export interface EntryField {
|
|
20
20
|
name: string
|
|
@@ -127,10 +127,20 @@ export interface EntryTarget {
|
|
|
127
127
|
* to a person typing it in.
|
|
128
128
|
*/
|
|
129
129
|
mutation: string
|
|
130
|
-
/** The mutation's variables for one row. */
|
|
131
|
-
variables(row: EntryRow): Record<string, unknown>
|
|
130
|
+
/** The mutation's variables for one row. Absent when the target enters rows by group (below). */
|
|
131
|
+
variables?(row: EntryRow): Record<string, unknown>
|
|
132
132
|
/** What the conversation says of an entered row - its id and a short name. */
|
|
133
133
|
appliedOf?(data: any, row: EntryRow): EntryApplied | undefined
|
|
134
|
+
/**
|
|
135
|
+
* A record with lines - one order, several item lines. Rows that share the value of `by` (the order number) are one
|
|
136
|
+
* record, entered by one call of the mutation with `variables(rows)`. If any row of a group is kept out, the whole
|
|
137
|
+
* group is: an order missing a line must not go in unseen.
|
|
138
|
+
*/
|
|
139
|
+
group?: {
|
|
140
|
+
by: string
|
|
141
|
+
variables(rows: EntryRow[]): Record<string, unknown>
|
|
142
|
+
appliedOf?(data: any, rows: EntryRow[]): EntryApplied | undefined
|
|
143
|
+
}
|
|
134
144
|
/** At most this many rows in one proposal. Default 200. */
|
|
135
145
|
maxRows?: number
|
|
136
146
|
/**
|
|
@@ -214,7 +224,10 @@ export function declareEntryTarget(target: EntryTarget): void {
|
|
|
214
224
|
} catch (error: any) {
|
|
215
225
|
throw new Error(`[ai-assistant] entry target '${target.key}': its mutation ${error?.message ?? 'does not parse'}.`)
|
|
216
226
|
}
|
|
217
|
-
if (typeof target.variables !== 'function'
|
|
227
|
+
if (typeof target.variables !== 'function' && typeof target.group?.variables !== 'function')
|
|
228
|
+
throw new Error(`[ai-assistant] entry target '${target.key}' has no variables (per row, or per group).`)
|
|
229
|
+
if (target.group && !target.fields.some(field => field.name === target.group!.by))
|
|
230
|
+
throw new Error(`[ai-assistant] entry target '${target.key}': group by '${target.group.by}' is not a declared field.`)
|
|
218
231
|
if (target.update) {
|
|
219
232
|
for (const [what, document, operation] of [
|
|
220
233
|
['update.read', target.update.read?.query, 'query'],
|
|
@@ -308,6 +321,22 @@ function toDate(value: unknown): string | undefined {
|
|
|
308
321
|
return `${match[1]}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
309
322
|
}
|
|
310
323
|
|
|
324
|
+
/**
|
|
325
|
+
* A moment as `YYYY-MM-DDTHH:mm:ss`, with the offset when it was written - "2026-10-01 08:30", "2026.10.1 8:30",
|
|
326
|
+
* an ISO string. Without an offset it stays without one: the application reads it in its own time zone, as it
|
|
327
|
+
* reads a time typed into its own screen.
|
|
328
|
+
*/
|
|
329
|
+
function toDateTime(value: unknown): string | undefined {
|
|
330
|
+
if (typeof value !== 'string') return undefined
|
|
331
|
+
const match = value.trim().match(/^(\d{4})[-./](\d{1,2})[-./](\d{1,2})[ T](\d{1,2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?\s*(Z|[+-]\d{2}:?\d{2})?$/)
|
|
332
|
+
if (!match) return undefined
|
|
333
|
+
const day = toDate(`${match[1]}-${match[2]}-${match[3]}`)
|
|
334
|
+
const [hour, minute, second] = [Number(match[4]), Number(match[5]), Number(match[6] ?? 0)]
|
|
335
|
+
if (!day || hour > 23 || minute > 59 || second > 59) return undefined
|
|
336
|
+
const offset = match[7] ? (match[7] === 'Z' ? 'Z' : match[7].replace(/^([+-]\d{2})(\d{2})$/, '$1:$2')) : ''
|
|
337
|
+
return `${day}T${String(hour).padStart(2, '0')}:${match[5]}:${String(second).padStart(2, '0')}${offset}`
|
|
338
|
+
}
|
|
339
|
+
|
|
311
340
|
function toBoolean(value: unknown): boolean | undefined {
|
|
312
341
|
if (typeof value === 'boolean') return value
|
|
313
342
|
if (typeof value !== 'string') return undefined
|
|
@@ -332,6 +361,10 @@ export function readFieldValue(field: EntryField, value: unknown): { read?: unkn
|
|
|
332
361
|
const read = toDate(value)
|
|
333
362
|
return read === undefined ? { code: 'not-a-date' } : { read }
|
|
334
363
|
}
|
|
364
|
+
case 'datetime': {
|
|
365
|
+
const read = toDateTime(value)
|
|
366
|
+
return read === undefined ? { code: 'not-a-datetime' } : { read }
|
|
367
|
+
}
|
|
335
368
|
case 'boolean': {
|
|
336
369
|
const read = toBoolean(value)
|
|
337
370
|
return read === undefined ? { code: 'not-a-boolean' } : { read }
|
|
@@ -452,6 +485,12 @@ export function writtenForms(type: EntryFieldType, value: unknown): string[] {
|
|
|
452
485
|
}
|
|
453
486
|
return [...forms].map(form => (negative ? `-${form}` : form))
|
|
454
487
|
}
|
|
488
|
+
if (type === 'datetime') {
|
|
489
|
+
/* A moment is found by its day and its hh:mm - the file writes seconds and zones its own way. */
|
|
490
|
+
const match = typeof value === 'string' ? value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})/) : null
|
|
491
|
+
if (!match) return []
|
|
492
|
+
return writtenForms('date', match[1])
|
|
493
|
+
}
|
|
455
494
|
if (type === 'date') {
|
|
456
495
|
const match = typeof value === 'string' ? value.match(/^(\d{4})-(\d{2})-(\d{2})$/) : null
|
|
457
496
|
if (!match) return []
|
|
@@ -631,3 +670,14 @@ export async function resolveReferences<T extends { values: Record<string, unkno
|
|
|
631
670
|
return rows
|
|
632
671
|
}
|
|
633
672
|
|
|
673
|
+
/** The rows by group, in the order groups first appear - `[key, rows]`. A row without the key is a group of its own. */
|
|
674
|
+
export function entryGroups<T extends { index: number; values: Record<string, unknown> }>(rows: T[], by: string): [string, T[]][] {
|
|
675
|
+
const groups = new Map<string, T[]>()
|
|
676
|
+
for (const row of rows) {
|
|
677
|
+
const value = row.values[by]
|
|
678
|
+
const key = value === undefined || value === null || value === '' ? `\u0000row-${row.index}` : String(typeof value === 'object' ? (value as any).id ?? (value as any).text : value)
|
|
679
|
+
groups.set(key, [...(groups.get(key) ?? []), row])
|
|
680
|
+
}
|
|
681
|
+
return [...groups.entries()]
|
|
682
|
+
}
|
|
683
|
+
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
entryToolName,
|
|
23
23
|
isBlocked,
|
|
24
24
|
normalizeEntryRows,
|
|
25
|
+
entryGroups,
|
|
25
26
|
referenceDoorsOf,
|
|
26
27
|
resolveReferences,
|
|
27
28
|
recordEntryEvent,
|
|
@@ -42,6 +43,7 @@ function fieldSchema(field: EntryField): any {
|
|
|
42
43
|
field.description,
|
|
43
44
|
field.required ? 'required' : 'optional',
|
|
44
45
|
field.type === 'date' ? 'YYYY-MM-DD' : undefined,
|
|
46
|
+
field.type === 'datetime' ? 'YYYY-MM-DD HH:mm, with the offset only when the material gives one' : undefined,
|
|
45
47
|
field.type === 'enum' ? `one of: ${field.values!.join(', ')}` : undefined
|
|
46
48
|
]
|
|
47
49
|
.filter(Boolean)
|
|
@@ -92,6 +94,9 @@ export async function proposeEntries(target: EntryTarget, args: any, context: an
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
const blocked = rows.filter(isBlocked).length
|
|
97
|
+
/* With lines grouped into records, the card counts the records and says which are kept out whole. */
|
|
98
|
+
const groups = target.group ? entryGroups(rows, target.group.by) : undefined
|
|
99
|
+
const groupCounts = groups ? { groups: groups.length, groupsReady: groups.filter(([, members]) => !members.some(isBlocked)).length } : {}
|
|
95
100
|
const totals = entryTotals(rows, target.fields, declared, texts)
|
|
96
101
|
const verified = rows.filter(row => row.evidence).length
|
|
97
102
|
const unmatched = rows.filter(row => row.issues.some(issue => issue.code === 'not-in-source')).length
|
|
@@ -105,7 +110,8 @@ export async function proposeEntries(target: EntryTarget, args: any, context: an
|
|
|
105
110
|
icon: 'playlist_add',
|
|
106
111
|
columns: columnsOf(target),
|
|
107
112
|
rows,
|
|
108
|
-
counts: { rows: rows.length, ready: rows.length - blocked, blocked, cut, verified, unmatched },
|
|
113
|
+
counts: { rows: rows.length, ready: rows.length - blocked, blocked, cut, verified, unmatched, ...groupCounts },
|
|
114
|
+
...(target.group ? { groupBy: target.group.by } : {}),
|
|
109
115
|
...(totals.length ? { totals } : {}),
|
|
110
116
|
...(typeof args?.note === 'string' && args.note.trim() ? { reason: args.note.trim().slice(0, 300) } : {}),
|
|
111
117
|
...(checkFailed ? { checkFailed } : {}),
|
|
@@ -191,6 +197,7 @@ export function entryCategoryOf(target: EntryTarget, run: (context: any) => Grap
|
|
|
191
197
|
doors: [entryDoorOf(target.mutation), ...referenceDoorsOf(target.fields)],
|
|
192
198
|
description:
|
|
193
199
|
`Propose records to enter: ${target.description} ` +
|
|
200
|
+
(target.group ? `Each row is one line; the rows of one record share the same ${target.group.by}. ` : '') +
|
|
194
201
|
'Pass every record found in the material as one row. The rows are shown to the person as a card; nothing is written until they press it.',
|
|
195
202
|
schema: {
|
|
196
203
|
type: 'object',
|
|
@@ -77,4 +77,15 @@ describe('the entry card', () => {
|
|
|
77
77
|
'changed 1 records.\nrow 2: amount: changed-since'
|
|
78
78
|
)
|
|
79
79
|
})
|
|
80
|
+
|
|
81
|
+
it('★ the button counts only the lines of records that go in whole', () => {
|
|
82
|
+
const p = { ...card('g'), groupBy: 'order', rows: [
|
|
83
|
+
{ index: 0, values: { order: 'A' }, sources: [], issues: [] },
|
|
84
|
+
{ index: 1, values: { order: 'A' }, sources: [], issues: [] },
|
|
85
|
+
{ index: 2, values: { order: 'B' }, sources: [], issues: [] },
|
|
86
|
+
{ index: 3, values: { order: 'B' }, sources: [], issues: [{ code: 'required', field: 'qty', blocking: true }] }
|
|
87
|
+
] }
|
|
88
|
+
expect(entryReadyCount(p)).toBe(2)
|
|
89
|
+
})
|
|
80
90
|
})
|
|
91
|
+
|
package/translations/en.json
CHANGED
|
@@ -273,5 +273,7 @@
|
|
|
273
273
|
"ai-assistant.entry.issue.reference-not-found": "\"{text}\" - not found",
|
|
274
274
|
"ai-assistant.entry.issue.reference-ambiguous": "\"{text}\" - matches several: {candidates}",
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" - could not be looked up",
|
|
276
|
-
"ai-assistant.entry.issue.not-a-reference": "not a record it can point to"
|
|
276
|
+
"ai-assistant.entry.issue.not-a-reference": "not a record it can point to",
|
|
277
|
+
"ai-assistant.entry.groups": "{count} records",
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count} records kept out whole"
|
|
277
279
|
}
|
package/translations/ja.json
CHANGED
|
@@ -273,5 +273,7 @@
|
|
|
273
273
|
"ai-assistant.entry.issue.reference-not-found": "「{text}」— 見つかりません",
|
|
274
274
|
"ai-assistant.entry.issue.reference-ambiguous": "「{text}」— 複数に一致: {candidates}",
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "「{text}」— 検索できませんでした",
|
|
276
|
-
"ai-assistant.entry.issue.not-a-reference": "参照先の記録が分かりません"
|
|
276
|
+
"ai-assistant.entry.issue.not-a-reference": "参照先の記録が分かりません",
|
|
277
|
+
"ai-assistant.entry.groups": "{count}件",
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count}件は行が止まり全体を入力しません"
|
|
277
279
|
}
|
package/translations/ko.json
CHANGED
|
@@ -273,5 +273,7 @@
|
|
|
273
273
|
"ai-assistant.entry.issue.reference-not-found": "\"{text}\" — 찾지 못했습니다",
|
|
274
274
|
"ai-assistant.entry.issue.reference-ambiguous": "\"{text}\" — 여럿과 맞습니다: {candidates}",
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" — 찾아보지 못했습니다",
|
|
276
|
-
"ai-assistant.entry.issue.not-a-reference": "가리키는 기록을 알 수 없습니다"
|
|
276
|
+
"ai-assistant.entry.issue.not-a-reference": "가리키는 기록을 알 수 없습니다",
|
|
277
|
+
"ai-assistant.entry.groups": "{count}건",
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count}건은 줄이 막혀 통째로 입력하지 않습니다"
|
|
277
279
|
}
|
package/translations/ms.json
CHANGED
|
@@ -273,5 +273,7 @@
|
|
|
273
273
|
"ai-assistant.entry.issue.reference-not-found": "\"{text}\" - tidak ditemui",
|
|
274
274
|
"ai-assistant.entry.issue.reference-ambiguous": "\"{text}\" - sepadan dengan beberapa: {candidates}",
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" - tidak dapat dicari",
|
|
276
|
-
"ai-assistant.entry.issue.not-a-reference": "bukan rekod yang boleh dirujuk"
|
|
276
|
+
"ai-assistant.entry.issue.not-a-reference": "bukan rekod yang boleh dirujuk",
|
|
277
|
+
"ai-assistant.entry.groups": "{count} rekod",
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count} rekod ditahan sepenuhnya"
|
|
277
279
|
}
|
package/translations/zh.json
CHANGED
|
@@ -273,5 +273,7 @@
|
|
|
273
273
|
"ai-assistant.entry.issue.reference-not-found": "\"{text}\" — 未找到",
|
|
274
274
|
"ai-assistant.entry.issue.reference-ambiguous": "\"{text}\" — 匹配多个:{candidates}",
|
|
275
275
|
"ai-assistant.entry.issue.reference-unreadable": "\"{text}\" — 无法查找",
|
|
276
|
-
"ai-assistant.entry.issue.not-a-reference": "无法确定所指记录"
|
|
276
|
+
"ai-assistant.entry.issue.not-a-reference": "无法确定所指记录",
|
|
277
|
+
"ai-assistant.entry.groups": "{count}条",
|
|
278
|
+
"ai-assistant.entry.groups-kept-out": "{count}条因有行被拦而整体不录入"
|
|
277
279
|
}
|