@dforge-core/dforge-mcp 0.1.4 → 0.1.6

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": "@dforge-core/dforge-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "MCP server for dForge module authoring. Exposes scaffold/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/iash44/dForge-core",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@dforge-core/dforge-cli": "^0.2.2",
35
- "@dforge-core/metadata": "^0.0.4",
35
+ "@dforge-core/metadata": "^0.0.5",
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "zod": "^4.4.3"
38
38
  },
@@ -52,7 +52,7 @@
52
52
  "vizType": {
53
53
  "type": "string",
54
54
  "enum": ["table", "chart", "kpi", "pivot", "tree", "markdown"],
55
- "description": "Visualization type. (Older packages may use `metric` for `kpi`.)"
55
+ "description": "Visualization type. For any chart the vizType is `chart`; the chart kind goes in `config.chartType`."
56
56
  },
57
57
  "datasetCd": {
58
58
  "type": "string",
@@ -61,9 +61,142 @@
61
61
  "title": { "type": "string" },
62
62
  "config": {
63
63
  "type": "object",
64
- "description": "Viz-type-specific configuration.\n- chart: { chartType ('bar'|'horizontalBar'|'stackedBar'|'combo'|'line'|'area'|'pie'|'doughnut'|'scatter'|'bubble'|'funnel'|'heatmap'), categoryCol, valueCol, agg, seriesCol?, sizeCol?, chartSize? ('sm'|'m'|'l'|'xl'), clickAction?, showTrend?, series? }. `series` (a single object OR an array) declares CROSS-SOURCE overlay series on bar/horizontalBar/line/area, each { source?, categoryCol, valueCol, agg, label? }; `source` names a sibling dataset code (omit = the panel's own dataset). Overlays share one category axis (union of category values; bar gaps fill 0, line/area gap with null).\n- kpi: { metrics: [ metric, ... ] }. Each metric is EITHER an aggregation metric { column, agg ('sum'|'avg'|'min'|'max'|'count'), display?, label?, target?, min?, max?, icon?, sparklineDimension? } OR a FORMULA metric { formula (e.g. '[won] / [all] * 100'), inputs: [ { alias, column, agg, source? } ], format?: { style?: 'number'|'percent', decimals? }, display?, label? }. A formula input's `source` names a sibling dataset code for CROSS-DATASET metrics (omit = the panel's own dataset). Auto format (omit `format.style`) inherits the first input column's own formatter (currency/grouping).\n- table: { groupRules, aggregations, colorRules }."
64
+ "description": "Viz-type-specific configuration. `kpi` and `chart` are structurally validated (see kpiConfig / chartConfig below); `table`/`pivot`/`tree`/`markdown` accept any object."
65
65
  }
66
66
  },
67
+ "additionalProperties": false,
68
+ "allOf": [
69
+ {
70
+ "if": { "required": ["vizType"], "properties": { "vizType": { "const": "kpi" } } },
71
+ "then": { "properties": { "config": { "$ref": "#/$defs/kpiConfig" } } }
72
+ },
73
+ {
74
+ "if": { "required": ["vizType"], "properties": { "vizType": { "const": "chart" } } },
75
+ "then": { "properties": { "config": { "$ref": "#/$defs/chartConfig" } } }
76
+ }
77
+ ]
78
+ },
79
+
80
+ "aggFunc": {
81
+ "type": "string",
82
+ "enum": ["sum", "avg", "min", "max", "count"],
83
+ "description": "Aggregation function."
84
+ },
85
+
86
+ "kpiConfig": {
87
+ "type": "object",
88
+ "description": "KPI panel config: one or more metric cards.",
89
+ "required": ["metrics"],
90
+ "properties": {
91
+ "metrics": {
92
+ "type": "array",
93
+ "minItems": 1,
94
+ "items": { "$ref": "#/$defs/kpiMetric" }
95
+ }
96
+ },
97
+ "additionalProperties": false
98
+ },
99
+
100
+ "kpiMetric": {
101
+ "type": "object",
102
+ "description": "A KPI metric — EITHER an aggregation metric (`column` + `agg`, and no formula/inputs) OR a formula metric (`formula` + `inputs`, and no column/agg).",
103
+ "properties": {
104
+ "column": { "type": "string", "description": "Column to aggregate (aggregation metric)." },
105
+ "agg": { "$ref": "#/$defs/aggFunc" },
106
+ "formula": { "type": "string", "description": "Expression over `[alias]` inputs, e.g. '[won] / [all] * 100' (formula metric)." },
107
+ "inputs": {
108
+ "type": "array",
109
+ "minItems": 1,
110
+ "description": "Named aggregation inputs the formula references by `[alias]`.",
111
+ "items": { "$ref": "#/$defs/kpiInput" }
112
+ },
113
+ "format": { "$ref": "#/$defs/metricFormat" },
114
+ "display": { "type": "string", "enum": ["value", "gauge", "progress", "sparkline", "icon"] },
115
+ "label": { "type": "string" },
116
+ "target": { "type": "number" },
117
+ "min": { "type": "number" },
118
+ "max": { "type": "number" },
119
+ "icon": { "type": "string" },
120
+ "sparklineDimension": { "type": "string" }
121
+ },
122
+ "additionalProperties": false,
123
+ "allOf": [
124
+ {
125
+ "if": { "required": ["formula"] },
126
+ "then": {
127
+ "required": ["formula", "inputs"],
128
+ "not": { "anyOf": [ { "required": ["column"] }, { "required": ["agg"] } ] }
129
+ },
130
+ "else": {
131
+ "required": ["column", "agg"],
132
+ "not": { "anyOf": [ { "required": ["formula"] }, { "required": ["inputs"] } ] }
133
+ }
134
+ }
135
+ ]
136
+ },
137
+
138
+ "kpiInput": {
139
+ "type": "object",
140
+ "description": "One named aggregation a formula metric references by `[alias]`.",
141
+ "required": ["alias", "column", "agg"],
142
+ "properties": {
143
+ "alias": { "type": "string", "description": "Token used in the formula as `[alias]`." },
144
+ "column": { "type": "string" },
145
+ "agg": { "$ref": "#/$defs/aggFunc" },
146
+ "source": { "type": "string", "description": "Sibling dataset code to aggregate over (CROSS-DATASET); omit = the panel's own dataset." }
147
+ },
148
+ "additionalProperties": false
149
+ },
150
+
151
+ "metricFormat": {
152
+ "type": "object",
153
+ "description": "Formula-result format. OMIT for Auto — inherits the first input column's own formatter (currency/grouping).",
154
+ "properties": {
155
+ "style": { "type": "string", "enum": ["number", "percent"] },
156
+ "decimals": { "type": "integer", "minimum": 0 }
157
+ },
158
+ "additionalProperties": false
159
+ },
160
+
161
+ "chartConfig": {
162
+ "type": "object",
163
+ "description": "Chart panel config. The chart kind is `chartType`; the panel `vizType` is always `chart`.",
164
+ "required": ["chartType", "categoryCol", "valueCol"],
165
+ "properties": {
166
+ "chartType": {
167
+ "type": "string",
168
+ "enum": ["bar", "horizontalBar", "stackedBar", "combo", "line", "area", "pie", "doughnut", "scatter", "bubble", "funnel", "heatmap"]
169
+ },
170
+ "categoryCol": { "type": "string" },
171
+ "valueCol": { "type": "string" },
172
+ "agg": { "$ref": "#/$defs/aggFunc" },
173
+ "seriesCol": { "type": "string", "description": "Split-by dimension (same dataset)." },
174
+ "sizeCol": { "type": "string", "description": "Bubble radius column." },
175
+ "chartSize": { "type": "string", "enum": ["sm", "m", "l", "xl"] },
176
+ "clickAction": { "type": "string", "enum": ["crossFilter", "drillThrough", "none"] },
177
+ "showTrend": { "type": "boolean" },
178
+ "series": {
179
+ "description": "CROSS-SOURCE overlay series (bar/horizontalBar/line/area) — a single series OR an array. Each aligns on the shared category axis.",
180
+ "oneOf": [
181
+ { "$ref": "#/$defs/chartSeries" },
182
+ { "type": "array", "items": { "$ref": "#/$defs/chartSeries" } }
183
+ ]
184
+ }
185
+ },
186
+ "additionalProperties": false
187
+ },
188
+
189
+ "chartSeries": {
190
+ "type": "object",
191
+ "description": "One cross-source overlay series.",
192
+ "required": ["categoryCol", "valueCol", "agg"],
193
+ "properties": {
194
+ "source": { "type": "string", "description": "Sibling dataset code; omit = the panel's own dataset." },
195
+ "categoryCol": { "type": "string" },
196
+ "valueCol": { "type": "string" },
197
+ "agg": { "$ref": "#/$defs/aggFunc" },
198
+ "label": { "type": "string" }
199
+ },
67
200
  "additionalProperties": false
68
201
  },
69
202
 
@@ -140,11 +273,13 @@
140
273
  "leafKey": { "type": "string", "description": "Column on the query entity identifying the tree node a row belongs to (e.g. 'account_id' on balance_register)." },
141
274
  "measures": {
142
275
  "type": "array",
276
+ "minItems": 1,
143
277
  "items": { "type": "string" },
144
278
  "description": "Value column codes (on the query entity) to SUM into ancestors. Must be additively-rollable."
145
279
  },
146
280
  "labels": {
147
281
  "type": "array",
282
+ "minItems": 1,
148
283
  "items": { "type": "string" },
149
284
  "description": "Node-describing columns, expressed as the report column keys ('account.account_code'). Sourced from the tree entity's physical column (the segment after the dot); emitted under the same key so the report layout is unchanged."
150
285
  },
@@ -116,7 +116,7 @@ Always-on cheat-sheet — enough to author inline; load the linked `references/*
116
116
  - **`toString`:** every entity needs one, `{column}` braces, e.g. `"{first_name} {last_name}"`.
117
117
  - **Data views:** `dataSources` array at root — never root-level `entityCode` + `columns`. → `data-views.md`
118
118
  - **Menus:** leaf items use `dataViewCode` (not `viewCode`); section nodes omit `itemType`; icons are Bootstrap names sans `bi-`. → `menus.md`
119
- - **Security roles:** `rights` (not `entityRights`); entities `SIUDC`, actions/reports/folders `E`. Rights keys: same-module entity bare (`product`), cross-module entity dotted (`fin.invoice`), and actions/reports/folders use a **colon** prefix — `action:approve`, `report:summary`, `folder:east` (never a dot). Omit a key to deny; never map to `""`. → `security.md`
119
+ - **Security roles:** `rights` (not `entityRights`); entities `SIUDC`, actions/reports/folders `E`. Rights keys: same-module entity bare (`product`), cross-module entity dotted (`fin.invoice`), and actions/reports/folders use a **colon** prefix — `action:approve`, `report:summary`, `folder:east` (never a dot). Omit a key to deny; never map to `""`. `roles.json` carries `description` (English fallback) + `rights` only — **no `label`**; the localized role display name lives in the translation files as `roles.<code>.label` and is completeness-enforced. → `security.md`, `translations.md`
120
120
  - **Action script** in `ui/actions.json` = bare filename (no path, no `.dsl`).
121
121
  - **Action DSL dates:** inside `execute:` use lowercase `now()`; `TODAY()`/`NOW()` are formula-only (`canExecute:`/formula columns) and are **undefined in `execute:`**. → `action-dsl.md`
122
122
  - **SQL placeholders** = `@paramName` (not `:paramName`).
@@ -397,7 +397,7 @@ Key areas (full checklist):
397
397
  - **Security**: every entity code in the manifest appears in at least one role's rights map; `rights` key used (not `entityRights`); entity rights use `SIUDC` letters; actions/reports use `E`.
398
398
  - **Actions**: every `script` value in `ui/actions.json` is a bare filename (no path, no `.dsl` extension); every action referenced by a trigger or job exists in `ui/actions.json`.
399
399
  - **Seed data**: numeric PKs; parent entities loaded before children; no circular references.
400
- - **Translations**: a `translations/<locale>.json` file exists for every locale in `supportedLocales`; every trait-provided field (`created_at`, `updated_at`, etc.) has a translation entry in each file.
400
+ - **Translations**: a `translations/<locale>.json` file exists for every locale in `supportedLocales` **plus the `en-US` base**; every trait-provided field (`created_at`, `updated_at`, etc.) has a translation entry in each file; a `roles` section carries a `label` for **every** role in `security/roles.json` (completeness-enforced in every locale incl. en-US — keys are module-qualified role codes like `crm.admin`).
401
401
 
402
402
  ### Step 3 — Translation deferral check
403
403
 
@@ -144,6 +144,7 @@ var note = params[note] // no quotes around param name in this DSL
144
144
  | `addDays(date, n)` | Date | Add `n` days to a date, e.g. `addDays(now(), 30)`. |
145
145
  | `nextNumber('entity')` | String | Generate next value from a number sequence. Usually not needed — platform auto-fills on `insert()`. Use for pre-generating numbers. |
146
146
  | `callProc('proc_name', { args })` | Result | Call a stored procedure. Args are passed as named parameters. |
147
+ | `entityLink('entityCd', record, description?)` | Link value | Build a clickable link to any entity record, for storing in an `entitylink` (jsonb) column. `record` is a row object (e.g. the result of `insert()` or `getRecord()`); its PK columns are read and stored as **strings**, so snowflake/cuid ids (> 2^53) stay exact. Optional `description` sets the link's display text. |
147
148
 
148
149
  > **Dates: `now()` in `execute:`, `TODAY()`/`NOW()` in formulas.** The `execute:` block runs as
149
150
  > JavaScript (Jint) and exposes **lowercase `now()`** only — `TODAY()` and `NOW()` (uppercase) are
@@ -67,6 +67,26 @@ Plus declare the FK constraint in the entity's `references` block:
67
67
  }
68
68
  ```
69
69
 
70
+ ### Referential actions — `onDelete` / `onUpdate` (optional)
71
+
72
+ A reference entry may declare `onDelete` and/or `onUpdate` to emit an `ON DELETE` / `ON UPDATE` clause on the FK. Both accept: `"cascade"`, `"setNull"`, `"restrict"`, `"noAction"`. **Omitted = `noAction`** (plain FK — the default and correct choice for most references).
73
+
74
+ ```json
75
+ "references": {
76
+ "FK_OpportunityLine_Opportunity": {
77
+ "from": { "field": "opportunity_id" },
78
+ "to": { "entity": "opportunity", "field": "opportunity_id" },
79
+ "onDelete": "cascade"
80
+ }
81
+ }
82
+ ```
83
+
84
+ - **`cascade`** — deleting the parent deletes its children. Use for owned child collections (line items under their header) so the parent delete isn't blocked by a FK violation.
85
+ - **`setNull`** — nulls the FK column when the parent is deleted; the FK column **must be nullable**.
86
+ - **`restrict` / `noAction`** — block the parent delete while children exist (the safe default; leave the keys off to get this).
87
+ - **`onUpdate`** — fires when the parent's key value changes. A **no-op for immutable `cuid` PKs** (identity-trait entities), so only meaningful for entities keyed on a natural/mutable PK.
88
+ - **Self-healing on reinstall:** the DDL generator reads the live FK's current rule and drops+recreates the FK only when it changed, so changing `onDelete` and reinstalling applies to already-provisioned tenants (and is a no-op otherwise). An unknown value fails install fast.
89
+
70
90
  > **FK column `dbDatatype` must exactly match the referenced entity's PK `dbDatatype`.** Never guess.
71
91
  > - Entities using the `identity` trait → PK is `dbDatatype: "cuid"` → FK column must also be `"cuid"`
72
92
  > - Cross-module or legacy entities → call `dforge_module_inspect` on the referenced module and read the PK column's `dbDatatype` before declaring the FK
@@ -65,7 +65,7 @@ Source of truth: `server/database/system-modules/metadata/seed-data/field_types.
65
65
  | `lookup` | `guid` | `R` | Reference to another entity (always paired with a hidden FK column — see SKILL.md "FK+Reference pattern") |
66
66
  | `user` | `cuid` | `D` | User picker. Writes a user ID directly, no paired FK needed. |
67
67
  | `grid` | `set` | `S` | Detail grid of related records (1:N backwards reference). Used with `link` declaring the relation. |
68
- | `entitylink` | `json` | `D` | Polymorphic link to any entity (stores `{entity, id}` pair). |
68
+ | `entitylink` | `json` | `D` | Polymorphic link to any entity (stores `{entity, id}` pair). Populate from action DSL with the `entityLink('entityCd', record, description?)` built-in (see `action-dsl.md`). |
69
69
 
70
70
  ## JSON
71
71
 
@@ -14,7 +14,11 @@ Lives in: `security/roles.json`
14
14
 
15
15
  ## Role definition
16
16
 
17
- Role codes use a `module.role` naming pattern (e.g. `crm.admin`, `crm.sales-rep`):
17
+ Role codes use a `module.role` naming pattern (e.g. `crm.admin`, `crm.sales-rep`).
18
+
19
+ > **Role display name & translations.** Each role has exactly two keys: `description` and `rights` (`roles.schema.json` is `additionalProperties: false` — **do not** add a `label` here). `description` is the English fallback display name. The **localized** display name lives in the translation files as `roles.<code>.label`, keyed by the module-qualified role code, and is **completeness-enforced**: every role must have a `roles.<code>.label` in every `translations/<locale>.json` (including the `en-US` base) or install fails. See `translations.md`.
20
+
21
+ Example:
18
22
 
19
23
  ```json
20
24
  {
@@ -33,6 +33,10 @@ Translation files mirror the module's content structure. Each translatable objec
33
33
  "folders": {
34
34
  "crm": { "label": "Sales CRM" }
35
35
  },
36
+ "roles": {
37
+ "crm.admin": { "label": "CRM Administrator" },
38
+ "crm.sales-rep": { "label": "Sales Representative" }
39
+ },
36
40
  "views": {
37
41
  "contacts": { "label": "Contacts" },
38
42
  "contacts_list": { "label": "Contact List" },
@@ -75,14 +79,29 @@ Translation files mirror the module's content structure. Each translatable objec
75
79
  |---|---|---|
76
80
  | `entities` | Entity labels (`label`, `desc`) and all field labels | Entity codes → `{ label, desc, fields: { field_code: { label } } }` |
77
81
  | `folders` | Folder labels | Folder codes (root folder key from `ui/folders.json`) |
82
+ | `roles` | Role display labels | Role codes (keys from `security/roles.json`, already module-qualified — e.g. `crm.admin`) → `{ label }` |
78
83
  | `views` | Data view labels | View codes (keys from `ui/data_views.json`) |
79
84
  | `menus` | Menu node labels (root + nested items) | Menu root key → `{ label, items: { item_code: { label } } }` matching `ui/menus.json` structure |
80
85
  | `actions` | Action labels, descriptions, and param labels | Action codes (keys from `ui/actions.json`) → `{ label, desc, params: { param_cd: { label } } }` |
81
86
  | `reports` | Report dataset captions and param labels | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label } } }` |
82
87
 
88
+ ### Roles ARE translated — and completeness is enforced
89
+
90
+ Role labels are **required and localized** (this changed — older docs said roles were skipped; they are not). `SecurityRegistrar` gives every role a `res_id`, so `TranslationRegistrar` wires the role label exactly like entities/views/menus:
91
+
92
+ ```json
93
+ "roles": {
94
+ "crm.admin": { "label": "CRM Administrator" },
95
+ "crm.sales-rep": { "label": "Sales Representative" }
96
+ }
97
+ ```
98
+
99
+ - **Keys are the role codes** from `security/roles.json` — already module-qualified (e.g. `crm.admin`, `crm.sales-rep`). They match `module_role.name`.
100
+ - **Value is `{ "label": ... }`** — the localized display name. It supersedes the `description` from `roles.json` at runtime; `description` remains the English fallback if a role has no translation entry.
101
+ - **Completeness is enforced.** `TranslationCompletenessValidator` requires `roles.<code>.label` for **every** role in `security/roles.json`, in **every** translation file — including the `en-US` base. A missing role label fails install with `Label for role '<code>'.` So ship a `roles` block with a label for each role in **each** `translations/<locale>.json` (en-US + every locale in `supportedLocales`).
102
+
83
103
  ### Sections that are NOT translated (write-time only)
84
104
 
85
- - `roles` — role rows have no `res_id` column. The `roles` key may appear in shipped translation files (e.g. `modules/crm/translations/de-DE.json`) but is **explicitly skipped** by the installer. Role display name comes from `security/roles.json` `description` only. (Comment in [`TranslationRegistrar.cs:248`](../../../server/src/dForge.Admin/Services/ModuleInstall/TranslationRegistrar.cs#L248): "reserved for future use.")
86
105
  - `settings` — completeness *is* checked by the validator when listed in `manifest.supportedLocales` (so you'll be forced to ship `settings.<cd>.label` for each declared locale), but the registrar doesn't display those translated labels at runtime. The English `label` in `settings.json` is what users see today.
87
106
  - `print_templates` — not currently consumed by the registrar. Template labels come from `ui/print_templates.json`.
88
107
 
@@ -112,6 +131,10 @@ If you include these sections, the install succeeds (they're silently ignored)
112
131
  "folders": {
113
132
  "crm": { "label": "Vertrieb CRM" }
114
133
  },
134
+ "roles": {
135
+ "crm.admin": { "label": "CRM-Administrator" },
136
+ "crm.sales-rep": { "label": "Vertriebsmitarbeiter" }
137
+ },
115
138
  "views": {
116
139
  "contacts": { "label": "Kontakte" }
117
140
  },
@@ -144,7 +167,7 @@ Every listed locale MUST have a matching `translations/<locale>.json` with a lab
144
167
 
145
168
  1. **Always provide `en-US`** as the base language. Other languages fall back to it for missing keys.
146
169
  2. **Locale format is `ll-CC`** (e.g. `en-US`, `de-DE`, `fr-FR`) — language code + country code, hyphen-separated.
147
- 3. **Translate everything user-visible**: entity labels, every field label (including trait-provided fields like `created_date`, `last_updated`), view names, menu items, action labels, role descriptions, setting labels, folder names.
170
+ 3. **Translate everything user-visible**: entity labels, every field label (including trait-provided fields like `created_date`, `last_updated`), view names, menu items, action labels, **role labels (required — completeness-enforced, `roles.<code>.label` for every role in every locale incl. en-US)**, setting labels, folder names.
148
171
  4. **Include virtual columns** (references, sets, formulas) in field translations — they appear in the UI just like physical columns.
149
172
  5. **Menu translations mirror the `ui/menus.json` structure** — root key → `items` → nested items.
150
173
  6. **Missing keys fall back** to the `en-US` value, then to the raw code name. So it's safe to ship partial translations — untranslated items show in English rather than breaking.
@@ -90,7 +90,7 @@ For each entity:
90
90
  ## Security
91
91
 
92
92
  - [ ] `security/roles.json` exists (at least one role)
93
- - [ ] Every role has `description` and `rights` (both required; no `label` field display name comes from `description`). The `roles.schema.json` sets `additionalProperties: false`, so any extra field (including `label`) is rejected at install time.
93
+ - [ ] Every role has `description` and `rights` (both required; no `label` field in `roles.json` the `roles.schema.json` sets `additionalProperties: false`, so any extra field including `label` is rejected here). `description` is the English display-name fallback; **localized role labels live in the translation files** (`roles.<code>.label`) and are completeness-enforced — see Translations below.
94
94
  - [ ] The field name is `rights`, NOT `entityRights`
95
95
  - [ ] Rights strings use only valid letters: `SIUDC` for entities, `E` for actions/reports/folders
96
96
  - [ ] Entity codes in `rights` all reference real entities (in this module or dependencies)
@@ -132,7 +132,7 @@ For each action:
132
132
  - [ ] Has `views` section with labels for every data view
133
133
  - [ ] Has `menus` section matching the `ui/menus.json` structure
134
134
  - [ ] Has `actions` section with labels for every action
135
- - [ ] Has `roles` section with labels for every role
135
+ - [ ] Has `roles` section with a `label` for **every** role in `security/roles.json` (completeness-enforced — a missing role label fails install; keys are the module-qualified role codes, e.g. `crm.admin`)
136
136
  - [ ] Has `settings` section with labels for every setting (if settings exist)
137
137
  - [ ] Has `folders` section with label for the root folder
138
138
  - [ ] Additional language files (if any) have the same structure as `en-US.json`