@things-factory/shell 10.1.28 → 10.1.31

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/shell",
3
- "version": "10.1.28",
3
+ "version": "10.1.31",
4
4
  "description": "Core module for framework",
5
5
  "bin": {
6
6
  "things-factory": "bin/things-factory",
@@ -126,5 +126,5 @@
126
126
  "pg": "^8.7.3",
127
127
  "sqlite3": "^5.0.8"
128
128
  },
129
- "gitHead": "3a260a0f79bc6b41a692a103955bfcc541aff60e"
129
+ "gitHead": "5213e6f9913b40ea2618a2111d9884b792dbaa01"
130
130
  }
@@ -12,11 +12,11 @@
12
12
  * compute time, so bundle order stops mattering, and a slot that is not opened cannot be reached.
13
13
  *
14
14
  * Three owners, one mechanism - no reliance on bundle order or specificity:
15
- * --brand-<slot> a product's accent (its own brand.css) 26 slots
15
+ * --brand-<slot> a product's accent (its own <product>-accent.css) 26 slots
16
16
  * --suite-<slot> the operato suite's shared greys (@things-factory/operato-suite) 17 slots
17
17
  * closed error x4, shadow, scrim - literal, nobody above the platform sets them 6 slots
18
18
  * A tenant's domain theme still wins over all of these: it writes --md-sys-color-* directly, later in
19
- * the bundle. Apps with neither brand.css nor the suite get the fallbacks, the shell values of before.
19
+ * the bundle. Apps with neither an accent file nor the suite get the fallbacks, the shell values of before.
20
20
  *
21
21
  * If md adds a slot the counts go red on purpose: someone has to decide whose it is.
22
22
  */
@@ -0,0 +1,63 @@
1
+ /**
2
+ * A refusal keeps its name on the way out, and gets its sentence from the caller's language.
3
+ *
4
+ * ── Why these four ────────────────────────────────────────────────────────
5
+ * The code is the contract (ADR-0042): a client reads `extensions.refusal.code` and decides what to
6
+ * show. If Apollo's own `code` were left empty it would read `INTERNAL_SERVER_ERROR`, and a refusal
7
+ * would look like a crash. If `t` were called without params the locale would be lost and a Korean
8
+ * reader would get English with no error anywhere. If a module has not declared the key, the English
9
+ * sentence has to win over the key printed raw.
10
+ *
11
+ * Moved here from operato-plant when worklist became the third consumer (ADR-0069 decision 3).
12
+ */
13
+
14
+ import { GraphQLError } from 'graphql'
15
+
16
+ import { Refusal, refusalKey, rethrowSpoken, speakRefusal } from '../server/refusal/refusal.js'
17
+
18
+ const refusal = () => new Refusal('approval-line-missing', { role: 'quality' }, 'no approval line stands for quality.')
19
+
20
+ describe('a refusal on the way out', () => {
21
+ it('carries its own name, and says the server refused on purpose', () => {
22
+ const error = speakRefusal(refusal(), {})
23
+
24
+ expect(error).toBeInstanceOf(GraphQLError)
25
+ expect(error.extensions.code).toBe('BAD_USER_INPUT')
26
+ expect(error.extensions.refusal).toEqual({ code: 'approval-line-missing', params: { role: 'quality' } })
27
+ })
28
+
29
+ it('asks for error.<code> and hands the values over, never undefined', () => {
30
+ const asked: Array<{ key: string; options?: Record<string, unknown> }> = []
31
+ const t = (key: string, options?: Record<string, unknown>) => {
32
+ asked.push({ key, options })
33
+ return '품질 결재선이 비어 있습니다.'
34
+ }
35
+
36
+ const error = speakRefusal(refusal(), { t })
37
+
38
+ expect(asked).toHaveLength(1)
39
+ expect(asked[0].key).toBe(refusalKey('approval-line-missing'))
40
+ /* `t(key, undefined)` answers in English however the request came in, so the values always travel. */
41
+ expect(asked[0].options).toEqual({ role: 'quality', defaultValue: 'no approval line stands for quality.' })
42
+ expect(error.message).toBe('품질 결재선이 비어 있습니다.')
43
+ })
44
+
45
+ it('reads as English when the module has not declared the key', () => {
46
+ /* i18next answers with defaultValue when the key is missing; a missing key must not print itself. */
47
+ const t = (_key: string, options?: Record<string, unknown>) => String(options?.defaultValue ?? _key)
48
+
49
+ expect(speakRefusal(refusal(), { t }).message).toBe('no approval line stands for quality.')
50
+ })
51
+ })
52
+
53
+ describe('rethrowSpoken', () => {
54
+ it('speaks a refusal', () => {
55
+ expect(() => rethrowSpoken(refusal(), {})).toThrow(GraphQLError)
56
+ })
57
+
58
+ it('leaves anything else exactly as it was - a crash is not a refusal', () => {
59
+ const crash = new TypeError('cannot read properties of undefined')
60
+
61
+ expect(() => rethrowSpoken(crash, {})).toThrow(crash)
62
+ })
63
+ })
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A theme file carries its owner's name - `themes/` is one namespace shared by every module.
3
+ *
4
+ * The builder's ThemeOverridePlugin collects `client/themes/*.css` from every module and keeps the
5
+ * FIRST file it meets for each file name, application first (webpack-plugins/theme-override-plugin.js).
6
+ * It never looks inside. So a module that ships `light.css` replaces the shell's `light.css` whole -
7
+ * including the 49 slots the shell opens as `var(--brand-*)` / `var(--suite-*)`, and including any
8
+ * fix made there later. A file with one declaration in it does that just as completely as a full
9
+ * palette.
10
+ *
11
+ * Hence the rule (ADR-0060 decision 2-1): inside things-factory only the shell carries the names of
12
+ * the shell's theme files. A product that means to replace one of them wholesale may still do it -
13
+ * that is what an application's own light.css was - but then it owns all 49 slots and stops
14
+ * receiving anything the platform decides.
15
+ *
16
+ * The exceptions below are named, with what has to happen for each to go. When one is dealt with
17
+ * this test goes red and the line gets deleted - that is the point of listing them rather than
18
+ * filtering them out.
19
+ */
20
+
21
+ import { readdirSync, existsSync } from 'fs'
22
+ import path from 'path'
23
+
24
+ const PACKAGES = path.resolve(__dirname, '..', '..')
25
+ const SHELL_THEMES = path.resolve(__dirname, '..', 'client', 'themes')
26
+
27
+ /**
28
+ * Files that carry a shell theme file's name today.
29
+ *
30
+ * - The generator templates are copied into a new application, so a new product is born replacing
31
+ * the shell theme - operato-warehouse was born that way. They are fixed once the four operato
32
+ * products have moved.
33
+ */
34
+ const KNOWN: string[] = [
35
+ 'generator/templates/app/client/themes/dark.css',
36
+ 'generator/templates/app/client/themes/light.css',
37
+ 'generator/templates/brand/client/themes/dark.css',
38
+ 'generator/templates/brand/client/themes/light.css'
39
+ ]
40
+
41
+ /** Every `client/themes/*.css` under packages, as a path relative to packages. */
42
+ function themeFiles(): string[] {
43
+ const found: string[] = []
44
+
45
+ const walk = (dir: string) => {
46
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
47
+ if (entry.name === 'node_modules' || entry.name.startsWith('.') || entry.name.startsWith('dist')) {
48
+ continue
49
+ }
50
+
51
+ const full = path.join(dir, entry.name)
52
+
53
+ if (entry.isDirectory()) {
54
+ walk(full)
55
+ } else if (entry.name.endsWith('.css') && path.basename(path.dirname(full)) === 'themes') {
56
+ if (path.basename(path.dirname(path.dirname(full))) === 'client') {
57
+ found.push(path.relative(PACKAGES, full))
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ walk(PACKAGES)
64
+
65
+ return found.sort()
66
+ }
67
+
68
+ describe('theme file names', () => {
69
+ it('finds the theme files at all - an empty scan would pass everything below', () => {
70
+ const files = themeFiles()
71
+
72
+ expect(files.length).toBeGreaterThan(20)
73
+ expect(files).toContain(path.join('shell', 'client', 'themes', 'light.css'))
74
+ })
75
+
76
+ it('only the shell carries the names of the shell theme files', () => {
77
+ const shellNames = new Set(readdirSync(SHELL_THEMES))
78
+ const collisions = themeFiles().filter(
79
+ file => !file.startsWith(`shell${path.sep}`) && shellNames.has(path.basename(file))
80
+ )
81
+
82
+ expect(collisions).toEqual(KNOWN.map(file => file.split('/').join(path.sep)))
83
+ })
84
+
85
+ it('every exception still exists - a stale line hides the next collision', () => {
86
+ for (const file of KNOWN) {
87
+ expect(existsSync(path.join(PACKAGES, file.split('/').join(path.sep)))).toBe(true)
88
+ }
89
+ })
90
+ })
@@ -3,25 +3,40 @@
3
3
  "button.cancel": "cancel",
4
4
  "button.close": "Close",
5
5
  "button.confirm": "confirm",
6
+ "button.copy": "copy",
7
+ "button.copy to draft": "copy to draft",
6
8
  "button.create": "create",
7
9
  "button.delete": "delete",
8
10
  "button.detail": "detail",
11
+ "button.diff with previous": "diff with previous",
12
+ "button.edit": "edit",
9
13
  "button.edit-contact": "edit contact",
10
14
  "button.empty": "empty",
11
15
  "button.export": "export",
12
16
  "button.refresh": "refresh",
17
+ "button.register": "register",
13
18
  "button.reset": "Reset",
14
19
  "button.save": "save",
20
+ "button.schedule-task": "schedule task",
21
+ "button.select": "select",
22
+ "button.start": "start",
15
23
  "button.update": "update",
24
+ "button.view": "view",
16
25
  "error.value is empty": "{value} is empty",
17
26
  "field.active": "active",
18
27
  "field.address": "address",
28
+ "field.approval-line": "approval line",
29
+ "field.approver": "approver",
30
+ "field.assignee": "assignee",
19
31
  "field.attributes": "attributes",
20
32
  "field.brand_image": "brand_image",
21
33
  "field.brand_name": "brand_name",
34
+ "field.category": "category",
22
35
  "field.code": "code",
23
36
  "field.company": "company",
24
37
  "field.content_image": "content_image",
38
+ "field.count": "count",
39
+ "field.country-code": "country",
25
40
  "field.country_code": "country code",
26
41
  "field.created-at": "created at",
27
42
  "field.created_at": "created at",
@@ -34,37 +49,71 @@
34
49
  "field.domain": "domain",
35
50
  "field.email": "email address",
36
51
  "field.group": "group",
52
+ "field.hidden": "hidden",
53
+ "field.i18n-label": "multi-lingual label",
37
54
  "field.id": "id",
55
+ "field.key": "key",
56
+ "field.maximum": "Maximum",
57
+ "field.minimum": "Minimum",
58
+ "field.model": "model",
38
59
  "field.name": "name",
60
+ "field.netmask": "network mask",
61
+ "field.options": "options",
39
62
  "field.params": "parameters",
63
+ "field.phone": "phone",
40
64
  "field.platform": "platform",
41
65
  "field.provider": "provider",
42
66
  "field.rank": "rank",
67
+ "field.reason": "reason",
68
+ "field.report-type": "report type",
43
69
  "field.required role": "required role",
70
+ "field.resource_url": "resource url",
71
+ "field.role": "role",
72
+ "field.schedule": "schedule",
73
+ "field.search-keys": "search keys",
74
+ "field.serial-no": "serial #",
75
+ "field.spec": "spec",
44
76
  "field.status": "status",
77
+ "field.store-id": "store id",
45
78
  "field.subdomain": "subdomain",
79
+ "field.supervisory-role": "supervisory role",
46
80
  "field.system_flag": "system_flag",
81
+ "field.t-key": "translation key",
82
+ "field.tag": "tag name",
47
83
  "field.theme": "theme",
48
84
  "field.thumbnail": "thumbnail",
49
85
  "field.timezone": "timezone",
50
86
  "field.type": "type",
87
+ "field.unit": "unit",
51
88
  "field.uom": "unit",
52
89
  "field.updated-at": "updated at",
53
90
  "field.updated_at": "updated at",
54
91
  "field.updater": "updater",
55
92
  "field.value": "value",
56
93
  "label.company": "company",
94
+ "label.country": "country",
57
95
  "label.days": "days",
58
96
  "label.description": "description",
97
+ "label.email": "email",
59
98
  "label.env-var": "environment variable",
99
+ "label.filter-deprecated": "deprecated",
100
+ "label.filter-draft": "draft",
101
+ "label.filter-released": "released",
60
102
  "label.hours": "hours",
61
103
  "label.minutes": "minutes",
62
104
  "label.name": "name",
105
+ "label.participation-approval": "approval",
106
+ "label.participation-reference": "reference",
107
+ "label.password": "password",
108
+ "label.platform": "platform",
109
+ "label.please_select_a_country": "please select a country",
110
+ "label.please_select_a_platform": "please select a platform",
63
111
  "label.seconds": "seconds",
64
112
  "label.state": "state",
65
113
  "label.status": "Status",
66
114
  "label.type": "type",
67
115
  "prompt.sure to navigate away?": "You have unsaved changes. Are you sure you want to navigate away?",
116
+ "text.approval-line-empty": "no approver — a submission completes at once",
68
117
  "text.are_you_sure": "are you sure?",
69
118
  "text.ask ai": "ask ai",
70
119
  "text.column visibility setting": "column visibility",
@@ -80,17 +129,29 @@
80
129
  "text.error": "error",
81
130
  "text.existing_item_found": "existing item found",
82
131
  "text.failed": "failed",
132
+ "text.form_has_invalid_input": "form has invalid input",
83
133
  "text.info_x_successfully": "{x} success.",
134
+ "text.initial version all new": "initial version — all new",
135
+ "text.loading": "loading…",
136
+ "text.no difference from previous": "no difference from the previous version",
137
+ "text.no released version information available": "no released version information available",
84
138
  "text.nothing_changed": "nothing changed",
85
139
  "text.nothing_selected": "nothing selected",
86
140
  "text.offline": "offline",
87
141
  "text.online": "online",
142
+ "text.release comment optional": "release comment (optional) — leave blank to auto-fill",
143
+ "text.saved": "saved",
88
144
  "text.sure_to_x": "Sure to {x}?",
89
145
  "text.technical detail": "technical detail",
90
146
  "text.there_is_nothing_to_delete": "there is nothing to delete",
91
147
  "text.there_is_nothing_to_process": "there is nothing to process",
92
148
  "text.there_is_nothing_to_save": "there is nothing to save",
93
149
  "text.you.are.now.in": "you are now in {state}",
150
+ "title.activity list": "activity list",
151
+ "title.activity model item list": "activity model item list",
152
+ "title.approval-line item list": "approval line item list",
153
+ "title.are_you_sure": "are you sure",
154
+ "title.contact": "contact",
94
155
  "title.event list": "event list",
95
156
  "title.import event": "import events",
96
157
  "title.lookup role": "lookup role",
@@ -3,25 +3,40 @@
3
3
  "button.cancel": "キャンセル",
4
4
  "button.close": "閉じる",
5
5
  "button.confirm": "確認",
6
+ "button.copy": "コピー",
7
+ "button.copy to draft": "ドラフトにコピー",
6
8
  "button.create": "作成",
7
9
  "button.delete": "削除",
8
10
  "button.detail": "詳細",
11
+ "button.diff with previous": "前のバージョンと比較",
12
+ "button.edit": "修正",
9
13
  "button.edit-contact": "連絡先を編集",
10
14
  "button.empty": "空にする",
11
15
  "button.export": "エクスポート",
12
16
  "button.refresh": "リロード",
17
+ "button.register": "登録",
13
18
  "button.reset": "リセット",
14
19
  "button.save": "保存",
20
+ "button.schedule-task": "タスクをスケジュール",
21
+ "button.select": "選択",
22
+ "button.start": "開始",
15
23
  "button.update": "更新",
24
+ "button.view": "見る",
16
25
  "error.value is empty": "{value}の値が存在しません.",
17
26
  "field.active": "アクティブ化",
18
27
  "field.address": "住所",
28
+ "field.approval-line": "決裁ライン",
29
+ "field.approver": "決裁者",
30
+ "field.assignee": "譲受人",
19
31
  "field.attributes": "属性",
20
32
  "field.brand_image": "ブランド写真",
21
33
  "field.brand_name": "ブランド名",
34
+ "field.category": "カテゴリー",
22
35
  "field.code": "コード",
23
36
  "field.company": "会社",
24
37
  "field.content_image": "コンテンツ イメージ",
38
+ "field.count": "件数",
39
+ "field.country-code": "国",
25
40
  "field.country_code": "国家コード",
26
41
  "field.created-at": "作成日時",
27
42
  "field.created_at": "作成日時",
@@ -34,37 +49,71 @@
34
49
  "field.domain": "ドメイン",
35
50
  "field.email": "メール",
36
51
  "field.group": "グループ",
52
+ "field.hidden": "隠し",
53
+ "field.i18n-label": "多言語ラベル",
37
54
  "field.id": "ID",
55
+ "field.key": "キ",
56
+ "field.maximum": "最大値",
57
+ "field.minimum": "最小値",
58
+ "field.model": "モデル",
38
59
  "field.name": "名前",
60
+ "field.netmask": "ネットマスク",
61
+ "field.options": "選択オプション",
39
62
  "field.params": "パラメーター",
63
+ "field.phone": "電話番号",
40
64
  "field.platform": "プラットフォーム",
41
65
  "field.provider": "プロバイダー",
42
66
  "field.rank": "順位",
67
+ "field.reason": "理由",
68
+ "field.report-type": "レポート用画面種類",
43
69
  "field.required role": "必要な役割",
70
+ "field.resource_url": "リソースurl",
71
+ "field.role": "ロール",
72
+ "field.schedule": "スケジュール",
73
+ "field.search-keys": "検索キー",
74
+ "field.serial-no": "シリアル番号",
75
+ "field.spec": "明細",
44
76
  "field.status": "状態",
77
+ "field.store-id": "店舗ID",
45
78
  "field.subdomain": "サブ ドメイン",
79
+ "field.supervisory-role": "管理者ロール",
46
80
  "field.system_flag": "system_flag",
81
+ "field.t-key": "翻訳キー",
82
+ "field.tag": "タグ",
47
83
  "field.theme": "テーマ",
48
84
  "field.thumbnail": "プレビュー",
49
85
  "field.timezone": "タイム ゾーン",
50
86
  "field.type": "種類",
87
+ "field.unit": "単位",
51
88
  "field.uom": "単位",
52
89
  "field.updated-at": "修正日時",
53
90
  "field.updated_at": "修正日時",
54
91
  "field.updater": "修正者",
55
92
  "field.value": "値",
56
93
  "label.company": "会社",
94
+ "label.country": "国",
57
95
  "label.days": "日",
58
96
  "label.description": "説明",
97
+ "label.email": "メール",
59
98
  "label.env-var": "環境変数",
99
+ "label.filter-deprecated": "廃棄",
100
+ "label.filter-draft": "ドラフト",
101
+ "label.filter-released": "配布",
60
102
  "label.hours": "時間",
61
103
  "label.minutes": "分",
62
104
  "label.name": "名前",
105
+ "label.participation-approval": "決裁",
106
+ "label.participation-reference": "参照",
107
+ "label.password": "パスワード",
108
+ "label.platform": "プラットフォーム",
109
+ "label.please_select_a_country": "国を選択してください",
110
+ "label.please_select_a_platform": "プラットフォームを選択してください",
63
111
  "label.seconds": "秒",
64
112
  "label.state": "状態",
65
113
  "label.status": "状態",
66
114
  "label.type": "タイプ",
67
115
  "prompt.sure to navigate away?": "未保存の変更があります。本当に移動しますか?",
116
+ "text.approval-line-empty": "決裁者がいません — 提出すると即完了します",
68
117
  "text.are_you_sure": "進行しますか?",
69
118
  "text.ask ai": "AIに聞く",
70
119
  "text.column visibility setting": "カラムの表示設定",
@@ -80,17 +129,29 @@
80
129
  "text.error": "エラー",
81
130
  "text.existing_item_found": "既にある項目です",
82
131
  "text.failed": "失敗",
132
+ "text.form_has_invalid_input": "無効な値が入力されました.",
83
133
  "text.info_x_successfully": "{x}に成功しました.",
134
+ "text.initial version all new": "最初のバージョン — すべて新規",
135
+ "text.loading": "読み込み中…",
136
+ "text.no difference from previous": "前のバージョンとの差分はありません",
137
+ "text.no released version information available": "リリースされたバージョン情報はありません",
84
138
  "text.nothing_changed": "変更なし",
85
139
  "text.nothing_selected": "選択されていない",
86
140
  "text.offline": "オフライン",
87
141
  "text.online": "オンライン",
142
+ "text.release comment optional": "リリースコメント(任意) — 空欄で自動入力",
143
+ "text.saved": "保存された",
88
144
  "text.sure_to_x": "{x}しますか?",
89
145
  "text.technical detail": "技術情報",
90
146
  "text.there_is_nothing_to_delete": "削除するデータがありません.",
91
147
  "text.there_is_nothing_to_process": "処理するデータがありません.",
92
148
  "text.there_is_nothing_to_save": "保存するデータがありません.",
93
149
  "text.you.are.now.in": "{state}になりました.",
150
+ "title.activity list": "業務定義リスト",
151
+ "title.activity model item list": "業務モデルリスト",
152
+ "title.approval-line item list": "決裁ライン詳細",
153
+ "title.are_you_sure": "本当に進めますか?",
154
+ "title.contact": "連絡先",
94
155
  "title.event list": "イベント一覧",
95
156
  "title.import event": "イベントの取り込み",
96
157
  "title.lookup role": "ロール検索",
@@ -3,25 +3,40 @@
3
3
  "button.cancel": "취소",
4
4
  "button.close": "닫기",
5
5
  "button.confirm": "확인",
6
+ "button.copy": "복사",
7
+ "button.copy to draft": "드래프트로 복사",
6
8
  "button.create": "생성",
7
9
  "button.delete": "삭제",
8
10
  "button.detail": "상세",
11
+ "button.diff with previous": "앞 버전과 비교",
12
+ "button.edit": "수정",
9
13
  "button.edit-contact": "연락처 편집",
10
14
  "button.empty": "비우기",
11
15
  "button.export": "내보내기",
12
16
  "button.refresh": "새로고침",
17
+ "button.register": "등록",
13
18
  "button.reset": "초기화",
14
19
  "button.save": "저장",
20
+ "button.schedule-task": "스케쥴 등록",
21
+ "button.select": "선택",
22
+ "button.start": "시작",
15
23
  "button.update": "수정",
24
+ "button.view": "보기",
16
25
  "error.value is empty": "{value}의 값이 존재하지 않습니다.",
17
26
  "field.active": "활성화",
18
27
  "field.address": "주소",
28
+ "field.approval-line": "결재선",
29
+ "field.approver": "결재자",
30
+ "field.assignee": "피할당자",
19
31
  "field.attributes": "속성",
20
32
  "field.brand_image": "브랜드 사진",
21
33
  "field.brand_name": "브랜드 이름",
34
+ "field.category": "카테고리",
22
35
  "field.code": "코드",
23
36
  "field.company": "회사",
24
37
  "field.content_image": "컨텐츠 이미지",
38
+ "field.count": "건수",
39
+ "field.country-code": "국가",
25
40
  "field.country_code": "국가 코드",
26
41
  "field.created-at": "생성일시",
27
42
  "field.created_at": "생성일시",
@@ -34,37 +49,71 @@
34
49
  "field.domain": "도메인",
35
50
  "field.email": "이메일",
36
51
  "field.group": "그룹",
52
+ "field.hidden": "숨기기",
53
+ "field.i18n-label": "다국어 라벨",
37
54
  "field.id": "아이디",
55
+ "field.key": "키",
56
+ "field.maximum": "최대값",
57
+ "field.minimum": "최소값",
58
+ "field.model": "모델",
38
59
  "field.name": "이름",
60
+ "field.netmask": "네트워크 마스크",
61
+ "field.options": "선택옵션",
39
62
  "field.params": "파라미터",
63
+ "field.phone": "전화번호",
40
64
  "field.platform": "플랫폼",
41
65
  "field.provider": "공급자",
42
66
  "field.rank": "순위",
67
+ "field.reason": "사유",
68
+ "field.report-type": "리포트용 화면종류",
43
69
  "field.required role": "필수 역할",
70
+ "field.resource_url": "리소스 url",
71
+ "field.role": "역할",
72
+ "field.schedule": "스케줄",
73
+ "field.search-keys": "검색키",
74
+ "field.serial-no": "일련번호",
75
+ "field.spec": "명세",
44
76
  "field.status": "상태",
77
+ "field.store-id": "스토어 ID",
45
78
  "field.subdomain": "서브도메인",
79
+ "field.supervisory-role": "관리자 역할",
46
80
  "field.system_flag": "system_flag",
81
+ "field.t-key": "번역 키",
82
+ "field.tag": "태그이름",
47
83
  "field.theme": "테마",
48
84
  "field.thumbnail": "미리보기",
49
85
  "field.timezone": "타임존",
50
86
  "field.type": "종류",
87
+ "field.unit": "단위",
51
88
  "field.uom": "단위",
52
89
  "field.updated-at": "수정일시",
53
90
  "field.updated_at": "수정일시",
54
91
  "field.updater": "수정자",
55
92
  "field.value": "값",
56
93
  "label.company": "회사",
94
+ "label.country": "국가",
57
95
  "label.days": "일",
58
96
  "label.description": "설명",
97
+ "label.email": "이메일",
59
98
  "label.env-var": "환경 변수",
99
+ "label.filter-deprecated": "폐기",
100
+ "label.filter-draft": "초안",
101
+ "label.filter-released": "배포",
60
102
  "label.hours": "시간",
61
103
  "label.minutes": "분",
62
104
  "label.name": "이름",
105
+ "label.participation-approval": "결재",
106
+ "label.participation-reference": "참조",
107
+ "label.password": "비밀번호",
108
+ "label.platform": "플랫폼",
109
+ "label.please_select_a_country": "국가를 선택하세요",
110
+ "label.please_select_a_platform": "플랫폼을 선택하세요",
63
111
  "label.seconds": "초",
64
112
  "label.state": "상태",
65
113
  "label.status": "상태",
66
114
  "label.type": "유형",
67
115
  "prompt.sure to navigate away?": "변경 사항이 저장되지 않았습니다. 정말 이동하시겠습니까?",
116
+ "text.approval-line-empty": "결재자가 없습니다 — 올리면 곧 완료됩니다",
68
117
  "text.are_you_sure": "진행하시겠습니까?",
69
118
  "text.ask ai": "AI에게 묻기",
70
119
  "text.column visibility setting": "컬럼 보이기",
@@ -80,17 +129,29 @@
80
129
  "text.error": "오류",
81
130
  "text.existing_item_found": "이미 있는 항목입니다",
82
131
  "text.failed": "실패",
132
+ "text.form_has_invalid_input": "유효하지 않은 값이 입력되었습니다.",
83
133
  "text.info_x_successfully": "{x}했습니다",
134
+ "text.initial version all new": "최초 버전 — 전체 신규",
135
+ "text.loading": "읽는 중…",
136
+ "text.no difference from previous": "앞 버전과 차이가 없습니다",
137
+ "text.no released version information available": "배포된 버전이 없습니다",
84
138
  "text.nothing_changed": "변경사항 없음",
85
139
  "text.nothing_selected": "선택되지 않음",
86
140
  "text.offline": "오프라인",
87
141
  "text.online": "온라인",
142
+ "text.release comment optional": "릴리즈 코멘트(선택) — 비우면 자동 입력",
143
+ "text.saved": "저장되었습니다",
88
144
  "text.sure_to_x": "{x}하시겠습니까?",
89
145
  "text.technical detail": "기술 정보",
90
146
  "text.there_is_nothing_to_delete": "삭제할 데이터가 없습니다.",
91
147
  "text.there_is_nothing_to_process": "처리할 데이터가 없습니다.",
92
148
  "text.there_is_nothing_to_save": "저장할 데이터가 없습니다.",
93
149
  "text.you.are.now.in": "{state} 되었습니다.",
150
+ "title.activity list": "업무정의 리스트",
151
+ "title.activity model item list": "업무 모델 항목",
152
+ "title.approval-line item list": "결재선 상세",
153
+ "title.are_you_sure": "정말 진행하시겠습니까?",
154
+ "title.contact": "연락처",
94
155
  "title.event list": "일정",
95
156
  "title.import event": "일정 가져오기",
96
157
  "title.lookup role": "역할 검색",