@things-factory/figure-service 10.1.45 → 10.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/service/figure/figure-format.d.ts +3 -2
- package/dist-server/service/figure/figure-format.js +16 -6
- package/dist-server/service/figure/figure-format.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/server/service/figure/figure-format.test.ts +35 -8
- package/server/service/figure/figure-format.ts +18 -7
- package/server/service/figure/figure-propose.test.ts +2 -1
- package/translations/ko.json +19 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-service",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.47",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -23,15 +23,15 @@
|
|
|
23
23
|
"migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@hatiolab/figure-model": "^0.1.
|
|
26
|
+
"@hatiolab/figure-model": "^0.1.53",
|
|
27
27
|
"@operato/typeorm-history": "^10.0.0",
|
|
28
28
|
"@things-factory/ai-client-base": "^10.1.39",
|
|
29
|
-
"@things-factory/auth-base": "^10.1.
|
|
29
|
+
"@things-factory/auth-base": "^10.1.47",
|
|
30
30
|
"@things-factory/env": "^10.1.20",
|
|
31
|
-
"@things-factory/shell": "^10.1.
|
|
31
|
+
"@things-factory/shell": "^10.1.47",
|
|
32
32
|
"graphql-scalars": "^1.22.4",
|
|
33
33
|
"graphql-upload": "^15.0.2",
|
|
34
34
|
"koa-router": "^7.4.0"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "185f20d7d9a15715a0ee2757a6a8189ec36cfb9d"
|
|
37
37
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - V3 자산도 저장·발행·되돌리기·소비처를 지난다. 이 넷은 전에 V2 만 지날 수 있었다.
|
|
11
11
|
* - 예산 칸(`triangles`·`groups`)은 두 판에서 같은 뜻이다.
|
|
12
12
|
*/
|
|
13
|
-
import { createV3Asset, addV3Box, addV3Primitive, serializeV3Asset, applyV3Authoring, proposeV3Occupancy, placeholderType } from '@hatiolab/figure-model'
|
|
13
|
+
import { createV3Asset, addV3Box, addV3Primitive, serializeV3Asset, applyV3Authoring, proposeV3Occupancy, placeholderType, v3ScoreOf } from '@hatiolab/figure-model'
|
|
14
14
|
import type { V3Asset } from '@hatiolab/figure-model'
|
|
15
15
|
|
|
16
16
|
import { consumablePayloadOf, formatOf, inspectAnySource, parseAnySource } from './figure-format'
|
|
@@ -89,8 +89,10 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
89
89
|
expect(parsed.derived.groups).toBe(1)
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
it('V3 의 점수 칸은
|
|
93
|
-
|
|
92
|
+
it('V3 의 점수 칸은 v3ScoreOf 다 — 저작면의 성숙도 탭과 같은 수(수석 아키텍트 판정 09-24)', () => {
|
|
93
|
+
const score = parseAnySource(v3Source()).derived.score
|
|
94
|
+
expect(score).toBe(v3ScoreOf(v3Asset())!.score)
|
|
95
|
+
expect(score).toBeGreaterThan(0)
|
|
94
96
|
})
|
|
95
97
|
|
|
96
98
|
it('형식을 어긴 V3 자산은 거절한다', () => {
|
|
@@ -151,9 +153,9 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
151
153
|
const sampled = inspection.findings.find(f => f.code === 'state-range-sampled')
|
|
152
154
|
expect(sampled).toBeDefined()
|
|
153
155
|
expect(sampled!.blocking).toBe(false)
|
|
154
|
-
/* 증명을 막은
|
|
155
|
-
expect(sampled!.at).
|
|
156
|
-
expect(sampled!.why).not.
|
|
156
|
+
/* 증명을 막은 움직임은 사람이 붙인 이름으로 — 연산 이름과 영어 설명은 화면에 나가지 않는다. */
|
|
157
|
+
expect(sampled!.at).toBe('wheel')
|
|
158
|
+
expect(`${sampled!.at}${sampled!.why}`).not.toMatch(/axis-turn@1|state-dependent|[a-z]+@\d/)
|
|
157
159
|
})
|
|
158
160
|
|
|
159
161
|
it('정본이 없으면 전과 같이 그 말로 막는다', () => {
|
|
@@ -267,13 +269,38 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
267
269
|
const finding = inspectAnySource(splitting(), null, translatorOf(KO)).findings.find(f => f.code === 'parts-split-on-resize')
|
|
268
270
|
|
|
269
271
|
expect(finding?.blocking).toBe(true)
|
|
270
|
-
expect(finding!.message).toBe("크기를 Y 로 2배로 하면 'lamp'
|
|
272
|
+
expect(finding!.message).toBe("크기를 Y 로 2배로 하면 'lamp' 부품이 나머지에서 떨어집니다.")
|
|
271
273
|
expect(finding!.why).toMatch(/부서진/)
|
|
272
274
|
expect(finding!.how).toContain("'lamp'")
|
|
273
275
|
expect(finding!.how).toContain('여기에 고정')
|
|
274
276
|
expect(`${finding!.message}${finding!.why}${finding!.how}`).not.toMatch(/come away|piece/)
|
|
275
277
|
})
|
|
276
278
|
|
|
279
|
+
/* The server's translator is i18next with its defaults, which escape values for HTML. The panel draws text. */
|
|
280
|
+
it('부품 이름의 따옴표가 HTML 로 바뀌지 않는다 — 서버와 같은 i18next 로', async () => {
|
|
281
|
+
const i18next = require('i18next').createInstance()
|
|
282
|
+
await i18next.init({
|
|
283
|
+
lng: 'ko',
|
|
284
|
+
resources: { ko: { translations: KO } },
|
|
285
|
+
ns: ['translations'],
|
|
286
|
+
defaultNS: 'translations',
|
|
287
|
+
interpolation: { prefix: '{', suffix: '}' },
|
|
288
|
+
keySeparator: false
|
|
289
|
+
})
|
|
290
|
+
const finding = inspectAnySource(splitting(), null, i18next.t.bind(i18next)).findings.find(f => f.code === 'parts-split-on-resize')!
|
|
291
|
+
|
|
292
|
+
expect(finding.message).toBe("크기를 Y 로 2배로 하면 'lamp' 부품이 나머지에서 떨어집니다.")
|
|
293
|
+
expect(`${finding.message}${finding.how}`).not.toContain(''')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
/* A particle fixed after a quoted name is wrong for half the names ('그리퍼' 이). A noun after the name is always right. */
|
|
297
|
+
it('따옴표로 싼 이름 바로 뒤에 조사가 오지 않는다 — 이름 뒤에는 명사를 둔다', () => {
|
|
298
|
+
const particle = /'\{[a-z]+\}'\s*(이|가|을|를|은|는|과|와|의|로|으로)(\s|$|[.,])/
|
|
299
|
+
const bare = /\{parts\}\s*(이|가|을|를|은|는)(\s|$)/
|
|
300
|
+
const offending = Object.entries(KO).filter(([key, text]) => key.startsWith('figure.gate.') && (particle.test(text) || bare.test(text)))
|
|
301
|
+
expect(offending).toEqual([])
|
|
302
|
+
})
|
|
303
|
+
|
|
277
304
|
it('번역이 없으면 영어로 답한다 — 키를 찍지 않는다', () => {
|
|
278
305
|
const finding = inspectAnySource(outside()).findings.find(f => f.code === 'part-outside-occupancy')!
|
|
279
306
|
|
|
@@ -291,7 +318,7 @@ describe('figure-format — 판을 알아보는 문', () => {
|
|
|
291
318
|
const finding = inspectAnySource(v3Source(), null, translatorOf(KO), placeholder).findings.find(f => f.code === 'type-is-placeholder')
|
|
292
319
|
|
|
293
320
|
expect(finding?.blocking).toBe(true)
|
|
294
|
-
expect(finding!.message).toBe(`'${placeholder}'
|
|
321
|
+
expect(finding!.message).toBe(`'${placeholder}' 타입은 처음 저장할 때 붙은 임시 타입입니다.`)
|
|
295
322
|
/* 화면의 칸 이름은 「타입」이다(`figure.label.figure-type`). */
|
|
296
323
|
expect(finding!.how).toContain('타입 칸')
|
|
297
324
|
expect(inspectAnySource(v3Source(), null, translatorOf(KO), 'ROLLER_CONVEYOR').blocked).toBe(false)
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* No conversion. This door reads what is stored; turning a V2 asset into a V3 one is `convertV2ToV3`, run
|
|
21
21
|
* deliberately against a recoverable copy (the designer's step 3), never as a side effect of a save.
|
|
22
22
|
*/
|
|
23
|
-
import { compile, compileV3Asset, inspectV3Asset, isPlaceholderType, parseV3Asset, serializeV3Asset, v3ContractChanges, v3CostOf } from '@hatiolab/figure-model'
|
|
23
|
+
import { compile, compileV3Asset, inspectV3Asset, isPlaceholderType, parseV3Asset, serializeV3Asset, v3ContractChanges, v3CostOf, v3ScoreOf } from '@hatiolab/figure-model'
|
|
24
24
|
import type { V3Asset, V3ContractChange, V3ContractError, V3Violation } from '@hatiolab/figure-model'
|
|
25
25
|
|
|
26
26
|
import type { GateFinding, Inspection } from './figure-gate.js'
|
|
@@ -63,8 +63,9 @@ export function formatOf(sourceJson: string): FigureFormat {
|
|
|
63
63
|
* Validates a source of either format and computes what the catalogue shows.
|
|
64
64
|
*
|
|
65
65
|
* A V3 asset is measured with `v3CostOf`, which charges the same per-shape estimate V2 charges, so the
|
|
66
|
-
* budget columns mean one thing across the two formats. `score` is
|
|
67
|
-
*
|
|
66
|
+
* budget columns mean one thing across the two formats. Its `score` is `v3ScoreOf`'s -- the headroom against
|
|
67
|
+
* placing `LIMITS.screenInstances` of it on one screen (chief architect, 2026-09-24), the same number the
|
|
68
|
+
* modeller's maturity tab shows. An asset that draws nothing has no score, not a full one.
|
|
68
69
|
*
|
|
69
70
|
* @throws {FigureSourceError} when the source breaks its own format.
|
|
70
71
|
*/
|
|
@@ -82,10 +83,11 @@ export function parseAnySource(sourceJson: string): ParsedSource & { format: Fig
|
|
|
82
83
|
])
|
|
83
84
|
}
|
|
84
85
|
const cost = v3CostOf(asset)
|
|
86
|
+
const score = v3ScoreOf(asset)
|
|
85
87
|
return {
|
|
86
88
|
format: 'v3',
|
|
87
89
|
source: serializeV3Asset(asset),
|
|
88
|
-
derived: { score: null as unknown as number, triangles: cost.triangles, groups: cost.groups },
|
|
90
|
+
derived: { score: (score?.score ?? null) as unknown as number, triangles: cost.triangles, groups: cost.groups },
|
|
89
91
|
violations: []
|
|
90
92
|
}
|
|
91
93
|
}
|
|
@@ -180,8 +182,13 @@ function v3Wording(violation: V3Violation, say: Say, hasParts: boolean): Pick<Ga
|
|
|
180
182
|
|
|
181
183
|
type Say = (key: string, params: Record<string, string | number>, english: string) => string
|
|
182
184
|
|
|
185
|
+
/*
|
|
186
|
+
The sentence is shown as text, never as HTML, so the values go in as they are. i18next escapes interpolated
|
|
187
|
+
values by default, and the screen then showed `'lamp-in'` as `'lamp-in'`.
|
|
188
|
+
*/
|
|
183
189
|
function sayerOf(t?: Translate): Say {
|
|
184
|
-
return (key, params, english) =>
|
|
190
|
+
return (key, params, english) =>
|
|
191
|
+
t ? t(key, { ...params, defaultValue: english, interpolation: { escapeValue: false } }) : english
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
export function inspectAnySource(
|
|
@@ -247,8 +254,12 @@ export function inspectAnySource(
|
|
|
247
254
|
message: say('figure.gate.state-range-sampled.message', { states }, `The whole range of motion could not be proven, so ${states} poses were checked.`),
|
|
248
255
|
why: say('figure.gate.state-range-sampled.why', {}, 'Between the poses that were checked, a part may still go past the declared space.'),
|
|
249
256
|
how: say('figure.gate.state-range-sampled.how', {}, 'Re-author the motion that could not be proven, and the whole range can be proven.'),
|
|
250
|
-
/*
|
|
251
|
-
|
|
257
|
+
/*
|
|
258
|
+
Which motions stopped the proof, by the name a person gave them: the kernel reports each operation it could
|
|
259
|
+
not prove (`j2-shoulder.chain: compose@1 turns …`), and the screen showed that English list as it was. The
|
|
260
|
+
name before the first dot is the joint or part; the operations stay in the logs.
|
|
261
|
+
*/
|
|
262
|
+
at: [...new Set((inspection.coverage.unprovable ?? []).map(entry => entry.split(':')[0]!.split('.')[0]!.trim()).filter(Boolean))].join(' · ') || undefined,
|
|
252
263
|
blocking: false
|
|
253
264
|
})
|
|
254
265
|
}
|
|
@@ -176,7 +176,8 @@ describe('프롬프트는 형식을 따라간다 — 손으로 적은 목록이
|
|
|
176
176
|
*/
|
|
177
177
|
V3_OPERATORS: 'V3 커널 연산자 이름이다 — 제안기가 V2 정본을 쓰는 동안은 저작자가 적는 값이 아니다',
|
|
178
178
|
V3_NON_AFFINE_OPERATORS: '위 목록의 부분집합이다 — 어느 연산이 다중선형을 안 나르나는 관문이 알 일이다',
|
|
179
|
-
V3_STATE_NON_AFFINE_OPERATORS: '같다'
|
|
179
|
+
V3_STATE_NON_AFFINE_OPERATORS: '같다',
|
|
180
|
+
V3_BUDGET_KEYS: 'V3 등급이 재는 예산의 이름이다 — 채점 결과이지 저작자가 적는 값이 아니다'
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
it('저작에 쓰이는 열거는 값 하나도 빠짐없이 프롬프트에 있다', () => {
|
package/translations/ko.json
CHANGED
|
@@ -5,23 +5,23 @@
|
|
|
5
5
|
"figure.gate.occupancy-undeclared.how-no-parts": "먼저 도형을 추가한 뒤, 「부품에 맞추기」로 공간을 선언하세요.",
|
|
6
6
|
"figure.gate.occupancy-undeclared.message": "이 도형은 차지하는 공간을 아직 선언하지 않았습니다.",
|
|
7
7
|
"figure.gate.occupancy-undeclared.why": "공간 선언이 없으면 도면에 놓일 때의 배율을 정할 수 없습니다.",
|
|
8
|
-
"figure.gate.part-outside-occupancy.how": "「부품에 맞추기」로 공간을 부품에서 다시 선언하거나, '{part}'
|
|
8
|
+
"figure.gate.part-outside-occupancy.how": "「부품에 맞추기」로 공간을 부품에서 다시 선언하거나, '{part}' 부품을 {axis} 로 {mm}mm 안으로 옮기세요.",
|
|
9
9
|
"figure.gate.part-outside-occupancy.message": "'{part}' 부품이 공간 선언을 {axis} 로 {mm}mm 넘습니다.",
|
|
10
10
|
"figure.gate.part-outside-occupancy.why": "공간 선언이 도면에 놓일 때의 배율을 정합니다. 넘은 만큼은 그 배율에 들어가지 않아서, 도면에서 이 부품이 도형에 주어진 자리 밖으로 나갑니다.",
|
|
11
|
-
"figure.gate.parts-off-placement-face.how": "'{part}'
|
|
11
|
+
"figure.gate.parts-off-placement-face.how": "'{part}' 부품을 Y 로 {mm}mm 옮겨 그 면을 평면에 맞추거나, 장착면을 다른 면으로 다시 정하세요.",
|
|
12
12
|
"figure.gate.parts-off-placement-face.message": "장착면으로 정한 '{part}' 부품의 면이 놓이는 평면에서 {mm}mm 떨어져 있습니다.",
|
|
13
13
|
"figure.gate.parts-off-placement-face.why": "도형은 그 면으로 놓인다고 선언돼 있습니다. 닿지 않으면 도면에서 그만큼 떠 보이고, 자리에 놓인 물건도 함께 뜹니다.",
|
|
14
|
-
"figure.gate.parts-split-on-resize.how": "{parts}
|
|
15
|
-
"figure.gate.parts-split-on-resize.message": "크기를 {axes} 로 {factor}배로 하면 {parts}
|
|
14
|
+
"figure.gate.parts-split-on-resize.how": "{parts} 부품을 올려 둔 부품의 면에 붙이거나, 부품의 「위치와 크기」에서 「여기에 고정」을 누르면 크기를 바꿔도 제자리에 있습니다.",
|
|
15
|
+
"figure.gate.parts-split-on-resize.message": "크기를 {axes} 로 {factor}배로 하면 {parts} 부품이 나머지에서 떨어집니다.",
|
|
16
16
|
"figure.gate.parts-split-on-resize.why": "다른 크기로 놓으면 그 부품이 공중에 떠 보입니다. 보는 사람에게는 부서진 도형입니다.",
|
|
17
|
-
"figure.gate.placement-face-tilted.how": "평면과 나란한 면을 장착면으로 다시 정하거나, '{part}'
|
|
17
|
+
"figure.gate.placement-face-tilted.how": "평면과 나란한 면을 장착면으로 다시 정하거나, '{part}' 부품을 바로 세우세요.",
|
|
18
18
|
"figure.gate.placement-face-tilted.message": "장착면으로 정한 '{part}' 부품의 면이 놓이는 평면에서 {degrees}° 기울어 있습니다.",
|
|
19
19
|
"figure.gate.placement-face-tilted.why": "기운 면은 평면에 닿을 수 없어서, 도형이 어떤 자세로 놓이는지 정해지지 않습니다.",
|
|
20
20
|
"figure.gate.state-range-sampled.how": "증명하지 못한 움직임을 다시 저작하면 범위 전체를 증명할 수 있습니다.",
|
|
21
21
|
"figure.gate.state-range-sampled.message": "움직이는 범위 전체를 증명하지 못해 자세 {states}개만 검사했습니다.",
|
|
22
22
|
"figure.gate.state-range-sampled.why": "검사한 자세 사이에서 부품이 공간 선언을 넘을 수 있습니다.",
|
|
23
23
|
"figure.gate.type-is-placeholder.how": "타입 칸에서 타입을 정한 뒤 발행하세요.",
|
|
24
|
-
"figure.gate.type-is-placeholder.message": "'{type}'
|
|
24
|
+
"figure.gate.type-is-placeholder.message": "'{type}' 타입은 처음 저장할 때 붙은 임시 타입입니다.",
|
|
25
25
|
"figure.gate.type-is-placeholder.why": "보드는 이 타입으로 도형을 부르고, 발행한 뒤에는 바꿀 수 없습니다.",
|
|
26
26
|
"figure.gate.v3-released-not-compared.message": "직전 발행판과 견주지 못했습니다. V3 도형은 발행 사이의 비교가 아직 없습니다.",
|
|
27
27
|
"figure.gate.v3-released-not-compared.why": "이 발행이 이미 놓인 인스턴스를 깨는지는 확인되지 않았습니다.",
|
|
@@ -37,30 +37,30 @@
|
|
|
37
37
|
"figure.gate.contract.size-range-narrowed.why": "새 범위 밖의 크기로 놓인 인스턴스는 그려지지 않습니다. 어떤 크기로 놓였는지는 기록되지 않습니다.",
|
|
38
38
|
"figure.gate.contract.authored-size-changed.message": "저작한 크기가 {before} 에서 {after} 로 바뀌었습니다.",
|
|
39
39
|
"figure.gate.contract.authored-size-changed.why": "놓인 인스턴스는 제 크기를 그대로 둡니다. 깊이를 따로 정하지 않은 인스턴스는 이 크기에서 깊이를 받습니다.",
|
|
40
|
-
"figure.gate.contract.state-input-gone.message": "
|
|
40
|
+
"figure.gate.contract.state-input-gone.message": "'{id}' 조작값이 없어졌습니다.",
|
|
41
41
|
"figure.gate.contract.state-input-gone.why": "놓인 인스턴스와 커넥터가 이 이름으로 값을 줍니다. 이 발행 뒤에는 그 값이 아무 일도 하지 않고, 화면에는 까닭이 나오지 않습니다.",
|
|
42
|
-
"figure.gate.contract.state-unit-changed.message": "
|
|
42
|
+
"figure.gate.contract.state-unit-changed.message": "'{id}' 조작값의 단위가 {before} 에서 {after} 로 바뀌었습니다.",
|
|
43
43
|
"figure.gate.contract.state-unit-changed.why": "저장된 값은 수를 그대로 가집니다. 단위가 바뀌면 같은 수가 다른 뜻이 됩니다.",
|
|
44
|
-
"figure.gate.contract.state-range-narrowed.message": "
|
|
44
|
+
"figure.gate.contract.state-range-narrowed.message": "'{id}' 조작값의 범위가 {before} 에서 {after} 로 좁아졌습니다.",
|
|
45
45
|
"figure.gate.contract.state-range-narrowed.why": "놓인 인스턴스에 새 범위 밖의 값이 이미 저장돼 있을 수 있습니다.",
|
|
46
|
-
"figure.gate.contract.state-input-added.message": "
|
|
46
|
+
"figure.gate.contract.state-input-added.message": "'{id}' 조작값이 새로 생겼습니다.",
|
|
47
47
|
"figure.gate.contract.state-input-added.why": "값을 주지 않은 인스턴스는 기본값에 섭니다.",
|
|
48
|
-
"figure.gate.contract.clip-gone.message": "
|
|
48
|
+
"figure.gate.contract.clip-gone.message": "'{clip}' 클립이 없어졌습니다.",
|
|
49
49
|
"figure.gate.contract.clip-gone.why": "놓인 인스턴스는 이름으로 클립을 재생합니다. 이 발행 뒤에는 그 클립이 아무것도 재생하지 않습니다.",
|
|
50
|
-
"figure.gate.contract.clip-added.message": "
|
|
50
|
+
"figure.gate.contract.clip-added.message": "'{clip}' 클립이 새로 생겼습니다.",
|
|
51
51
|
"figure.gate.contract.clip-added.why": "인스턴스가 재생하기 전에는 멈춰 있습니다.",
|
|
52
|
-
"figure.gate.contract.binding-gone.message": "
|
|
52
|
+
"figure.gate.contract.binding-gone.message": "'{binding}' 자리가 없어졌습니다.",
|
|
53
53
|
"figure.gate.contract.binding-gone.why": "놓인 인스턴스에서 그 자리에 든 것이 갈 곳을 잃습니다.",
|
|
54
|
-
"figure.gate.contract.binding-role-changed.message": "
|
|
54
|
+
"figure.gate.contract.binding-role-changed.message": "'{binding}' 자리의 역할이 {before} 에서 {after} 로 바뀌었습니다.",
|
|
55
55
|
"figure.gate.contract.binding-role-changed.why": "그 자리에 든 것은 옛 역할에 맞춰 놓였습니다.",
|
|
56
|
-
"figure.gate.contract.binding-capacity-reduced.message": "
|
|
56
|
+
"figure.gate.contract.binding-capacity-reduced.message": "'{binding}' 자리가 {before} 개 대신 {after} 개를 받습니다.",
|
|
57
57
|
"figure.gate.contract.binding-capacity-reduced.why": "없어진 칸에 든 것이 갈 곳을 잃습니다.",
|
|
58
|
-
"figure.gate.contract.binding-accepts-narrowed.message": "
|
|
58
|
+
"figure.gate.contract.binding-accepts-narrowed.message": "'{binding}' 자리가 더는 받지 않는 종류: {types}.",
|
|
59
59
|
"figure.gate.contract.binding-accepts-narrowed.why": "이미 든 것은 그대로 있고, 그 타입의 다음 것부터 거절됩니다.",
|
|
60
|
-
"figure.gate.contract.binding-added.message": "
|
|
60
|
+
"figure.gate.contract.binding-added.message": "'{binding}' 자리가 새로 생겼습니다.",
|
|
61
61
|
"figure.gate.contract.binding-added.why": "놓인 인스턴스는 아직 이 자리를 모르고 그대로 서 있습니다.",
|
|
62
|
-
"figure.gate.contract.capability-gone.message": "
|
|
62
|
+
"figure.gate.contract.capability-gone.message": "'{name}' 능력이 없어졌습니다.",
|
|
63
63
|
"figure.gate.contract.capability-gone.why": "씬이 이 이름으로 동작을 붙입니다. 놓인 인스턴스가 그 동작을 잃습니다.",
|
|
64
|
-
"figure.gate.contract.capability-added.message": "
|
|
64
|
+
"figure.gate.contract.capability-added.message": "'{name}' 능력이 새로 생겼습니다.",
|
|
65
65
|
"figure.gate.contract.capability-added.why": "도형이 할 수 있는 일이 늘어납니다. 놓인 인스턴스는 그대로 서 있습니다."
|
|
66
66
|
}
|