@things-factory/figure-service 10.1.157 → 10.1.160

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 (31) hide show
  1. package/dist-server/index.d.ts +1 -0
  2. package/dist-server/index.js +2 -0
  3. package/dist-server/index.js.map +1 -1
  4. package/dist-server/service/figure/figure-namespace-resolver.d.ts +10 -0
  5. package/dist-server/service/figure/figure-namespace-resolver.js +65 -0
  6. package/dist-server/service/figure/figure-namespace-resolver.js.map +1 -0
  7. package/dist-server/service/figure/figure-namespace.d.ts +20 -2
  8. package/dist-server/service/figure/figure-namespace.js +45 -6
  9. package/dist-server/service/figure/figure-namespace.js.map +1 -1
  10. package/dist-server/service/figure/figure-subject.d.ts +12 -0
  11. package/dist-server/service/figure/figure-subject.js +24 -0
  12. package/dist-server/service/figure/figure-subject.js.map +1 -0
  13. package/dist-server/service/figure/index.d.ts +2 -1
  14. package/dist-server/service/figure/index.js +2 -1
  15. package/dist-server/service/figure/index.js.map +1 -1
  16. package/dist-server/service/index.d.ts +1 -1
  17. package/dist-server/tsconfig.tsbuildinfo +1 -1
  18. package/package.json +6 -6
  19. package/server/index.ts +2 -0
  20. package/server/service/figure/figure-namespace-db.test.ts +45 -6
  21. package/server/service/figure/figure-namespace-resolver.ts +41 -0
  22. package/server/service/figure/figure-namespace.ts +63 -7
  23. package/server/service/figure/figure-subject.test.ts +41 -0
  24. package/server/service/figure/figure-subject.ts +21 -0
  25. package/server/service/figure/index.ts +2 -1
  26. package/translations/en.json +1 -0
  27. package/translations/es.json +1 -0
  28. package/translations/ja.json +1 -0
  29. package/translations/ko.json +1 -0
  30. package/translations/ms.json +1 -0
  31. package/translations/zh.json +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-service",
3
- "version": "10.1.157",
3
+ "version": "10.1.160",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,16 +27,16 @@
27
27
  "dependencies": {
28
28
  "@hatiolab/figure-model": "^0.6.5",
29
29
  "@operato/typeorm-history": "^10.0.0",
30
- "@things-factory/ai-client-base": "^10.1.157",
31
- "@things-factory/attachment-base": "^10.1.157",
32
- "@things-factory/auth-base": "^10.1.157",
30
+ "@things-factory/ai-client-base": "^10.1.159",
31
+ "@things-factory/attachment-base": "^10.1.159",
32
+ "@things-factory/auth-base": "^10.1.159",
33
33
  "@things-factory/env": "^10.1.149",
34
- "@things-factory/setting-base": "^10.1.157",
34
+ "@things-factory/setting-base": "^10.1.159",
35
35
  "@things-factory/shell": "^10.1.157",
36
36
  "graphql-scalars": "^1.22.4",
37
37
  "graphql-upload": "^15.0.2",
38
38
  "jszip": "^3.10.1",
39
39
  "koa-router": "^7.4.0"
40
40
  },
41
- "gitHead": "bde810f804966549a7041457ad7ede40fcb78488"
41
+ "gitHead": "b5e86be374e8b913a3f2b8459b6001e8f44e345b"
42
42
  }
package/server/index.ts CHANGED
@@ -9,6 +9,8 @@ import './routes.js'
9
9
  */
10
10
  import { registerFigureToolCategories } from './service/figure/figure-tools.js'
11
11
  registerFigureToolCategories()
12
+ /* The figure the modeller has open, said to the AI each turn (design/ai/screen-pattern.md §2-1). */
13
+ import './service/figure/figure-subject.js'
12
14
 
13
15
  /*
14
16
  * The recipe templates are read once at start (ADR-0095, amended): what was served, and any file whose form is wrong,
@@ -90,15 +90,25 @@ describe('the namespace a figure is created under', () => {
90
90
  expect(figure.type).toBe('conveyor')
91
91
  })
92
92
 
93
- it('is refused by name when the domain has none, or an invalid one', async () => {
94
- for (const value of [null, 'Hatiolab.COM']) {
95
- const { context } = await domainWith(value)
96
- await expect(
97
- new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
98
- ).rejects.toMatchObject({ code: 'figure-namespace-missing' })
93
+ it('with no setting, or an empty one, is the subdomain', async () => {
94
+ for (const value of [null, '', ' ']) {
95
+ const { domain, context } = await domainWith(value)
96
+ const figure = await new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
97
+ expect(figure.namespace).toBe(domain.subdomain)
99
98
  }
100
99
  })
101
100
 
101
+ it('is refused by name when the setting is not a namespace, or there is none and the subdomain is not one either', async () => {
102
+ const { context } = await domainWith('Hatiolab.COM')
103
+ await expect(
104
+ new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
105
+ ).rejects.toMatchObject({ code: 'figure-namespace-missing' })
106
+ const odd = await dataSource!.getRepository(shell.Domain).save({ name: `odd-${++seq}`, subdomain: 'Odd_Sub' })
107
+ await expect(
108
+ new FigureMutation().saveFigure(releasable(), { state: { domain: odd, user: undefined, tx: dataSource!.manager } }, undefined, undefined, creationId(), undefined, 'conveyor')
109
+ ).rejects.toMatchObject({ code: 'figure-namespace-missing' })
110
+ })
111
+
102
112
  it('cannot change: a new figure or a release is refused while the setting disagrees with the figures already made', async () => {
103
113
  const { domain, context } = await domainWith('hatiolab.com')
104
114
  const draft = await new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
@@ -120,6 +130,35 @@ describe('the namespace a figure is created under', () => {
120
130
  })
121
131
  })
122
132
 
133
+ describe('the namespace setting, as the setting screen reads and writes it', () => {
134
+ const resolver = () => new (loadCompiled('service/figure/figure-namespace-resolver').FigureNamespaceResolver)()
135
+
136
+ it('reads the subdomain when nothing is written, and is not fixed before a figure is made', async () => {
137
+ const { domain, context } = await domainWith(null)
138
+ expect(await resolver().figureNamespace(context)).toEqual({ namespace: domain.subdomain, written: '', subdomain: domain.subdomain, fixed: false })
139
+ })
140
+
141
+ it('writes a namespace, and empty goes back to the subdomain; a new figure takes what it says', async () => {
142
+ const { domain, context } = await domainWith(null)
143
+ expect(await resolver().setFigureNamespace(' hatiolab.com ', context)).toMatchObject({ namespace: 'hatiolab.com', written: 'hatiolab.com', fixed: false })
144
+ expect(await resolver().setFigureNamespace('', context)).toMatchObject({ namespace: domain.subdomain, written: '', fixed: false })
145
+ await resolver().setFigureNamespace('hatiolab.com', context)
146
+ const figure = await new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
147
+ expect(figure.namespace).toBe('hatiolab.com')
148
+ expect(await resolver().figureNamespace(context)).toMatchObject({ namespace: 'hatiolab.com', fixed: true })
149
+ })
150
+
151
+ it('refuses what is not a namespace, and another namespace once figures carry one; the setting is left as it was', async () => {
152
+ const { context } = await domainWith('hatiolab.com')
153
+ await expect(resolver().setFigureNamespace('Hatiolab.COM', context)).rejects.toMatchObject({ code: 'figure-namespace-invalid' })
154
+ await new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
155
+ await expect(resolver().setFigureNamespace('other.com', context)).rejects.toMatchObject({ code: 'figure-namespace-changed' })
156
+ expect(await resolver().figureNamespace(context)).toMatchObject({ namespace: 'hatiolab.com', written: 'hatiolab.com', fixed: true })
157
+ /* The same one again is not a change. */
158
+ await expect(resolver().setFigureNamespace('hatiolab.com', context)).resolves.toMatchObject({ namespace: 'hatiolab.com' })
159
+ })
160
+ })
161
+
123
162
  describe('figureBlueprints answers each full name, an item or a refusal', () => {
124
163
  it('gives released figures by full name and refuses the rest by cause, none taking another away', async () => {
125
164
  const { context } = await domainWith('hatiolab.com')
@@ -0,0 +1,41 @@
1
+ import { Arg, Ctx, Directive, Field, Mutation, ObjectType, Query, Resolver, UseMiddleware } from 'type-graphql'
2
+
3
+ import { getDataSource, SpeakRefusals } from '@things-factory/shell'
4
+
5
+ import { figureNamespaceStateOf, setFigureNamespace } from './figure-namespace.js'
6
+
7
+ @ObjectType({ description: "A domain's figure namespace, as the setting screen shows it (ADR-0114)." })
8
+ export class FigureNamespace {
9
+ @Field({ nullable: true, description: 'The namespace new figures are created under; null when neither the setting nor the subdomain is one.' })
10
+ namespace?: string
11
+
12
+ @Field({ description: 'The figure-namespace setting as written; empty when the subdomain is used.' })
13
+ written: string
14
+
15
+ @Field({ description: "The domain's subdomain, used when the setting is empty." })
16
+ subdomain: string
17
+
18
+ @Field({ description: "True once the domain's figures carry the namespace: it cannot be set any more." })
19
+ fixed: boolean
20
+ }
21
+
22
+ @Resolver()
23
+ @UseMiddleware(SpeakRefusals)
24
+ export class FigureNamespaceResolver {
25
+ @Directive('@privilege(category: "figure", privilege: "query", domainOwnerGranted: true)')
26
+ @Query(returns => FigureNamespace, { description: "The domain's figure namespace and whether it can still be set." })
27
+ async figureNamespace(@Ctx() context: ResolverContext): Promise<FigureNamespace> {
28
+ const { domain, tx } = context.state
29
+ return (await figureNamespaceStateOf(tx ?? getDataSource().manager, domain)) as FigureNamespace
30
+ }
31
+
32
+ @Directive('@transaction')
33
+ @Directive('@privilege(category: "setting", privilege: "mutation", domainOwnerGranted: true)')
34
+ @Mutation(returns => FigureNamespace, {
35
+ description: "Sets the domain's figure namespace. Empty takes the subdomain. Refused when it is not a namespace, or when the domain's figures already carry another."
36
+ })
37
+ async setFigureNamespace(@Arg('namespace') namespace: string, @Ctx() context: ResolverContext): Promise<FigureNamespace> {
38
+ const { domain, tx } = context.state
39
+ return (await setFigureNamespace(tx, domain, namespace)) as FigureNamespace
40
+ }
41
+ }
@@ -1,4 +1,4 @@
1
- import { EntityManager, Not } from 'typeorm'
1
+ import { EntityManager, IsNull, Not } from 'typeorm'
2
2
 
3
3
  import { isFigureNamespace } from '@hatiolab/figure-model'
4
4
  import { Domain, Refusal } from '@things-factory/shell'
@@ -13,14 +13,13 @@ export const FIGURE_NAMESPACE_SETTING = 'figure-namespace'
13
13
  * The namespace a new figure is created under.
14
14
  *
15
15
  * It is read from the domain setting and copied onto the figure, so the full name `{namespace}/{type}` a board stores
16
- * never moves. The setting is set once: a value that differs from the namespace this domain's figures already carry
17
- * is refused, so editing it on the settings screen cannot rename anything.
16
+ * never moves. A domain with no setting, or an empty one, takes its subdomain (user, 2026-10-13: 「기본적으로는
17
+ * subdomain 명을 사용하면 되는데」) -- refused only when the subdomain is not a namespace either. A value written in
18
+ * the setting is used as it is, and refused when it is not a namespace. Either way it is set once: a value that differs
19
+ * from the namespace this domain's figures already carry is refused, so editing the setting cannot rename anything.
18
20
  */
19
21
  export async function namespaceForNewFigure(tx: EntityManager, domain: Domain): Promise<string> {
20
- const setting = await tx.getRepository(Setting).findOne({
21
- where: { domain: { id: domain.id }, name: FIGURE_NAMESPACE_SETTING }
22
- })
23
- const namespace = setting?.value?.trim() ?? ''
22
+ const namespace = await chosenNamespace(tx, domain)
24
23
  if (!isFigureNamespace(namespace)) {
25
24
  throw new Refusal(
26
25
  'figure-namespace-missing',
@@ -57,3 +56,60 @@ export function namespaceOf(figure: Figure): string {
57
56
  }
58
57
  return figure.namespace
59
58
  }
59
+
60
+ /** What the setting says, or the subdomain when it says nothing: the namespace new figures are created under. */
61
+ async function chosenNamespace(tx: EntityManager, domain: Domain): Promise<string> {
62
+ return (await writtenNamespace(tx, domain)) || (domain.subdomain ?? '').trim()
63
+ }
64
+
65
+ async function writtenNamespace(tx: EntityManager, domain: Domain): Promise<string> {
66
+ const setting = await tx.getRepository(Setting).findOne({
67
+ where: { domain: { id: domain.id }, name: FIGURE_NAMESPACE_SETTING }
68
+ })
69
+ return setting?.value?.trim() ?? ''
70
+ }
71
+
72
+ /** A domain's figure namespace as the setting screen shows it. */
73
+ export interface FigureNamespaceState {
74
+ /** What new figures get, or null when neither the setting nor the subdomain is a namespace. */
75
+ namespace: string | null
76
+ /** The setting as written; empty when the subdomain is used. */
77
+ written: string
78
+ subdomain: string
79
+ /** The domain's figures already carry the namespace, so it cannot be set any more. */
80
+ fixed: boolean
81
+ }
82
+
83
+ export async function figureNamespaceStateOf(tx: EntityManager, domain: Domain): Promise<FigureNamespaceState> {
84
+ const written = await writtenNamespace(tx, domain)
85
+ const subdomain = (domain.subdomain ?? '').trim()
86
+ const carried = await tx.getRepository(Figure).findOne({ where: { domain: { id: domain.id }, namespace: Not(IsNull()) } })
87
+ const chosen = written || subdomain
88
+ return {
89
+ namespace: carried?.namespace ?? (isFigureNamespace(chosen) ? chosen : null),
90
+ written,
91
+ subdomain,
92
+ fixed: !!carried?.namespace
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Writes the domain's figure namespace (ADR-0114). Empty takes the subdomain. Refused when the value, or the subdomain
98
+ * it falls back to, is not a namespace, and when the domain's figures already carry another one.
99
+ */
100
+ export async function setFigureNamespace(tx: EntityManager, domain: Domain, value: string): Promise<FigureNamespaceState> {
101
+ const written = value.trim()
102
+ const namespace = written || (domain.subdomain ?? '').trim()
103
+ if (!isFigureNamespace(namespace)) {
104
+ throw new Refusal(
105
+ 'figure-namespace-invalid',
106
+ { value: namespace },
107
+ '{value} is not a figure namespace. use lower-case letters, digits, dots and hyphens, for example hatiolab.com.'
108
+ )
109
+ }
110
+ await assertNamespaceUnchanged(tx, domain, namespace)
111
+ const repository = tx.getRepository(Setting)
112
+ const setting = await repository.findOne({ where: { domain: { id: domain.id }, name: FIGURE_NAMESPACE_SETTING } })
113
+ await repository.save({ ...setting, domain, name: FIGURE_NAMESPACE_SETTING, category: 'figure', value: written })
114
+ return figureNamespaceStateOf(tx, domain)
115
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ * The modeller's open figure, named for the AI (operato-application design/ai/screen-pattern.md §2-1).
3
+ */
4
+ const mockFigures: any[] = [
5
+ { id: 'f1', domain: 'plant-a', name: '선풍기', type: 'system/fan' },
6
+ { id: 'f2', domain: 'plant-a', name: '', type: 'system/agv-lift' },
7
+ { id: 'f3', domain: 'plant-b', name: '남의 도형', type: 'other/one' }
8
+ ]
9
+ const mockRegistered: any[] = []
10
+
11
+ jest.mock('@things-factory/shell', () => ({
12
+ getRepository: (name: string) => {
13
+ if (name !== 'Figure') throw new Error(`unexpected repository ${name}`)
14
+ return {
15
+ findOne: async ({ where }: any) => mockFigures.find(one => one.id === where.id && one.domain === where.domain.id) ?? null
16
+ }
17
+ }
18
+ }))
19
+ jest.mock('@things-factory/ai-client-base', () => ({ registerAssistantSubject: (one: any) => mockRegistered.push(one) }))
20
+
21
+ /* Required after the mocks hold their lists - an import would load it before them. */
22
+ const { figureSubject } = require('./figure-subject')
23
+
24
+ describe('the figure the modeller has open', () => {
25
+ it('★ is registered for the figure anchor when the package loads', () => {
26
+ expect(mockRegistered).toEqual([figureSubject])
27
+ expect(figureSubject.anchorType).toBe('figure')
28
+ })
29
+
30
+ it('★ is named by its name, in this domain', async () => {
31
+ expect(await figureSubject.describe({ anchorId: 'f1', domain: { id: 'plant-a' } })).toEqual({ noun: 'figure', name: '선풍기' })
32
+ })
33
+
34
+ it('a figure with no name yet is called by its type', async () => {
35
+ expect(await figureSubject.describe({ anchorId: 'f2', domain: { id: 'plant-a' } })).toEqual({ noun: 'figure', name: 'system/agv-lift' })
36
+ })
37
+
38
+ it('★ another domain\'s figure is not named', async () => {
39
+ expect(await figureSubject.describe({ anchorId: 'f3', domain: { id: 'plant-a' } })).toBeUndefined()
40
+ })
41
+ })
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The figure the modeller has open, named for the AI (operato-application design/ai/screen-pattern.md §2-1).
3
+ *
4
+ * A conversation in the modeller is anchored to the open figure. Each turn the door reads this to say which figure
5
+ * that is, so the model starts from "the figure 선풍기 is open" and passes its id to the tools. A figure of another
6
+ * domain, or one that is gone, is not named.
7
+ */
8
+ import { registerAssistantSubject } from '@things-factory/ai-client-base'
9
+ import { getRepository } from '@things-factory/shell'
10
+
11
+ /* By entity name - the entity's decorators stay out of what a test of this has to load. */
12
+ export const figureSubject = {
13
+ anchorType: 'figure',
14
+ async describe({ anchorId, domain }: { anchorId: string; domain: { id: string } }) {
15
+ const figure: any = await getRepository('Figure').findOne({ where: { id: anchorId, domain: { id: domain.id } } })
16
+ if (!figure) return undefined
17
+ return { noun: 'figure', name: figure.name || figure.type }
18
+ }
19
+ }
20
+
21
+ registerAssistantSubject(figureSubject)
@@ -4,8 +4,9 @@ import { FigureReplacedDraft } from './figure-replaced-draft.js'
4
4
  import { FigureProposalRecord } from './figure-proposal-record.js'
5
5
  import { FigureQuery } from './figure-query.js'
6
6
  import { FigureMutation } from './figure-mutation.js'
7
+ import { FigureNamespaceResolver } from './figure-namespace-resolver.js'
7
8
  import { FigureHistoryEntitySubscriber } from './event-subscriber.js'
8
9
 
9
10
  export const entities = [Figure, FigureHistory, FigureReplacedDraft, FigureProposalRecord]
10
- export const resolvers = [FigureQuery, FigureMutation]
11
+ export const resolvers = [FigureQuery, FigureMutation, FigureNamespaceResolver]
11
12
  export const subscribers = [FigureHistoryEntitySubscriber]
@@ -100,6 +100,7 @@
100
100
  "figure.gate.origin-off-centre.how": "Keep it when the figure is fastened there, as a door at its hinge. Move the origin to the middle to place it by its room.",
101
101
  "error.figure-namespace-missing": "this domain has no valid figure namespace ({value}). set {setting} first, for example hatiolab.com.",
102
102
  "error.figure-namespace-changed": "the figure namespace of this domain is {namespace} and cannot change. set {setting} back to {namespace}.",
103
+ "error.figure-namespace-invalid": "{value} is not a figure namespace. use lower-case letters, digits, dots and hyphens, for example hatiolab.com.",
103
104
  "error.figure-type-malformed": "{type} is not a figure type. use lower-case letters, digits and single hyphens, {min} to {max} characters, starting with a letter.",
104
105
  "error.figure-name-malformed": "{type} is not a full figure name of the form namespace/type.",
105
106
  "error.figure-not-released": "no release is published here.",
@@ -100,6 +100,7 @@
100
100
  "figure.gate.origin-off-centre.how": "Manténgalo si la figura se fija en ese punto, como una puerta por su bisagra. Mueva el origen al centro para colocarla por su espacio.",
101
101
  "error.figure-namespace-missing": "este dominio no tiene un namespace de figura válido ({value}). Defina primero {setting}, por ejemplo hatiolab.com.",
102
102
  "error.figure-namespace-changed": "el namespace de figura de este dominio es {namespace} y no se puede cambiar. Restablezca {setting} a {namespace}.",
103
+ "error.figure-namespace-invalid": "{value} no es un namespace de Figure. Use minúsculas, dígitos, puntos y guiones, por ejemplo hatiolab.com.",
103
104
  "error.figure-type-malformed": "{type} no es un tipo de figura. Use letras minúsculas, dígitos y guiones simples, de {min} a {max} caracteres, empezando por una letra.",
104
105
  "error.figure-name-malformed": "{type} no es un nombre completo de figura con el formato namespace/type.",
105
106
  "error.figure-not-released": "aquí no hay ninguna publicación.",
@@ -100,6 +100,7 @@
100
100
  "figure.gate.origin-off-centre.how": "ドアの蝶番のように、Figure をそこで取り付ける場合はそのままにしてください。空間の中央で配置するには、原点を中央に移動してください。",
101
101
  "error.figure-namespace-missing": "このドメインには有効な Figure namespace がありません({value})。先に {setting} を設定してください。例: hatiolab.com。",
102
102
  "error.figure-namespace-changed": "このドメインの Figure namespace は {namespace} で、変更できません。{setting} を {namespace} に戻してください。",
103
+ "error.figure-namespace-invalid": "Figure namespace の形式ではありません({value})。英小文字・数字・ドット・ハイフンで入力してください。例: hatiolab.com",
103
104
  "error.figure-type-malformed": "{type} は Figure type ではありません。英小文字で始め、英小文字・数字・単一のハイフンのみを使って {min}〜{max} 文字で指定してください。",
104
105
  "error.figure-name-malformed": "{type} は namespace/type 形式の正式な Figure 名ではありません。",
105
106
  "error.figure-not-released": "ここではリリースが公開されていません。",
@@ -100,6 +100,7 @@
100
100
  "figure.gate.origin-off-centre.how": "문의 경첩처럼 붙이는 자리라면 그대로 둡니다. 공간 가운데로 놓이게 하려면 원점을 가운데로 옮깁니다.",
101
101
  "error.figure-namespace-missing": "이 도메인에 Figure namespace 가 없거나 형식이 맞지 않습니다({value}). 설정 {setting} 에 예를 들어 hatiolab.com 을 먼저 넣어 주세요.",
102
102
  "error.figure-namespace-changed": "이 도메인의 Figure namespace 는 {namespace} 이며 바꿀 수 없습니다. 설정 {setting} 을 {namespace} 로 되돌려 주세요.",
103
+ "error.figure-namespace-invalid": "Figure namespace 형식이 아닙니다({value}). 영문 소문자·숫자·점·하이픈으로 적어 주세요. 예: hatiolab.com",
103
104
  "error.figure-type-malformed": "{type} 은 Figure type 이 아닙니다. 소문자로 시작하고 소문자·숫자·하이픈 하나씩만 써서 {min}~{max}자로 정해 주세요.",
104
105
  "error.figure-name-malformed": "{type} 은 namespace/type 형식의 정식 이름이 아닙니다.",
105
106
  "error.figure-not-released": "이 도메인에 발행판이 없습니다.",
@@ -100,6 +100,7 @@
100
100
  "figure.gate.origin-off-centre.how": "Kekalkan jika figure dilekatkan di situ, seperti pintu pada engselnya. Alihkan titik asal ke tengah untuk meletakkannya mengikut ruangnya.",
101
101
  "error.figure-namespace-missing": "domain ini tiada namespace figure yang sah ({value}). tetapkan {setting} dahulu, contohnya hatiolab.com.",
102
102
  "error.figure-namespace-changed": "namespace figure domain ini ialah {namespace} dan tidak boleh diubah. tetapkan {setting} kembali kepada {namespace}.",
103
+ "error.figure-namespace-invalid": "{value} bukan namespace Figure. Gunakan huruf kecil, digit, titik dan sempang, contohnya hatiolab.com.",
103
104
  "error.figure-type-malformed": "{type} bukan jenis figure. gunakan huruf kecil, digit dan tanda sempang tunggal, {min} hingga {max} aksara, bermula dengan huruf.",
104
105
  "error.figure-name-malformed": "{type} bukan nama figure penuh dalam bentuk namespace/type.",
105
106
  "error.figure-not-released": "tiada terbitan diterbitkan di sini.",
@@ -100,6 +100,7 @@
100
100
  "figure.gate.origin-off-centre.how": "如果图形固定在该处(如门的铰链),请保持不变。若要按空间放置,请将原点移到中心。",
101
101
  "error.figure-namespace-missing": "此域没有有效的图形 namespace({value})。请先设置 {setting},例如 hatiolab.com。",
102
102
  "error.figure-namespace-changed": "此域的图形 namespace 为 {namespace},不能更改。请将 {setting} 改回 {namespace}。",
103
+ "error.figure-namespace-invalid": "不是 Figure namespace 的格式({value})。请使用小写字母、数字、点和连字符,例如 hatiolab.com。",
103
104
  "error.figure-type-malformed": "{type} 不是图形类型。请使用小写字母、数字和单个连字符,长度 {min} 到 {max} 个字符,并以字母开头。",
104
105
  "error.figure-name-malformed": "{type} 不是 namespace/type 形式的完整图形名称。",
105
106
  "error.figure-not-released": "此处没有已发布的版本。",