@richscripts/richtexteditor 2.14.1 → 2.15.0

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 (237) hide show
  1. package/README.md +173 -10
  2. package/angular/build-manifest.json +23 -0
  3. package/angular/index.d.ts +71 -76
  4. package/angular/index.mjs +2 -0
  5. package/angular/package.json +4 -0
  6. package/headless/index.d.ts +3 -1
  7. package/headless/index.js +72 -16
  8. package/index.d.ts +33 -54
  9. package/integrations/initialization-scope.js +27 -0
  10. package/integrations/native-mount.js +30 -0
  11. package/integrations/shared.d.ts +23 -1
  12. package/integrations/shared.js +58 -22
  13. package/package.json +25 -3
  14. package/react/index.js +118 -37
  15. package/react/index.mjs +29 -0
  16. package/richtexteditor/all_plugins.js +7001 -1078
  17. package/richtexteditor/all_plugins.min.js +62 -59
  18. package/richtexteditor/lang/emoji/LICENSE-CLDR.txt +41 -0
  19. package/richtexteditor/lang/emoji/rte-emoji-de.js +3 -0
  20. package/richtexteditor/lang/emoji/rte-emoji-es.js +3 -0
  21. package/richtexteditor/lang/emoji/rte-emoji-fr.js +3 -0
  22. package/richtexteditor/lang/emoji/rte-emoji-it.js +3 -0
  23. package/richtexteditor/lang/emoji/rte-emoji-ja.js +3 -0
  24. package/richtexteditor/lang/emoji/rte-emoji-ko.js +3 -0
  25. package/richtexteditor/lang/emoji/rte-emoji-nl.js +3 -0
  26. package/richtexteditor/lang/emoji/rte-emoji-pt-pt.js +3 -0
  27. package/richtexteditor/lang/emoji/rte-emoji-pt.js +3 -0
  28. package/richtexteditor/lang/emoji/rte-emoji-ru.js +3 -0
  29. package/richtexteditor/lang/emoji/rte-emoji-zh-Hans.js +3 -0
  30. package/richtexteditor/lang/emoji/rte-emoji-zh-Hant.js +3 -0
  31. package/richtexteditor/lang/rte-lang-af.js +1 -0
  32. package/richtexteditor/lang/rte-lang-ar.js +1 -0
  33. package/richtexteditor/lang/rte-lang-bg.js +1 -0
  34. package/richtexteditor/lang/rte-lang-bn.js +1 -0
  35. package/richtexteditor/lang/rte-lang-bs.js +1 -0
  36. package/richtexteditor/lang/rte-lang-ca.js +1 -0
  37. package/richtexteditor/lang/rte-lang-cs.js +1 -0
  38. package/richtexteditor/lang/rte-lang-cy.js +1 -0
  39. package/richtexteditor/lang/rte-lang-da.js +1 -0
  40. package/richtexteditor/lang/rte-lang-de.js +1017 -2
  41. package/richtexteditor/lang/rte-lang-el.js +1 -0
  42. package/richtexteditor/lang/rte-lang-en.js +1 -0
  43. package/richtexteditor/lang/rte-lang-es.js +1023 -2
  44. package/richtexteditor/lang/rte-lang-et.js +1 -0
  45. package/richtexteditor/lang/rte-lang-fa.js +1 -0
  46. package/richtexteditor/lang/rte-lang-fi.js +1 -0
  47. package/richtexteditor/lang/rte-lang-fil.js +1 -0
  48. package/richtexteditor/lang/rte-lang-fj.js +1 -0
  49. package/richtexteditor/lang/rte-lang-fr.js +1023 -2
  50. package/richtexteditor/lang/rte-lang-ga.js +1 -0
  51. package/richtexteditor/lang/rte-lang-gu.js +1 -0
  52. package/richtexteditor/lang/rte-lang-he.js +1 -0
  53. package/richtexteditor/lang/rte-lang-hi.js +1 -0
  54. package/richtexteditor/lang/rte-lang-hr.js +1 -0
  55. package/richtexteditor/lang/rte-lang-ht.js +1 -0
  56. package/richtexteditor/lang/rte-lang-hu.js +1 -0
  57. package/richtexteditor/lang/rte-lang-id.js +1 -0
  58. package/richtexteditor/lang/rte-lang-is.js +1 -0
  59. package/richtexteditor/lang/rte-lang-it.js +1023 -2
  60. package/richtexteditor/lang/rte-lang-ja.js +1023 -2
  61. package/richtexteditor/lang/rte-lang-kn.js +1 -0
  62. package/richtexteditor/lang/rte-lang-ko.js +1023 -2
  63. package/richtexteditor/lang/rte-lang-lt.js +1 -0
  64. package/richtexteditor/lang/rte-lang-lv.js +1 -0
  65. package/richtexteditor/lang/rte-lang-mg.js +1 -0
  66. package/richtexteditor/lang/rte-lang-mi.js +1 -0
  67. package/richtexteditor/lang/rte-lang-ml.js +1 -0
  68. package/richtexteditor/lang/rte-lang-mr.js +1 -0
  69. package/richtexteditor/lang/rte-lang-ms.js +1 -0
  70. package/richtexteditor/lang/rte-lang-mt.js +1 -0
  71. package/richtexteditor/lang/rte-lang-mww.js +1 -0
  72. package/richtexteditor/lang/rte-lang-nb.js +1 -0
  73. package/richtexteditor/lang/rte-lang-nl.js +1023 -2
  74. package/richtexteditor/lang/rte-lang-otq.js +1 -0
  75. package/richtexteditor/lang/rte-lang-pa.js +1 -0
  76. package/richtexteditor/lang/rte-lang-pl.js +1 -0
  77. package/richtexteditor/lang/rte-lang-pt-pt.js +1023 -2
  78. package/richtexteditor/lang/rte-lang-pt.js +1023 -2
  79. package/richtexteditor/lang/rte-lang-ro.js +1 -0
  80. package/richtexteditor/lang/rte-lang-ru.js +1023 -2
  81. package/richtexteditor/lang/rte-lang-sk.js +1 -0
  82. package/richtexteditor/lang/rte-lang-sl.js +1 -0
  83. package/richtexteditor/lang/rte-lang-sm.js +1 -0
  84. package/richtexteditor/lang/rte-lang-sr-Cyrl.js +1 -0
  85. package/richtexteditor/lang/rte-lang-sr-Latn.js +1 -0
  86. package/richtexteditor/lang/rte-lang-sv.js +1 -0
  87. package/richtexteditor/lang/rte-lang-sw.js +1 -0
  88. package/richtexteditor/lang/rte-lang-ta.js +1 -0
  89. package/richtexteditor/lang/rte-lang-te.js +1 -0
  90. package/richtexteditor/lang/rte-lang-th.js +1 -0
  91. package/richtexteditor/lang/rte-lang-tlh-Latn.js +1 -0
  92. package/richtexteditor/lang/rte-lang-tlh-Piqd.js +1 -0
  93. package/richtexteditor/lang/rte-lang-to.js +1 -0
  94. package/richtexteditor/lang/rte-lang-tr.js +1 -0
  95. package/richtexteditor/lang/rte-lang-ty.js +1 -0
  96. package/richtexteditor/lang/rte-lang-uk.js +1 -0
  97. package/richtexteditor/lang/rte-lang-ur.js +1 -0
  98. package/richtexteditor/lang/rte-lang-vi.js +1 -0
  99. package/richtexteditor/lang/rte-lang-yua.js +1 -0
  100. package/richtexteditor/lang/rte-lang-zh-Hans.js +1017 -2
  101. package/richtexteditor/lang/rte-lang-zh-Hant.js +1017 -2
  102. package/richtexteditor/plugins/a11yenhance.js +2 -1
  103. package/richtexteditor/plugins/a11yenhance.min.js +1 -1
  104. package/richtexteditor/plugins/accessibilitychecker.js +252 -60
  105. package/richtexteditor/plugins/accessibilitychecker.min.js +1 -1
  106. package/richtexteditor/plugins/aitoolkit.js +966 -303
  107. package/richtexteditor/plugins/aitoolkit.min.js +7 -7
  108. package/richtexteditor/plugins/all_plugins.js +7001 -1078
  109. package/richtexteditor/plugins/all_plugins.min.js +62 -59
  110. package/richtexteditor/plugins/autocorrect.js +66 -31
  111. package/richtexteditor/plugins/autocorrect.min.js +1 -1
  112. package/richtexteditor/plugins/autoembed.js +17 -0
  113. package/richtexteditor/plugins/autoembed.min.js +1 -1
  114. package/richtexteditor/plugins/blocktypes.js +18 -9
  115. package/richtexteditor/plugins/blocktypes.min.js +1 -1
  116. package/richtexteditor/plugins/bookmarkcard.js +12 -5
  117. package/richtexteditor/plugins/bookmarkcard.min.js +2 -2
  118. package/richtexteditor/plugins/changecase.js +72 -19
  119. package/richtexteditor/plugins/changecase.min.js +1 -1
  120. package/richtexteditor/plugins/charlimit.js +62 -2
  121. package/richtexteditor/plugins/charlimit.min.js +2 -2
  122. package/richtexteditor/plugins/comments.js +458 -59
  123. package/richtexteditor/plugins/comments.min.js +4 -3
  124. package/richtexteditor/plugins/contentminimap.js +25 -6
  125. package/richtexteditor/plugins/contentminimap.min.js +1 -1
  126. package/richtexteditor/plugins/crossreference.js +1 -1
  127. package/richtexteditor/plugins/crossreference.min.js +1 -1
  128. package/richtexteditor/plugins/dialoga11y.js +26 -4
  129. package/richtexteditor/plugins/dialoga11y.min.js +1 -1
  130. package/richtexteditor/plugins/dictation.js +1 -1
  131. package/richtexteditor/plugins/documentimport.js +220 -51
  132. package/richtexteditor/plugins/documentimport.min.js +4 -4
  133. package/richtexteditor/plugins/documentoutline.js +47 -8
  134. package/richtexteditor/plugins/documentoutline.min.js +1 -1
  135. package/richtexteditor/plugins/docxexport.js +204 -32
  136. package/richtexteditor/plugins/docxexport.min.js +2 -2
  137. package/richtexteditor/plugins/draghandle.js +267 -3
  138. package/richtexteditor/plugins/draghandle.min.js +1 -1
  139. package/richtexteditor/plugins/emailtoolkit.js +28 -14
  140. package/richtexteditor/plugins/emailtoolkit.min.js +1 -1
  141. package/richtexteditor/plugins/emojiautocomplete.js +110 -56
  142. package/richtexteditor/plugins/emojiautocomplete.min.js +1 -1
  143. package/richtexteditor/plugins/footnotes.js +65 -11
  144. package/richtexteditor/plugins/footnotes.min.js +1 -1
  145. package/richtexteditor/plugins/formatpainter.js +10 -0
  146. package/richtexteditor/plugins/formatpainter.min.js +1 -1
  147. package/richtexteditor/plugins/formattingmarks.js +6 -6
  148. package/richtexteditor/plugins/formattingmarks.min.js +1 -1
  149. package/richtexteditor/plugins/gapcursor.js +60 -0
  150. package/richtexteditor/plugins/gapcursor.min.js +1 -1
  151. package/richtexteditor/plugins/ghostcomplete.js +1 -1
  152. package/richtexteditor/plugins/html2pdf.js +1 -1
  153. package/richtexteditor/plugins/html2pdf.min.js +1 -1
  154. package/richtexteditor/plugins/imageeditor.js +12 -6
  155. package/richtexteditor/plugins/imageeditor.min.js +1 -1
  156. package/richtexteditor/plugins/importstyles.js +1 -1
  157. package/richtexteditor/plugins/inlinecode.js +25 -0
  158. package/richtexteditor/plugins/inlinecode.min.js +1 -1
  159. package/richtexteditor/plugins/insertcode.js +21 -3
  160. package/richtexteditor/plugins/insertcode.min.js +4 -4
  161. package/richtexteditor/plugins/insertemoji.js +112 -15
  162. package/richtexteditor/plugins/insertemoji.min.js +1 -1
  163. package/richtexteditor/plugins/insertgallery.js +42 -36
  164. package/richtexteditor/plugins/insertgallery.min.js +1 -1
  165. package/richtexteditor/plugins/inserttemplate.js +33 -17
  166. package/richtexteditor/plugins/inserttemplate.min.js +1 -1
  167. package/richtexteditor/plugins/keyboarda11y.js +66 -3
  168. package/richtexteditor/plugins/keyboarda11y.min.js +1 -1
  169. package/richtexteditor/plugins/layouttable.js +20 -0
  170. package/richtexteditor/plugins/layouttable.min.js +1 -1
  171. package/richtexteditor/plugins/linkchecker.js +29 -9
  172. package/richtexteditor/plugins/linkchecker.min.js +1 -1
  173. package/richtexteditor/plugins/localdraft.js +51 -7
  174. package/richtexteditor/plugins/localdraft.min.js +1 -1
  175. package/richtexteditor/plugins/markdownexport.js +159 -0
  176. package/richtexteditor/plugins/markdownexport.min.js +2 -2
  177. package/richtexteditor/plugins/mathdialog.js +11 -5
  178. package/richtexteditor/plugins/mathdialog.min.js +1 -1
  179. package/richtexteditor/plugins/mentionplugin.js +88 -25
  180. package/richtexteditor/plugins/mentionplugin.min.js +1 -1
  181. package/richtexteditor/plugins/mermaiddiagram.js +9 -3
  182. package/richtexteditor/plugins/mermaiddiagram.min.js +2 -2
  183. package/richtexteditor/plugins/multilevellist.js +4 -0
  184. package/richtexteditor/plugins/multilevellist.min.js +1 -1
  185. package/richtexteditor/plugins/pdfexport.js +235 -35
  186. package/richtexteditor/plugins/pdfexport.min.js +12 -12
  187. package/richtexteditor/plugins/permanentpen.js +3 -3
  188. package/richtexteditor/plugins/permanentpen.min.js +1 -1
  189. package/richtexteditor/plugins/readabilitystats.js +25 -11
  190. package/richtexteditor/plugins/readabilitystats.min.js +1 -1
  191. package/richtexteditor/plugins/readaloud.js +29 -15
  192. package/richtexteditor/plugins/readaloud.min.js +1 -1
  193. package/richtexteditor/plugins/restrictedediting.js +166 -0
  194. package/richtexteditor/plugins/restrictedediting.min.js +2 -2
  195. package/richtexteditor/plugins/revisionhistory.js +169 -37
  196. package/richtexteditor/plugins/revisionhistory.min.js +3 -3
  197. package/richtexteditor/plugins/rubyannotation.js +19 -9
  198. package/richtexteditor/plugins/rubyannotation.min.js +1 -1
  199. package/richtexteditor/plugins/sanitizer.js +62 -3
  200. package/richtexteditor/plugins/sanitizer.min.js +1 -1
  201. package/richtexteditor/plugins/slashcommand.js +30 -16
  202. package/richtexteditor/plugins/slashcommand.min.js +2 -2
  203. package/richtexteditor/plugins/smartchips.js +36 -22
  204. package/richtexteditor/plugins/smartchips.min.js +2 -2
  205. package/richtexteditor/plugins/spellcheck.js +17 -8
  206. package/richtexteditor/plugins/spellcheck.min.js +2 -2
  207. package/richtexteditor/plugins/stateinspector.js +19 -9
  208. package/richtexteditor/plugins/stateinspector.min.js +2 -2
  209. package/richtexteditor/plugins/structured-content-bridge.js +56 -24
  210. package/richtexteditor/plugins/structured-content-bridge.min.js +7 -7
  211. package/richtexteditor/plugins/tableofcontents.js +5 -2
  212. package/richtexteditor/plugins/tableofcontents.min.js +1 -1
  213. package/richtexteditor/plugins/tabletools.js +388 -3
  214. package/richtexteditor/plugins/tabletools.min.js +1 -1
  215. package/richtexteditor/plugins/textdirection.js +18 -5
  216. package/richtexteditor/plugins/textdirection.min.js +1 -1
  217. package/richtexteditor/plugins/textpartlanguage.js +161 -32
  218. package/richtexteditor/plugins/textpartlanguage.min.js +1 -1
  219. package/richtexteditor/plugins/todolist.js +90 -6
  220. package/richtexteditor/plugins/todolist.min.js +1 -1
  221. package/richtexteditor/plugins/trackedchanges.js +1812 -25
  222. package/richtexteditor/plugins/trackedchanges.min.js +5 -3
  223. package/richtexteditor/plugins/wordexport.js +2 -2
  224. package/richtexteditor/plugins/yjscollab.js +9 -4
  225. package/richtexteditor/plugins/yjscollab.min.js +1 -1
  226. package/richtexteditor/plugins/zz_richtextbox_dialog_style.js +22 -13
  227. package/richtexteditor/plugins/zz_richtextbox_dialog_style.min.js +1 -1
  228. package/richtexteditor/rte-config.js +130 -50
  229. package/richtexteditor/rte-headless.js +19 -4
  230. package/richtexteditor/rte-webcomponent.js +89 -16
  231. package/richtexteditor/rte.js +132 -52
  232. package/scripts/build-angular.mjs +97 -0
  233. package/svelte/index.d.ts +2 -1
  234. package/svelte/index.js +80 -9
  235. package/vue/index.js +130 -34
  236. package/angular/component.ts +0 -297
  237. package/angular/index.ts +0 -24
@@ -22,7 +22,10 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
22
22
  var scripts = document.getElementsByTagName("script");
23
23
  for (var j = scripts.length - 1; j >= 0; j--) {
24
24
  var src = scripts[j].getAttribute("src") || "";
25
- if (/(?:^|\/)(?:aitoolkit|all_plugins)\.js(?:\?.*)?$/i.test(src)) {
25
+ // 2026-10-17 the .min names too: a page loading all_plugins.min.js matched nothing and fell back to the fixed
26
+ // /richtexteditor path - the drive root over file://, so the AI stylesheet never loaded from a download opened
27
+ // from disk. tools/ai/aitoolkit-css-location.mjs
28
+ if (/(?:^|\/)(?:aitoolkit|all_plugins)(?:\.min)?\.js(?:[?#].*)?$/i.test(src)) {
26
29
  ourScript = scripts[j];
27
30
  break;
28
31
  }
@@ -33,11 +36,13 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
33
36
  // paying for it. The readable .css is still shipped and still the one to edit
34
37
  // or override; this only changes what gets fetched by default.
35
38
  function resolve(name) {
36
- return srcAttr
37
- ? srcAttr.replace(/[^/]+$/, name)
38
- : (((window.RTE_DefaultConfig && window.RTE_DefaultConfig.url_base) || "/richtexteditor") + "/plugins/" + name);
39
+ if (srcAttr) return srcAttr.replace(/[?#].*$/, "").replace(/[^/]+$/, name);
40
+ // no plugin script of ours (bundled into an app script): the folder rte.js was loaded from, then the old default
41
+ var base = (typeof RichTextEditor !== "undefined" && RichTextEditor.__scriptBase) ||
42
+ (window.RTE_DefaultConfig && window.RTE_DefaultConfig.url_base) || "/richtexteditor";
43
+ return base + "/plugins/" + name;
39
44
  }
40
- function bust(href) { return href.indexOf("?") < 0 ? href + "?v=20260830a" : href; }
45
+ function bust(href) { return href.indexOf("?") < 0 ? href + "?v=20261018a" : href; }
41
46
 
42
47
  var link = document.createElement("link");
43
48
  link.rel = "stylesheet";
@@ -127,26 +132,26 @@ function RTE_Plugin_AIToolkit() {
127
132
  // in a later pass. Customer overrides via `aiToolkitActions` keep
128
133
  // any legacy long-form labels untouched.
129
134
  config.aiToolkitActions = config.aiToolkitActions || [
130
- { id: "chat-panel", section: "Start", icon: "chat", title: "Chat", description: "Ask follow-up questions about this document.", target: "chat-panel" },
131
- { id: "open-dialog", section: "Start", icon: "open_dialog", title: "Draft", description: "Generate, rewrite, translate, or summarize before applying.", target: "dialog" },
132
- { id: "review-panel", section: "Start", icon: "review", title: "Review", description: "Accept or reject prepared AI edits.", target: "review-panel" },
133
- { id: "proofread", section: "Quick edit", icon: "proofread", title: "Proofread", description: "Check grammar, spacing, and clarity.", resolverMode: "proofread", target: "selection-preview" },
134
- { id: "rewrite", section: "Quick edit", icon: "rewrite", title: "Rewrite", description: "Improve clarity without changing intent.", resolverMode: "rewrite", target: "selection-preview" },
135
- { id: "translate", section: "Quick edit", icon: "translate", title: "Translate", description: "Choose a language and preview the result.", resolverMode: "translate", target: "dialog", autoRun: false },
136
- { id: "justify", section: "Quick edit", icon: "justify", title: "Explain", description: "Rewrite and show why it changed.", resolverMode: "justify", target: "dialog", autoRun: false },
137
- { id: "comment", section: "Insert", icon: "comment", title: "Add note", description: "Insert a short comment near the selection.", resolverMode: "comment", target: "comment" },
138
- { id: "paragraph", section: "Insert", icon: "paragraph", title: "Add paragraph", description: "Draft a supporting paragraph below.", resolverMode: "paragraph", target: "insert" }
135
+ { id: "chat-panel", section: aiT("aia_sec_start", "Start"), icon: "chat", title: aiT("aia_chat_panel_t", "Chat"), description: aiT("aia_chat_panel_d", "Ask follow-up questions about this document."), target: "chat-panel" },
136
+ { id: "open-dialog", section: aiT("aia_sec_start", "Start"), icon: "open_dialog", title: aiT("aia_open_dialog_t", "Draft"), description: aiT("aia_open_dialog_d", "Generate, rewrite, translate, or summarize before applying."), target: "dialog" },
137
+ { id: "review-panel", section: aiT("aia_sec_start", "Start"), icon: "review", title: aiT("aia_review_panel_t", "Review"), description: aiT("aia_review_panel_d", "Accept or reject prepared AI edits."), target: "review-panel" },
138
+ { id: "proofread", section: aiT("aia_sec_quick", "Quick edit"), icon: "proofread", title: aiT("aia_proofread_t", "Proofread"), description: aiT("aia_proofread_d", "Check grammar, spacing, and clarity."), resolverMode: "proofread", target: "selection-preview" },
139
+ { id: "rewrite", section: aiT("aia_sec_quick", "Quick edit"), icon: "rewrite", title: aiT("aia_rewrite_t", "Rewrite"), description: aiT("aia_rewrite_d", "Improve clarity without changing intent."), resolverMode: "rewrite", target: "selection-preview" },
140
+ { id: "translate", section: aiT("aia_sec_quick", "Quick edit"), icon: "translate", title: aiT("aia_translate_t", "Translate"), description: aiT("aia_translate_d", "Choose a language and preview the result."), resolverMode: "translate", target: "dialog", autoRun: false },
141
+ { id: "justify", section: aiT("aia_sec_quick", "Quick edit"), icon: "justify", title: aiT("aia_justify_t", "Explain"), description: aiT("aia_justify_d", "Rewrite and show why it changed."), resolverMode: "justify", target: "dialog", autoRun: false },
142
+ { id: "comment", section: aiT("aia_sec_insert", "Insert"), icon: "comment", title: aiT("aia_comment_t", "Add note"), description: aiT("aia_comment_d", "Insert a short comment near the selection."), resolverMode: "comment", target: "comment" },
143
+ { id: "paragraph", section: aiT("aia_sec_insert", "Insert"), icon: "paragraph", title: aiT("aia_paragraph_t", "Add paragraph"), description: aiT("aia_paragraph_d", "Draft a supporting paragraph below."), resolverMode: "paragraph", target: "insert" }
139
144
  ];
140
145
  // 2026-05-09 (v20260509j): single-verb default chip labels matching
141
146
  // the toolbar dropdown idiom — chips render on one line at the
142
147
  // default 390px chat panel width. Customer overrides via
143
148
  // aiToolkitChatPrompts keep their own labels untouched.
144
149
  config.aiToolkitChatPrompts = config.aiToolkitChatPrompts || [
145
- { id: "summarize", label: "Summarize", prompt: "Summarize the current content and suggest the clearest next edit." },
146
- { id: "proofread", label: "Proofread", prompt: "Proofread the current content and prepare a cleaner version I can review." },
147
- { id: "translate", label: "Translate", prompt: "Translate the current content into Spanish and prepare a reviewable version." },
148
- { id: "headings", label: "Headings", prompt: "Suggest better section titles and subheadings for this content." },
149
- { id: "expand", label: "Expand", prompt: "Add a short supporting paragraph that strengthens this content." }
150
+ { id: "summarize", label: aiT("aip_summarize", "Summarize"), prompt: "Summarize the current content and suggest the clearest next edit." },
151
+ { id: "proofread", label: aiT("aip_proofread", "Proofread"), prompt: "Proofread the current content and prepare a cleaner version I can review." },
152
+ { id: "translate", label: aiT("aip_translate", "Translate"), prompt: "Translate the current content into Spanish and prepare a reviewable version." },
153
+ { id: "headings", label: aiT("aip_headings", "Headings"), prompt: "Suggest better section titles and subheadings for this content." },
154
+ { id: "expand", label: aiT("aip_expand", "Expand"), prompt: "Add a short supporting paragraph that strengthens this content." }
150
155
  ];
151
156
  // 2026-05-20 (v20260520c): descriptions trimmed to ~5-8 words so
152
157
  // each fits the one-line mode caption without wrapping at the 480px
@@ -165,16 +170,23 @@ function RTE_Plugin_AIToolkit() {
165
170
  // on narrow viewports. Same brevity Notion AI inline composer subtitles
166
171
  // ship — "Improve writing", "Make shorter" — single 2-4 word verb
167
172
  // phrases.
173
+ // 2026-10-18 the Ask AI dialog's words read config.text_ keys where they are SHOWN (English as the fallback);
174
+ // labels that also feed logic (Document / Selection) are translated at the display sites only.
175
+ // tools/i18n/ask-ai-language.mjs
176
+ function aiT(key, fallback, arg) { var s = (config && config["text_" + key]) || fallback; return arg === undefined ? s : s.split("{0}").join(String(arg)); }
177
+ obj.__aiT = aiT;
168
178
  config.aiToolkitDialogModes = config.aiToolkitDialogModes || [
169
- { id: "proofread", title: "Proofread", description: "Fix grammar & clarity." },
170
- { id: "rewrite", title: "Rewrite", description: "Rewrite for clarity." },
171
- { id: "translate", title: "Translate", description: "Translate to a language." },
172
- { id: "justify", title: "Explain rewrite", description: "Rewrite and explain why." },
173
- { id: "shorten", title: "Shorten", description: "Compress to be shorter." },
174
- { id: "expand", title: "Expand", description: "Add supporting detail." },
175
- { id: "summarize", title: "Summarize", description: "Summarize the source." }
179
+ { id: "proofread", title: aiT("aim_proofread_t", "Proofread"), description: aiT("aim_proofread_d", "Fix grammar & clarity.") },
180
+ { id: "rewrite", title: aiT("aim_rewrite_t", "Rewrite"), description: aiT("aim_rewrite_d", "Rewrite for clarity.") },
181
+ { id: "translate", title: aiT("aim_translate_t", "Translate"), description: aiT("aim_translate_d", "Translate to a language.") },
182
+ { id: "justify", title: aiT("aim_justify_t", "Explain rewrite"), description: aiT("aim_justify_d", "Rewrite and explain why.") },
183
+ { id: "shorten", title: aiT("aim_shorten_t", "Shorten"), description: aiT("aim_shorten_d", "Compress to be shorter.") },
184
+ { id: "expand", title: aiT("aim_expand_t", "Expand"), description: aiT("aim_expand_d", "Add supporting detail.") },
185
+ { id: "summarize", title: aiT("aim_summarize_t", "Summarize"), description: aiT("aim_summarize_d", "Summarize the source.") }
176
186
  ];
177
187
  config.aiToolkitResolver = config.aiToolkitResolver || null;
188
+ // 2026-10-13 image generation: the host's provider call, bring your own key. null = not configured.
189
+ config.aiToolkitImageResolver = config.aiToolkitImageResolver || null;
178
190
  config.aiToolkitOperationHandlers = config.aiToolkitOperationHandlers || {};
179
191
  config.aiToolkitPersistenceKey = config.aiToolkitPersistenceKey || "";
180
192
  config.aiToolkitReviewSyncInterval = config.aiToolkitReviewSyncInterval || 15000;
@@ -200,10 +212,129 @@ function RTE_Plugin_AIToolkit() {
200
212
 
201
213
  obj.InitEditor = function (argeditor) {
202
214
  editor = argeditor;
215
+
216
+ // 2026-10-13 Image generation (tools/ai/image-generation.mjs). The host supplies the provider call through
217
+ // config.aiToolkitImageResolver, so the editor makes no outbound request of its own. What comes back is checked
218
+ // before anything is inserted: an image data URL (png, jpeg, webp or gif) or an https address, at most 6 million
219
+ // characters.
220
+ var IMAGE_SRC_MAX = 6000000;
221
+ function checkGeneratedImageSrc(src) {
222
+ var s = typeof src === "string" ? src.replace(/^\s+|\s+$/g, "") : "";
223
+ if (!s) throw new Error("The image generator returned no image.");
224
+ if (s.length > IMAGE_SRC_MAX) throw new Error("The generated image is too large to insert.");
225
+ if (/^data:image\/(png|jpe?g|webp|gif);base64,[A-Za-z0-9+\/=]+$/i.test(s)) return s;
226
+ if (/^https:\/\/[^\s"'<>]+$/i.test(s)) return s;
227
+ throw new Error("The image generator returned something that is not an image address.");
228
+ }
229
+ function generateImage(prompt) {
230
+ var text = String(prompt || "").replace(/^\s+|\s+$/g, "");
231
+ if (!text) return Promise.reject(new Error("Describe the image first."));
232
+ var resolver = config.aiToolkitImageResolver;
233
+ if (typeof resolver !== "function") {
234
+ return Promise.reject(new Error("No image generator is configured (config.aiToolkitImageResolver)."));
235
+ }
236
+ return Promise.resolve().then(function () { return resolver(text); }).then(checkGeneratedImageSrc);
237
+ }
238
+ function insertGeneratedImage(src, alt) {
239
+ editor.insertHTML('<img src="' + escapeAttribute(src) + '" alt="' + escapeAttribute(alt || "") + '">');
240
+ try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) { /* ignore */ }
241
+ }
242
+ var imageComposer = null;
243
+ function closeImageComposer() {
244
+ // 2026-10-14 Focus inside the card (Escape, Cancel) goes back to the editor when it closes, so a
245
+ // keyboard user does not land on the page body (WCAG 2.4.3). tools/ai/image-generator-focus.mjs
246
+ var hadFocus = false;
247
+ try { hadFocus = !!(imageComposer && imageComposer.contains(imageComposer.ownerDocument.activeElement)); } catch (e) { /* ignore */ }
248
+ if (imageComposer && imageComposer.parentNode) imageComposer.parentNode.removeChild(imageComposer);
249
+ imageComposer = null;
250
+ if (hadFocus) { try { editor.focus(); } catch (e) { /* ignore */ } }
251
+ }
252
+ // A small prompt card over the editor. Generate inserts the image at the caret and closes the card; Cancel and
253
+ // Escape close it without inserting anything.
254
+ function openImageComposer() {
255
+ closeImageComposer();
256
+ var frame = editor.iframe;
257
+ if (!frame || !frame.ownerDocument) return;
258
+ var host = frame.ownerDocument;
259
+ var box = host.createElement("div");
260
+ box.className = "rte-image-gen";
261
+ box.setAttribute("role", "dialog");
262
+ box.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aig_title", "Generate an image") : "Generate an image"));
263
+ var bs = box.style;
264
+ bs.position = "fixed"; bs.zIndex = "2147483000"; bs.width = "320px"; bs.padding = "12px";
265
+ bs.background = "#fff"; bs.border = "1px solid rgba(0,0,0,.15)"; bs.borderRadius = "8px";
266
+ bs.boxShadow = "0 8px 24px rgba(15,23,42,.18)"; bs.font = "13px/1.4 system-ui, sans-serif"; bs.color = "#1f2937";
267
+ var fr = frame.getBoundingClientRect();
268
+ bs.left = Math.round(fr.left + 16) + "px";
269
+ bs.top = Math.round(fr.top + 16) + "px";
270
+ var label = host.createElement("label");
271
+ label.textContent = (obj.__aiT ? obj.__aiT("aig_describe", "Describe the image") : "Describe the image");
272
+ label.style.display = "block";
273
+ label.style.marginBottom = "6px";
274
+ var area = host.createElement("textarea");
275
+ area.rows = 3;
276
+ area.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aig_describe", "Describe the image") : "Describe the image"));
277
+ area.style.width = "100%"; area.style.boxSizing = "border-box"; area.style.font = "inherit";
278
+ var status = host.createElement("div");
279
+ status.className = "rte-image-gen-status";
280
+ status.setAttribute("aria-live", "polite");
281
+ status.style.minHeight = "18px"; status.style.margin = "6px 0"; status.style.color = "#b91c1c";
282
+ var row = host.createElement("div");
283
+ row.style.textAlign = "right";
284
+ var cancel = host.createElement("button");
285
+ cancel.type = "button";
286
+ cancel.className = "rte-image-gen-cancel";
287
+ cancel.textContent = (obj.__aiT ? obj.__aiT("aig_cancel", "Cancel") : "Cancel");
288
+ cancel.style.marginRight = "6px";
289
+ var go = host.createElement("button");
290
+ go.type = "button";
291
+ go.className = "rte-image-gen-go";
292
+ go.textContent = (obj.__aiT ? obj.__aiT("aig_generate", "Generate") : "Generate");
293
+ go.style.background = "#1d67ba"; go.style.color = "#fff"; go.style.border = "0";
294
+ go.style.borderRadius = "4px"; go.style.padding = "4px 12px";
295
+ go.addEventListener("click", function () {
296
+ var text = area.value.replace(/^\s+|\s+$/g, "");
297
+ if (!text) { status.textContent = (obj.__aiT ? obj.__aiT("aig_describefirst", "Describe the image first.") : "Describe the image first."); return; }
298
+ go.disabled = true;
299
+ status.style.color = "#475569";
300
+ status.textContent = (obj.__aiT ? obj.__aiT("aig_generating", "Generating...") : "Generating...");
301
+ generateImage(text).then(function (src) {
302
+ insertGeneratedImage(src, text);
303
+ closeImageComposer();
304
+ // the Generate button was disabled while it ran, which moved focus out of the card: return it here
305
+ try { editor.focus(); } catch (e) { /* ignore */ }
306
+ }, function (err) {
307
+ go.disabled = false;
308
+ status.style.color = "#b91c1c";
309
+ status.textContent = (err && err.message) || (obj.__aiT ? obj.__aiT("aig_failed", "The image could not be generated.") : "The image could not be generated.");
310
+ });
311
+ });
312
+ cancel.addEventListener("click", closeImageComposer);
313
+ box.addEventListener("keydown", function (e) {
314
+ if (e.key === "Escape" || e.key === "Esc") { e.preventDefault(); closeImageComposer(); }
315
+ });
316
+ row.appendChild(cancel);
317
+ row.appendChild(go);
318
+ box.appendChild(label);
319
+ box.appendChild(area);
320
+ box.appendChild(status);
321
+ box.appendChild(row);
322
+ host.body.appendChild(box);
323
+ imageComposer = box;
324
+ try { area.focus(); } catch (e) { /* focus is best effort */ }
325
+ }
326
+ editor.generateImage = function (prompt) {
327
+ return generateImage(prompt).then(function (src) { insertGeneratedImage(src, prompt); return true; });
328
+ };
329
+ editor.openImageGenerator = function () { openImageComposer(); };
330
+
203
331
  editor.aiToolkit = {
332
+ generateImage: generateImage,
333
+ setImageResolver: function (fn) { config.aiToolkitImageResolver = typeof fn === "function" ? fn : null; },
204
334
  normalizeText: normalizeText,
205
335
  ensureSentence: ensureSentence,
206
336
  buildDemoResult: buildDemoResult,
337
+ isDemoMode: isDemoMode,
207
338
  getTranslateLanguages: getTranslateLanguages,
208
339
  getTranslateLanguageLabel: getTranslateLanguageLabel,
209
340
  getOperationContract: getOperationContract,
@@ -690,6 +821,21 @@ function RTE_Plugin_AIToolkit() {
690
821
  config[key] = current ? current + token : token;
691
822
  }
692
823
 
824
+ // 2026-10-15 with no resolver the toolkit answers from built-in samples, and nothing said so: an evaluating
825
+ // customer reported the AI feature as "not working". The dialog and the chat panel now carry a notice while
826
+ // no resolver is set. tools/ai/demo-mode-notice.mjs
827
+ function isDemoMode() {
828
+ return typeof config.aiToolkitResolver !== "function";
829
+ }
830
+
831
+ function appendDemoNotice(parent) {
832
+ if (!isDemoMode()) return null;
833
+ var notice = append(parent, "div", "", "rte-ai-demo-notice",
834
+ config.text_aitoolkitdemonotice || "Demo mode: these are built-in sample results, not a real AI model. Connect an AI provider with the aiToolkitResolver setting to get real suggestions.");
835
+ notice.setAttribute("role", "note");
836
+ return notice;
837
+ }
838
+
693
839
  function append(parent, tagName, cssText, className, text) {
694
840
  var node = parent.ownerDocument.createElement(tagName);
695
841
  if (cssText) {
@@ -1100,7 +1246,9 @@ function RTE_Plugin_AIToolkit() {
1100
1246
  if (mode === "insert" && editor && editor.insertHTML) {
1101
1247
  editor.insertHTML(html);
1102
1248
  } else if (editor && editor.setHTMLCode) {
1103
- editor.setHTMLCode(html);
1249
+ // undoable: Ctrl+Z gives back exactly the document this replaced
1250
+ // (tools/import/replace-undo-plugins.mjs)
1251
+ editor.setHTMLCode(html, { undoable: true });
1104
1252
  }
1105
1253
  return html;
1106
1254
  }).catch(function (err) {
@@ -1137,6 +1285,385 @@ function RTE_Plugin_AIToolkit() {
1137
1285
  .trim();
1138
1286
  }
1139
1287
 
1288
+ // 2026-10-18 Spoken labels are phrases joined into sentences. A phrase that already ended one ("... is ready.")
1289
+ // gave "ready.. Document thread"; after such a phrase the next follows with a space. tools/ai/aria-double-period.mjs
1290
+ function joinSentences(parts) {
1291
+ var out = "";
1292
+ for (var i = 0; i < parts.length; i++) {
1293
+ var s = String(parts[i] == null ? "" : parts[i]).replace(/[ \t\r\n]+$/, "");
1294
+ if (!s) continue;
1295
+ out += !out ? s : (/[.?!\u2026\u3002\uff01\uff1f]$/.test(out) ? " " : ". ") + s;
1296
+ }
1297
+ return out;
1298
+ }
1299
+
1300
+ // 2026-10-18 The inline AI suggestion card and the AI chat compose most of their words - shown and announced -
1301
+ // from many small builders. With a language pack loaded, this catalog translates them where they are displayed:
1302
+ // a whole string, else sentence by sentence, with patterns for the counts ("Queue 1 of 2"). Text a person wrote
1303
+ // (the card's Current / Suggested compare, the chat's messages) is never touched; anything not catalogued stays
1304
+ // as it is; an English page does nothing here. tools/i18n/ai-card-chat-language.mjs
1305
+ var AIQ_EXACT = [
1306
+ ["air_accept", "Accept"],
1307
+ ["air_reject", "Reject"],
1308
+ ["air_current", "Current"],
1309
+ ["air_suggested", "Suggested"],
1310
+ ["air_why", "Why"],
1311
+ ["air_scope_sel", "Selection"],
1312
+ ["air_scope_doc", "Document"],
1313
+ ["air_type_proofread", "Proofread"],
1314
+ ["air_type_rewrite", "Rewrite"],
1315
+ ["air_type_translate", "Translate"],
1316
+ ["air_type_comment", "Comment"],
1317
+ ["air_type_paragraph", "Paragraph"],
1318
+ ["air_type_summarize", "Summarize"],
1319
+ ["air_type_expand", "Expand"],
1320
+ ["air_type_shorten", "Shorten"],
1321
+ ["aip_headings", "Headings"],
1322
+ ["aichat", "AI Chat"],
1323
+ ["aic_newchat", "Start a fresh chat"],
1324
+ ["aic_closechat", "Close AI chat"],
1325
+ ["aic_close", "Close"],
1326
+ ["aic_usesel", "Use selection"],
1327
+ ["aic_usedoc", "Use document"],
1328
+ ["aic_copymsg", "Copy message text"],
1329
+ ["aic_copy", "Copy"],
1330
+ ["aic_regen", "Regenerate response"],
1331
+ ["aid_regenerate", "Regenerate"],
1332
+ ["aic_actions", "AI chat actions"],
1333
+ ["aic_send", "Send"],
1334
+ ["aic_starttitle", "Start with AI Chat"],
1335
+ ["aic_pill_doc", "Document context"],
1336
+ ["aic_pill_sel", "Selection context"],
1337
+ ["aic_compose_doc", "Compose next AI request for the current document"],
1338
+ ["aic_compose_sel", "Compose next AI request for the current selection"],
1339
+ ["aic_emptytitle_doc", "Type a prompt or load a starter for the current document first"],
1340
+ ["aic_emptytitle_sel", "Type a prompt or load a starter for the current selection first"],
1341
+ ["aic_empty_doc", "Ask anything about the current document"],
1342
+ ["aic_empty_sel", "Ask anything about the current selection"],
1343
+ ["air_focusaria", "Focus the current AI review item"],
1344
+ ["aiq_preview", "AI suggestion preview"],
1345
+ ["aiq_proposed", "Proposed edit"],
1346
+ ["aiq_curandsug", "Current and suggested text"],
1347
+ ["aiq_curtext", "Current text"],
1348
+ ["aiq_sugtext", "Suggested text"],
1349
+ ["aiq_ready", "Ready to review"],
1350
+ ["aiq_curfocus", "Current focus"],
1351
+ ["aiq_seldraft", "Selection draft"],
1352
+ ["aiq_docdraft", "Document draft"],
1353
+ ["aiq_prepdraft", "Prepared draft"],
1354
+ ["aiq_customdraft", "Custom draft"],
1355
+ ["aiq_draftready", "Draft ready"],
1356
+ ["aiq_glance", "Change at a glance"],
1357
+ ["aiq_surface", "Surface edit"],
1358
+ ["aiq_textrewrite", "Text rewrite"],
1359
+ ["aiq_textchange", "Text change"],
1360
+ ["aiq_langchange", "Language change"],
1361
+ ["aiq_reviewchange", "Reviewable change"],
1362
+ ["aiq_acceptedchange", "Accepted change"],
1363
+ ["aiq_rejectedchange", "Rejected change"],
1364
+ ["aiq_lowimpact", "Low impact"],
1365
+ ["aiq_medimpact", "Medium impact"],
1366
+ ["aiq_highimpact", "High impact"],
1367
+ ["aiq_whatnext", "What happens next"],
1368
+ ["aiq_qcomplete", "Queue complete"],
1369
+ ["aiq_qopened", "Queue opened"],
1370
+ ["aiq_qhandoff", "Queue handoff"],
1371
+ ["aiq_qstate", "Queue state"],
1372
+ ["aiq_recready", "Recovery ready"],
1373
+ ["aiq_recavail", "Recovery available"],
1374
+ ["aiq_finalitem", "Accepting or rejecting this final pending item finishes the current review run"],
1375
+ ["aiq_moveson", "Accepting or rejecting this item moves review straight to the next pending suggestion"],
1376
+ ["aiq_nolater", "No later pending items remain in this queue"],
1377
+ ["aiq_thenrec", "Then recovery and next queues appear"],
1378
+ ["aiq_thencomplete", "Then queue complete"],
1379
+ ["aiq_undolast", "Undo the last decision or open another queue"],
1380
+ ["aiq_afterfinal", "After reviewing the final pending item, recovery and next queues appear"],
1381
+ ["aiq_afterdec", "After this decision"],
1382
+ ["aiq_handoff", "Action handoff"],
1383
+ ["aiq_decidelane", "Decide lane"],
1384
+ ["aiq_curlane", "Current lane"],
1385
+ ["aiq_curactionlane", "Current action lane"],
1386
+ ["aiq_kblane", "Keyboard lane"],
1387
+ ["aiq_curenter", "Current Enter target"],
1388
+ ["aiq_spaceact", "Enter or Space activates this action"],
1389
+ ["aiq_acceptfinish", "Accept & finish"],
1390
+ ["aiq_rejectfinish", "Reject & finish"],
1391
+ ["aiq_acceptnext", "Accept & next"],
1392
+ ["aiq_rejectnext", "Reject & next"],
1393
+ ["aiq_accfinishdesc", "Accept & finish this AI change and finish the current review queue"],
1394
+ ["aiq_rejfinishdesc", "Reject & finish this AI change and finish the current review queue"],
1395
+ ["aiq_open", "Open"],
1396
+ ["aiq_review", "Review"],
1397
+ ["aiq_move", "Move"],
1398
+ ["aiq_previous", "Previous"],
1399
+ ["aiq_next", "Next"],
1400
+ ["aiq_decide", "Decide"],
1401
+ ["aiq_recover", "Recover"],
1402
+ ["aiq_actions", "AI suggestion actions"],
1403
+ ["aiq_openactions", "Open actions"],
1404
+ ["aiq_moveactions", "Move actions"],
1405
+ ["aiq_decideactions", "Decide actions"],
1406
+ ["aiq_recoveractions", "Recover actions"],
1407
+ ["aiq_whysug", "Why this suggestion"],
1408
+ ["aiq_upnext", "Up next"],
1409
+ ["aiq_upnextcur", "Up next becomes current"],
1410
+ ["aiq_follows", "This follows after the current decision"],
1411
+ ["aiq_nextitem", "Next item"],
1412
+ ["aiq_reviewnow", "Review this item now"],
1413
+ ["aiq_reopen", "Reopen this suggestion in the current review lane"],
1414
+ ["aiq_jump", "Jump earlier or later in this queue before deciding"],
1415
+ ["aiq_previtem", "Previous AI review item"],
1416
+ ["aiq_nextitemai", "Next AI review item"],
1417
+ ["aiq_noprev", "No previous AI review item in the current queue"],
1418
+ ["aiq_nonext", "No next AI review item in the current queue"],
1419
+ ["aiq_shortcuts", "Inline AI review shortcuts"],
1420
+ ["aiq_kbguide", "Keyboard guide"],
1421
+ ["aiq_move_l", "move"],
1422
+ ["aiq_jump_l", "jump"],
1423
+ ["aiq_enter", "Enter"],
1424
+ ["aiq_custom", "Custom"],
1425
+ ["aiq_reason_proof", "Normalizes spacing, punctuation, and capitalization while keeping the original meaning intact"],
1426
+ ["aiq_reason_rewrite", "Tightens the wording by removing filler phrases so the point comes through faster"],
1427
+ ["aiq_c_ready", "Ready"],
1428
+ ["aiq_c_docfocus", "Document focus"],
1429
+ ["aiq_c_selfocus", "Selection focus"],
1430
+ ["aiq_c_inplace", "Ask, rewrite, or review the current document in place"],
1431
+ ["aiq_c_docthread", "Document thread"],
1432
+ ["aiq_c_selthread", "Selection thread"],
1433
+ ["aiq_c_newask", "Ready for a new ask"],
1434
+ ["aiq_c_latest", "Latest reply is ready"],
1435
+ ["aiq_c_context", "Context"],
1436
+ ["aiq_c_docactive", "Document active"],
1437
+ ["aiq_c_selactive", "Selection active"],
1438
+ ["aiq_c_nosel", "No current selection"],
1439
+ ["aiq_c_selready", "Selection ready"],
1440
+ ["aiq_c_wholedoc", "Using the whole document as the active source"],
1441
+ ["aiq_c_narrow", "Select text any time to narrow the ask"],
1442
+ ["aiq_c_snapshot", "Document snapshot"],
1443
+ ["aiq_c_scopeswitch", "Context scope switch"],
1444
+ ["aiq_c_unavail", "Unavailable until text is selected"],
1445
+ ["aiq_c_activescope", "Active scope"],
1446
+ ["aiq_c_usingwhole", "AI Chat is using the whole document"],
1447
+ ["aiq_c_quickstarts", "Quick starts"],
1448
+ ["aiq_c_readychoose", "Ready to choose"],
1449
+ ["aiq_c_choosestarter", "Choose a starter for the current document or keep typing your own prompt in Compose"],
1450
+ ["aiq_c_starterbtns", "Starter buttons for the current document"],
1451
+ ["aiq_c_loadsstarter", "Loads this starter into Compose for the current document"],
1452
+ ["aiq_c_summarystarter", "Summary starter"],
1453
+ ["aiq_c_reviewstarter", "Review starter"],
1454
+ ["aiq_c_langstarter", "Language starter"],
1455
+ ["aiq_c_structstarter", "Structure starter"],
1456
+ ["aiq_c_expandstarter", "Expand starter"],
1457
+ ["aiq_c_starterready", "Starter ready"],
1458
+ ["aiq_c_p_summarize", "Summarize the current content and suggest the clearest next edit"],
1459
+ ["aiq_c_p_proofread", "Proofread the current content and prepare a cleaner version I can review"],
1460
+ ["aiq_c_p_translate", "Translate the current content into Spanish and prepare a reviewable version"],
1461
+ ["aiq_c_p_headings", "Suggest better section titles and subheadings for this content"],
1462
+ ["aiq_c_p_expand", "Add a short supporting paragraph that strengthens this content"],
1463
+ ["aiq_c_conversation", "Conversation"],
1464
+ ["aiq_c_firstex", "First exchange"],
1465
+ ["aiq_c_waitfirst", "Waiting for first ask"],
1466
+ ["aiq_c_firstanswer", "Your first answer, draft, or reviewable change for the current document will appear here"],
1467
+ ["aiq_c_askexplore", "Ask and explore"],
1468
+ ["aiq_c_getanswers", "Get answers, summaries, headings, or rewrite direction grounded in the current context"],
1469
+ ["aiq_c_prepedits", "Prepare reviewable edits"],
1470
+ ["aiq_c_previewinline", "Preview inline changes, apply structured plans, or turn responses into review-ready suggestions"],
1471
+ ["aiq_c_usestarters", "Use starters or type your own"],
1472
+ ["aiq_c_choosecurated", "Choose one of the curated quick starts above or write a custom prompt in the composer below"],
1473
+ ["aiq_c_compose", "Compose"],
1474
+ ["aiq_c_readyask", "Ready to ask about the current document"],
1475
+ ["aiq_c_entersends", "Enter sends"],
1476
+ ["aiq_c_composeactions", "Compose actions"],
1477
+ ["aiq_c_openask", "Open Ask AI"],
1478
+ ["aiq_c_senddisabled", "Send disabled"],
1479
+ ["aiq_c_responseready", "AI response ready"],
1480
+ ["aiq_c_replyready", "Reply ready"],
1481
+ ["aiq_c_textchange", "This is a text change"],
1482
+ ["aiq_c_exchange", "AI chat exchange"],
1483
+ ["aiq_c_customex", "Custom exchange"],
1484
+ ["aiq_c_request", "Request"],
1485
+ ["aiq_c_curexchange", "Current exchange"],
1486
+ ["aiq_c_you", "You"],
1487
+ ["aiq_c_prompt", "Prompt"],
1488
+ ["aiq_c_promptsent", "Prompt sent"],
1489
+ ["aiq_c_waitingai", "Waiting on AI for the current document context"],
1490
+ ["aiq_c_whatyouasked", "What you asked"],
1491
+ ["aiq_c_usingdoc", "Using current document"],
1492
+ ["aiq_c_response", "Response"],
1493
+ ["aiq_c_answer", "Answer"],
1494
+ ["aiq_c_answerready", "Answer ready"],
1495
+ ["aiq_c_readanswer", "Read the answer grounded in the current document context"],
1496
+ ["aiq_c_copyonly", "Copy only"],
1497
+ ["aiq_c_copyonlygroup", "Copy only group"],
1498
+ ["aiq_c_copyprep", "Copy this prepared reply without changing the document"],
1499
+ ["aiq_c_copyprepai", "Copy this prepared AI reply without changing the document"],
1500
+ ["aiq_c_copytext", "Copy text"],
1501
+ ["aiq_c_actions", "Actions"],
1502
+ ["aiq_c_ai", "AI"]
1503
+ ];
1504
+ var AIQ_PATTERNS = [
1505
+ ["aiq_p_queue", new RegExp("^Queue (\\d+) of (\\d+)$")],
1506
+ ["aiq_p_step", new RegExp("^(\\d+) steps?$")],
1507
+ ["aiq_p_keeps", new RegExp("^Keeps (\\d+) words? across (\\d+) sentences?$")],
1508
+ ["aiq_p_queuein", new RegExp("^It is queue (\\d+) of (\\d+) in this view$")],
1509
+ ["aiq_p_enterfocuses", new RegExp("^Enter focuses (.+)$")],
1510
+ ["aiq_p_entercolon", new RegExp("^Enter: (.+)$")],
1511
+ ["aiq_p_focus", new RegExp("^[Ff]ocus (.+)$")],
1512
+ ["aiq_p_reviewopens", new RegExp("^Review opens on (.+)$")],
1513
+ ["aiq_p_openfocus", new RegExp("^Open this AI suggestion in review and focus (.+)$")],
1514
+ ["aiq_p_auses", new RegExp("^A uses (.+)$")],
1515
+ ["aiq_p_ruses", new RegExp("^R uses (.+)$")],
1516
+ ["aiq_p_exchanges", new RegExp("^(\\d+) exchanges?$")],
1517
+ ["aiq_p_messages", new RegExp("^(\\d+) messages?$")],
1518
+ ["aiq_p_words", new RegExp("^(\\d+) words?$")],
1519
+ ["aid_chars", new RegExp("^(\\d+) chars?$")],
1520
+ ["aiq_p_paragraphs", new RegExp("^(\\d+) paragraphs?$")],
1521
+ ["aic_pill_starters", new RegExp("^(\\d+) starters?$")],
1522
+ ["aic_pill_words", new RegExp("^(\\d+) words in scope$")],
1523
+ ["aiq_p_exchangen", new RegExp("^Exchange (\\d+)$")],
1524
+ ["aiq_p_turn", new RegExp("^Turn (\\d+)$")],
1525
+ ["aiq_p_convdoc", new RegExp("^(\\d+) exchanges?, (\\d+) messages? in this conversation for the current document$")],
1526
+ ["aiq_p_convsel", new RegExp("^(\\d+) exchanges?, (\\d+) messages? in this conversation for the current selection$")],
1527
+ ["aiq_p_threaddoc", new RegExp("^(\\d+) exchanges?, (\\d+) messages? in this document thread$")],
1528
+ ["aiq_p_threadsel", new RegExp("^(\\d+) exchanges?, (\\d+) messages? in this selection thread$")],
1529
+ ["aiq_p_responding", new RegExp("^Responding to (.+) request$")],
1530
+ ["aiq_p_actions", new RegExp("^Actions: (.+)$")]
1531
+ ];
1532
+ var aiqIndex = null;
1533
+ function aiqLocale() {
1534
+ var l = config && config.text_uilocale;
1535
+ return l && !/^en(-|$)/i.test(l) ? l : "";
1536
+ }
1537
+ function aiqCjk() { return /^(zh|ja)/i.test(aiqLocale()); }
1538
+ function aiqExact(s) {
1539
+ if (!aiqIndex) {
1540
+ aiqIndex = {};
1541
+ for (var i = 0; i < AIQ_EXACT.length; i++) {
1542
+ var en = AIQ_EXACT[i][1];
1543
+ if (!aiqIndex["=" + en]) aiqIndex["=" + en] = AIQ_EXACT[i][0];
1544
+ if (!aiqIndex["~" + en.toLowerCase()]) aiqIndex["~" + en.toLowerCase()] = AIQ_EXACT[i][0];
1545
+ }
1546
+ }
1547
+ var key = aiqIndex["=" + s] || aiqIndex["~" + s.toLowerCase()];
1548
+ return (key && config["text_" + key]) || null;
1549
+ }
1550
+ // a phrase without its closing stop: the catalog, then the patterns (a word capture must translate too)
1551
+ function aiqCore(s) {
1552
+ var v = aiqExact(s);
1553
+ if (v) return v;
1554
+ for (var i = 0; i < AIQ_PATTERNS.length; i++) {
1555
+ var m = AIQ_PATTERNS[i][1].exec(s);
1556
+ if (!m) continue;
1557
+ var tpl = config["text_" + AIQ_PATTERNS[i][0]];
1558
+ if (!tpl) return null;
1559
+ var out = tpl;
1560
+ for (var j = 1; j < m.length; j++) {
1561
+ var cap = m[j], tr = /^[0-9]+$/.test(cap) ? cap : aiqCore(cap);
1562
+ if (tr == null) return null;
1563
+ out = out.split("{" + (j - 1) + "}").join(tr);
1564
+ }
1565
+ return out;
1566
+ }
1567
+ return null;
1568
+ }
1569
+ function aiqPiece(piece) {
1570
+ var body = piece, punct = "";
1571
+ var last = body.charAt(body.length - 1);
1572
+ if (last === "." || last === ":") { punct = last; body = body.substring(0, body.length - 1); }
1573
+ var v = aiqCore(body);
1574
+ if (v == null) return null;
1575
+ if (punct) {
1576
+ var end = v.charAt(v.length - 1);
1577
+ var closed = ".:!?".indexOf(end) >= 0 || end === String.fromCharCode(0x3002) || end === String.fromCharCode(0xff1a);
1578
+ if (!closed) v += aiqCjk() ? String.fromCharCode(punct === ":" ? 0xff1a : 0x3002) : punct;
1579
+ }
1580
+ return v;
1581
+ }
1582
+ function aiqText(text) {
1583
+ var t = String(text == null ? "" : text).replace(/^\s+|\s+$/g, "");
1584
+ if (!t || !/[A-Za-z]/.test(t)) return null;
1585
+ var whole = aiqPiece(t);
1586
+ if (whole != null) return whole;
1587
+ var dash = t.indexOf(" - ");
1588
+ if (dash > 0) {
1589
+ var a = aiqText(t.substring(0, dash)), b = aiqText(t.substring(dash + 3));
1590
+ if (a != null || b != null) return (a != null ? a : t.substring(0, dash)) + " - " + (b != null ? b : t.substring(dash + 3));
1591
+ }
1592
+ var pieces = t.match(/[^.:!?]+[.:!?]*/g);
1593
+ if (!pieces || pieces.length < 2) return null;
1594
+ var out = "", prevTranslated = false, changed = false;
1595
+ for (var i = 0; i < pieces.length; i++) {
1596
+ var p = pieces[i].replace(/^\s+|\s+$/g, "");
1597
+ if (!p) continue;
1598
+ var tr = aiqPiece(p);
1599
+ if (tr != null) changed = true;
1600
+ var local = tr != null || !/[A-Za-z]/.test(p);
1601
+ var cur = tr != null ? tr : p;
1602
+ out += !out ? cur : ((local && prevTranslated && aiqCjk()) ? "" : " ") + cur;
1603
+ prevTranslated = local;
1604
+ }
1605
+ return changed ? out : null;
1606
+ }
1607
+ var aiqCjkStop = null, aiqCjkGap = null;
1608
+ function aiqFinish(s) {
1609
+ if (s == null || !aiqCjk()) return s;
1610
+ if (!aiqCjkStop) {
1611
+ var cjk = String.fromCharCode(0x4e00) + "-" + String.fromCharCode(0x9fff) + String.fromCharCode(0x3040) + "-" + String.fromCharCode(0x30ff) + String.fromCharCode(0x201d) + String.fromCharCode(0x300d);
1612
+ aiqCjkStop = new RegExp("([" + cjk + "])\\.", "g");
1613
+ aiqCjkGap = new RegExp("([" + String.fromCharCode(0x3002) + String.fromCharCode(0xff1a) + "])\\s+(?=[^A-Za-z0-9\\s])", "g");
1614
+ }
1615
+ return s.replace(aiqCjkStop, "$1" + String.fromCharCode(0x3002)).replace(aiqCjkGap, "$1");
1616
+ }
1617
+ var AIQ_SKIP = ".rte-ai-inline-preview-compare-text, .rte-ai-chat-message-text, .rte-ai-chat-message-request-body, textarea, input, script, style, svg";
1618
+ function aiqLocalize(root) {
1619
+ if (!root || !aiqLocale()) return;
1620
+ var stack = [root];
1621
+ while (stack.length) {
1622
+ var el = stack.pop();
1623
+ if (el.nodeType !== 1) continue;
1624
+ if (el !== root && el.matches && el.matches(AIQ_SKIP)) continue;
1625
+ for (var a = 0; a < 3; a++) {
1626
+ var name = a === 0 ? "aria-label" : (a === 1 ? "title" : "placeholder");
1627
+ var val = el.getAttribute(name);
1628
+ if (val) { var tv = aiqFinish(aiqText(val) || val); if (tv !== val) el.setAttribute(name, tv); }
1629
+ }
1630
+ for (var c = el.firstChild; c; c = c.nextSibling) {
1631
+ if (c.nodeType === 1) stack.push(c);
1632
+ else if (c.nodeType === 3 && /[A-Za-z.]/.test(c.data)) {
1633
+ var tt = aiqFinish(aiqText(c.data) || c.data.replace(/^\s+|\s+$/g, ""));
1634
+ if (tt) {
1635
+ var lead = /^\s*/.exec(c.data)[0], trail = /\s*$/.exec(c.data)[0];
1636
+ if (lead + tt + trail !== c.data) c.data = lead + tt + trail;
1637
+ }
1638
+ }
1639
+ }
1640
+ }
1641
+ }
1642
+ // keep a root localized as the toolkit rebuilds it (idempotent: a translated string has nothing left to match)
1643
+ function aiqWatch(root, select) {
1644
+ if (!root || !aiqLocale() || root.__aiqWatched) return;
1645
+ root.__aiqWatched = true;
1646
+ var run = function () {
1647
+ if (!select) { aiqLocalize(root); return; }
1648
+ var list = root.querySelectorAll(select);
1649
+ for (var i = 0; i < list.length; i++) aiqLocalize(list[i]);
1650
+ };
1651
+ run();
1652
+ var win = root.ownerDocument && root.ownerDocument.defaultView;
1653
+ var MO = win && win.MutationObserver;
1654
+ if (!MO) return;
1655
+ var pending = false;
1656
+ new MO(function () {
1657
+ if (pending) return;
1658
+ pending = true;
1659
+ win.setTimeout(function () { pending = false; run(); }, 0);
1660
+ }).observe(root, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ["aria-label", "title", "placeholder"] });
1661
+ }
1662
+ function aiqWatchCards() {
1663
+ var editable = editor.getEditable ? editor.getEditable() : null;
1664
+ if (editable) aiqWatch(editable, ".rte-ai-inline-preview");
1665
+ }
1666
+
1140
1667
  function ensureSentence(text) {
1141
1668
  var clean = normalizeText(text);
1142
1669
  if (!clean) {
@@ -1157,6 +1684,24 @@ function RTE_Plugin_AIToolkit() {
1157
1684
  return normalizeText(language || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
1158
1685
  }
1159
1686
 
1687
+ // 2026-10-18 the Translate-to list names the built-in languages in the page's language (Intl.DisplayNames in the
1688
+ // pack's locale, text_uilocale) at the display site only; values and the language a request names are unchanged,
1689
+ // and an application's own label is kept. tools/i18n/ai-translate-languages.mjs
1690
+ var TRANSLATE_BUILTIN = { spanish: ["es", "Spanish"], french: ["fr", "French"], german: ["de", "German"], italian: ["it", "Italian"], portuguese: ["pt", "Portuguese"], japanese: ["ja", "Japanese"] };
1691
+ function getTranslateLanguageDisplay(entry) {
1692
+ var label = entry.label || entry.value;
1693
+ var builtIn = TRANSLATE_BUILTIN[normalizeLanguageValue(entry.value)];
1694
+ var locale = config && config.text_uilocale;
1695
+ if (!builtIn || label !== builtIn[1] || !locale || /^en(-|$)/i.test(locale)) return label;
1696
+ try {
1697
+ if (typeof Intl !== "undefined" && Intl.DisplayNames) {
1698
+ var n = new Intl.DisplayNames([locale], { type: "language" }).of(builtIn[0]);
1699
+ if (n && n !== builtIn[0]) return n;
1700
+ }
1701
+ } catch (e) { /* the English label */ }
1702
+ return label;
1703
+ }
1704
+
1160
1705
  function getTranslateLanguageLabel(language) {
1161
1706
  var normalized = normalizeLanguageValue(language);
1162
1707
  var languages = getTranslateLanguages();
@@ -1187,27 +1732,27 @@ function RTE_Plugin_AIToolkit() {
1187
1732
  function getSuggestionTypeLabel(type) {
1188
1733
  switch (getSuggestionTypeValue(type)) {
1189
1734
  case "proofread":
1190
- return "Proofread";
1735
+ return (obj.__aiT ? obj.__aiT("air_type_proofread", "Proofread") : "Proofread");
1191
1736
  case "rewrite":
1192
- return "Rewrite";
1737
+ return (obj.__aiT ? obj.__aiT("air_type_rewrite", "Rewrite") : "Rewrite");
1193
1738
  case "translate":
1194
- return "Translate";
1739
+ return (obj.__aiT ? obj.__aiT("air_type_translate", "Translate") : "Translate");
1195
1740
  case "justify":
1196
- return "Justify";
1741
+ return (obj.__aiT ? obj.__aiT("air_type_justify", "Justify") : "Justify");
1197
1742
  case "summarize":
1198
- return "Summarize";
1743
+ return (obj.__aiT ? obj.__aiT("air_type_summarize", "Summarize") : "Summarize");
1199
1744
  case "shorten":
1200
- return "Shorten";
1745
+ return (obj.__aiT ? obj.__aiT("air_type_shorten", "Shorten") : "Shorten");
1201
1746
  case "expand":
1202
- return "Expand";
1747
+ return (obj.__aiT ? obj.__aiT("air_type_expand", "Expand") : "Expand");
1203
1748
  case "comment":
1204
- return "Comment";
1749
+ return (obj.__aiT ? obj.__aiT("air_type_comment", "Comment") : "Comment");
1205
1750
  case "paragraph":
1206
- return "Paragraph";
1751
+ return (obj.__aiT ? obj.__aiT("air_type_paragraph", "Paragraph") : "Paragraph");
1207
1752
  case "chat":
1208
- return "Chat";
1753
+ return (obj.__aiT ? obj.__aiT("air_type_chat", "Chat") : "Chat");
1209
1754
  default:
1210
- return "Other";
1755
+ return (obj.__aiT ? obj.__aiT("air_type_other", "Other") : "Other");
1211
1756
  }
1212
1757
  }
1213
1758
 
@@ -1358,7 +1903,7 @@ function RTE_Plugin_AIToolkit() {
1358
1903
  if (options.redoNextLabel) {
1359
1904
  parts.push(options.redoNextLabel);
1360
1905
  }
1361
- return parts.join(". ");
1906
+ return joinSentences(parts);
1362
1907
  }
1363
1908
 
1364
1909
  function buildSuggestionSourceLabel(type, snapshot, language, scope) {
@@ -1383,11 +1928,11 @@ function RTE_Plugin_AIToolkit() {
1383
1928
  var languageValue = getTranslateLanguageValue(language);
1384
1929
  var languageLabel = getTranslateLanguageLabel(languageValue);
1385
1930
  var leadMap = {
1386
- spanish: "Version en español",
1387
- french: "Version française",
1931
+ spanish: "Version en espa\xf1ol",
1932
+ french: "Version fran\xe7aise",
1388
1933
  german: "Deutsche Fassung",
1389
1934
  italian: "Versione italiana",
1390
- portuguese: "Versão em português",
1935
+ portuguese: "Vers\xe3o em portugu\xeas",
1391
1936
  japanese: "Japanese draft"
1392
1937
  };
1393
1938
  var lead = leadMap[languageValue] || (languageLabel + " version");
@@ -1402,20 +1947,29 @@ function RTE_Plugin_AIToolkit() {
1402
1947
  }
1403
1948
 
1404
1949
  var words = clean.split(/\s+/);
1950
+ // 2026-10-18 an in-place edit ends like the selection it replaces: one that stops before the end of its
1951
+ // paragraph gains no closing stop, one that starts mid-sentence keeps its first letter. Proofreading "Hello world"
1952
+ // in "Hello world, this is a test." gave "Hello world., this is a test.". tools/ai/demo-proofread-fragment.mjs
1953
+ function finishEdit(text) {
1954
+ var s = normalizeText(text);
1955
+ if (!s) return s;
1956
+ if (!options.midStart) s = s.charAt(0).toUpperCase() + s.substring(1);
1957
+ if (!options.midEnd && !/[.!?]$/.test(s)) s += ".";
1958
+ return s;
1959
+ }
1405
1960
  switch (mode) {
1406
1961
  case "proofread":
1407
1962
  var proofed = clean
1408
1963
  .replace(/\s{2,}/g, " ")
1409
1964
  .replace(/\s+([,.;:!?])/g, "$1")
1410
1965
  .replace(/([,.;:!?])([^\s"'\)\]])/g, "$1 $2")
1411
- .replace(/\.{2,}(?!\.)/g, ".")
1412
- .replace(/^(.)/, function (m) { return m.toUpperCase(); });
1413
- return ensureSentence(proofed);
1966
+ .replace(/\.{2,}(?!\.)/g, ".");
1967
+ return finishEdit(proofed);
1414
1968
  case "rewrite":
1415
- return ensureSentence(clean
1416
- .replace(/\bvery\s+/gi, "")
1417
- .replace(/\breally\s+/gi, "")
1418
- .replace(/\bjust\s+/gi, "")
1969
+ return finishEdit(clean
1970
+ .replace(/\bvery\b\s*/gi, "")
1971
+ .replace(/\breally\b\s*/gi, "")
1972
+ .replace(/\bjust\b\s*/gi, "")
1419
1973
  .replace(/\bin order to\b/gi, "to")
1420
1974
  .replace(/\bdue to the fact that\b/gi, "because")
1421
1975
  .replace(/\s{2,}/g, " "));
@@ -1445,7 +1999,7 @@ function RTE_Plugin_AIToolkit() {
1445
1999
  if (!clean) {
1446
2000
  return "Additional paragraph: This section adds a concise supporting explanation so the reader gets more context without losing momentum.";
1447
2001
  }
1448
- return ensureSentence("Additional paragraph: This section adds a concise supporting explanation that reinforces " + clean.split(/\s+/).slice(0, 10).join(" ") + " for the reader.");
2002
+ return ensureSentence("Additional paragraph: This section adds a concise supporting explanation that reinforces " + clean.split(/\s+/).slice(0, 10).join(" ").replace(/[.!?;:,]+$/, "") + " for the reader.");
1449
2003
  }
1450
2004
 
1451
2005
  function truncateText(text, maxLength) {
@@ -1457,7 +2011,9 @@ function RTE_Plugin_AIToolkit() {
1457
2011
  }
1458
2012
 
1459
2013
  function buildChatAnswer(prompt, source) {
1460
- var focus = truncateText(source, 160) || "the current document";
2014
+ // 2026-10-18 the quoted text loses its own closing stop, or the reply read "this is a test.. Ask me" and
2015
+ // "test., I would". tools/ai/aria-double-period.mjs
2016
+ var focus = String(truncateText(source, 160) || "").replace(/[ \t\r\n.!?;:,]+$/, "") || "the current document";
1461
2017
  if (/\b(headings?|titles?)\b/.test(prompt)) {
1462
2018
  return "You could tighten the structure by using shorter headings that mirror the main claims in " + focus + ". Ask me to draft heading options and I can turn them into reviewable suggestions.";
1463
2019
  }
@@ -1996,7 +2552,7 @@ function RTE_Plugin_AIToolkit() {
1996
2552
  if (unavailableReason) {
1997
2553
  parts.push(unavailableReason);
1998
2554
  }
1999
- button.setAttribute("aria-label", parts.join(". "));
2555
+ button.setAttribute("aria-label", joinSentences(parts));
2000
2556
  button.title = detail
2001
2557
  ? (label || "Action") + " - " + detail + (unavailableReason ? " " + unavailableReason : "")
2002
2558
  : (label || "Action") + (unavailableReason ? " - " + unavailableReason : "");
@@ -2011,13 +2567,34 @@ function RTE_Plugin_AIToolkit() {
2011
2567
  }, action, request);
2012
2568
  }
2013
2569
 
2570
+ // Whether a selection starts mid-sentence (text before it in its block that does not end a sentence) and ends
2571
+ // before its block does (text after it). Only for a selection; a document-wide request is a whole text.
2572
+ function selectionSentenceContext(request) {
2573
+ var out = { midStart: false, midEnd: false };
2574
+ try {
2575
+ var snap = request && request.snapshot;
2576
+ var r = snap && snap.hasSelection && request.scope !== "document" ? snap.range : null;
2577
+ if (!r) return out;
2578
+ var doc = r.startContainer.ownerDocument;
2579
+ var blockOf = function (n) {
2580
+ var el = n && n.nodeType === 1 ? n : (n && n.parentNode);
2581
+ while (el && el.nodeType === 1 && !/^(P|LI|TD|TH|H[1-6]|DIV|BLOCKQUOTE|PRE|BODY)$/.test(el.nodeName)) el = el.parentNode;
2582
+ return el && el.nodeType === 1 ? el : null;
2583
+ };
2584
+ var b1 = blockOf(r.startContainer), b2 = blockOf(r.endContainer);
2585
+ if (b1) { var pre = doc.createRange(); pre.selectNodeContents(b1); pre.setEnd(r.startContainer, r.startOffset); var before = pre.toString(); out.midStart = /\S/.test(before) && !/[.!?]["'\u201d)\]]*\s*$/.test(before); }
2586
+ if (b2) { var post = doc.createRange(); post.selectNodeContents(b2); post.setStart(r.endContainer, r.endOffset); out.midEnd = /\S/.test(post.toString()); }
2587
+ } catch (e) { /* a whole text */ }
2588
+ return out;
2589
+ }
2590
+
2014
2591
  function defaultResolveAction(request) {
2015
2592
  switch (request.mode) {
2016
2593
  case "proofread":
2017
2594
  var proofReason = "Normalizes spacing, punctuation, and capitalization while keeping the original meaning intact.";
2018
2595
  return {
2019
2596
  operations: [
2020
- buildLegacyOperation(request.action && request.action.target ? request.action.target : "selection-preview", buildDemoResult("proofread", request.source), { reason: proofReason }, request, request.action)
2597
+ buildLegacyOperation(request.action && request.action.target ? request.action.target : "selection-preview", buildDemoResult("proofread", request.source, selectionSentenceContext(request)), { reason: proofReason }, request, request.action)
2021
2598
  ],
2022
2599
  reason: proofReason
2023
2600
  };
@@ -2025,7 +2602,7 @@ function RTE_Plugin_AIToolkit() {
2025
2602
  var rewriteReason = "Tightens the wording by removing filler phrases so the point comes through faster.";
2026
2603
  return {
2027
2604
  operations: [
2028
- buildLegacyOperation(request.action && request.action.target ? request.action.target : "selection-preview", buildDemoResult("rewrite", request.source), { reason: rewriteReason }, request, request.action)
2605
+ buildLegacyOperation(request.action && request.action.target ? request.action.target : "selection-preview", buildDemoResult("rewrite", request.source, selectionSentenceContext(request)), { reason: rewriteReason }, request, request.action)
2029
2606
  ],
2030
2607
  reason: rewriteReason
2031
2608
  };
@@ -2048,7 +2625,7 @@ function RTE_Plugin_AIToolkit() {
2048
2625
  operations: [
2049
2626
  {
2050
2627
  type: "preview-suggestion",
2051
- text: buildDemoResult("rewrite", request.source),
2628
+ text: buildDemoResult("rewrite", request.source, selectionSentenceContext(request)),
2052
2629
  reason: "This rewrite reduces filler language, clarifies the main point, and makes the sentence easier to review before accepting."
2053
2630
  }
2054
2631
  ],
@@ -2534,6 +3111,8 @@ function RTE_Plugin_AIToolkit() {
2534
3111
  scope: suggestion.snapshot && suggestion.snapshot.hasSelection ? "selection" : "document",
2535
3112
  text: suggestion.text || "",
2536
3113
  blockPath: suggestion.blockPath || "",
3114
+ mentions: cleanLedgerMentions(suggestion.mentions),
3115
+ reactions: cleanLedgerReactions(suggestion.reactions),
2537
3116
  replies: Array.isArray(suggestion.replies) ? suggestion.replies : []
2538
3117
  };
2539
3118
  }
@@ -2560,6 +3139,33 @@ function RTE_Plugin_AIToolkit() {
2560
3139
  }
2561
3140
  return doc.body.innerHTML;
2562
3141
  }
3142
+ // 2026-10-12 comment mentions: the { id, name } pairs a comment names, kept with the comment in the shared store.
3143
+ function cleanLedgerMentions(list) {
3144
+ var out = [];
3145
+ if (!Array.isArray(list)) return out;
3146
+ for (var i = 0; i < list.length; i++) {
3147
+ var m = list[i];
3148
+ if (m && m.id != null && m.name) out.push({ id: String(m.id), name: String(m.name) });
3149
+ }
3150
+ return out;
3151
+ }
3152
+ // 2026-10-15 comment reactions: [{ emoji, users: [id] }], kept with the comment like mentions.
3153
+ function cleanLedgerReactions(list) {
3154
+ var out = [];
3155
+ if (!Array.isArray(list)) return out;
3156
+ for (var i = 0; i < list.length && out.length < 20; i++) {
3157
+ var r = list[i];
3158
+ if (!r || typeof r.emoji !== "string" || !r.emoji || r.emoji.length > 8 || !Array.isArray(r.users)) continue;
3159
+ var users = [];
3160
+ for (var j = 0; j < r.users.length; j++) {
3161
+ var u = r.users[j] == null ? "" : String(r.users[j]);
3162
+ if (u && users.indexOf(u) < 0) users.push(u);
3163
+ }
3164
+ if (users.length) out.push({ emoji: r.emoji, users: users });
3165
+ }
3166
+ return out;
3167
+ }
3168
+
2563
3169
 
2564
3170
  function normalizeLedgerEntry(raw) {
2565
3171
  if (!raw || !raw.id) return null;
@@ -2592,6 +3198,8 @@ function RTE_Plugin_AIToolkit() {
2592
3198
  sourceLabel: raw.sourceLabel || "",
2593
3199
  snapshot: raw.snapshot || { hasSelection: raw.scope === "selection" },
2594
3200
  text: raw.text || "",
3201
+ mentions: cleanLedgerMentions(raw.mentions),
3202
+ reactions: cleanLedgerReactions(raw.reactions),
2595
3203
  blockPath: raw.blockPath || "",
2596
3204
  replies: Array.isArray(raw.replies) ? raw.replies : []
2597
3205
  };
@@ -2634,6 +3242,8 @@ function RTE_Plugin_AIToolkit() {
2634
3242
  hasSelection: raw.scope === "selection"
2635
3243
  },
2636
3244
  text: raw.text || "",
3245
+ mentions: cleanLedgerMentions(raw.mentions),
3246
+ reactions: cleanLedgerReactions(raw.reactions),
2637
3247
  blockPath: raw.blockPath || "",
2638
3248
  replies: Array.isArray(raw.replies) ? raw.replies : []
2639
3249
  };
@@ -2689,6 +3299,8 @@ function RTE_Plugin_AIToolkit() {
2689
3299
  createdAt: suggestion.createdAt || 0,
2690
3300
  decidedAt: suggestion.decidedAt || 0,
2691
3301
  sourceLabel: suggestion.sourceLabel || "",
3302
+ mentions: cleanLedgerMentions(suggestion.mentions),
3303
+ reactions: cleanLedgerReactions(suggestion.reactions),
2692
3304
  snapshot: {
2693
3305
  hasSelection: !!(suggestion.snapshot && suggestion.snapshot.hasSelection)
2694
3306
  }
@@ -4942,13 +5554,13 @@ function RTE_Plugin_AIToolkit() {
4942
5554
  var bodyNode = ownerDocument.createElement("span");
4943
5555
  bodyNode.className = "rte-ai-inline-preview-shortcuts-body";
4944
5556
  shortcutsChip.appendChild(bodyNode);
4945
- var pieces = String(text || "").split(/(\s+·\s+|\s+-\s+)/);
5557
+ var pieces = String(text || "").split(/(\s+\xb7\s+|\s+-\s+)/);
4946
5558
  for (var pieceIndex = 0; pieceIndex < pieces.length; pieceIndex++) {
4947
5559
  var piece = pieces[pieceIndex];
4948
5560
  if (!piece) {
4949
5561
  continue;
4950
5562
  }
4951
- if (/^\s+[·-]\s+$/.test(piece)) {
5563
+ if (/^\s+[\xb7-]\s+$/.test(piece)) {
4952
5564
  var separator = ownerDocument.createElement("span");
4953
5565
  separator.className = "rte-ai-inline-preview-shortcuts-separator";
4954
5566
  separator.innerText = piece.replace(/^\s+|\s+$/g, "");
@@ -5141,13 +5753,13 @@ function RTE_Plugin_AIToolkit() {
5141
5753
  function getSuggestionStatusLabel(status) {
5142
5754
  switch (status) {
5143
5755
  case "accepted":
5144
- return "Accepted";
5756
+ return (obj.__aiT ? obj.__aiT("air_status_accepted", "Accepted") : "Accepted");
5145
5757
  case "rejected":
5146
- return "Rejected";
5758
+ return (obj.__aiT ? obj.__aiT("air_status_rejected", "Rejected") : "Rejected");
5147
5759
  case "stale":
5148
- return "Needs refresh";
5760
+ return (obj.__aiT ? obj.__aiT("air_status_stale", "Needs refresh") : "Needs refresh");
5149
5761
  default:
5150
- return "Pending";
5762
+ return (obj.__aiT ? obj.__aiT("air_status_pending", "Pending") : "Pending");
5151
5763
  }
5152
5764
  }
5153
5765
 
@@ -5407,7 +6019,7 @@ function RTE_Plugin_AIToolkit() {
5407
6019
  if (summaryData.detail) {
5408
6020
  parts.push(summaryData.detail);
5409
6021
  }
5410
- return parts.join(". ");
6022
+ return joinSentences(parts);
5411
6023
  }
5412
6024
 
5413
6025
  function createInlinePreviewActionSummaryHtml(previewState) {
@@ -5566,10 +6178,10 @@ function RTE_Plugin_AIToolkit() {
5566
6178
  }
5567
6179
  var ariaLabel = label ? (label + " actions") : "AI suggestion actions";
5568
6180
  if (detailText) {
5569
- ariaLabel += ". " + detailText;
6181
+ ariaLabel = joinSentences([ariaLabel, detailText]);
5570
6182
  }
5571
6183
  if (isCurrentGroup && badgeText) {
5572
- ariaLabel += ". Current lane. " + badgeText;
6184
+ ariaLabel = joinSentences([ariaLabel, "Current lane", badgeText]);
5573
6185
  }
5574
6186
  groupNode.setAttribute("aria-label", ariaLabel);
5575
6187
  }
@@ -5618,7 +6230,7 @@ function RTE_Plugin_AIToolkit() {
5618
6230
  button.setAttribute("aria-keyshortcuts", shortcutKey ? ("Enter Space " + shortcutKey) : "Enter Space");
5619
6231
  button.setAttribute("data-rte-ai-enter-target-label", "Enter");
5620
6232
  if (ariaLabel && !/current enter target/i.test(ariaLabel)) {
5621
- button.setAttribute("aria-label", ariaLabel + ". Current Enter target.");
6233
+ button.setAttribute("aria-label", joinSentences([ariaLabel, "Current Enter target."]));
5622
6234
  }
5623
6235
  if (title && !/enter or space activates this action/i.test(title)) {
5624
6236
  title += ". Enter or Space activates this action";
@@ -5627,7 +6239,7 @@ function RTE_Plugin_AIToolkit() {
5627
6239
  }
5628
6240
  }
5629
6241
  if (title && !/current enter target/i.test(title)) {
5630
- button.title = title + ". Current Enter target.";
6242
+ button.title = joinSentences([title, "Current Enter target."]);
5631
6243
  }
5632
6244
  }
5633
6245
  else {
@@ -6161,7 +6773,32 @@ function RTE_Plugin_AIToolkit() {
6161
6773
  + "</" + resolvedTag + ">";
6162
6774
  }
6163
6775
 
6776
+ // 2026-10-18 The card is inserted into the editing document - an iframe by default - but the toolkit's stylesheet
6777
+ // was linked into the host page only, so every card rendered as unstyled run-on text. The host page's copy is
6778
+ // linked into the editing document once (never into the saved HTML). tools/ai/inline-card-styled.mjs
6779
+ function ensureCardStylesheet() {
6780
+ try {
6781
+ var editable = editor.getEditable ? editor.getEditable() : null;
6782
+ var doc = editable && editable.ownerDocument;
6783
+ if (!doc || doc === document || doc.querySelector("link[data-rte-ai-card-css]")) return;
6784
+ var src = document.querySelector("link[data-rte-auto-injected=aitoolkit]");
6785
+ if (!src) {
6786
+ var all = document.querySelectorAll("link[rel=stylesheet]");
6787
+ for (var i = 0; i < all.length; i++) {
6788
+ if (/(?:^|\/)aitoolkit(?:\.min)?\.css(?:[?#].*)?$/i.test(all[i].getAttribute("href") || "")) { src = all[i]; break; }
6789
+ }
6790
+ }
6791
+ if (!src || !src.href) return;
6792
+ var link = doc.createElement("link");
6793
+ link.rel = "stylesheet";
6794
+ link.href = src.href;
6795
+ link.setAttribute("data-rte-ai-card-css", "1");
6796
+ (doc.head || doc.documentElement).appendChild(link);
6797
+ } catch (e) { /* the card stays unstyled, as before */ }
6798
+ }
6799
+
6164
6800
  function createInlinePreviewHtml(previewState) {
6801
+ ensureCardStylesheet();
6165
6802
  var isBlock = !!previewState.isBlock;
6166
6803
  var wrapperTag = isBlock ? "div" : "span";
6167
6804
  var partTag = isBlock ? "div" : "span";
@@ -6239,6 +6876,7 @@ function RTE_Plugin_AIToolkit() {
6239
6876
  editor.__aiBindInlinePreviewEventHandlers();
6240
6877
  }
6241
6878
  editor.insertHTML(createInlinePreviewHtml(previewState));
6879
+ aiqWatchCards();
6242
6880
  persistSuggestionStore();
6243
6881
  emitReviewLogEvent("suggestion-created", previewState);
6244
6882
  if (editor.__aiReviewPanel && editor.__aiReviewPanel.isConnected) {
@@ -6326,8 +6964,8 @@ function RTE_Plugin_AIToolkit() {
6326
6964
  var searchInput = panel.ownerDocument.createElement("input");
6327
6965
  searchInput.type = "text";
6328
6966
  searchInput.className = "rte-ai-menu-search-input";
6329
- searchInput.placeholder = "Filter actions…";
6330
- searchInput.setAttribute("aria-label", "Filter AI actions");
6967
+ searchInput.placeholder = (obj.__aiT ? obj.__aiT("aia_filter", "Filter actions\u2026") : "Filter actions\u2026");
6968
+ searchInput.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aia_filteraria", "Filter AI actions") : "Filter AI actions"));
6331
6969
  searchInput.setAttribute("autocomplete", "off");
6332
6970
  searchInput.setAttribute("spellcheck", "false");
6333
6971
  searchRow.appendChild(searchInput);
@@ -6387,7 +7025,7 @@ function RTE_Plugin_AIToolkit() {
6387
7025
  // 26px row floor and 2-col grid measure identically.
6388
7026
  if (action.description) {
6389
7027
  button.title = action.description;
6390
- button.setAttribute("aria-label", (action.title || action.id) + " — " + action.description);
7028
+ button.setAttribute("aria-label", (action.title || action.id) + " \u2014 " + action.description);
6391
7029
  }
6392
7030
  var icon = append(button, "span", "", "rte-ai-menu-item-icon");
6393
7031
  icon.innerHTML = getActionIconSvg(action);
@@ -6455,7 +7093,7 @@ function RTE_Plugin_AIToolkit() {
6455
7093
  if (!searchRow.__aiEmptyHint) {
6456
7094
  var hint = panel.ownerDocument.createElement("div");
6457
7095
  hint.className = "rte-ai-menu-empty-hint";
6458
- hint.textContent = "No actions match.";
7096
+ hint.textContent = (obj.__aiT ? obj.__aiT("aia_nomatch", "No actions match.") : "No actions match.");
6459
7097
  panel.appendChild(hint);
6460
7098
  searchRow.__aiEmptyHint = hint;
6461
7099
  }
@@ -6644,6 +7282,19 @@ function RTE_Plugin_AIToolkit() {
6644
7282
  });
6645
7283
  }
6646
7284
 
7285
+ // 2026-10-17 a locked document stays locked: in read-only mode (Ask AI opened through the API) and in comments-only
7286
+ // mode (its button stays enabled there) "Replace document", applyResult() and executeResolvedAction() rewrote the
7287
+ // document. Text-changing operations are refused and the refusal is announced; opening a panel and adding a
7288
+ // comment still work. tools/ai/ai-locked.mjs
7289
+ function documentLocked() {
7290
+ try { return !!((editor.getReadOnly && editor.getReadOnly()) || (editor.getCommentsOnly && editor.getCommentsOnly())); } catch (e) { return false; }
7291
+ }
7292
+
7293
+ function refuseLocked() {
7294
+ announceReviewStatus(config.text_aitoolkitlocked || "This document is read-only, so the AI result was not applied.");
7295
+ return false;
7296
+ }
7297
+
6647
7298
  function executeOperations(operations, options) {
6648
7299
  options = options || {};
6649
7300
  var snapshot = options.snapshot || captureSelectionSnapshot();
@@ -6680,6 +7331,7 @@ function RTE_Plugin_AIToolkit() {
6680
7331
  });
6681
7332
  }
6682
7333
 
7334
+ if (documentLocked() && !/^open-|comment/i.test(String(operation.type))) return refuseLocked();
6683
7335
  switch (operation.type) {
6684
7336
  case "open-chat-panel":
6685
7337
  openChatPanel({ focusComposer: true });
@@ -6891,6 +7543,7 @@ function RTE_Plugin_AIToolkit() {
6891
7543
  }
6892
7544
 
6893
7545
  function applyResult(result, options) {
7546
+ if (documentLocked()) return refuseLocked();
6894
7547
  var clean = normalizeText(result);
6895
7548
  if (!clean) {
6896
7549
  return false;
@@ -6903,7 +7556,9 @@ function RTE_Plugin_AIToolkit() {
6903
7556
  var snapshot = options.snapshot || captureSelectionSnapshot();
6904
7557
 
6905
7558
  if (mode === "document") {
6906
- editor.setHTMLCode(textToHtml(clean));
7559
+ // 2026-10-03 undoable: an AI rewrite of the whole document is one Ctrl+Z back to exactly
7560
+ // what was there, edits made just before included (tools/import/replace-undo-plugins.mjs)
7561
+ editor.setHTMLCode(textToHtml(clean), { undoable: true });
6907
7562
  syncSuggestionStates();
6908
7563
  persistSuggestionStore();
6909
7564
  if (editor.__aiReviewPanel && editor.__aiReviewPanel.isConnected) {
@@ -7116,7 +7771,7 @@ function RTE_Plugin_AIToolkit() {
7116
7771
  if (options.redoNextLabel) {
7117
7772
  parts.push(options.redoNextLabel);
7118
7773
  }
7119
- return parts.join(". ");
7774
+ return joinSentences(parts);
7120
7775
  }
7121
7776
 
7122
7777
  function buildReviewQueuePositionData(visibleSuggestions, activeSuggestionId, suggestion) {
@@ -7897,7 +8552,7 @@ function RTE_Plugin_AIToolkit() {
7897
8552
  parts.push(options.queueTransitionData.followupDetail);
7898
8553
  }
7899
8554
  }
7900
- return parts.join(". ");
8555
+ return joinSentences(parts);
7901
8556
  }
7902
8557
 
7903
8558
  function updateReviewCondensedPreviewAria(item, suggestion, panel) {
@@ -8222,7 +8877,7 @@ function RTE_Plugin_AIToolkit() {
8222
8877
  focusActionLabel: getReviewFocusActionDisplayLabel(findSuggestionById(nextOverallPendingId), preferredQueueFocusAction)
8223
8878
  } : null,
8224
8879
  undoAction,
8225
- { id: "all-types", label: "All types" },
8880
+ { id: "all-types", label: (obj.__aiT ? obj.__aiT("air_alltypes", "All types") : "All types") },
8226
8881
  hasResolved && !showResolved ? { id: "show-resolved", label: "Show resolved" } : null,
8227
8882
  { id: "open-chat", label: "AI Chat" }
8228
8883
  ])
@@ -8250,7 +8905,7 @@ function RTE_Plugin_AIToolkit() {
8250
8905
  announceMessage: "",
8251
8906
  actions: [
8252
8907
  undoAction,
8253
- typeFilter !== "all" ? { id: "all-types", label: "All types" } : null,
8908
+ typeFilter !== "all" ? { id: "all-types", label: (obj.__aiT ? obj.__aiT("air_alltypes", "All types") : "All types") } : null,
8254
8909
  hasResolved && !showResolved ? { id: "show-resolved", label: "Show resolved" } : null,
8255
8910
  { id: "open-chat", label: "AI Chat" }
8256
8911
  ]
@@ -9416,7 +10071,7 @@ function RTE_Plugin_AIToolkit() {
9416
10071
  if (contextCardData.copy) {
9417
10072
  parts.push(contextCardData.copy);
9418
10073
  }
9419
- return parts.join(". ");
10074
+ return joinSentences(parts);
9420
10075
  }
9421
10076
 
9422
10077
  function getChatMessageStateTitle(message) {
@@ -9629,7 +10284,7 @@ function RTE_Plugin_AIToolkit() {
9629
10284
  if (isRecommended) {
9630
10285
  parts.push("Recommended next step.");
9631
10286
  }
9632
- button.setAttribute("aria-label", parts.join(". "));
10287
+ button.setAttribute("aria-label", joinSentences(parts));
9633
10288
  button.title = detail
9634
10289
  ? label + " - " + detail + (isRecommended ? " Recommended next step." : "")
9635
10290
  : label + (isRecommended ? " - Recommended next step." : "");
@@ -9864,7 +10519,7 @@ function RTE_Plugin_AIToolkit() {
9864
10519
  if (actionLabels.length) {
9865
10520
  parts.push("Actions: " + actionLabels.join(", "));
9866
10521
  }
9867
- return parts.join(". ");
10522
+ return joinSentences(parts);
9868
10523
  }
9869
10524
 
9870
10525
  function syncChatActionAccessibility(actionsNode, recommendedActionId, message, currentSnapshot) {
@@ -9874,8 +10529,8 @@ function RTE_Plugin_AIToolkit() {
9874
10529
  var recommendedLabel = getChatRecommendedActionLabel(recommendedActionId);
9875
10530
  actionsNode.setAttribute("role", "group");
9876
10531
  actionsNode.setAttribute("aria-label", recommendedLabel
9877
- ? "AI chat actions. Recommended next step: " + recommendedLabel + "."
9878
- : "AI chat actions");
10532
+ ? (obj.__aiT ? obj.__aiT("aic_actionsrec", "AI chat actions. Recommended next step: {0}.", recommendedLabel) : "AI chat actions. Recommended next step: " + recommendedLabel + ".")
10533
+ : (obj.__aiT ? obj.__aiT("aic_actions", "AI chat actions") : "AI chat actions"));
9879
10534
  var groupNodes = actionsNode.querySelectorAll(".rte-ai-chat-message-action-group");
9880
10535
  for (var groupIndex = 0; groupIndex < groupNodes.length; groupIndex++) {
9881
10536
  var groupNode = groupNodes[groupIndex];
@@ -10001,7 +10656,7 @@ function RTE_Plugin_AIToolkit() {
10001
10656
  if (planItem.detail) {
10002
10657
  parts.push(planItem.detail);
10003
10658
  }
10004
- return parts.join(". ");
10659
+ return joinSentences(parts);
10005
10660
  }
10006
10661
 
10007
10662
  function buildChatPreviewPlanItems(message, recommendedActionId, currentSnapshot) {
@@ -10128,7 +10783,7 @@ function RTE_Plugin_AIToolkit() {
10128
10783
  parts.push("+" + resolvedPreview.hiddenPlanCount + " more");
10129
10784
  }
10130
10785
  }
10131
- return parts.join(". ");
10786
+ return joinSentences(parts);
10132
10787
  }
10133
10788
 
10134
10789
  function summarizeChatPromptCopy(text) {
@@ -10227,7 +10882,7 @@ function RTE_Plugin_AIToolkit() {
10227
10882
  if (message.text) {
10228
10883
  parts.push(message.text);
10229
10884
  }
10230
- return parts.join(". ");
10885
+ return joinSentences(parts);
10231
10886
  }
10232
10887
 
10233
10888
  function buildChatAssistantRequestCardAriaLabel(requestCardData, exchangeLabel) {
@@ -10250,7 +10905,7 @@ function RTE_Plugin_AIToolkit() {
10250
10905
  if (requestCardData.promptText) {
10251
10906
  parts.push(requestCardData.promptText);
10252
10907
  }
10253
- return parts.join(". ");
10908
+ return joinSentences(parts);
10254
10909
  }
10255
10910
 
10256
10911
  function buildChatMessageAriaLabel(messages, messageIndex, currentSnapshot) {
@@ -10322,7 +10977,7 @@ function RTE_Plugin_AIToolkit() {
10322
10977
  parts.push(actionSummary.title);
10323
10978
  }
10324
10979
  }
10325
- return parts.join(". ");
10980
+ return joinSentences(parts);
10326
10981
  }
10327
10982
 
10328
10983
  function buildChatMessageStateCardAriaLabel(message) {
@@ -10342,7 +10997,7 @@ function RTE_Plugin_AIToolkit() {
10342
10997
  if (stateDetail) {
10343
10998
  parts.push(stateDetail);
10344
10999
  }
10345
- return parts.join(". ");
11000
+ return joinSentences(parts);
10346
11001
  }
10347
11002
 
10348
11003
  function getChatMessageThreadRole(messages, messageIndex) {
@@ -10496,7 +11151,7 @@ function RTE_Plugin_AIToolkit() {
10496
11151
  if (bannerData.detail) {
10497
11152
  parts.push(bannerData.detail);
10498
11153
  }
10499
- return parts.join(". ");
11154
+ return joinSentences(parts);
10500
11155
  }
10501
11156
 
10502
11157
  function buildChatExchangeGroupAriaLabel(messages, requestIndex, currentSnapshot) {
@@ -10552,7 +11207,7 @@ function RTE_Plugin_AIToolkit() {
10552
11207
  if (bannerData && bannerData.isCurrent) {
10553
11208
  parts.push("Current exchange");
10554
11209
  }
10555
- return parts.join(". ");
11210
+ return joinSentences(parts);
10556
11211
  }
10557
11212
 
10558
11213
  function buildChatExchangeOutcomeData(messages, messageIndex, currentSnapshot) {
@@ -10636,7 +11291,7 @@ function RTE_Plugin_AIToolkit() {
10636
11291
  if (outcomeData.detail) {
10637
11292
  parts.push(outcomeData.detail);
10638
11293
  }
10639
- return parts.join(". ");
11294
+ return joinSentences(parts);
10640
11295
  }
10641
11296
 
10642
11297
  function getChatContextWordCount(text) {
@@ -10779,46 +11434,51 @@ function RTE_Plugin_AIToolkit() {
10779
11434
  }
10780
11435
  }
10781
11436
  }
10782
- return parts.join(". ");
11437
+ return joinSentences(parts);
10783
11438
  }
10784
11439
 
10785
11440
  function buildChatSendActionData(state, context, activePrompt) {
10786
11441
  var scopeLabel = context && context.scope === "selection" ? "selection" : "document";
11442
+ // 2026-10-18 every sentence reads a config.text_ key per scope (word order and gender differ by language);
11443
+ // the English fallbacks are the sentences this built before. tools/i18n/ai-chat-language.mjs
11444
+ var sc = scopeLabel === "selection" ? "sel" : "doc";
11445
+ var L = function (key, en, arg) { var s = (obj.__aiT ? obj.__aiT(key, en) : en); return arg === undefined ? s : s.split("{0}").join(String(arg)); };
11446
+ var LS = function (key, enSel, enDoc, arg) { return L(key + "_" + sc, sc === "sel" ? enSel : enDoc, arg); };
10787
11447
  var hasCustomDraft = !!normalizeText(state && state.draft ? state.draft : "");
10788
11448
  if (state && state.busy) {
10789
11449
  return {
10790
- text: "Thinking...",
11450
+ text: L("aic_thinking", "Thinking..."),
10791
11451
  disabled: true,
10792
- ariaLabel: "Send disabled while AI Chat is thinking about the current " + scopeLabel + ".",
10793
- title: "AI Chat is thinking about the current " + scopeLabel + "."
11452
+ ariaLabel: LS("aic_busyaria", "Send disabled while AI Chat is thinking about the current selection.", "Send disabled while AI Chat is thinking about the current document."),
11453
+ title: LS("aic_busytitle", "AI Chat is thinking about the current selection.", "AI Chat is thinking about the current document.")
10794
11454
  };
10795
11455
  }
10796
11456
  if (activePrompt && activePrompt.label) {
10797
11457
  return {
10798
- text: "Send starter",
11458
+ text: L("aic_sendstarter", "Send starter"),
10799
11459
  disabled: false,
10800
- ariaLabel: "Send starter. Send the " + activePrompt.label + " starter for the current " + scopeLabel + ".",
10801
- title: "Send the " + activePrompt.label + " starter for the current " + scopeLabel + "."
11460
+ ariaLabel: LS("aic_starteraria", "Send starter. Send the {0} starter for the current selection.", "Send starter. Send the {0} starter for the current document.", activePrompt.label),
11461
+ title: LS("aic_startertitle", "Send the {0} starter for the current selection.", "Send the {0} starter for the current document.", activePrompt.label)
10802
11462
  };
10803
11463
  }
10804
11464
  if (hasCustomDraft) {
10805
11465
  return {
10806
- text: "Send prompt",
11466
+ text: L("aic_sendprompt", "Send prompt"),
10807
11467
  disabled: false,
10808
- ariaLabel: "Send prompt. Send the custom prompt for the current " + scopeLabel + ".",
10809
- title: "Send the custom prompt for the current " + scopeLabel + "."
11468
+ ariaLabel: LS("aic_promptaria", "Send prompt. Send the custom prompt for the current selection.", "Send prompt. Send the custom prompt for the current document."),
11469
+ title: LS("aic_prompttitle", "Send the custom prompt for the current selection.", "Send the custom prompt for the current document.")
10810
11470
  };
10811
11471
  }
10812
11472
  return {
10813
- text: "Send",
11473
+ text: L("aic_send", "Send"),
10814
11474
  disabled: true,
10815
- ariaLabel: "Send disabled. Type a prompt or load a starter for the current " + scopeLabel + " first.",
10816
- title: "Type a prompt or load a starter for the current " + scopeLabel + " first."
11475
+ ariaLabel: LS("aic_emptyaria", "Send disabled. Type a prompt or load a starter for the current selection first.", "Send disabled. Type a prompt or load a starter for the current document first."),
11476
+ title: LS("aic_emptytitle", "Type a prompt or load a starter for the current selection first.", "Type a prompt or load a starter for the current document first.")
10817
11477
  };
10818
11478
  }
10819
11479
 
10820
11480
  function buildChatComposerActionsAriaLabel(sendActionData) {
10821
- return "Compose actions. Open Ask AI. " + (sendActionData && sendActionData.ariaLabel ? sendActionData.ariaLabel : "Send.");
11481
+ return (obj.__aiT ? obj.__aiT("aic_composeactions", "Compose actions. Open Ask AI.") : "Compose actions. Open Ask AI.") + " " + (sendActionData && sendActionData.ariaLabel ? sendActionData.ariaLabel : (obj.__aiT ? obj.__aiT("aic_sendp", "Send.") : "Send."));
10822
11482
  }
10823
11483
 
10824
11484
  function getLatestChatMessage(state) {
@@ -11032,7 +11692,7 @@ function RTE_Plugin_AIToolkit() {
11032
11692
  parts.push(summaryData.changeGlance.detail);
11033
11693
  }
11034
11694
  }
11035
- return parts.join(". ");
11695
+ return joinSentences(parts);
11036
11696
  }
11037
11697
 
11038
11698
  function buildChatFeedIntroData(state, context, activePrompt) {
@@ -11104,7 +11764,7 @@ function RTE_Plugin_AIToolkit() {
11104
11764
  if (feedIntroData.detail) {
11105
11765
  parts.push(feedIntroData.detail);
11106
11766
  }
11107
- return parts.join(". ");
11767
+ return joinSentences(parts);
11108
11768
  }
11109
11769
 
11110
11770
  function buildChatFeedSectionAriaLabel(state, context, activePrompt) {
@@ -11133,7 +11793,7 @@ function RTE_Plugin_AIToolkit() {
11133
11793
  if (summaryData && summaryData.detail) {
11134
11794
  parts.push(summaryData.detail);
11135
11795
  }
11136
- return parts.join(". ");
11796
+ return joinSentences(parts);
11137
11797
  }
11138
11798
  var feedIntroData = buildChatFeedIntroData(state, context, activePrompt);
11139
11799
  if (feedIntroData.title) {
@@ -11149,7 +11809,7 @@ function RTE_Plugin_AIToolkit() {
11149
11809
  if (feedIntroData.detail) {
11150
11810
  parts.push(feedIntroData.detail);
11151
11811
  }
11152
- return parts.join(". ");
11812
+ return joinSentences(parts);
11153
11813
  }
11154
11814
 
11155
11815
  function getChatConversationSummaryTitle(state, context) {
@@ -11256,7 +11916,7 @@ function RTE_Plugin_AIToolkit() {
11256
11916
  parts.push(titleText);
11257
11917
  }
11258
11918
  if (!headerData) {
11259
- return parts.join(". ");
11919
+ return joinSentences(parts);
11260
11920
  }
11261
11921
  if (headerData.statusLabel) {
11262
11922
  parts.push(headerData.statusLabel);
@@ -11274,7 +11934,7 @@ function RTE_Plugin_AIToolkit() {
11274
11934
  }
11275
11935
  }
11276
11936
  }
11277
- return parts.join(". ");
11937
+ return joinSentences(parts);
11278
11938
  }
11279
11939
 
11280
11940
  function buildChatContextData(context) {
@@ -11354,7 +12014,7 @@ function RTE_Plugin_AIToolkit() {
11354
12014
  if (contextData.previewSummary) {
11355
12015
  parts.push(contextData.previewSummary);
11356
12016
  }
11357
- return parts.join(". ");
12017
+ return joinSentences(parts);
11358
12018
  }
11359
12019
 
11360
12020
  function buildChatContextBarAriaLabel(contextData) {
@@ -11369,7 +12029,7 @@ function RTE_Plugin_AIToolkit() {
11369
12029
  }
11370
12030
  }
11371
12031
  }
11372
- return parts.join(". ");
12032
+ return joinSentences(parts);
11373
12033
  }
11374
12034
 
11375
12035
  function buildChatContextScopeButtonAriaLabel(scopeId, contextData) {
@@ -11405,7 +12065,7 @@ function RTE_Plugin_AIToolkit() {
11405
12065
  if (contextData.previewSummary) {
11406
12066
  parts.push(contextData.previewSummary);
11407
12067
  }
11408
- return parts.join(". ");
12068
+ return joinSentences(parts);
11409
12069
  }
11410
12070
 
11411
12071
  function buildChatQuickStartData(state, prompts, activePrompt, context) {
@@ -11458,7 +12118,7 @@ function RTE_Plugin_AIToolkit() {
11458
12118
  if (quickStartData.detail) {
11459
12119
  parts.push(quickStartData.detail);
11460
12120
  }
11461
- return parts.join(". ");
12121
+ return joinSentences(parts);
11462
12122
  }
11463
12123
 
11464
12124
  function buildChatQuickButtonAriaLabel(prompt, isActivePrompt, context) {
@@ -11477,7 +12137,7 @@ function RTE_Plugin_AIToolkit() {
11477
12137
  parts.push(isActivePrompt
11478
12138
  ? "Current starter loaded in Compose for the current " + scopeLabel
11479
12139
  : "Loads this starter into Compose for the current " + scopeLabel);
11480
- return parts.join(". ");
12140
+ return joinSentences(parts);
11481
12141
  }
11482
12142
 
11483
12143
  function buildChatQuickRowAriaLabel(context) {
@@ -11512,21 +12172,21 @@ function RTE_Plugin_AIToolkit() {
11512
12172
  // The capability-cards data is still attached to emptyStateData.aria
11513
12173
  // so screen-reader users hear the long form via the aria-label.
11514
12174
  return {
11515
- title: "Start with AI Chat",
11516
- detail: context.scope === "selection"
11517
- ? "Ask anything about the current selection."
11518
- : "Ask anything about the current document.",
12175
+ title: (obj.__aiT ? obj.__aiT("aic_starttitle", "Start with AI Chat") : "Start with AI Chat"),
12176
+ detail: (context.scope === "selection"
12177
+ ? (obj.__aiT ? obj.__aiT("aic_empty_sel", "Ask anything about the current selection") : "Ask anything about the current selection")
12178
+ : (obj.__aiT ? obj.__aiT("aic_empty_doc", "Ask anything about the current document") : "Ask anything about the current document")) + ".",
11519
12179
  metaPills: [
11520
12180
  {
11521
- text: context.scope === "selection" ? "Selection context" : "Document context",
12181
+ text: context.scope === "selection" ? (obj.__aiT ? obj.__aiT("aic_pill_sel", "Selection context") : "Selection context") : (obj.__aiT ? obj.__aiT("aic_pill_doc", "Document context") : "Document context"),
11522
12182
  className: "is-scope"
11523
12183
  },
11524
12184
  {
11525
- text: (prompts && prompts.length ? prompts.length : 0) + " starters",
12185
+ text: (obj.__aiT ? obj.__aiT("aic_pill_starters", "{0} starters", prompts && prompts.length ? prompts.length : 0) : (prompts && prompts.length ? prompts.length : 0) + " starters"),
11526
12186
  className: "is-prompts"
11527
12187
  },
11528
12188
  {
11529
- text: getChatContextWordCount(context.source) + " words in scope",
12189
+ text: (obj.__aiT ? obj.__aiT("aic_pill_words", "{0} words in scope", getChatContextWordCount(context.source)) : getChatContextWordCount(context.source) + " words in scope"),
11530
12190
  className: "is-size"
11531
12191
  }
11532
12192
  ],
@@ -11545,7 +12205,7 @@ function RTE_Plugin_AIToolkit() {
11545
12205
  if (capabilityCard.copy) {
11546
12206
  parts.push(capabilityCard.copy);
11547
12207
  }
11548
- return parts.join(". ");
12208
+ return joinSentences(parts);
11549
12209
  }
11550
12210
 
11551
12211
  function buildChatEmptyStateAriaLabel(emptyStateData) {
@@ -11571,7 +12231,7 @@ function RTE_Plugin_AIToolkit() {
11571
12231
  parts.push(buildChatEmptyCapabilityAriaLabel(emptyStateData.capabilityCards[cardIndex]));
11572
12232
  }
11573
12233
  }
11574
- return parts.join(". ");
12234
+ return joinSentences(parts);
11575
12235
  }
11576
12236
 
11577
12237
  function clearChatNode(node) {
@@ -11930,6 +12590,8 @@ function RTE_Plugin_AIToolkit() {
11930
12590
  panel.setAttribute("aria-label", config.text_aichat || "AI Chat");
11931
12591
  panel.tabIndex = -1;
11932
12592
  editor.__aiChatPanel = panel;
12593
+ aiqWatch(panel);
12594
+ appendDemoNotice(panel);
11933
12595
  panel.onkeydown = function (e) {
11934
12596
  if (e.key === "Escape") {
11935
12597
  e.preventDefault();
@@ -11971,8 +12633,8 @@ function RTE_Plugin_AIToolkit() {
11971
12633
  var clearButton = append(headerActions, "button", "", "secondary rte-ai-chat-header-button is-icon-only");
11972
12634
  clearButton.type = "button";
11973
12635
  clearButton.setAttribute("data-rte-ai-chat-header-action", "new-chat");
11974
- clearButton.title = "Start a fresh chat";
11975
- clearButton.setAttribute("aria-label", "Start a fresh chat");
12636
+ clearButton.title = (obj.__aiT ? obj.__aiT("aic_newchat", "Start a fresh chat") : "Start a fresh chat");
12637
+ clearButton.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aic_newchat", "Start a fresh chat") : "Start a fresh chat"));
11976
12638
  setReviewV2ButtonContent(clearButton, "refresh", "");
11977
12639
  clearButton.onclick = function () {
11978
12640
  resetChatConversationState(state, "Started a fresh AI chat.");
@@ -11981,8 +12643,8 @@ function RTE_Plugin_AIToolkit() {
11981
12643
  var closeButton = append(headerActions, "button", "", "rte-ai-panel-close-button");
11982
12644
  closeButton.type = "button";
11983
12645
  closeButton.setAttribute("data-rte-ai-chat-header-action", "close");
11984
- closeButton.setAttribute("aria-label", "Close AI chat");
11985
- closeButton.title = "Close";
12646
+ closeButton.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aic_closechat", "Close AI chat") : "Close AI chat"));
12647
+ closeButton.title = (obj.__aiT ? obj.__aiT("aic_close", "Close") : "Close");
11986
12648
  closeButton.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6l12 12"/><path d="M18 6L6 18"/></svg>';
11987
12649
  closeButton.onclick = function () {
11988
12650
  closeChatPanel({ restoreFocus: true });
@@ -12026,7 +12688,7 @@ function RTE_Plugin_AIToolkit() {
12026
12688
  selectionButton.disabled = !context.snapshot.hasSelection;
12027
12689
  selectionButton.setAttribute("data-rte-ai-chat-scope-target", "selection");
12028
12690
  selectionButton.setAttribute("aria-label", buildChatContextScopeButtonAriaLabel("selection", contextData));
12029
- selectionButton.title = "Use selection";
12691
+ selectionButton.title = (obj.__aiT ? obj.__aiT("aic_usesel", "Use selection") : "Use selection");
12030
12692
  setReviewV2ButtonContent(selectionButton, "cursorText", "");
12031
12693
  selectionButton.onclick = function () {
12032
12694
  var currentSnapshot = captureSelectionSnapshot();
@@ -12038,7 +12700,7 @@ function RTE_Plugin_AIToolkit() {
12038
12700
  documentButton.type = "button";
12039
12701
  documentButton.setAttribute("data-rte-ai-chat-scope-target", "document");
12040
12702
  documentButton.setAttribute("aria-label", buildChatContextScopeButtonAriaLabel("document", contextData));
12041
- documentButton.title = "Use document";
12703
+ documentButton.title = (obj.__aiT ? obj.__aiT("aic_usedoc", "Use document") : "Use document");
12042
12704
  setReviewV2ButtonContent(documentButton, "document", "");
12043
12705
  documentButton.onclick = function () {
12044
12706
  state.scope = "document";
@@ -12093,7 +12755,7 @@ function RTE_Plugin_AIToolkit() {
12093
12755
  if (recentSessions.length > 1) {
12094
12756
  var historySection = append(panel, "div", "", "rte-ai-chat-stack rte-ai-chat-history-shell is-minimal");
12095
12757
  historySection.setAttribute("role", "group");
12096
- historySection.setAttribute("aria-label", "Recent AI chats");
12758
+ historySection.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aic_recent", "Recent AI chats") : "Recent AI chats"));
12097
12759
  var historyRow = append(historySection, "div", "", "rte-ai-chat-quick-row rte-ai-chat-history-row");
12098
12760
  for (var s = 0; s < recentSessions.length; s++) {
12099
12761
  (function (session, index) {
@@ -12160,10 +12822,10 @@ function RTE_Plugin_AIToolkit() {
12160
12822
  // reference drops with the chips.
12161
12823
  var hasQuickStarts = !!(prompts && prompts.length);
12162
12824
  var emptyHintBase = context.scope === "selection"
12163
- ? "Ask anything about the current selection"
12164
- : "Ask anything about the current document";
12825
+ ? (obj.__aiT ? obj.__aiT("aic_empty_sel", "Ask anything about the current selection") : "Ask anything about the current selection")
12826
+ : (obj.__aiT ? obj.__aiT("aic_empty_doc", "Ask anything about the current document") : "Ask anything about the current document");
12165
12827
  append(emptyState, "div", "", "rte-ai-chat-empty-detail",
12166
- hasQuickStarts ? (emptyHintBase + " — or tap a quick start above.") : (emptyHintBase + "."));
12828
+ hasQuickStarts ? (emptyHintBase + (obj.__aiT ? obj.__aiT("aic_orquick", " \u2014 or tap a quick start above.") : " \u2014 or tap a quick start above.")) : (emptyHintBase + "."));
12167
12829
  }
12168
12830
  var activeExchangeGroup = null;
12169
12831
  var activeExchangeIndex = 0;
@@ -12315,8 +12977,8 @@ function RTE_Plugin_AIToolkit() {
12315
12977
  var msgActions = append(item, "div", "", "rte-ai-chat-message-actions-row");
12316
12978
  var msgCopyBtn = append(msgActions, "button", "", "rte-ai-chat-message-action is-copy");
12317
12979
  msgCopyBtn.type = "button";
12318
- msgCopyBtn.title = "Copy message text";
12319
- setReviewV2ButtonContent(msgCopyBtn, "checkAll", "Copy");
12980
+ msgCopyBtn.title = (obj.__aiT ? obj.__aiT("aic_copymsg", "Copy message text") : "Copy message text");
12981
+ setReviewV2ButtonContent(msgCopyBtn, "checkAll", (obj.__aiT ? obj.__aiT("aic_copy", "Copy") : "Copy"));
12320
12982
  msgCopyBtn.onclick = (function (text, btn) {
12321
12983
  return function (e) {
12322
12984
  if (e && e.stopPropagation) e.stopPropagation();
@@ -12341,10 +13003,10 @@ function RTE_Plugin_AIToolkit() {
12341
13003
  }
12342
13004
  if (copied) {
12343
13005
  btn.classList.add("is-just-copied");
12344
- setReviewV2ButtonContent(btn, "check", "Copied");
13006
+ setReviewV2ButtonContent(btn, "check", (obj.__aiT ? obj.__aiT("aic_copied", "Copied") : "Copied"));
12345
13007
  setTimeout(function () {
12346
13008
  btn.classList.remove("is-just-copied");
12347
- setReviewV2ButtonContent(btn, "checkAll", "Copy");
13009
+ setReviewV2ButtonContent(btn, "checkAll", (obj.__aiT ? obj.__aiT("aic_copy", "Copy") : "Copy"));
12348
13010
  }, 1400);
12349
13011
  }
12350
13012
  };
@@ -12362,8 +13024,8 @@ function RTE_Plugin_AIToolkit() {
12362
13024
  if (precedingUserPrompt) {
12363
13025
  var msgRegenBtn = append(msgActions, "button", "", "rte-ai-chat-message-action is-regen");
12364
13026
  msgRegenBtn.type = "button";
12365
- msgRegenBtn.title = "Regenerate response";
12366
- setReviewV2ButtonContent(msgRegenBtn, "refresh", "Regenerate");
13027
+ msgRegenBtn.title = (obj.__aiT ? obj.__aiT("aic_regen", "Regenerate response") : "Regenerate response");
13028
+ setReviewV2ButtonContent(msgRegenBtn, "refresh", (obj.__aiT ? obj.__aiT("aid_regenerate", "Regenerate") : "Regenerate"));
12367
13029
  msgRegenBtn.onclick = (function (prompt) {
12368
13030
  return function (e) {
12369
13031
  if (e && e.stopPropagation) e.stopPropagation();
@@ -12622,7 +13284,7 @@ function RTE_Plugin_AIToolkit() {
12622
13284
  moreToggle.type = "button";
12623
13285
  moreToggle.setAttribute("data-rte-ai-chat-action", "more");
12624
13286
  moreToggle.setAttribute("aria-expanded", "false");
12625
- moreToggle.title = "More apply options";
13287
+ moreToggle.title = (obj.__aiT ? obj.__aiT("ai2_moreopts", "More apply options") : "More apply options");
12626
13288
  setReviewV2ButtonContent(moreToggle, "more", "More");
12627
13289
  moreToggle.onclick = (function (host) {
12628
13290
  return function (e) {
@@ -12711,8 +13373,8 @@ function RTE_Plugin_AIToolkit() {
12711
13373
  // 51 chars → 14 chars. Same shape Claude composer ("Ask
12712
13374
  // anything") / ChatGPT composer ("Message ChatGPT") / Notion AI
12713
13375
  // ("Tell AI what to do…") ship.
12714
- composerArea.placeholder = "Ask anything…";
12715
- composerArea.setAttribute("aria-label", "Compose next AI request for the current " + (context.scope === "selection" ? "selection" : "document"));
13376
+ composerArea.placeholder = (obj.__aiT ? obj.__aiT("aic_askanything", "Ask anything\u2026") : "Ask anything\u2026");
13377
+ composerArea.setAttribute("aria-label", context.scope === "selection" ? (obj.__aiT ? obj.__aiT("aic_compose_sel", "Compose next AI request for the current selection") : "Compose next AI request for the current selection") : (obj.__aiT ? obj.__aiT("aic_compose_doc", "Compose next AI request for the current document") : "Compose next AI request for the current document"));
12716
13378
  composerArea.value = state.draft || "";
12717
13379
  composerArea.disabled = !!state.busy;
12718
13380
  composerArea.oninput = function () {
@@ -12839,7 +13501,7 @@ function RTE_Plugin_AIToolkit() {
12839
13501
  }
12840
13502
 
12841
13503
  function getReviewTypeOptions(suggestions) {
12842
- var map = { all: "All types" };
13504
+ var map = { all: (obj.__aiT ? obj.__aiT("air_alltypes", "All types") : "All types") };
12843
13505
  for (var i = 0; i < suggestions.length; i++) {
12844
13506
  var typeValue = getSuggestionTypeValue(suggestions[i] && suggestions[i].suggestionType || "");
12845
13507
  if (!map[typeValue]) {
@@ -13782,7 +14444,7 @@ function RTE_Plugin_AIToolkit() {
13782
14444
  if (summaryData.detail) {
13783
14445
  parts.push(summaryData.detail);
13784
14446
  }
13785
- return parts.join(". ");
14447
+ return joinSentences(parts);
13786
14448
  }
13787
14449
 
13788
14450
  function syncActiveReviewActionSummary(preferredAction) {
@@ -14005,10 +14667,10 @@ function RTE_Plugin_AIToolkit() {
14005
14667
  if (isTarget) {
14006
14668
  button.setAttribute("data-rte-ai-review-enter-target-label", "Enter");
14007
14669
  if (ariaLabel && !/current enter target/i.test(ariaLabel)) {
14008
- button.setAttribute("aria-label", ariaLabel + ". Current Enter target.");
14670
+ button.setAttribute("aria-label", joinSentences([ariaLabel, "Current Enter target."]));
14009
14671
  }
14010
14672
  if (title && !/current enter target/i.test(title)) {
14011
- button.title = title + ". Current Enter target.";
14673
+ button.title = joinSentences([title, "Current Enter target."]);
14012
14674
  }
14013
14675
  }
14014
14676
  else {
@@ -14793,8 +15455,8 @@ function RTE_Plugin_AIToolkit() {
14793
15455
  // header pill would have duplicated that signal.
14794
15456
  var closeBtn = append(header, "button", "", "rte-ai-review-v2-close");
14795
15457
  closeBtn.type = "button";
14796
- closeBtn.setAttribute("aria-label", "Close AI review");
14797
- closeBtn.title = "Close";
15458
+ closeBtn.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("air_closearia", "Close AI review") : "Close AI review"));
15459
+ closeBtn.title = (obj.__aiT ? obj.__aiT("air_close", "Close") : "Close");
14798
15460
  closeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6l12 12"/><path d="M18 6L6 18"/></svg>';
14799
15461
  closeBtn.onclick = function () {
14800
15462
  closeReviewPanel();
@@ -14840,9 +15502,9 @@ function RTE_Plugin_AIToolkit() {
14840
15502
  return tab;
14841
15503
  }
14842
15504
 
14843
- makeTab("pending", "inbox", "To review", pendingCountForTabs);
14844
- makeTab("resolved", "history", "History", resolvedCountForTabs);
14845
- makeTab("all", "filter", "All", counts.total);
15505
+ makeTab("pending", "inbox", (obj.__aiT ? obj.__aiT("air_tab_pending", "To review") : "To review"), pendingCountForTabs);
15506
+ makeTab("resolved", "history", (obj.__aiT ? obj.__aiT("air_tab_history", "History") : "History"), resolvedCountForTabs);
15507
+ makeTab("all", "filter", (obj.__aiT ? obj.__aiT("air_tab_all", "All") : "All"), counts.total);
14846
15508
 
14847
15509
  // Toolbar: type filter + (on Pending tab only) Accept all / Reject all
14848
15510
  var toolbar = append(panel, "div", "", "rte-ai-review-v2-toolbar");
@@ -14872,14 +15534,14 @@ function RTE_Plugin_AIToolkit() {
14872
15534
  var acceptAllBtn = append(toolbar, "button", "", "rte-ai-review-v2-batch is-accept");
14873
15535
  acceptAllBtn.type = "button";
14874
15536
  acceptAllBtn.disabled = !pendingForBatch;
14875
- setReviewV2ButtonContent(acceptAllBtn, "checkAll", "Accept all");
15537
+ setReviewV2ButtonContent(acceptAllBtn, "checkAll", (obj.__aiT ? obj.__aiT("air_acceptall", "Accept all") : "Accept all"));
14876
15538
  acceptAllBtn.onclick = function () {
14877
15539
  if (acceptPendingSuggestionsByType(typeFilter)) renderReviewPanelV2(true);
14878
15540
  };
14879
15541
  var rejectAllBtn = append(toolbar, "button", "", "rte-ai-review-v2-batch is-reject");
14880
15542
  rejectAllBtn.type = "button";
14881
15543
  rejectAllBtn.disabled = !pendingForBatch;
14882
- setReviewV2ButtonContent(rejectAllBtn, "x", "Reject all");
15544
+ setReviewV2ButtonContent(rejectAllBtn, "x", (obj.__aiT ? obj.__aiT("air_rejectall", "Reject all") : "Reject all"));
14883
15545
  rejectAllBtn.onclick = function () {
14884
15546
  if (rejectPendingSuggestionsByType(typeFilter)) renderReviewPanelV2(true);
14885
15547
  };
@@ -14923,11 +15585,11 @@ function RTE_Plugin_AIToolkit() {
14923
15585
  }
14924
15586
  var activeV2Suggestion = findSuggestionById(activeId);
14925
15587
  var resolvedProgress = counts.total ? Math.max(0, Math.min(100, Math.round((resolvedCountForTabs / counts.total) * 100))) : 0;
14926
- var queueScopeLabel = typeFilter === "all" ? "All types" : getSuggestionTypeLabel(typeFilter);
15588
+ var queueScopeLabel = typeFilter === "all" ? (obj.__aiT ? obj.__aiT("air_alltypes", "All types") : "All types") : getSuggestionTypeLabel(typeFilter);
14927
15589
  var v2Center = append(panel, "div", "", "rte-ai-review-v2-center");
14928
15590
  panel.insertBefore(v2Center, body);
14929
15591
  v2Center.setAttribute("role", "group");
14930
- v2Center.setAttribute("aria-label", "AI review summary");
15592
+ v2Center.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("air_summary", "AI review summary") : "AI review summary"));
14931
15593
  function appendReviewV2CenterTile(className, label, value, detail) {
14932
15594
  var tile = append(v2Center, "div", "", "rte-ai-review-v2-center-tile " + className);
14933
15595
  append(tile, "span", "", "rte-ai-review-v2-center-label", label);
@@ -14935,19 +15597,19 @@ function RTE_Plugin_AIToolkit() {
14935
15597
  append(tile, "span", "", "rte-ai-review-v2-center-detail", detail);
14936
15598
  return tile;
14937
15599
  }
14938
- appendReviewV2CenterTile("is-queue", "Queue", pendingForBatch + " pending", queueScopeLabel + (activeTab === "pending" ? " to review" : " filter"));
14939
- appendReviewV2CenterTile("is-progress", "Progress", resolvedProgress + "% done", resolvedCountForTabs + " resolved of " + counts.total);
15600
+ appendReviewV2CenterTile("is-queue", (obj.__aiT ? obj.__aiT("air_queue", "Queue") : "Queue"), (obj.__aiT ? obj.__aiT("air_npending", "{0} pending", pendingForBatch) : pendingForBatch + " pending"), activeTab === "pending" ? (obj.__aiT ? obj.__aiT("air_toreview", "{0} to review", queueScopeLabel) : queueScopeLabel + " to review") : (obj.__aiT ? obj.__aiT("air_filter", "{0} filter", queueScopeLabel) : queueScopeLabel + " filter"));
15601
+ appendReviewV2CenterTile("is-progress", (obj.__aiT ? obj.__aiT("air_progress", "Progress") : "Progress"), (obj.__aiT ? obj.__aiT("air_pctdone", "{0}% done", resolvedProgress) : resolvedProgress + "% done"), (obj.__aiT ? obj.__aiT("air_resolvedof", "{0} resolved of {1}").split("{0}").join(String(resolvedCountForTabs)).split("{1}").join(String(counts.total)) : resolvedCountForTabs + " resolved of " + counts.total));
14940
15602
  var nextV2Value = activeV2Suggestion
14941
- ? ((activeV2Suggestion.status === "pending" || activeV2Suggestion.status === "stale") ? "Review current" : "Inspect item")
14942
- : (pendingForBatch ? "Pick an item" : (counts.total ? "All caught up" : "Start with Ask AI"));
15603
+ ? ((activeV2Suggestion.status === "pending" || activeV2Suggestion.status === "stale") ? (obj.__aiT ? obj.__aiT("air_reviewcurrent", "Review current") : "Review current") : (obj.__aiT ? obj.__aiT("air_inspect", "Inspect item") : "Inspect item"))
15604
+ : (pendingForBatch ? (obj.__aiT ? obj.__aiT("air_pick", "Pick an item") : "Pick an item") : (counts.total ? (obj.__aiT ? obj.__aiT("air_caughtup", "All caught up") : "All caught up") : (obj.__aiT ? obj.__aiT("air_startask", "Start with Ask AI") : "Start with Ask AI")));
14943
15605
  var nextV2Detail = activeV2Suggestion
14944
15606
  ? summarizeSuggestionText(activeV2Suggestion.originalText || activeV2Suggestion.resultText || "AI suggestion", 72)
14945
- : (pendingForBatch ? "Select a card below to compare the change." : (counts.total ? "History keeps accepted and rejected changes." : "AI suggestions will appear here."));
14946
- var nextV2Tile = appendReviewV2CenterTile("is-next", "Next step", nextV2Value, nextV2Detail);
15607
+ : (pendingForBatch ? (obj.__aiT ? obj.__aiT("air_d_pick", "Select a card below to compare the change.") : "Select a card below to compare the change.") : (counts.total ? (obj.__aiT ? obj.__aiT("air_d_history", "History keeps accepted and rejected changes.") : "History keeps accepted and rejected changes.") : (obj.__aiT ? obj.__aiT("air_d_empty", "AI suggestions will appear here.") : "AI suggestions will appear here.")));
15608
+ var nextV2Tile = appendReviewV2CenterTile("is-next", (obj.__aiT ? obj.__aiT("air_next", "Next step") : "Next step"), nextV2Value, nextV2Detail);
14947
15609
  if (activeV2Suggestion) {
14948
- var focusV2Button = append(nextV2Tile, "button", "", "rte-ai-review-v2-center-action", "Focus");
15610
+ var focusV2Button = append(nextV2Tile, "button", "", "rte-ai-review-v2-center-action", (obj.__aiT ? obj.__aiT("air_focus", "Focus") : "Focus"));
14949
15611
  focusV2Button.type = "button";
14950
- focusV2Button.setAttribute("aria-label", "Focus the current AI review item");
15612
+ focusV2Button.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("air_focusaria", "Focus the current AI review item") : "Focus the current AI review item"));
14951
15613
  focusV2Button.onclick = function () {
14952
15614
  setActiveSuggestionId(activeId);
14953
15615
  renderReviewPanelV2(true);
@@ -14959,19 +15621,19 @@ function RTE_Plugin_AIToolkit() {
14959
15621
  if (counts.total === 0) {
14960
15622
  // Truly empty store — guide the user to AI Chat / Ask AI.
14961
15623
  append(empty, "div", "", "rte-ai-review-v2-empty-art is-prompt");
14962
- append(empty, "div", "", "rte-ai-review-v2-empty-title", "No AI suggestions yet");
14963
- append(empty, "div", "", "rte-ai-review-v2-empty-detail", "Ask AI or AI Chat will drop suggestions here for you to accept or reject.");
15624
+ append(empty, "div", "", "rte-ai-review-v2-empty-title", (obj.__aiT ? obj.__aiT("air_e_none_t", "No AI suggestions yet") : "No AI suggestions yet"));
15625
+ append(empty, "div", "", "rte-ai-review-v2-empty-detail", (obj.__aiT ? obj.__aiT("air_e_none_d", "Ask AI or AI Chat will drop suggestions here for you to accept or reject.") : "Ask AI or AI Chat will drop suggestions here for you to accept or reject."));
14964
15626
  } else if (activeTab === "pending") {
14965
15627
  // History exists but Pending is empty — celebrate.
14966
15628
  append(empty, "div", "", "rte-ai-review-v2-empty-art is-clear");
14967
- append(empty, "div", "", "rte-ai-review-v2-empty-title", "Nothing to review");
14968
- append(empty, "div", "", "rte-ai-review-v2-empty-detail", "You're all caught up. Switch to History to see past decisions.");
15629
+ append(empty, "div", "", "rte-ai-review-v2-empty-title", (obj.__aiT ? obj.__aiT("air_e_done_t", "Nothing to review") : "Nothing to review"));
15630
+ append(empty, "div", "", "rte-ai-review-v2-empty-detail", (obj.__aiT ? obj.__aiT("air_e_done_d", "You're all caught up. Switch to History to see past decisions.") : "You're all caught up. Switch to History to see past decisions."));
14969
15631
  } else if (activeTab === "resolved") {
14970
- append(empty, "div", "", "rte-ai-review-v2-empty-title", "No history yet");
14971
- append(empty, "div", "", "rte-ai-review-v2-empty-detail", "Decisions you make on the To review tab will show up here.");
15632
+ append(empty, "div", "", "rte-ai-review-v2-empty-title", (obj.__aiT ? obj.__aiT("air_e_nohist_t", "No history yet") : "No history yet"));
15633
+ append(empty, "div", "", "rte-ai-review-v2-empty-detail", (obj.__aiT ? obj.__aiT("air_e_nohist_d", "Decisions you make on the To review tab will show up here.") : "Decisions you make on the To review tab will show up here."));
14972
15634
  } else {
14973
- append(empty, "div", "", "rte-ai-review-v2-empty-title", "No suggestions match this filter");
14974
- append(empty, "div", "", "rte-ai-review-v2-empty-detail", "Try a different type, or pick a different tab.");
15635
+ append(empty, "div", "", "rte-ai-review-v2-empty-title", (obj.__aiT ? obj.__aiT("air_e_nomatch_t", "No suggestions match this filter") : "No suggestions match this filter"));
15636
+ append(empty, "div", "", "rte-ai-review-v2-empty-detail", (obj.__aiT ? obj.__aiT("air_e_nomatch_d", "Try a different type, or pick a different tab.") : "Try a different type, or pick a different tab."));
14975
15637
  }
14976
15638
  } else {
14977
15639
  for (var vi = 0; vi < visible.length; vi++) {
@@ -14986,9 +15648,9 @@ function RTE_Plugin_AIToolkit() {
14986
15648
  append(head, "span", "", "rte-ai-review-v2-item-type", getSuggestionTypeLabel(sug.suggestionType));
14987
15649
  }
14988
15650
  if (sug.snapshot && sug.snapshot.hasSelection) {
14989
- append(head, "span", "", "rte-ai-review-v2-item-scope", "Selection");
15651
+ append(head, "span", "", "rte-ai-review-v2-item-scope", (obj.__aiT ? obj.__aiT("air_scope_sel", "Selection") : "Selection"));
14990
15652
  } else {
14991
- append(head, "span", "", "rte-ai-review-v2-item-scope", "Document");
15653
+ append(head, "span", "", "rte-ai-review-v2-item-scope", (obj.__aiT ? obj.__aiT("air_scope_doc", "Document") : "Document"));
14992
15654
  }
14993
15655
 
14994
15656
  var titleText = summarizeSuggestionText(sug.originalText || sug.resultText || "AI suggestion", 120);
@@ -14998,7 +15660,7 @@ function RTE_Plugin_AIToolkit() {
14998
15660
  if (sug.originalText && sug.resultText && normalizeText(sug.originalText) !== normalizeText(sug.resultText)) {
14999
15661
  var compare = append(item, "div", "", "rte-ai-review-v2-compare");
15000
15662
  var before = append(compare, "div", "", "rte-ai-review-v2-before");
15001
- append(before, "span", "", "rte-ai-review-v2-compare-label", "Current");
15663
+ append(before, "span", "", "rte-ai-review-v2-compare-label", (obj.__aiT ? obj.__aiT("air_current", "Current") : "Current"));
15002
15664
  append(before, "div", "", "rte-ai-review-v2-compare-text", normalizeText(sug.originalText));
15003
15665
  // Decorative arrow on the centerline of the
15004
15666
  // grid pointing from Current -> Suggested.
@@ -15006,7 +15668,7 @@ function RTE_Plugin_AIToolkit() {
15006
15668
  compareArrow.setAttribute("aria-hidden", "true");
15007
15669
  compareArrow.innerHTML = REVIEW_V2_ICONS.arrowRight;
15008
15670
  var after = append(compare, "div", "", "rte-ai-review-v2-after");
15009
- append(after, "span", "", "rte-ai-review-v2-compare-label", "Suggested");
15671
+ append(after, "span", "", "rte-ai-review-v2-compare-label", (obj.__aiT ? obj.__aiT("air_suggested", "Suggested") : "Suggested"));
15010
15672
  append(after, "div", "", "rte-ai-review-v2-compare-text", normalizeText(sug.resultText));
15011
15673
  }
15012
15674
  if (sug.reason) {
@@ -15018,14 +15680,14 @@ function RTE_Plugin_AIToolkit() {
15018
15680
  bulbWrap.innerHTML = REVIEW_V2_ICONS.bulb;
15019
15681
  reasonLabel.appendChild(bulbWrap);
15020
15682
  var reasonLabelText = reason.ownerDocument.createElement("span");
15021
- reasonLabelText.textContent = "Why";
15683
+ reasonLabelText.textContent = (obj.__aiT ? obj.__aiT("air_why", "Why") : "Why");
15022
15684
  reasonLabel.appendChild(reasonLabelText);
15023
15685
  append(reason, "span", "", "rte-ai-review-v2-reason-text", sug.reason);
15024
15686
  }
15025
15687
  var actions = append(item, "div", "", "rte-ai-review-v2-actions");
15026
15688
  var locateBtn = append(actions, "button", "", "rte-ai-review-v2-action is-locate");
15027
15689
  locateBtn.type = "button";
15028
- setReviewV2ButtonContent(locateBtn, "target", "Locate");
15690
+ setReviewV2ButtonContent(locateBtn, "target", (obj.__aiT ? obj.__aiT("air_locate", "Locate") : "Locate"));
15029
15691
  locateBtn.onclick = function (e) {
15030
15692
  if (e && e.stopPropagation) e.stopPropagation();
15031
15693
  locateSuggestion(sug.id);
@@ -15033,14 +15695,14 @@ function RTE_Plugin_AIToolkit() {
15033
15695
  if (sug.status === "pending" || sug.status === "stale") {
15034
15696
  var acceptBtn = append(actions, "button", "", "rte-ai-review-v2-action is-accept");
15035
15697
  acceptBtn.type = "button";
15036
- setReviewV2ButtonContent(acceptBtn, "check", "Accept");
15698
+ setReviewV2ButtonContent(acceptBtn, "check", (obj.__aiT ? obj.__aiT("air_accept", "Accept") : "Accept"));
15037
15699
  acceptBtn.onclick = function (e) {
15038
15700
  if (e && e.stopPropagation) e.stopPropagation();
15039
15701
  applyReviewDecision(sug.id, "accept", { focusAction: "accept" });
15040
15702
  };
15041
15703
  var rejectBtn = append(actions, "button", "", "rte-ai-review-v2-action is-reject");
15042
15704
  rejectBtn.type = "button";
15043
- setReviewV2ButtonContent(rejectBtn, "x", "Reject");
15705
+ setReviewV2ButtonContent(rejectBtn, "x", (obj.__aiT ? obj.__aiT("air_reject", "Reject") : "Reject"));
15044
15706
  rejectBtn.onclick = function (e) {
15045
15707
  if (e && e.stopPropagation) e.stopPropagation();
15046
15708
  applyReviewDecision(sug.id, "reject", { focusAction: "reject" });
@@ -15903,7 +16565,7 @@ function RTE_Plugin_AIToolkit() {
15903
16565
  if (previewItem.reasonText) {
15904
16566
  previewAriaParts.push("Why this next: " + previewItem.reasonText);
15905
16567
  }
15906
- previewRow.setAttribute("aria-label", previewAriaParts.join(". "));
16568
+ previewRow.setAttribute("aria-label", joinSentences(previewAriaParts));
15907
16569
  previewRow.setAttribute("data-rte-ai-primary-label", previewItem.actionLabel || previewItem.label || "Open next queue");
15908
16570
  if (previewItem.focusActionLabel) {
15909
16571
  previewRow.setAttribute("data-rte-ai-empty-focus-label", previewItem.focusActionLabel);
@@ -16433,6 +17095,7 @@ function RTE_Plugin_AIToolkit() {
16433
17095
  modeChip.className = "rte-ai-dialog-mode-chip is-detached";
16434
17096
  modeChip.setAttribute("aria-hidden", "true");
16435
17097
 
17098
+ appendDemoNotice(dialoginner);
16436
17099
  var grid = append(dialoginner, "div", "", "demo-ai-dialog-grid");
16437
17100
  var dialogIdSuffix = String(Date.now()) + "-" + String(Math.floor(Math.random() * 1000000));
16438
17101
  var modeHelpId = "rte-ai-mode-help-" + dialogIdSuffix;
@@ -16441,11 +17104,11 @@ function RTE_Plugin_AIToolkit() {
16441
17104
  var guidanceId = "rte-ai-apply-guidance-" + dialogIdSuffix;
16442
17105
  var statusId = "rte-ai-dialog-status-" + dialogIdSuffix;
16443
17106
  grid.setAttribute("role", "form");
16444
- grid.setAttribute("aria-label", "Ask AI controls");
17107
+ grid.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_controls", "Ask AI controls") : "Ask AI controls"));
16445
17108
 
16446
17109
  var compactControls = append(grid, "div", "", "demo-ai-compact-controls");
16447
17110
  compactControls.setAttribute("role", "group");
16448
- compactControls.setAttribute("aria-label", "Ask AI setup");
17111
+ compactControls.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_setup", "Ask AI setup") : "Ask AI setup"));
16449
17112
 
16450
17113
  // 2026-05-08 compact pass: dropped the per-field uppercase mini-labels
16451
17114
  // ("Action" / "Language" / "Use" / blank-spacer) and the duplicate
@@ -16454,7 +17117,7 @@ function RTE_Plugin_AIToolkit() {
16454
17117
  // select's title attribute (still surfaced on hover).
16455
17118
  var actionField = append(compactControls, "div", "", "demo-ai-field demo-ai-action-field");
16456
17119
  var modeSelect = append(actionField, "select");
16457
- modeSelect.setAttribute("aria-label", "AI action");
17120
+ modeSelect.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_action", "AI action") : "AI action"));
16458
17121
  // 2026-05-17 (v20260517a): the mode help line is back on-screen,
16459
17122
  // attached directly to the dialog grid (full-width row under the
16460
17123
  // controls strip). The prior hover-only `title` attribute meant
@@ -16481,7 +17144,7 @@ function RTE_Plugin_AIToolkit() {
16481
17144
 
16482
17145
  var languageField = append(compactControls, "div", "", "demo-ai-field demo-ai-language-field");
16483
17146
  var languageSelect = append(languageField, "select");
16484
- languageSelect.setAttribute("aria-label", "Translation target language");
17147
+ languageSelect.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_targetlang", "Translation target language") : "Translation target language"));
16485
17148
  populateDialogModes();
16486
17149
  if (options.presetMode) {
16487
17150
  // 2026-05-19 (v20260519a): only honor presetMode when it
@@ -16532,11 +17195,11 @@ function RTE_Plugin_AIToolkit() {
16532
17195
  // can override with `.demo-ai-scope-select { display: none }` +
16533
17196
  // `.demo-ai-scope-row.is-detached { display: flex !important }`.
16534
17197
  var scopeSelect = append(scopeField, "select", "", "demo-ai-scope-select");
16535
- scopeSelect.setAttribute("aria-label", "Source for Ask AI");
17198
+ scopeSelect.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_source", "Source for Ask AI") : "Source for Ask AI"));
16536
17199
  scopeSelect.setAttribute("data-rte-ai-dialog-action", "scope");
16537
- var scopeOptionSelection = append(scopeSelect, "option", "", "", "Selection");
17200
+ var scopeOptionSelection = append(scopeSelect, "option", "", "", (obj.__aiT ? obj.__aiT("aid_selection", "Selection") : "Selection"));
16538
17201
  scopeOptionSelection.value = "selection";
16539
- var scopeOptionDocument = append(scopeSelect, "option", "", "", "Document");
17202
+ var scopeOptionDocument = append(scopeSelect, "option", "", "", (obj.__aiT ? obj.__aiT("aid_document", "Document") : "Document"));
16540
17203
  scopeOptionDocument.value = "document";
16541
17204
 
16542
17205
  // 2026-06-01 (v20260601a): scope caption surfaced inline when only
@@ -16561,21 +17224,21 @@ function RTE_Plugin_AIToolkit() {
16561
17224
  var loadRow = scopeField.ownerDocument.createElement("div");
16562
17225
  loadRow.className = "demo-actions-row demo-ai-scope-row is-detached";
16563
17226
  loadRow.setAttribute("role", "group");
16564
- loadRow.setAttribute("aria-label", "Source scope");
17227
+ loadRow.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_scope", "Source scope") : "Source scope"));
16565
17228
  var loadSelectionButton = scopeField.ownerDocument.createElement("button");
16566
17229
  loadSelectionButton.type = "button";
16567
17230
  loadSelectionButton.className = "is-icon-only";
16568
17231
  loadSelectionButton.setAttribute("data-rte-ai-dialog-action", "load-selection");
16569
- loadSelectionButton.title = "Use selection";
16570
- loadSelectionButton.setAttribute("aria-label", "Use selection");
17232
+ loadSelectionButton.title = (obj.__aiT ? obj.__aiT("aid_usesel", "Use selection") : "Use selection");
17233
+ loadSelectionButton.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_usesel", "Use selection") : "Use selection"));
16571
17234
  setReviewV2ButtonContent(loadSelectionButton, "cursorText", "");
16572
17235
  loadRow.appendChild(loadSelectionButton);
16573
17236
  var loadDocumentButton = scopeField.ownerDocument.createElement("button");
16574
17237
  loadDocumentButton.type = "button";
16575
17238
  loadDocumentButton.className = "secondary is-icon-only";
16576
17239
  loadDocumentButton.setAttribute("data-rte-ai-dialog-action", "load-document");
16577
- loadDocumentButton.title = "Use document";
16578
- loadDocumentButton.setAttribute("aria-label", "Use document");
17240
+ loadDocumentButton.title = (obj.__aiT ? obj.__aiT("aid_usedoc", "Use document") : "Use document");
17241
+ loadDocumentButton.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_usedoc", "Use document") : "Use document"));
16579
17242
  setReviewV2ButtonContent(loadDocumentButton, "document", "");
16580
17243
  loadRow.appendChild(loadDocumentButton);
16581
17244
  // 2026-05-17 (v20260523a): append the legacy scope row to
@@ -16589,20 +17252,20 @@ function RTE_Plugin_AIToolkit() {
16589
17252
  var runField = append(compactControls, "div", "", "demo-ai-field demo-ai-run-field");
16590
17253
  var runRow = append(runField, "div", "", "demo-actions-row demo-ai-run-row");
16591
17254
  runRow.setAttribute("role", "group");
16592
- runRow.setAttribute("aria-label", "Ask AI run actions");
17255
+ runRow.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_run", "Ask AI run actions") : "Ask AI run actions"));
16593
17256
  var runButton = append(runRow, "button", "", "is-primary");
16594
17257
  runButton.type = "button";
16595
17258
  runButton.setAttribute("data-rte-ai-dialog-action", "run");
16596
17259
  // Sparkle glyph + "Generate" — same icon vocabulary as AI Chat
16597
17260
  // composer's Send and AI Review V2's Accept buttons.
16598
- setReviewV2ButtonContent(runButton, "sparkles", "Generate");
17261
+ setReviewV2ButtonContent(runButton, "sparkles", (obj.__aiT ? obj.__aiT("aid_generate", "Generate") : "Generate"));
16599
17262
  // 2026-05-09 (v20260509j): surface the Ctrl/Cmd+Enter shortcut on
16600
17263
  // hover and to screen readers. Same idiom every modern AI surface
16601
17264
  // uses (ChatGPT Send: "Send (⌘↵)", Notion AI Generate: "Generate
16602
17265
  // (Ctrl+Enter)"). aria-keyshortcuts is the WAI-ARIA standard way
16603
17266
  // to advertise a keyboard shortcut so screen readers announce
16604
17267
  // "Generate, Control+Enter" alongside the button name.
16605
- runButton.title = "Generate AI suggestion (Ctrl+Enter)";
17268
+ runButton.title = (obj.__aiT ? obj.__aiT("ai2_gentitle", "Generate AI suggestion (Ctrl+Enter)") : "Generate AI suggestion (Ctrl+Enter)");
16606
17269
  runButton.setAttribute("aria-keyshortcuts", "Control+Enter");
16607
17270
  // 2026-05-17 (v20260517a): visible Ctrl+↵ / ⌘↵ chip baked into
16608
17271
  // the Generate button. Pre-v20260517a the shortcut only lived in
@@ -16614,7 +17277,7 @@ function RTE_Plugin_AIToolkit() {
16614
17277
  // from navigator.platform so the hint matches the actual key
16615
17278
  // the user would press. */
16616
17279
  var isMacOs = /Mac|iPad|iPhone|iPod/i.test((navigator.platform || "") + " " + (navigator.userAgent || ""));
16617
- var runKbdLabel = isMacOs ? "⌘⏎" : "Ctrl ⏎";
17280
+ var runKbdLabel = isMacOs ? "\u2318\u23ce" : "Ctrl \u23ce";
16618
17281
  function refreshRunKbdHint() {
16619
17282
  if (!runButton) return;
16620
17283
  var existing = runButton.querySelector(".demo-ai-kbd-hint");
@@ -16664,7 +17327,7 @@ function RTE_Plugin_AIToolkit() {
16664
17327
 
16665
17328
  var textGrid = append(grid, "div", "", "demo-ai-text-grid");
16666
17329
  textGrid.setAttribute("role", "group");
16667
- textGrid.setAttribute("aria-label", "Ask AI source and result");
17330
+ textGrid.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_srcresult", "Ask AI source and result") : "Ask AI source and result"));
16668
17331
 
16669
17332
  // 2026-05-09 (v20260509j): dropped the "Source" / "Result"
16670
17333
  // text-tag spans. They had no CSS rule (the class was
@@ -16682,7 +17345,7 @@ function RTE_Plugin_AIToolkit() {
16682
17345
  // Pre-result it was already hidden (CSS .is-pre-result cascade); the
16683
17346
  // remaining showing case was post-result + source-expanded, but the
16684
17347
  // user only reaches that flow by clicking the "Edit" toggle pill —
16685
- // which already names the field's role ("Edit the prompt and retry").
17348
+ // which already names the field's role ((obj.__aiT ? obj.__aiT("ai2_editretry", "Edit the prompt and retry") : "Edit the prompt and retry")).
16686
17349
  // Restating "Source" 4px above the textarea was a third copy of the
16687
17350
  // same fact (toggle label + textarea placeholder + this eyebrow).
16688
17351
  // Saves ~16px vertical when the user expands source for retry/refine.
@@ -16692,7 +17355,7 @@ function RTE_Plugin_AIToolkit() {
16692
17355
  var sourceHelp = append(sourceField, "span", "", "demo-ai-field-help is-detached", "Edit the source text or prompt that Ask AI will use.");
16693
17356
  sourceHelp.id = sourceHelpId;
16694
17357
  var sourceArea = append(sourceField, "textarea");
16695
- sourceArea.setAttribute("aria-label", "Source text for Ask AI");
17358
+ sourceArea.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_srctext", "Source text for Ask AI") : "Source text for Ask AI"));
16696
17359
  sourceArea.setAttribute("aria-describedby", sourceHelpId + " " + modeHelpId);
16697
17360
  // 2026-05-26 (v20260526a): reparent the run-row (Generate + Copy
16698
17361
  // buttons) from the compact-controls strip INTO the source field,
@@ -16743,7 +17406,7 @@ function RTE_Plugin_AIToolkit() {
16743
17406
  // verb phrase, no inline shortcut hint.
16744
17407
  // Earlier baseline:
16745
17408
  // v20260509j: 95 → 47 chars after the scope toggle landed.
16746
- sourceArea.placeholder = "Type or paste text…";
17409
+ sourceArea.placeholder = (obj.__aiT ? obj.__aiT("aid_typeorpaste", "Type or paste text\u2026") : "Type or paste text\u2026");
16747
17410
 
16748
17411
  // 2026-06-03 (v20260603a): live character counter pinned to the
16749
17412
  // bottom-LEFT corner of the source textarea. Pre-pass users
@@ -16777,8 +17440,8 @@ function RTE_Plugin_AIToolkit() {
16777
17440
  return;
16778
17441
  }
16779
17442
  var label = len > 999
16780
- ? (Math.round(len / 100) / 10) + "K chars"
16781
- : len + (len === 1 ? " char" : " chars");
17443
+ ? (obj.__aiT ? obj.__aiT("aid_kchars", "{0}K chars", Math.round(len / 100) / 10) : (Math.round(len / 100) / 10) + "K chars")
17444
+ : (len === 1 ? (obj.__aiT ? obj.__aiT("aid_char", "1 char") : "1 char") : (obj.__aiT ? obj.__aiT("aid_chars", "{0} chars", len) : len + " chars"));
16782
17445
  sourceCounter.textContent = label;
16783
17446
  sourceCounter.classList.add("is-visible");
16784
17447
  }
@@ -16832,9 +17495,9 @@ function RTE_Plugin_AIToolkit() {
16832
17495
  var copyClipboardButton = append(resultHead, "button", "", "demo-ai-clipboard-toggle");
16833
17496
  copyClipboardButton.type = "button";
16834
17497
  copyClipboardButton.setAttribute("data-rte-ai-dialog-action", "copy-clipboard");
16835
- copyClipboardButton.title = "Copy AI output to clipboard";
16836
- copyClipboardButton.setAttribute("aria-label", "Copy AI output to clipboard");
16837
- setReviewV2ButtonContent(copyClipboardButton, "clipboard", "Copy");
17498
+ copyClipboardButton.title = (obj.__aiT ? obj.__aiT("ai2_copyclip", "Copy AI output to clipboard") : "Copy AI output to clipboard");
17499
+ copyClipboardButton.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("ai2_copyclip", "Copy AI output to clipboard") : "Copy AI output to clipboard"));
17500
+ setReviewV2ButtonContent(copyClipboardButton, "clipboard", (obj.__aiT ? obj.__aiT("ai2_copy", "Copy") : "Copy"));
16838
17501
  var copyClipboardResetTimer = null;
16839
17502
  copyClipboardButton.onclick = function (e) {
16840
17503
  if (e && e.stopPropagation) e.stopPropagation();
@@ -16846,13 +17509,13 @@ function RTE_Plugin_AIToolkit() {
16846
17509
  return;
16847
17510
  }
16848
17511
  copyClipboardButton.classList.add("is-copied");
16849
- setReviewV2ButtonContent(copyClipboardButton, "check", "Copied");
17512
+ setReviewV2ButtonContent(copyClipboardButton, "check", (obj.__aiT ? obj.__aiT("aic_copied", "Copied") : "Copied"));
16850
17513
  if (copyClipboardResetTimer) {
16851
17514
  clearTimeout(copyClipboardResetTimer);
16852
17515
  }
16853
17516
  copyClipboardResetTimer = setTimeout(function () {
16854
17517
  copyClipboardButton.classList.remove("is-copied");
16855
- setReviewV2ButtonContent(copyClipboardButton, "clipboard", "Copy");
17518
+ setReviewV2ButtonContent(copyClipboardButton, "clipboard", (obj.__aiT ? obj.__aiT("ai2_copy", "Copy") : "Copy"));
16856
17519
  copyClipboardResetTimer = null;
16857
17520
  }, 1400);
16858
17521
  });
@@ -16870,8 +17533,8 @@ function RTE_Plugin_AIToolkit() {
16870
17533
  var whyButton = append(resultHead, "button", "", "demo-ai-clipboard-toggle demo-ai-why-toggle");
16871
17534
  whyButton.type = "button";
16872
17535
  whyButton.setAttribute("data-rte-ai-dialog-action", "why");
16873
- whyButton.title = "Why this AI suggestion?";
16874
- whyButton.setAttribute("aria-label", "Show why this AI suggestion was generated");
17536
+ whyButton.title = (obj.__aiT ? obj.__aiT("ai2_whytitle", "Why this AI suggestion?") : "Why this AI suggestion?");
17537
+ whyButton.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("ai2_whyaria", "Show why this AI suggestion was generated") : "Show why this AI suggestion was generated"));
16875
17538
  whyButton.setAttribute("aria-expanded", "false");
16876
17539
  setReviewV2ButtonContent(whyButton, "info", "");
16877
17540
 
@@ -16890,12 +17553,12 @@ function RTE_Plugin_AIToolkit() {
16890
17553
  // saying the same thing twice (open the prompt + edit the prompt).
16891
17554
  // "Edit" preserves the verb-action shape Pass v20260513b adopted,
16892
17555
  // saves ~30px horizontal real estate in the result-head pill row,
16893
- // and the title attribute ("Edit the prompt and retry") still
17556
+ // and the title attribute ((obj.__aiT ? obj.__aiT("ai2_editretry", "Edit the prompt and retry") : "Edit the prompt and retry")) still
16894
17557
  // surfaces the long form on hover + to screen readers via
16895
17558
  // aria-label.
16896
- sourceToggle.title = "Edit the prompt and retry";
16897
- sourceToggle.setAttribute("aria-label", "Edit the prompt and retry");
16898
- sourceToggle.innerText = "Edit";
17559
+ sourceToggle.title = (obj.__aiT ? obj.__aiT("ai2_editretry", "Edit the prompt and retry") : "Edit the prompt and retry");
17560
+ sourceToggle.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("ai2_editretry", "Edit the prompt and retry") : "Edit the prompt and retry"));
17561
+ sourceToggle.innerText = (obj.__aiT ? obj.__aiT("ai2_edit", "Edit") : "Edit");
16899
17562
  sourceField.id = sourceToggle.getAttribute("aria-controls");
16900
17563
  sourceToggle.onclick = function (e) {
16901
17564
  if (e && e.stopPropagation) e.stopPropagation();
@@ -16905,7 +17568,7 @@ function RTE_Plugin_AIToolkit() {
16905
17568
  };
16906
17569
  var resultArea = append(resultField, "textarea");
16907
17570
  resultArea.readOnly = true;
16908
- resultArea.setAttribute("aria-label", "AI suggestion result");
17571
+ resultArea.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("ai2_result", "AI suggestion result") : "AI suggestion result"));
16909
17572
  resultArea.setAttribute("aria-describedby", resultHelpId + " " + statusId);
16910
17573
  // 2026-05-17 (v20260520b): rows=1 lets the CSS min-height: 52px
16911
17574
  // floor (since v20260513a) actually bind. Browser default rows=2
@@ -16921,7 +17584,7 @@ function RTE_Plugin_AIToolkit() {
16921
17584
  // here." (20 chars). Same idiom Notion AI's inline composer
16922
17585
  // uses ("AI response..."). At 20 chars it never line-wraps
16923
17586
  // inside the textarea even at 320px viewport.
16924
- resultArea.placeholder = "Output appears here.";
17587
+ resultArea.placeholder = (obj.__aiT ? obj.__aiT("ai2_outputph", "Output appears here.") : "Output appears here.");
16925
17588
 
16926
17589
  var oldPreview = document.createElement("pre");
16927
17590
  var newPreview = document.createElement("pre");
@@ -16970,19 +17633,19 @@ function RTE_Plugin_AIToolkit() {
16970
17633
  // setBusyState / focus-traversal walker keeps resolving.
16971
17634
  var refineRetryChip = append(refineRow, "button", "", "secondary demo-ai-refine-chip");
16972
17635
  refineRetryChip.type = "button";
16973
- refineRetryChip.title = "Re-run with the same prompt";
17636
+ refineRetryChip.title = (obj.__aiT ? obj.__aiT("ai2_retrytitle", "Re-run with the same prompt") : "Re-run with the same prompt");
16974
17637
  refineRetryChip.setAttribute("data-rte-ai-dialog-action", "refine-retry");
16975
- setReviewV2ButtonContent(refineRetryChip, "refresh", "Try again");
17638
+ setReviewV2ButtonContent(refineRetryChip, "refresh", (obj.__aiT ? obj.__aiT("ai2_tryagain", "Try again") : "Try again"));
16976
17639
  var refineShorterChip = append(refineRow, "button", "", "secondary demo-ai-refine-chip");
16977
17640
  refineShorterChip.type = "button";
16978
- refineShorterChip.title = "Make the suggestion shorter";
17641
+ refineShorterChip.title = (obj.__aiT ? obj.__aiT("ai2_shortertitle", "Make the suggestion shorter") : "Make the suggestion shorter");
16979
17642
  refineShorterChip.setAttribute("data-rte-ai-dialog-action", "refine-shorter");
16980
- setReviewV2ButtonContent(refineShorterChip, "shrink", "Shorter");
17643
+ setReviewV2ButtonContent(refineShorterChip, "shrink", (obj.__aiT ? obj.__aiT("ai2_shorter", "Shorter") : "Shorter"));
16981
17644
  var refineLongerChip = append(refineRow, "button", "", "secondary demo-ai-refine-chip");
16982
17645
  refineLongerChip.type = "button";
16983
- refineLongerChip.title = "Expand the suggestion with more detail";
17646
+ refineLongerChip.title = (obj.__aiT ? obj.__aiT("ai2_longertitle", "Expand the suggestion with more detail") : "Expand the suggestion with more detail");
16984
17647
  refineLongerChip.setAttribute("data-rte-ai-dialog-action", "refine-longer");
16985
- setReviewV2ButtonContent(refineLongerChip, "expand", "Longer");
17648
+ setReviewV2ButtonContent(refineLongerChip, "expand", (obj.__aiT ? obj.__aiT("ai2_longer", "Longer") : "Longer"));
16986
17649
  // 2026-05-29 (v20260529a): "Why" disclosure folded into the refine
16987
17650
  // chip row as a 4th ghost chip — the standalone Details toggle row
16988
17651
  // that previously sat on its own line between the chip strip and
@@ -17029,7 +17692,7 @@ function RTE_Plugin_AIToolkit() {
17029
17692
  refineWhyChip.type = "button";
17030
17693
  refineWhyChip.className = "secondary demo-ai-refine-chip demo-ai-refine-why-chip is-detached";
17031
17694
  refineWhyChip.innerText = "Why";
17032
- refineWhyChip.title = "Show why this AI suggestion was generated";
17695
+ refineWhyChip.title = (obj.__aiT ? obj.__aiT("ai2_whyaria", "Show why this AI suggestion was generated") : "Show why this AI suggestion was generated");
17033
17696
  refineWhyChip.setAttribute("data-rte-ai-dialog-action", "refine-why");
17034
17697
  refineWhyChip.setAttribute("aria-expanded", "false");
17035
17698
 
@@ -17102,7 +17765,7 @@ function RTE_Plugin_AIToolkit() {
17102
17765
 
17103
17766
  var applyRow = append(grid, "div", "", "demo-actions-row demo-ai-apply-row");
17104
17767
  applyRow.setAttribute("role", "group");
17105
- applyRow.setAttribute("aria-label", "Ask AI apply actions");
17768
+ applyRow.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("ai2_applyrow", "Ask AI apply actions") : "Ask AI apply actions"));
17106
17769
  var applyPlanButton = append(applyRow, "button", "", "is-primary");
17107
17770
  applyPlanButton.type = "button";
17108
17771
  applyPlanButton.disabled = true;
@@ -17114,34 +17777,34 @@ function RTE_Plugin_AIToolkit() {
17114
17777
  // dialog-level keydown handler (added below) fires this button
17115
17778
  // when the shortcut hits from anywhere in the dialog outside the
17116
17779
  // source textarea.
17117
- applyPlanButton.title = "Apply AI suggestion (Ctrl+Enter)";
17780
+ applyPlanButton.title = (obj.__aiT ? obj.__aiT("ai2_applytitle", "Apply AI suggestion (Ctrl+Enter)") : "Apply AI suggestion (Ctrl+Enter)");
17118
17781
  applyPlanButton.setAttribute("aria-keyshortcuts", "Control+Enter");
17119
- setReviewV2ButtonContent(applyPlanButton, "check", "Apply");
17782
+ setReviewV2ButtonContent(applyPlanButton, "check", (obj.__aiT ? obj.__aiT("ai2_apply", "Apply") : "Apply"));
17120
17783
  var acceptSelectionButton = append(applyRow, "button", "", "");
17121
17784
  acceptSelectionButton.type = "button";
17122
17785
  acceptSelectionButton.disabled = !snapshot.hasSelection;
17123
17786
  acceptSelectionButton.setAttribute("data-rte-ai-dialog-action", "selection");
17124
- setReviewV2ButtonContent(acceptSelectionButton, "check", "Replace selection");
17787
+ setReviewV2ButtonContent(acceptSelectionButton, "check", (obj.__aiT ? obj.__aiT("ai2_replsel", "Replace selection") : "Replace selection"));
17125
17788
  var previewSelectionButton = append(applyRow, "button", "", "secondary");
17126
17789
  previewSelectionButton.type = "button";
17127
17790
  previewSelectionButton.disabled = !snapshot.hasSelection;
17128
17791
  previewSelectionButton.setAttribute("data-rte-ai-dialog-action", "preview");
17129
- setReviewV2ButtonContent(previewSelectionButton, "eyeOn", "Preview");
17792
+ setReviewV2ButtonContent(previewSelectionButton, "eyeOn", (obj.__aiT ? obj.__aiT("ai2_preview", "Preview") : "Preview"));
17130
17793
  var acceptBelowButton = append(applyRow, "button", "", "secondary");
17131
17794
  acceptBelowButton.type = "button";
17132
17795
  acceptBelowButton.setAttribute("data-rte-ai-dialog-action", "insert");
17133
- setReviewV2ButtonContent(acceptBelowButton, "arrowRight", "Insert below");
17796
+ setReviewV2ButtonContent(acceptBelowButton, "arrowRight", (obj.__aiT ? obj.__aiT("ai2_insbelow", "Insert below") : "Insert below"));
17134
17797
  var acceptDocumentButton = append(applyRow, "button", "", "secondary");
17135
17798
  acceptDocumentButton.type = "button";
17136
17799
  acceptDocumentButton.setAttribute("data-rte-ai-dialog-action", "document");
17137
- // 2026-05-20 (v20260520h): "Replace doc" → "Replace document". The
17800
+ // 2026-05-20 (v20260520h): "Replace doc" → (obj.__aiT ? obj.__aiT("ai2_repldoc", "Replace document") : "Replace document"). The
17138
17801
  // abbreviation read as developer shorthand; the full word costs ~28px
17139
17802
  // of horizontal real estate at the apply row, but this button only
17140
17803
  // appears behind the ⋯ More toggle so the row's spare space is
17141
17804
  // available. Vocabulary now matches getOperationPlanButtonLabel's
17142
- // primary path ("Replace document") and the chat panel's secondary
17143
- // button ("Replace document").
17144
- setReviewV2ButtonContent(acceptDocumentButton, "checkAll", "Replace document");
17805
+ // primary path ((obj.__aiT ? obj.__aiT("ai2_repldoc", "Replace document") : "Replace document")) and the chat panel's secondary
17806
+ // button ((obj.__aiT ? obj.__aiT("ai2_repldoc", "Replace document") : "Replace document")).
17807
+ setReviewV2ButtonContent(acceptDocumentButton, "checkAll", (obj.__aiT ? obj.__aiT("ai2_repldoc", "Replace document") : "Replace document"));
17145
17808
  // 2026-05-08 (v20260508t): "More" toggle pinned just before Discard.
17146
17809
  // Hides Replace selection / Preview / Insert below / Replace doc by
17147
17810
  // default so the default surface only shows Apply (smart primary) +
@@ -17165,8 +17828,8 @@ function RTE_Plugin_AIToolkit() {
17165
17828
  applyRowMoreToggle.type = "button";
17166
17829
  applyRowMoreToggle.setAttribute("data-rte-ai-dialog-action", "more");
17167
17830
  applyRowMoreToggle.setAttribute("aria-expanded", "false");
17168
- applyRowMoreToggle.title = "More apply options";
17169
- applyRowMoreToggle.setAttribute("aria-label", "More apply options");
17831
+ applyRowMoreToggle.title = (obj.__aiT ? obj.__aiT("ai2_moreopts", "More apply options") : "More apply options");
17832
+ applyRowMoreToggle.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("ai2_moreopts", "More apply options") : "More apply options"));
17170
17833
  setReviewV2ButtonContent(applyRowMoreToggle, "more", "");
17171
17834
  applyRowMoreToggle.onclick = function (e) {
17172
17835
  if (e && e.stopPropagation) e.stopPropagation();
@@ -17225,7 +17888,7 @@ function RTE_Plugin_AIToolkit() {
17225
17888
  applyGuidance.className = "demo-ai-apply-guidance";
17226
17889
  applyGuidance.style.display = "none";
17227
17890
  applyGuidance.setAttribute("role", "group");
17228
- applyGuidance.setAttribute("aria-label", "Best next step");
17891
+ applyGuidance.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_bestnext", "Best next step") : "Best next step"));
17229
17892
  var applyGuidanceHeader = append(applyGuidance, "div", "", "demo-ai-apply-guidance-header");
17230
17893
  var applyGuidanceTitle = append(applyGuidanceHeader, "span", "", "demo-ai-apply-guidance-title", "Next");
17231
17894
  var applyGuidanceBadge = append(applyGuidanceHeader, "span", "", "demo-ai-apply-guidance-badge");
@@ -17405,7 +18068,7 @@ function RTE_Plugin_AIToolkit() {
17405
18068
  var languages = getTranslateLanguages();
17406
18069
  languageSelect.innerHTML = "";
17407
18070
  for (var index = 0; index < languages.length; index++) {
17408
- var option = append(languageSelect, "option", "", "", languages[index].label || languages[index].value);
18071
+ var option = append(languageSelect, "option", "", "", getTranslateLanguageDisplay(languages[index]));
17409
18072
  option.value = languages[index].value || languages[index].label;
17410
18073
  }
17411
18074
  if (options.presetLanguage) {
@@ -17481,7 +18144,7 @@ function RTE_Plugin_AIToolkit() {
17481
18144
  else {
17482
18145
  scopeLabel = "Document";
17483
18146
  }
17484
- return scopeLabel + " · " + countLabel + " chars";
18147
+ return scopeLabel + " \xb7 " + countLabel + " chars";
17485
18148
  }
17486
18149
 
17487
18150
  // 2026-05-19 (v20260519a): mirror the active AI mode into the
@@ -17588,7 +18251,7 @@ function RTE_Plugin_AIToolkit() {
17588
18251
  }
17589
18252
  titleSpan.innerText = resolvedLabel || base;
17590
18253
  if (dialoginner.setAttribute) {
17591
- dialoginner.setAttribute("aria-label", resolvedLabel ? (base + " · " + resolvedLabel) : base);
18254
+ dialoginner.setAttribute("aria-label", resolvedLabel ? (base + " \xb7 " + resolvedLabel) : base);
17592
18255
  }
17593
18256
  }
17594
18257
 
@@ -17674,16 +18337,16 @@ function RTE_Plugin_AIToolkit() {
17674
18337
  if (sourceArea && !sourceArea.value) {
17675
18338
  var modeId = modeSelect.value || "";
17676
18339
  var placeholders = {
17677
- proofread: "Paste text to proofread…",
17678
- rewrite: "Paste text to rewrite…",
17679
- shorten: "Paste text to shorten…",
17680
- expand: "Paste text to expand…",
17681
- summarize: "Paste text to summarize…",
17682
- translate: "Paste text to translate…",
17683
- justify: "Paste text to rewrite + explain…",
17684
- explain: "Paste text to explain…"
18340
+ proofread: "Paste text to proofread\u2026",
18341
+ rewrite: "Paste text to rewrite\u2026",
18342
+ shorten: "Paste text to shorten\u2026",
18343
+ expand: "Paste text to expand\u2026",
18344
+ summarize: "Paste text to summarize\u2026",
18345
+ translate: "Paste text to translate\u2026",
18346
+ justify: "Paste text to rewrite + explain\u2026",
18347
+ explain: "Paste text to explain\u2026"
17685
18348
  };
17686
- sourceArea.placeholder = placeholders[modeId] || "Type or paste text…";
18349
+ sourceArea.placeholder = placeholders[modeId] || "Type or paste text\u2026";
17687
18350
  }
17688
18351
  // 2026-05-08 (v20260508s): refresh the header mode-name chip.
17689
18352
  // For Translate the chip carries the target language so users
@@ -17691,7 +18354,7 @@ function RTE_Plugin_AIToolkit() {
17691
18354
  if (modeChip) {
17692
18355
  var chipLabel = modeLabel || "";
17693
18356
  if (modeLabel && modeSelect.value === "translate") {
17694
- chipLabel = modeLabel + " · " + getTranslateLanguageLabel(languageSelect.value || "spanish");
18357
+ chipLabel = modeLabel + " \xb7 " + getTranslateLanguageLabel(languageSelect.value || "spanish");
17695
18358
  }
17696
18359
  modeChip.innerText = chipLabel;
17697
18360
  modeChip.style.display = chipLabel ? "" : "none";
@@ -17860,9 +18523,9 @@ function RTE_Plugin_AIToolkit() {
17860
18523
  var docChars = snap ? (snap.wholeText || "").length : 0;
17861
18524
  scopeOptionSelection.disabled = !hasSelection;
17862
18525
  scopeOptionSelection.text = hasSelection
17863
- ? "Selection"
17864
- : "Selection (none)";
17865
- scopeOptionDocument.text = "Document";
18526
+ ? (obj.__aiT ? obj.__aiT("aid_selection", "Selection") : "Selection")
18527
+ : (obj.__aiT ? obj.__aiT("aid_selectionnone", "Selection (none)") : "Selection (none)");
18528
+ scopeOptionDocument.text = (obj.__aiT ? obj.__aiT("aid_document", "Document") : "Document");
17866
18529
  scopeSelect.value = activeScope === "selection" ? "selection" : "document";
17867
18530
  scopeSelect.setAttribute("title", buildDialogScopeHint(snap, scope));
17868
18531
  // 2026-06-01 (v20260601a): swap the <select> for a quiet inline
@@ -17877,7 +18540,7 @@ function RTE_Plugin_AIToolkit() {
17877
18540
  // 2026-05-20 (v20260520i) — Pass 61: caption mirrors the option
17878
18541
  // label — scope name only, no char-count suffix.
17879
18542
  if (scopeCaption) {
17880
- scopeCaption.innerText = "Document";
18543
+ scopeCaption.innerText = (obj.__aiT ? obj.__aiT("aid_document", "Document") : "Document");
17881
18544
  }
17882
18545
  if (compactControls && compactControls.classList) {
17883
18546
  if (hasSelection) {
@@ -17892,8 +18555,8 @@ function RTE_Plugin_AIToolkit() {
17892
18555
 
17893
18556
  function formatDialogScopeCount(n) {
17894
18557
  if (!n) return "0";
17895
- if (n > 999) return (Math.round(n / 100) / 10) + "K chars";
17896
- return n + " chars";
18558
+ if (n > 999) return (obj.__aiT ? obj.__aiT("aid_kchars", "{0}K chars", Math.round(n / 100) / 10) : (Math.round(n / 100) / 10) + "K chars");
18559
+ return obj.__aiT ? obj.__aiT("aid_chars", "{0} chars", n) : n + " chars";
17897
18560
  }
17898
18561
 
17899
18562
  function markDialogSourceDetached() {
@@ -17940,11 +18603,11 @@ function RTE_Plugin_AIToolkit() {
17940
18603
  }
17941
18604
 
17942
18605
  function getDialogLanguageChangeMessage(language) {
17943
- return "Language → " + getTranslateLanguageLabel(language || "spanish") + ". Re-run to refresh.";
18606
+ return "Language \u2192 " + getTranslateLanguageLabel(language || "spanish") + ". Re-run to refresh.";
17944
18607
  }
17945
18608
 
17946
18609
  function getDialogScopeChangeMessage(scope) {
17947
- return "Scope → " + (scope === "selection" ? "selection" : "document") + ". Re-run to refresh.";
18610
+ return "Scope \u2192 " + (scope === "selection" ? "selection" : "document") + ". Re-run to refresh.";
17948
18611
  }
17949
18612
 
17950
18613
  function getDialogEditorReloadMessage(scope) {
@@ -18029,70 +18692,70 @@ function RTE_Plugin_AIToolkit() {
18029
18692
  : "Select text in the editor, then reload it into Ask AI.";
18030
18693
  }
18031
18694
  if (actionId === "load-document") {
18032
- return "Reload the current editor document into Ask AI.";
18695
+ return (obj.__aiT ? obj.__aiT("aid_reloaddesc", "Reload the current editor document into Ask AI.") : "Reload the current editor document into Ask AI.");
18033
18696
  }
18034
18697
  if (actionId === "run") {
18035
18698
  return actionState.hasSource
18036
- ? "Generate a new AI suggestion from the current source text using the selected mode."
18037
- : "Load or type source text, then run Ask AI to generate a suggestion.";
18699
+ ? (obj.__aiT ? obj.__aiT("aid_gendesc", "Generate a new AI suggestion from the current source text using the selected mode.") : "Generate a new AI suggestion from the current source text using the selected mode.")
18700
+ : (obj.__aiT ? obj.__aiT("aid_loaddesc", "Load or type source text, then run Ask AI to generate a suggestion.") : "Load or type source text, then run Ask AI to generate a suggestion.");
18038
18701
  }
18039
18702
  if (actionId === "rerun") {
18040
18703
  return actionState.staleAction === "source"
18041
- ? "Refresh this suggestion from the current dialog source before applying it."
18042
- : "Refresh this suggestion from the current editor state before applying it.";
18704
+ ? (obj.__aiT ? obj.__aiT("ai2_d_refreshsrc", "Refresh this suggestion from the current dialog source before applying it.") : "Refresh this suggestion from the current dialog source before applying it.")
18705
+ : (obj.__aiT ? obj.__aiT("ai2_d_refreshed", "Refresh this suggestion from the current editor state before applying it.") : "Refresh this suggestion from the current editor state before applying it.");
18043
18706
  }
18044
18707
  if (actionId === "copy-to-source") {
18045
18708
  return actionState.hasResult
18046
- ? "Copy the current AI result back into the source field before rerunning it."
18047
- : "Run Ask AI first, then copy the generated result back into the source field.";
18709
+ ? (obj.__aiT ? obj.__aiT("ai2_d_copyback", "Copy the current AI result back into the source field before rerunning it.") : "Copy the current AI result back into the source field before rerunning it.")
18710
+ : (obj.__aiT ? obj.__aiT("ai2_d_copyfirst", "Run Ask AI first, then copy the generated result back into the source field.") : "Run Ask AI first, then copy the generated result back into the source field.");
18048
18711
  }
18049
18712
  if (actionId === "apply") {
18050
18713
  if (actionState.pendingIndexes.length > 1) {
18051
18714
  return actionState.pendingSelectionDependent && !actionState.hasSelection
18052
- ? "Select text before applying this selection-based AI plan."
18053
- : "Run the remaining " + actionState.pendingIndexes.length + "-step AI plan directly in the editor.";
18715
+ ? (obj.__aiT ? obj.__aiT("ai2_d_selectplan", "Select text before applying this selection-based AI plan.") : "Select text before applying this selection-based AI plan.")
18716
+ : (obj.__aiT ? obj.__aiT("ai2_d_steps", "Run the remaining {0}-step AI plan directly in the editor.", actionState.pendingIndexes.length) : "Run the remaining " + actionState.pendingIndexes.length + "-step AI plan directly in the editor.");
18054
18717
  }
18055
18718
  switch (actionState.pendingOperationType) {
18056
18719
  case "preview-suggestion":
18057
18720
  return actionState.hasSelection
18058
- ? "Show the prepared inline diff in the editor before deciding it."
18059
- : "Select text before previewing this prepared inline diff in the editor.";
18721
+ ? (obj.__aiT ? obj.__aiT("ai2_d_showdiff", "Show the prepared inline diff in the editor before deciding it.") : "Show the prepared inline diff in the editor before deciding it.")
18722
+ : (obj.__aiT ? obj.__aiT("ai2_d_selectdiff", "Select text before previewing this prepared inline diff in the editor.") : "Select text before previewing this prepared inline diff in the editor.");
18060
18723
  case "replace-selection":
18061
18724
  return actionState.hasSelection
18062
- ? "Replace the current selection with this prepared draft."
18063
- : "Select text before replacing it with this prepared draft.";
18725
+ ? (obj.__aiT ? obj.__aiT("ai2_d_replsel", "Replace the current selection with this prepared draft.") : "Replace the current selection with this prepared draft.")
18726
+ : (obj.__aiT ? obj.__aiT("ai2_d_selectrepl", "Select text before replacing it with this prepared draft.") : "Select text before replacing it with this prepared draft.");
18064
18727
  case "replace-document":
18065
- return "Replace the full document with this prepared draft.";
18728
+ return (obj.__aiT ? obj.__aiT("ai2_d_repldoc", "Replace the full document with this prepared draft.") : "Replace the full document with this prepared draft.");
18066
18729
  case "insert-below":
18067
18730
  return actionState.hasSelection
18068
- ? "Insert this prepared draft below the current selection."
18069
- : "Insert this prepared draft below the current content.";
18731
+ ? (obj.__aiT ? obj.__aiT("ai2_d_insbelowsel", "Insert this prepared draft below the current selection.") : "Insert this prepared draft below the current selection.")
18732
+ : (obj.__aiT ? obj.__aiT("ai2_d_insbelow", "Insert this prepared draft below the current content.") : "Insert this prepared draft below the current content.");
18070
18733
  case "add-comment":
18071
- return "Add this prepared AI comment without rewriting the document body.";
18734
+ return (obj.__aiT ? obj.__aiT("ai2_d_comment", "Add this prepared AI comment without rewriting the document body.") : "Add this prepared AI comment without rewriting the document body.");
18072
18735
  default:
18073
- return "Run the prepared AI plan directly in the editor.";
18736
+ return (obj.__aiT ? obj.__aiT("ai2_d_runplan", "Run the prepared AI plan directly in the editor.") : "Run the prepared AI plan directly in the editor.");
18074
18737
  }
18075
18738
  }
18076
18739
  if (actionId === "selection") {
18077
18740
  return actionState.hasSelection
18078
- ? "Replace the current selection with this prepared draft."
18079
- : "Select text before replacing it with this prepared draft.";
18741
+ ? (obj.__aiT ? obj.__aiT("ai2_d_replsel", "Replace the current selection with this prepared draft.") : "Replace the current selection with this prepared draft.")
18742
+ : (obj.__aiT ? obj.__aiT("ai2_d_selectrepl", "Select text before replacing it with this prepared draft.") : "Select text before replacing it with this prepared draft.");
18080
18743
  }
18081
18744
  if (actionId === "preview") {
18082
18745
  return actionState.hasSelection
18083
- ? "Show this change inline in the editor before deciding it."
18084
- : "Select text to preview this change inline in the editor.";
18746
+ ? (obj.__aiT ? obj.__aiT("ai2_d_previewsel", "Show this change inline in the editor before deciding it.") : "Show this change inline in the editor before deciding it.")
18747
+ : (obj.__aiT ? obj.__aiT("ai2_d_previewselect", "Select text to preview this change inline in the editor.") : "Select text to preview this change inline in the editor.");
18085
18748
  }
18086
18749
  if (actionId === "insert") {
18087
18750
  return actionState.hasSelection
18088
- ? "Insert this prepared draft below the current selection."
18089
- : "Insert this prepared draft below the current content.";
18751
+ ? (obj.__aiT ? obj.__aiT("ai2_d_insbelowsel", "Insert this prepared draft below the current selection.") : "Insert this prepared draft below the current selection.")
18752
+ : (obj.__aiT ? obj.__aiT("ai2_d_insbelow", "Insert this prepared draft below the current content.") : "Insert this prepared draft below the current content.");
18090
18753
  }
18091
18754
  if (actionId === "document") {
18092
- return "Replace the full document with this prepared draft.";
18755
+ return (obj.__aiT ? obj.__aiT("ai2_d_repldoc", "Replace the full document with this prepared draft.") : "Replace the full document with this prepared draft.");
18093
18756
  }
18094
18757
  if (actionId === "reject") {
18095
- return "Dismiss this prepared suggestion without changing the document.";
18758
+ return (obj.__aiT ? obj.__aiT("ai2_d_dismiss", "Dismiss this prepared suggestion without changing the document.") : "Dismiss this prepared suggestion without changing the document.");
18096
18759
  }
18097
18760
  return "";
18098
18761
  }
@@ -18180,22 +18843,22 @@ function RTE_Plugin_AIToolkit() {
18180
18843
  var pendingOperationType = actionState.pendingOperationType || "";
18181
18844
  if (actionState.actionScope === "selection") {
18182
18845
  if (actionState.hasSelection && pendingOperationType !== "preview-suggestion") {
18183
- addDialogReadyAlternative(alternatives, covered, "preview", "preview inline");
18846
+ addDialogReadyAlternative(alternatives, covered, "preview", (obj.__aiT ? obj.__aiT("ai2_alt_preview", "preview inline") : "preview inline"));
18184
18847
  }
18185
18848
  if (actionState.hasSelection && pendingOperationType !== "replace-selection") {
18186
- addDialogReadyAlternative(alternatives, covered, "selection", "replace the selection");
18849
+ addDialogReadyAlternative(alternatives, covered, "selection", (obj.__aiT ? obj.__aiT("ai2_alt_selection", "replace the selection") : "replace the selection"));
18187
18850
  }
18188
18851
  if (actionState.canInsert && pendingOperationType !== "insert-below") {
18189
- addDialogReadyAlternative(alternatives, covered, "insert", "insert below");
18852
+ addDialogReadyAlternative(alternatives, covered, "insert", (obj.__aiT ? obj.__aiT("ai2_alt_insert", "insert below") : "insert below"));
18190
18853
  }
18191
18854
  return alternatives;
18192
18855
  }
18193
18856
  if (actionState.actionScope === "document") {
18194
18857
  if (actionState.canReplaceDocument && pendingOperationType !== "replace-document") {
18195
- addDialogReadyAlternative(alternatives, covered, "document", "replace the document");
18858
+ addDialogReadyAlternative(alternatives, covered, "document", (obj.__aiT ? obj.__aiT("ai2_alt_document", "replace the document") : "replace the document"));
18196
18859
  }
18197
18860
  if (actionState.canInsert && pendingOperationType !== "insert-below") {
18198
- addDialogReadyAlternative(alternatives, covered, "insert", "insert below");
18861
+ addDialogReadyAlternative(alternatives, covered, "insert", (obj.__aiT ? obj.__aiT("ai2_alt_insert", "insert below") : "insert below"));
18199
18862
  }
18200
18863
  }
18201
18864
  return alternatives;
@@ -18213,10 +18876,10 @@ function RTE_Plugin_AIToolkit() {
18213
18876
  ? (recommended.label || "").replace(/\s+/g, " ").trim()
18214
18877
  : "";
18215
18878
  if (recommendedLabel) {
18216
- return "Ready · " + recommendedLabel;
18879
+ return "Ready \xb7 " + recommendedLabel;
18217
18880
  }
18218
18881
  if (actionState.actionScope === "selection" && !actionState.hasSelection) {
18219
- return "Ready · Insert below";
18882
+ return "Ready \xb7 Insert below";
18220
18883
  }
18221
18884
  return "Ready";
18222
18885
  }
@@ -18294,25 +18957,25 @@ function RTE_Plugin_AIToolkit() {
18294
18957
  parts.push(detail);
18295
18958
  }
18296
18959
  if (alternativeText) {
18297
- parts.push("Other available actions: " + alternativeText + ".");
18960
+ parts.push(obj.__aiT ? obj.__aiT("aid_otheractions", "Other available actions: {0}.", alternativeText) : "Other available actions: " + alternativeText + ".");
18298
18961
  }
18299
18962
  if (isRecommended) {
18300
- parts.push("Recommended next step.");
18963
+ parts.push((obj.__aiT ? obj.__aiT("aid_recommended", "Recommended next step.") : "Recommended next step."));
18301
18964
  button.setAttribute("data-rte-ai-dialog-recommended", "true");
18302
18965
  }
18303
18966
  else {
18304
18967
  button.removeAttribute("data-rte-ai-dialog-recommended");
18305
18968
  }
18306
- button.setAttribute("aria-label", parts.join(". "));
18969
+ button.setAttribute("aria-label", joinSentences(parts));
18307
18970
  var titleParts = [label];
18308
18971
  if (detail) {
18309
18972
  titleParts.push(detail);
18310
18973
  }
18311
18974
  if (alternativeText) {
18312
- titleParts.push("Other available actions: " + alternativeText + ".");
18975
+ titleParts.push(obj.__aiT ? obj.__aiT("aid_otheractions", "Other available actions: {0}.", alternativeText) : "Other available actions: " + alternativeText + ".");
18313
18976
  }
18314
18977
  if (isRecommended) {
18315
- titleParts.push("Recommended next step.");
18978
+ titleParts.push((obj.__aiT ? obj.__aiT("aid_recommended", "Recommended next step.") : "Recommended next step."));
18316
18979
  }
18317
18980
  // 2026-05-22 (v20260522a): preserve any pre-set keyboard shortcut
18318
18981
  // hint on the button by reading aria-keyshortcuts (the WAI-ARIA
@@ -18322,7 +18985,7 @@ function RTE_Plugin_AIToolkit() {
18322
18985
  // shortcut would only be discoverable via screen-reader
18323
18986
  // announcement of aria-keyshortcuts and never via mouse hover.
18324
18987
  // Formatting matches the Generate button's pre-existing title
18325
- // suffix ("Generate AI suggestion (Ctrl+Enter)").
18988
+ // suffix ((obj.__aiT ? obj.__aiT("ai2_gentitle", "Generate AI suggestion (Ctrl+Enter)") : "Generate AI suggestion (Ctrl+Enter)")).
18326
18989
  var keyHint = button.getAttribute && button.getAttribute("aria-keyshortcuts");
18327
18990
  if (keyHint) {
18328
18991
  var keyHintLabel = keyHint
@@ -18379,7 +19042,7 @@ function RTE_Plugin_AIToolkit() {
18379
19042
  return {
18380
19043
  actionId: "run",
18381
19044
  button: runButton,
18382
- label: "Ask AI",
19045
+ label: (obj.__aiT ? obj.__aiT("aiassist", "Ask AI") : "Ask AI"),
18383
19046
  detail: getDialogActionDetail("run", actionState)
18384
19047
  };
18385
19048
  }
@@ -18411,7 +19074,7 @@ function RTE_Plugin_AIToolkit() {
18411
19074
  return {
18412
19075
  actionId: "document",
18413
19076
  button: acceptDocumentButton,
18414
- label: (acceptDocumentButton.innerText || acceptDocumentButton.textContent || "").replace(/\s+/g, " ").trim() || "Replace document",
19077
+ label: (acceptDocumentButton.innerText || acceptDocumentButton.textContent || "").replace(/\s+/g, " ").trim() || (obj.__aiT ? obj.__aiT("ai2_repldoc", "Replace document") : "Replace document"),
18415
19078
  detail: getDialogActionDetail("document", actionState)
18416
19079
  };
18417
19080
  }
@@ -18464,11 +19127,11 @@ function RTE_Plugin_AIToolkit() {
18464
19127
  return;
18465
19128
  }
18466
19129
  applyGuidance.style.display = "";
18467
- applyGuidanceTitle.innerText = "Best next step";
19130
+ applyGuidanceTitle.innerText = (obj.__aiT ? obj.__aiT("aid_bestnext", "Best next step") : "Best next step");
18468
19131
  applyGuidanceBadge.innerText = recommended.label || "";
18469
19132
  var summaryDetail = getDialogRecommendedSummaryDetail(actionState, recommended);
18470
19133
  applyGuidanceDetail.innerText = summaryDetail || "";
18471
- applyGuidance.setAttribute("aria-label", "Best next step: " + (recommended.label || "Action") + "." + (summaryDetail ? " " + summaryDetail : ""));
19134
+ applyGuidance.setAttribute("aria-label", (obj.__aiT ? obj.__aiT("aid_bestnextaria", "Best next step: {0}.", recommended.label || "Action") : "Best next step: " + (recommended.label || "Action") + ".") + (summaryDetail ? " " + summaryDetail : ""));
18472
19135
  }
18473
19136
 
18474
19137
  function rerunDialogPlanFromEditor() {
@@ -18672,7 +19335,7 @@ function RTE_Plugin_AIToolkit() {
18672
19335
  // setBusyState(false) is called.
18673
19336
  if (isBusy) {
18674
19337
  runButton.classList.add("is-busy");
18675
- setReviewV2ButtonContent(runButton, "spinner", "Thinking...");
19338
+ setReviewV2ButtonContent(runButton, "spinner", (obj.__aiT ? obj.__aiT("aic_thinking", "Thinking...") : "Thinking..."));
18676
19339
  } else {
18677
19340
  runButton.classList.remove("is-busy");
18678
19341
  }
@@ -18792,7 +19455,7 @@ function RTE_Plugin_AIToolkit() {
18792
19455
  // hidden apply options on the kebab's title + aria-label so
18793
19456
  // users discover *what's behind the dots* on hover or with a
18794
19457
  // screen reader, instead of clicking blind. Pre-pass the
18795
- // tooltip read as "More apply options" — accurate but content-
19458
+ // tooltip read as (obj.__aiT ? obj.__aiT("ai2_moreopts", "More apply options") : "More apply options") — accurate but content-
18796
19459
  // free; now it reads "More apply options (3): Replace
18797
19460
  // selection, Insert below, Replace doc" so the count + the
18798
19461
  // names live where the affordance is. Same shape Notion AI's
@@ -18804,7 +19467,7 @@ function RTE_Plugin_AIToolkit() {
18804
19467
  if (showsSelectionAction) moreOptionNames.push("Replace selection");
18805
19468
  if (showsPreviewAction) moreOptionNames.push("Preview");
18806
19469
  if (showsInsertAction) moreOptionNames.push("Insert below");
18807
- if (showsDocumentAction) moreOptionNames.push("Replace document");
19470
+ if (showsDocumentAction) moreOptionNames.push((obj.__aiT ? obj.__aiT("ai2_repldoc", "Replace document") : "Replace document"));
18808
19471
  var moreCount = moreOptionNames.length;
18809
19472
  var anyMoreAvailable = moreCount > 0;
18810
19473
  applyRowMoreToggle.style.display = anyMoreAvailable ? "" : "none";
@@ -18829,7 +19492,7 @@ function RTE_Plugin_AIToolkit() {
18829
19492
  // 2026-05-08 (v20260508s): clarify the run button — when a
18830
19493
  // result already exists the button re-runs the same source, so
18831
19494
  // call it Regenerate. Also flips when the plan goes stale.
18832
- var runLabel = (hasResult || isStale) ? "Regenerate" : "Generate";
19495
+ var runLabel = (hasResult || isStale) ? (obj.__aiT ? obj.__aiT("aid_regenerate", "Regenerate") : "Regenerate") : (obj.__aiT ? obj.__aiT("aid_generate", "Generate") : "Generate");
18833
19496
  setReviewV2ButtonContent(runButton, "sparkles", runLabel);
18834
19497
  // 2026-05-17 (v20260517a): re-attach the keyboard hint chip,
18835
19498
  // since setReviewV2ButtonContent clears textContent and loses