@willwade/aac-processors 0.0.12 → 0.0.14

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 (60) hide show
  1. package/README.md +46 -44
  2. package/dist/core/baseProcessor.d.ts +41 -0
  3. package/dist/core/baseProcessor.js +41 -0
  4. package/dist/core/treeStructure.d.ts +35 -2
  5. package/dist/core/treeStructure.js +18 -3
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/processors/astericsGridProcessor.d.ts +15 -0
  9. package/dist/processors/astericsGridProcessor.js +17 -0
  10. package/dist/processors/gridset/helpers.d.ts +4 -1
  11. package/dist/processors/gridset/helpers.js +4 -0
  12. package/dist/processors/gridset/pluginTypes.js +51 -50
  13. package/dist/processors/gridset/symbolAlignment.d.ts +125 -0
  14. package/dist/processors/gridset/symbolAlignment.js +283 -0
  15. package/dist/processors/gridset/symbolExtractor.js +3 -2
  16. package/dist/processors/gridset/symbolSearch.js +9 -7
  17. package/dist/processors/gridsetProcessor.d.ts +26 -0
  18. package/dist/processors/gridsetProcessor.js +178 -25
  19. package/dist/processors/obfProcessor.d.ts +26 -0
  20. package/dist/processors/obfProcessor.js +94 -1
  21. package/dist/processors/snap/helpers.d.ts +5 -1
  22. package/dist/processors/snap/helpers.js +5 -0
  23. package/dist/processors/snapProcessor.d.ts +2 -0
  24. package/dist/processors/snapProcessor.js +156 -5
  25. package/dist/processors/touchchatProcessor.d.ts +26 -0
  26. package/dist/processors/touchchatProcessor.js +106 -6
  27. package/dist/types/aac.d.ts +63 -0
  28. package/dist/types/aac.js +33 -0
  29. package/dist/{optional → utilities}/analytics/history.d.ts +12 -1
  30. package/dist/{optional → utilities}/analytics/index.d.ts +2 -0
  31. package/dist/{optional → utilities}/analytics/index.js +6 -1
  32. package/dist/{optional → utilities}/analytics/metrics/comparison.js +8 -4
  33. package/dist/{optional → utilities}/analytics/metrics/core.d.ts +9 -0
  34. package/dist/{optional → utilities}/analytics/metrics/core.js +190 -37
  35. package/dist/{optional → utilities}/analytics/metrics/effort.d.ts +10 -0
  36. package/dist/{optional → utilities}/analytics/metrics/effort.js +13 -0
  37. package/dist/utilities/analytics/metrics/obl-types.d.ts +93 -0
  38. package/dist/utilities/analytics/metrics/obl-types.js +7 -0
  39. package/dist/utilities/analytics/metrics/obl.d.ts +40 -0
  40. package/dist/utilities/analytics/metrics/obl.js +287 -0
  41. package/dist/{optional → utilities}/analytics/metrics/vocabulary.js +6 -4
  42. package/dist/{optional → utilities}/symbolTools.js +13 -16
  43. package/dist/utilities/translation/translationProcessor.d.ts +119 -0
  44. package/dist/utilities/translation/translationProcessor.js +204 -0
  45. package/dist/validation/gridsetValidator.js +10 -0
  46. package/package.json +1 -1
  47. /package/dist/{optional → utilities}/analytics/history.js +0 -0
  48. /package/dist/{optional → utilities}/analytics/metrics/comparison.d.ts +0 -0
  49. /package/dist/{optional → utilities}/analytics/metrics/index.d.ts +0 -0
  50. /package/dist/{optional → utilities}/analytics/metrics/index.js +0 -0
  51. /package/dist/{optional → utilities}/analytics/metrics/sentence.d.ts +0 -0
  52. /package/dist/{optional → utilities}/analytics/metrics/sentence.js +0 -0
  53. /package/dist/{optional → utilities}/analytics/metrics/types.d.ts +0 -0
  54. /package/dist/{optional → utilities}/analytics/metrics/types.js +0 -0
  55. /package/dist/{optional → utilities}/analytics/metrics/vocabulary.d.ts +0 -0
  56. /package/dist/{optional → utilities}/analytics/reference/index.d.ts +0 -0
  57. /package/dist/{optional → utilities}/analytics/reference/index.js +0 -0
  58. /package/dist/{optional → utilities}/analytics/utils/idGenerator.d.ts +0 -0
  59. /package/dist/{optional → utilities}/analytics/utils/idGenerator.js +0 -0
  60. /package/dist/{optional → utilities}/symbolTools.d.ts +0 -0
@@ -106,10 +106,10 @@ function detectPluginCellType(content) {
106
106
  if (!content) {
107
107
  return { cellType: Grid3CellType.Regular };
108
108
  }
109
- const contentType = content.ContentType || content.contenttype || content.ContentType;
110
- const contentSubType = content.ContentSubType || content.contentsubtype || content.ContentSubType;
109
+ const contentType = content.ContentType || content.contenttype;
110
+ const contentSubType = content.ContentSubType || content.contentsubtype;
111
111
  // Workspace cells - full editing workspaces
112
- if (contentType === 'Workspace') {
112
+ if (contentType === 'Workspace' || content.Style?.BasedOnStyle === 'Workspace') {
113
113
  return {
114
114
  cellType: Grid3CellType.Workspace,
115
115
  subType: contentSubType || undefined,
@@ -118,7 +118,7 @@ function detectPluginCellType(content) {
118
118
  };
119
119
  }
120
120
  // LiveCell detection - dynamic content displays
121
- if (contentType === 'LiveCell') {
121
+ if (contentType === 'LiveCell' || content.Style?.BasedOnStyle === 'LiveCell') {
122
122
  return {
123
123
  cellType: Grid3CellType.LiveCell,
124
124
  liveCellType: contentSubType || undefined,
@@ -127,7 +127,7 @@ function detectPluginCellType(content) {
127
127
  };
128
128
  }
129
129
  // AutoContent detection - dynamic word/content suggestions
130
- if (contentType === 'AutoContent') {
130
+ if (contentType === 'AutoContent' || content.Style?.BasedOnStyle === 'AutoContent') {
131
131
  const autoContentType = extractAutoContentType(content);
132
132
  return {
133
133
  cellType: Grid3CellType.AutoContent,
@@ -172,38 +172,38 @@ function inferWorkspacePlugin(subType) {
172
172
  return undefined;
173
173
  const normalized = subType.toLowerCase();
174
174
  if (normalized.includes('chat'))
175
- return 'chat';
175
+ return 'Grid3.Chat';
176
176
  if (normalized.includes('email') || normalized.includes('mail'))
177
- return 'email';
178
- if (normalized.includes('word') || normalized.includes('processor'))
179
- return 'wordprocessor';
177
+ return 'Grid3.Email';
178
+ if (normalized.includes('word') || normalized.includes('doc'))
179
+ return 'Grid3.WordProcessor';
180
180
  if (normalized.includes('phone'))
181
- return 'phone';
181
+ return 'Grid3.Phone';
182
182
  if (normalized.includes('sms') || normalized.includes('text'))
183
- return 'sms';
184
- if (normalized.includes('web') || normalized.includes('browser'))
185
- return 'webbrowser';
186
- if (normalized.includes('computer') || normalized.includes('control'))
187
- return 'computercontrol';
188
- if (normalized.includes('calculator') || normalized.includes('calc'))
189
- return 'calculator';
190
- if (normalized.includes('timer') || normalized.includes('stopwatch'))
191
- return 'timer';
183
+ return 'Grid3.Sms';
184
+ if (normalized.includes('browser') || normalized.includes('web'))
185
+ return 'Grid3.WebBrowser';
186
+ if (normalized.includes('computer'))
187
+ return 'Grid3.ComputerControl';
188
+ if (normalized.includes('calc'))
189
+ return 'Grid3.Calculator';
190
+ if (normalized.includes('timer'))
191
+ return 'Grid3.Timer';
192
192
  if (normalized.includes('music') || normalized.includes('video'))
193
- return 'musicvideo';
193
+ return 'Grid3.MusicVideo';
194
194
  if (normalized.includes('photo') || normalized.includes('image'))
195
- return 'photos';
195
+ return 'Grid3.Photos';
196
196
  if (normalized.includes('contact'))
197
- return 'contacts';
197
+ return 'Grid3.Contacts';
198
198
  if (normalized.includes('learning'))
199
- return 'interactivelearning';
200
- if (normalized.includes('message') && normalized.includes('bank'))
201
- return 'messagebanking';
202
- if (normalized.includes('env') || normalized.includes('ir'))
203
- return 'environmentcontrol';
204
- if (normalized.includes('setting'))
205
- return 'settings';
206
- return undefined;
199
+ return 'Grid3.InteractiveLearning';
200
+ if (normalized.includes('message') && normalized.includes('banking'))
201
+ return 'Grid3.MessageBanking';
202
+ if (normalized.includes('control'))
203
+ return 'Grid3.EnvironmentControl';
204
+ if (normalized.includes('settings'))
205
+ return 'Grid3.Settings';
206
+ return `Grid3.${subType}`;
207
207
  }
208
208
  /**
209
209
  * Infer plugin ID from live cell type
@@ -212,24 +212,25 @@ function inferLiveCellPlugin(liveCellType) {
212
212
  if (!liveCellType)
213
213
  return undefined;
214
214
  const normalized = liveCellType.toLowerCase();
215
- if (normalized.includes('clock') || normalized.includes('time') || normalized.includes('date')) {
216
- return 'clock';
217
- }
215
+ if (normalized.includes('clock'))
216
+ return 'Grid3.Clock';
217
+ if (normalized.includes('date'))
218
+ return 'Grid3.Clock';
218
219
  if (normalized.includes('volume'))
219
- return 'speech';
220
+ return 'Grid3.Volume';
220
221
  if (normalized.includes('speed'))
221
- return 'speech';
222
+ return 'Grid3.Speed';
222
223
  if (normalized.includes('voice'))
223
- return 'speech';
224
+ return 'Grid3.Speech';
224
225
  if (normalized.includes('message'))
225
- return 'chat';
226
+ return 'Grid3.Chat';
226
227
  if (normalized.includes('battery'))
227
- return 'settings';
228
- if (normalized.includes('wifi') || normalized.includes('network'))
229
- return 'settings';
228
+ return 'Grid3.Battery';
229
+ if (normalized.includes('wifi'))
230
+ return 'Grid3.Wifi';
230
231
  if (normalized.includes('bluetooth'))
231
- return 'settings';
232
- return undefined;
232
+ return 'Grid3.Bluetooth';
233
+ return `Grid3.${liveCellType}`;
233
234
  }
234
235
  /**
235
236
  * Infer plugin ID from auto content type
@@ -239,24 +240,24 @@ function inferAutoContentPlugin(autoContentType) {
239
240
  return undefined;
240
241
  const normalized = autoContentType.toLowerCase();
241
242
  if (normalized.includes('voice') || normalized.includes('speed'))
242
- return 'speech';
243
+ return 'Grid3.Speech';
243
244
  if (normalized.includes('email') || normalized.includes('mail'))
244
- return 'email';
245
+ return 'Grid3.Email';
245
246
  if (normalized.includes('phone'))
246
- return 'phone';
247
+ return 'Grid3.Phone';
247
248
  if (normalized.includes('sms') || normalized.includes('text'))
248
- return 'sms';
249
+ return 'Grid3.Sms';
249
250
  if (normalized.includes('web') ||
250
251
  normalized.includes('favorite') ||
251
252
  normalized.includes('history')) {
252
- return 'webbrowser';
253
+ return 'Grid3.WebBrowser';
253
254
  }
254
255
  if (normalized.includes('prediction'))
255
- return 'prediction';
256
+ return 'Grid3.Prediction';
256
257
  if (normalized.includes('grammar'))
257
- return 'grammar';
258
+ return 'Grid3.Grammar';
258
259
  if (normalized.includes('context'))
259
- return 'autocontent';
260
+ return 'Grid3.AutoContent';
260
261
  return undefined;
261
262
  }
262
263
  /**
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Symbol Alignment for Translation
3
+ *
4
+ * Utilities to preserve symbol positions during text translation.
5
+ * When translating AAC gridset messages that contain symbols attached
6
+ * to specific words, we need to maintain the symbol-to-word associations
7
+ * across languages.
8
+ *
9
+ * Example:
10
+ * English: "I want apple juice" with apple symbol on "apple"
11
+ * Spanish: "Yo quiero jugo de manzana" with apple symbol on "manzana"
12
+ */
13
+ /**
14
+ * Represents a symbol anchored to a specific word in the text
15
+ */
16
+ export interface SymbolAnchor {
17
+ symbolRef: string;
18
+ wordIndex: number;
19
+ originalWord: string;
20
+ startPos: number;
21
+ endPos: number;
22
+ }
23
+ /**
24
+ * Parsed message with symbol anchors
25
+ */
26
+ export interface ParsedMessage {
27
+ text: string;
28
+ words: string[];
29
+ symbols: SymbolAnchor[];
30
+ }
31
+ /**
32
+ * Translation result with preserved symbols
33
+ */
34
+ export interface TranslatedMessage {
35
+ text: string;
36
+ alignment: {
37
+ originalWord: string;
38
+ translatedWord: string;
39
+ originalIndex: number;
40
+ translatedIndex: number;
41
+ }[];
42
+ }
43
+ /**
44
+ * Parse a message to extract text and symbol anchors.
45
+ *
46
+ * This handles various formats:
47
+ * 1. Plain text with no symbols
48
+ * 2. Rich text with embedded symbol markers (future enhancement)
49
+ * 3. Text where symbols are tracked separately (via richText.symbols)
50
+ *
51
+ * For now, this assumes symbols are tracked separately in the richText structure.
52
+ * The text itself is plain, and we need to tokenize it to find word positions.
53
+ *
54
+ * @param message - The message text (may contain words or be plain)
55
+ * @param richTextSymbols - Optional symbols from richText.symbols array
56
+ * @returns Parsed message with word positions and symbol anchors
57
+ */
58
+ export declare function parseMessageWithSymbols(message: string, richTextSymbols?: Array<{
59
+ text: string;
60
+ image?: string;
61
+ }>): ParsedMessage;
62
+ /**
63
+ * Align words from original text to translated text.
64
+ *
65
+ * This is a simple alignment strategy that works for many cases:
66
+ * 1. Exact word matching (for cognates, names, numbers)
67
+ * 2. Position-based alignment (assumes similar word order)
68
+ *
69
+ * For more accurate alignment, you could integrate with:
70
+ * - Translation APIs that return alignment (e.g., Google Translate's word alignment)
71
+ * - Statistical machine translation alignment tools
72
+ * - Bilingual dictionaries
73
+ *
74
+ * @param originalWords - Words from the original text
75
+ * @param translatedWords - Words from the translated text
76
+ * @returns Alignment mapping between original and translated word indices
77
+ */
78
+ export declare function alignWords(originalWords: string[], translatedWords: string[]): TranslatedMessage['alignment'];
79
+ /**
80
+ * Reattach symbols to translated text based on word alignment.
81
+ *
82
+ * @param translatedText - The translated plain text
83
+ * @param originalParsed - The original parsed message with symbols
84
+ * @param alignment - Word alignment between original and translation
85
+ * @returns Translated text with symbols embedded (as rich text structure)
86
+ */
87
+ export declare function reattachSymbols(translatedText: string, originalParsed: ParsedMessage, alignment: TranslatedMessage['alignment']): {
88
+ text: string;
89
+ richTextSymbols: Array<{
90
+ text: string;
91
+ image?: string;
92
+ }>;
93
+ };
94
+ /**
95
+ * Complete pipeline: translate a message while preserving symbol positions.
96
+ *
97
+ * @param originalMessage - The original message text
98
+ * @param translatedText - The translated text (from translation API)
99
+ * @param richTextSymbols - Original symbols from richText.symbols
100
+ * @returns Object with translated text and aligned symbols
101
+ */
102
+ export declare function translateWithSymbols(originalMessage: string, translatedText: string, richTextSymbols?: Array<{
103
+ text: string;
104
+ image?: string;
105
+ }>): {
106
+ text: string;
107
+ richTextSymbols: Array<{
108
+ text: string;
109
+ image?: string;
110
+ }>;
111
+ };
112
+ /**
113
+ * Extract symbols from a button for use during translation.
114
+ *
115
+ * This helper extracts symbols from either:
116
+ * - button.semanticAction.richText.symbols
117
+ * - button.image (if it's a symbol library reference)
118
+ *
119
+ * @param button - The AAC button
120
+ * @returns Array of symbol attachments
121
+ */
122
+ export declare function extractSymbolsFromButton(button: any): Array<{
123
+ text: string;
124
+ image?: string;
125
+ }> | undefined;
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ /**
3
+ * Symbol Alignment for Translation
4
+ *
5
+ * Utilities to preserve symbol positions during text translation.
6
+ * When translating AAC gridset messages that contain symbols attached
7
+ * to specific words, we need to maintain the symbol-to-word associations
8
+ * across languages.
9
+ *
10
+ * Example:
11
+ * English: "I want apple juice" with apple symbol on "apple"
12
+ * Spanish: "Yo quiero jugo de manzana" with apple symbol on "manzana"
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.parseMessageWithSymbols = parseMessageWithSymbols;
16
+ exports.alignWords = alignWords;
17
+ exports.reattachSymbols = reattachSymbols;
18
+ exports.translateWithSymbols = translateWithSymbols;
19
+ exports.extractSymbolsFromButton = extractSymbolsFromButton;
20
+ /**
21
+ * Parse a message to extract text and symbol anchors.
22
+ *
23
+ * This handles various formats:
24
+ * 1. Plain text with no symbols
25
+ * 2. Rich text with embedded symbol markers (future enhancement)
26
+ * 3. Text where symbols are tracked separately (via richText.symbols)
27
+ *
28
+ * For now, this assumes symbols are tracked separately in the richText structure.
29
+ * The text itself is plain, and we need to tokenize it to find word positions.
30
+ *
31
+ * @param message - The message text (may contain words or be plain)
32
+ * @param richTextSymbols - Optional symbols from richText.symbols array
33
+ * @returns Parsed message with word positions and symbol anchors
34
+ */
35
+ function parseMessageWithSymbols(message, richTextSymbols) {
36
+ // Normalize whitespace for consistent tokenization
37
+ const normalizedMessage = message.trim().replace(/\s+/g, ' ');
38
+ // Tokenize into words, preserving punctuation
39
+ const words = [];
40
+ const wordPositions = [];
41
+ // Split by whitespace but track positions
42
+ let currentPos = 0;
43
+ const parts = normalizedMessage.split(/(\s+)/); // Keep delimiters
44
+ for (const part of parts) {
45
+ if (part.trim().length > 0) {
46
+ // This is a word
47
+ const startPos = currentPos;
48
+ const endPos = currentPos + part.length;
49
+ words.push(part);
50
+ wordPositions.push({ start: startPos, end: endPos, word: part });
51
+ currentPos = endPos;
52
+ }
53
+ else {
54
+ // This is whitespace
55
+ currentPos += part.length;
56
+ }
57
+ }
58
+ // Extract symbol anchors from richText.symbols if provided
59
+ const symbols = [];
60
+ if (richTextSymbols && richTextSymbols.length > 0) {
61
+ for (const sym of richTextSymbols) {
62
+ // Find which word this symbol is attached to
63
+ const wordIndex = words.findIndex((w) => w === sym.text);
64
+ if (wordIndex !== -1) {
65
+ const pos = wordPositions[wordIndex];
66
+ symbols.push({
67
+ symbolRef: sym.image || '',
68
+ wordIndex,
69
+ originalWord: sym.text,
70
+ startPos: pos.start,
71
+ endPos: pos.end,
72
+ });
73
+ }
74
+ else {
75
+ // Fuzzy match - find closest word (handles case differences, punctuation)
76
+ const normalizedSymText = sym.text.toLowerCase().replace(/[^\w]/g, '');
77
+ const fuzzyIndex = words.findIndex((w) => w.toLowerCase().replace(/[^\w]/g, '') === normalizedSymText);
78
+ if (fuzzyIndex !== -1) {
79
+ const pos = wordPositions[fuzzyIndex];
80
+ symbols.push({
81
+ symbolRef: sym.image || '',
82
+ wordIndex: fuzzyIndex,
83
+ originalWord: words[fuzzyIndex],
84
+ startPos: pos.start,
85
+ endPos: pos.end,
86
+ });
87
+ }
88
+ }
89
+ }
90
+ }
91
+ return {
92
+ text: normalizedMessage,
93
+ words,
94
+ symbols,
95
+ };
96
+ }
97
+ /**
98
+ * Align words from original text to translated text.
99
+ *
100
+ * This is a simple alignment strategy that works for many cases:
101
+ * 1. Exact word matching (for cognates, names, numbers)
102
+ * 2. Position-based alignment (assumes similar word order)
103
+ *
104
+ * For more accurate alignment, you could integrate with:
105
+ * - Translation APIs that return alignment (e.g., Google Translate's word alignment)
106
+ * - Statistical machine translation alignment tools
107
+ * - Bilingual dictionaries
108
+ *
109
+ * @param originalWords - Words from the original text
110
+ * @param translatedWords - Words from the translated text
111
+ * @returns Alignment mapping between original and translated word indices
112
+ */
113
+ function alignWords(originalWords, translatedWords) {
114
+ const alignment = [];
115
+ // Strategy 1: Try to match identical words (numbers, names, cognates)
116
+ const matchedTranslatedIndices = new Set();
117
+ for (let origIdx = 0; origIdx < originalWords.length; origIdx++) {
118
+ const origWord = originalWords[origIdx];
119
+ const normalizedOrig = origWord.toLowerCase().replace(/[^\w]/g, '');
120
+ // Try to find this word in the translation
121
+ for (let transIdx = 0; transIdx < translatedWords.length; transIdx++) {
122
+ if (matchedTranslatedIndices.has(transIdx))
123
+ continue;
124
+ const transWord = translatedWords[transIdx];
125
+ const normalizedTrans = transWord.toLowerCase().replace(/[^\w]/g, '');
126
+ // Exact match (case-insensitive, ignoring punctuation)
127
+ if (normalizedOrig === normalizedTrans && normalizedOrig.length > 0) {
128
+ alignment.push({
129
+ originalWord: origWord,
130
+ translatedWord: transWord,
131
+ originalIndex: origIdx,
132
+ translatedIndex: transIdx,
133
+ });
134
+ matchedTranslatedIndices.add(transIdx);
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ // Strategy 2: For unmatched words, use positional alignment
140
+ // This is a simple fallback that assumes similar word order
141
+ for (let origIdx = 0; origIdx < originalWords.length; origIdx++) {
142
+ if (alignment.find((a) => a.originalIndex === origIdx))
143
+ continue; // Already matched
144
+ // Find the closest unmatched position in translation
145
+ let bestTransIdx = -1;
146
+ let minDistance = Infinity;
147
+ for (let transIdx = 0; transIdx < translatedWords.length; transIdx++) {
148
+ if (matchedTranslatedIndices.has(transIdx))
149
+ continue;
150
+ // Calculate relative position
151
+ const relativeOrigPos = origIdx / originalWords.length;
152
+ const relativeTransPos = transIdx / translatedWords.length;
153
+ const distance = Math.abs(relativeOrigPos - relativeTransPos);
154
+ if (distance < minDistance) {
155
+ minDistance = distance;
156
+ bestTransIdx = transIdx;
157
+ }
158
+ }
159
+ if (bestTransIdx !== -1) {
160
+ alignment.push({
161
+ originalWord: originalWords[origIdx],
162
+ translatedWord: translatedWords[bestTransIdx],
163
+ originalIndex: origIdx,
164
+ translatedIndex: bestTransIdx,
165
+ });
166
+ matchedTranslatedIndices.add(bestTransIdx);
167
+ }
168
+ }
169
+ return alignment;
170
+ }
171
+ /**
172
+ * Reattach symbols to translated text based on word alignment.
173
+ *
174
+ * @param translatedText - The translated plain text
175
+ * @param originalParsed - The original parsed message with symbols
176
+ * @param alignment - Word alignment between original and translation
177
+ * @returns Translated text with symbols embedded (as rich text structure)
178
+ */
179
+ function reattachSymbols(translatedText, originalParsed, alignment) {
180
+ // Tokenize the translated text
181
+ const translatedWords = translatedText
182
+ .trim()
183
+ .replace(/\s+/g, ' ')
184
+ .split(/\s+/)
185
+ .filter((w) => w.length > 0);
186
+ // Create the rich text symbols array
187
+ const richTextSymbols = [];
188
+ for (const symbol of originalParsed.symbols) {
189
+ // Find the alignment for this word
190
+ const wordAlignment = alignment.find((a) => a.originalIndex === symbol.wordIndex);
191
+ if (wordAlignment && wordAlignment.translatedIndex < translatedWords.length) {
192
+ const translatedWord = translatedWords[wordAlignment.translatedIndex];
193
+ // Attach the symbol to the translated word
194
+ richTextSymbols.push({
195
+ text: translatedWord,
196
+ image: symbol.symbolRef,
197
+ });
198
+ }
199
+ else {
200
+ // Fallback: keep symbol on original word if no alignment found
201
+ richTextSymbols.push({
202
+ text: symbol.originalWord,
203
+ image: symbol.symbolRef,
204
+ });
205
+ }
206
+ }
207
+ return {
208
+ text: translatedText,
209
+ richTextSymbols,
210
+ };
211
+ }
212
+ /**
213
+ * Complete pipeline: translate a message while preserving symbol positions.
214
+ *
215
+ * @param originalMessage - The original message text
216
+ * @param translatedText - The translated text (from translation API)
217
+ * @param richTextSymbols - Original symbols from richText.symbols
218
+ * @returns Object with translated text and aligned symbols
219
+ */
220
+ function translateWithSymbols(originalMessage, translatedText, richTextSymbols) {
221
+ // Step 1: Parse original message
222
+ const parsedOriginal = parseMessageWithSymbols(originalMessage, richTextSymbols);
223
+ // If no symbols, return as-is
224
+ if (parsedOriginal.symbols.length === 0) {
225
+ return {
226
+ text: translatedText,
227
+ richTextSymbols: [],
228
+ };
229
+ }
230
+ // Step 2: Tokenize translated text
231
+ const translatedWords = translatedText
232
+ .trim()
233
+ .replace(/\s+/g, ' ')
234
+ .split(/\s+/)
235
+ .filter((w) => w.length > 0);
236
+ // Step 3: Align words
237
+ const alignment = alignWords(parsedOriginal.words, translatedWords);
238
+ // Step 4: Reattach symbols
239
+ const result = reattachSymbols(translatedText, parsedOriginal, alignment);
240
+ return result;
241
+ }
242
+ /**
243
+ * Extract symbols from a button for use during translation.
244
+ *
245
+ * This helper extracts symbols from either:
246
+ * - button.semanticAction.richText.symbols
247
+ * - button.image (if it's a symbol library reference)
248
+ *
249
+ * @param button - The AAC button
250
+ * @returns Array of symbol attachments
251
+ */
252
+ function extractSymbolsFromButton(button) {
253
+ // First check richText structure
254
+ if (button.semanticAction?.richText?.symbols) {
255
+ return button.semanticAction.richText.symbols;
256
+ }
257
+ // Check if button has a symbol library reference as image
258
+ if (button.symbolLibrary && button.symbolPath) {
259
+ // Create a symbol attachment for the label/message
260
+ const text = button.label || button.message || '';
261
+ if (text) {
262
+ return [
263
+ {
264
+ text,
265
+ image: `[${button.symbolLibrary}]${button.symbolPath}`,
266
+ },
267
+ ];
268
+ }
269
+ }
270
+ // Check if image field contains a symbol reference
271
+ if (button.image && button.image.startsWith('[')) {
272
+ const text = button.label || button.message || '';
273
+ if (text) {
274
+ return [
275
+ {
276
+ text,
277
+ image: button.image,
278
+ },
279
+ ];
280
+ }
281
+ }
282
+ return undefined;
283
+ }
@@ -154,10 +154,11 @@ function extractSymbolLibraryImage(reference, options = {}) {
154
154
  };
155
155
  }
156
156
  // Successfully extracted!
157
- const format = detectImageFormat(resolved.data);
157
+ const data = resolved.data;
158
+ const format = data ? detectImageFormat(data) : 'unknown';
158
159
  return {
159
160
  found: true,
160
- data: resolved.data,
161
+ data,
161
162
  format,
162
163
  source: 'symbol-library',
163
164
  reference: reference,
@@ -126,13 +126,15 @@ function searchSymbols(searchTerm, options = {}) {
126
126
  // Exact match first
127
127
  if (index.searchTerms.has(lowerSearchTerm)) {
128
128
  const symbolFilename = index.searchTerms.get(lowerSearchTerm);
129
- results.push({
130
- searchTerm: lowerSearchTerm,
131
- symbolFilename,
132
- displayName: index.filenames.get(symbolFilename) || lowerSearchTerm,
133
- library: libraryName,
134
- exactMatch: true,
135
- });
129
+ if (symbolFilename) {
130
+ results.push({
131
+ searchTerm: lowerSearchTerm,
132
+ symbolFilename,
133
+ displayName: index.filenames.get(symbolFilename) || lowerSearchTerm,
134
+ library: libraryName,
135
+ exactMatch: true,
136
+ });
137
+ }
136
138
  }
137
139
  // Fuzzy match if enabled
138
140
  if (options.fuzzyMatch !== false) {
@@ -1,5 +1,6 @@
1
1
  import { BaseProcessor, ProcessorOptions, ExtractStringsResult, TranslatedString, SourceString } from '../core/baseProcessor';
2
2
  import { AACTree } from '../core/treeStructure';
3
+ import { type ButtonForTranslation, type LLMLTranslationResult } from '../utilities/translation/translationProcessor';
3
4
  import { ValidationResult } from '../validation/validationTypes';
4
5
  declare class GridsetProcessor extends BaseProcessor {
5
6
  constructor(options?: ProcessorOptions);
@@ -18,6 +19,31 @@ declare class GridsetProcessor extends BaseProcessor {
18
19
  extractTexts(filePathOrBuffer: string | Buffer): string[];
19
20
  loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
20
21
  processTexts(filePathOrBuffer: string | Buffer, translations: Map<string, string>, outputPath: string): Buffer;
22
+ /**
23
+ * Extract symbol information from a gridset for LLM-based translation.
24
+ * Returns a structured format showing which buttons have symbols and their context.
25
+ *
26
+ * This method uses shared translation utilities that work across all AAC formats.
27
+ *
28
+ * @param filePathOrBuffer - Path to gridset file or buffer
29
+ * @returns Array of symbol information for LLM processing
30
+ */
31
+ extractSymbolsForLLM(filePathOrBuffer: string | Buffer): ButtonForTranslation[];
32
+ /**
33
+ * Apply LLM translations with symbol information.
34
+ * The LLM should provide translations with symbol attachments in the correct positions.
35
+ *
36
+ * This method uses shared translation utilities that work across all AAC formats.
37
+ *
38
+ * @param filePathOrBuffer - Path to gridset file or buffer
39
+ * @param llmTranslations - Array of LLM translations with symbol info
40
+ * @param outputPath - Where to save the translated gridset
41
+ * @param options - Translation options (e.g., allowPartial for testing)
42
+ * @returns Buffer of the translated gridset
43
+ */
44
+ processLLMTranslations(filePathOrBuffer: string | Buffer, llmTranslations: LLMLTranslationResult[], outputPath: string, options?: {
45
+ allowPartial?: boolean;
46
+ }): Buffer;
21
47
  saveFromTree(tree: AACTree, outputPath: string): void;
22
48
  private calculateColumnDefinitions;
23
49
  private calculateRowDefinitions;