Skip to content

Commit 50b1e79

Browse files
akoclaude
andcommitted
feat: add ElementId column to catalog strings table (#83)
Addresses feedback that (QualifiedName, StringContext, Language) is not unique — e.g., two LOG messages in the same microflow produce identical rows. The ElementId column stores the activity/widget/value ID to distinguish them. ElementId values: - Microflow activities: ActionActivity.$ID - Page: Page.$ID (for title/URL) - Enumeration values: EnumerationValue.$ID - Workflow activities: WorkflowActivity.$ID Enables grouping translations for the same element: SELECT ElementId, Language, StringValue FROM catalog.strings WHERE QualifiedName = 'Module.Flow' AND StringContext = 'log_message' ORDER BY ElementId, Language; Note: page widget strings (button captions, labels) are not yet extracted — tracked separately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2ab9a1b commit 50b1e79

File tree

3 files changed

+42
-26
lines changed

3 files changed

+42
-26
lines changed

mdl/catalog/builder_strings.go

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@ func (b *Builder) buildStrings() error {
1515
}
1616

1717
stmt, err := b.tx.Prepare(`
18-
INSERT INTO strings (QualifiedName, ObjectType, StringValue, StringContext, Language, ModuleName)
19-
VALUES (?, ?, ?, ?, ?, ?)
18+
INSERT INTO strings (QualifiedName, ObjectType, StringValue, StringContext, Language, ElementId, ModuleName)
19+
VALUES (?, ?, ?, ?, ?, ?, ?)
2020
`)
2121
if err != nil {
2222
return err
2323
}
2424
defer stmt.Close()
2525

2626
count := 0
27-
insert := func(qn, objType, value, ctx, lang, module string) {
27+
insert := func(qn, objType, value, ctx, lang, elementID, module string) {
2828
if value == "" {
2929
return
3030
}
31-
stmt.Exec(qn, objType, value, ctx, lang, module)
31+
stmt.Exec(qn, objType, value, ctx, lang, elementID, module)
3232
count++
3333
}
3434

@@ -40,16 +40,18 @@ func (b *Builder) buildStrings() error {
4040
moduleName := b.hierarchy.getModuleName(moduleID)
4141
qn := moduleName + "." + pg.Name
4242

43+
pageID := string(pg.ID)
44+
4345
// Page title translations (with language code)
4446
if pg.Title != nil && pg.Title.Translations != nil {
4547
for lang, t := range pg.Title.Translations {
46-
insert(qn, "PAGE", t, "page_title", lang, moduleName)
48+
insert(qn, "PAGE", t, "page_title", lang, pageID, moduleName)
4749
}
4850
}
4951

5052
// Page URL (no language)
5153
if pg.URL != "" {
52-
insert(qn, "PAGE", pg.URL, "page_url", "", moduleName)
54+
insert(qn, "PAGE", pg.URL, "page_url", "", pageID, moduleName)
5355
}
5456
}
5557
}
@@ -62,9 +64,11 @@ func (b *Builder) buildStrings() error {
6264
moduleName := b.hierarchy.getModuleName(moduleID)
6365
qn := moduleName + "." + mf.Name
6466

67+
mfID := string(mf.ID)
68+
6569
// Documentation (no language)
6670
if mf.Documentation != "" {
67-
insert(qn, "MICROFLOW", mf.Documentation, "documentation", "", moduleName)
71+
insert(qn, "MICROFLOW", mf.Documentation, "documentation", "", mfID, moduleName)
6872
}
6973

7074
// Extract strings from activities
@@ -80,10 +84,15 @@ func (b *Builder) buildStrings() error {
8084
moduleName := b.hierarchy.getModuleName(moduleID)
8185
qn := moduleName + "." + enum.Name
8286

87+
enumID := string(enum.ID)
8388
for _, val := range enum.Values {
8489
if val.Caption != nil && val.Caption.Translations != nil {
90+
valID := string(val.ID)
91+
if valID == "" {
92+
valID = enumID
93+
}
8594
for lang, t := range val.Caption.Translations {
86-
insert(qn, "ENUMERATION", t, "enum_caption", lang, moduleName)
95+
insert(qn, "ENUMERATION", t, "enum_caption", lang, valID, moduleName)
8796
}
8897
}
8998
}
@@ -98,14 +107,15 @@ func (b *Builder) buildStrings() error {
98107
moduleName := b.hierarchy.getModuleName(moduleID)
99108
qn := moduleName + "." + wf.Name
100109

110+
wfID := string(wf.ID)
101111
if wf.WorkflowName != "" {
102-
insert(qn, "WORKFLOW", wf.WorkflowName, "workflow_name", "", moduleName)
112+
insert(qn, "WORKFLOW", wf.WorkflowName, "workflow_name", "", wfID, moduleName)
103113
}
104114
if wf.WorkflowDescription != "" {
105-
insert(qn, "WORKFLOW", wf.WorkflowDescription, "workflow_description", "", moduleName)
115+
insert(qn, "WORKFLOW", wf.WorkflowDescription, "workflow_description", "", wfID, moduleName)
106116
}
107117
if wf.Documentation != "" {
108-
insert(qn, "WORKFLOW", wf.Documentation, "documentation", "", moduleName)
118+
insert(qn, "WORKFLOW", wf.Documentation, "documentation", "", wfID, moduleName)
109119
}
110120

111121
if wf.Flow != nil {
@@ -119,23 +129,24 @@ func (b *Builder) buildStrings() error {
119129
}
120130

121131
// extractWorkflowFlowStrings extracts strings from workflow activities recursively.
122-
func extractWorkflowFlowStrings(flow *workflows.Flow, qn, moduleName string, insert func(string, string, string, string, string, string)) {
132+
func extractWorkflowFlowStrings(flow *workflows.Flow, qn, moduleName string, insert func(string, string, string, string, string, string, string)) {
123133
for _, act := range flow.Activities {
134+
actID := string(act.GetID())
124135
if act.GetCaption() != "" {
125-
insert(qn, "WORKFLOW", act.GetCaption(), "activity_caption", "", moduleName)
136+
insert(qn, "WORKFLOW", act.GetCaption(), "activity_caption", "", actID, moduleName)
126137
}
127138

128139
switch a := act.(type) {
129140
case *workflows.UserTask:
130141
if a.TaskName != "" {
131-
insert(qn, "WORKFLOW", a.TaskName, "task_name", "", moduleName)
142+
insert(qn, "WORKFLOW", a.TaskName, "task_name", "", actID, moduleName)
132143
}
133144
if a.TaskDescription != "" {
134-
insert(qn, "WORKFLOW", a.TaskDescription, "task_description", "", moduleName)
145+
insert(qn, "WORKFLOW", a.TaskDescription, "task_description", "", actID, moduleName)
135146
}
136147
for _, outcome := range a.Outcomes {
137148
if outcome.Caption != "" {
138-
insert(qn, "WORKFLOW", outcome.Caption, "outcome_caption", "", moduleName)
149+
insert(qn, "WORKFLOW", outcome.Caption, "outcome_caption", "", actID, moduleName)
139150
}
140151
if outcome.Flow != nil {
141152
extractWorkflowFlowStrings(outcome.Flow, qn, moduleName, insert)
@@ -170,7 +181,7 @@ func extractWorkflowFlowStrings(flow *workflows.Flow, qn, moduleName string, ins
170181
}
171182

172183
// extractActivityStrings extracts string literals from microflow/nanoflow activities.
173-
func extractActivityStrings(oc *microflows.MicroflowObjectCollection, qn, objType, moduleName string, insert func(string, string, string, string, string, string)) {
184+
func extractActivityStrings(oc *microflows.MicroflowObjectCollection, qn, objType, moduleName string, insert func(string, string, string, string, string, string, string)) {
174185
if oc == nil {
175186
return
176187
}
@@ -181,26 +192,28 @@ func extractActivityStrings(oc *microflows.MicroflowObjectCollection, qn, objTyp
181192
continue
182193
}
183194

195+
actID := string(act.ID)
196+
184197
switch a := act.Action.(type) {
185198
case *microflows.LogMessageAction:
186199
if a.MessageTemplate != nil && a.MessageTemplate.Translations != nil {
187200
for lang, t := range a.MessageTemplate.Translations {
188-
insert(qn, objType, t, "log_message", lang, moduleName)
201+
insert(qn, objType, t, "log_message", lang, actID, moduleName)
189202
}
190203
}
191204
if a.LogNodeName != "" {
192-
insert(qn, objType, a.LogNodeName, "log_node", "", moduleName)
205+
insert(qn, objType, a.LogNodeName, "log_node", "", actID, moduleName)
193206
}
194207
case *microflows.ShowMessageAction:
195208
if a.Template != nil && a.Template.Translations != nil {
196209
for lang, t := range a.Template.Translations {
197-
insert(qn, objType, t, "show_message", lang, moduleName)
210+
insert(qn, objType, t, "show_message", lang, actID, moduleName)
198211
}
199212
}
200213
case *microflows.ValidationFeedbackAction:
201214
if a.Template != nil && a.Template.Translations != nil {
202215
for lang, t := range a.Template.Translations {
203-
insert(qn, objType, t, "validation_message", lang, moduleName)
216+
insert(qn, objType, t, "validation_message", lang, actID, moduleName)
204217
}
205218
}
206219
}

mdl/catalog/tables.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,7 @@ func (c *Catalog) createTables() error {
828828
StringValue,
829829
StringContext,
830830
Language,
831+
ElementId,
831832
ModuleName
832833
)`,
833834

mdl/linter/rules/missing_translations_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import (
1212
)
1313

1414
// setupTranslationsDB creates an in-memory SQLite database with the strings FTS5 table
15-
// and inserts test data.
15+
// and inserts test data. Rows are [QualifiedName, ObjectType, StringValue, StringContext, Language, ModuleName].
16+
// ElementId is auto-generated from QualifiedName+StringContext for simplicity.
1617
func setupTranslationsDB(t *testing.T, rows [][]string) *sql.DB {
1718
t.Helper()
1819
db, err := sql.Open("sqlite", ":memory:")
@@ -21,14 +22,14 @@ func setupTranslationsDB(t *testing.T, rows [][]string) *sql.DB {
2122
}
2223

2324
_, err = db.Exec(`CREATE VIRTUAL TABLE strings USING fts5(
24-
QualifiedName, ObjectType, StringValue, StringContext, Language, ModuleName
25+
QualifiedName, ObjectType, StringValue, StringContext, Language, ElementId, ModuleName
2526
)`)
2627
if err != nil {
2728
t.Fatalf("failed to create strings table: %v", err)
2829
}
2930

30-
stmt, err := db.Prepare(`INSERT INTO strings (QualifiedName, ObjectType, StringValue, StringContext, Language, ModuleName)
31-
VALUES (?, ?, ?, ?, ?, ?)`)
31+
stmt, err := db.Prepare(`INSERT INTO strings (QualifiedName, ObjectType, StringValue, StringContext, Language, ElementId, ModuleName)
32+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
3233
if err != nil {
3334
t.Fatalf("failed to prepare insert: %v", err)
3435
}
@@ -38,7 +39,8 @@ func setupTranslationsDB(t *testing.T, rows [][]string) *sql.DB {
3839
if len(row) != 6 {
3940
t.Fatalf("expected 6 columns, got %d", len(row))
4041
}
41-
_, err := stmt.Exec(row[0], row[1], row[2], row[3], row[4], row[5])
42+
elementID := row[0] + ":" + row[3] // synthetic ID for tests
43+
_, err := stmt.Exec(row[0], row[1], row[2], row[3], row[4], elementID, row[5])
4244
if err != nil {
4345
t.Fatalf("failed to insert row: %v", err)
4446
}

0 commit comments

Comments
 (0)