Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions DistFiles/localization/en/Bloom.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,64 @@
<note>ID: CollectionSettingsDialog.AdvancedTab.Experimental.SpreadsheetImportExport</note>
<note>This is the label of a checkbox which, if enabled, allows a menu command to be used that can import Bloom books from an Excel spreadsheet or to an Excel spreadsheet.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.Experimental.AiSourceBubbles" translate="no">
<source xml:lang="en">AI Source Bubbles</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.Experimental.AiSourceBubbles</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.SectionLabel" translate="no">
<source xml:lang="en">AI Source Bubbles</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.SectionLabel</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.TargetLanguageLabel" translate="no">
<source xml:lang="en">Target Language</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.TargetLanguageLabel</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.TargetLanguageDescription" translate="no">
<source xml:lang="en">Choose one of this collection's languages, or Other... to select another language.</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.TargetLanguageDescription</note>
<note>Currently unused (orphaned from an earlier design); left in place rather than deleted.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.OtherLanguageLabel" translate="no">
<source xml:lang="en">Other...</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.OtherLanguageLabel</note>
<note>Currently unused (orphaned from an earlier design); left in place rather than deleted.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.DeepLEnabledLabel" translate="no">
<source xml:lang="en">DeepL</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.DeepLEnabledLabel</note>
<note>Label of the checkbox that enables the DeepL translation engine for AI source bubbles. "DeepL" is a product name and must not be translated.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.DeepLApiKeyLabel" translate="no">
<source xml:lang="en">DeepL API key</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.DeepLApiKeyLabel</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.GoogleEnabledLabel" translate="no">
<source xml:lang="en">Google Translate</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.GoogleEnabledLabel</note>
<note>Label of the checkbox that enables the Google Translate engine for AI source bubbles. "Google Translate" is a product name and must not be translated.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.GoogleServiceAccountEmailLabel" translate="no">
<source xml:lang="en">Google service account email</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.GoogleServiceAccountEmailLabel</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.GooglePrivateKeyLabel" translate="no">
<source xml:lang="en">Google private key</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.GooglePrivateKeyLabel</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.Alpha2EnabledLabel" translate="no">
<source xml:lang="en">SIL Alpha2</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.Alpha2EnabledLabel</note>
<note>Label of the checkbox that enables the SIL Alpha2 translation engine for AI source bubbles. "SIL Alpha2" is a product name and must not be translated.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.Alpha2ApiKeyLabel" translate="no">
<source xml:lang="en">Alpha2 API key</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.Alpha2ApiKeyLabel</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.TranslationTestLabel" translate="no">
<source xml:lang="en">Translation Test</source>
<note>ID: CollectionSettingsDialog.AdvancedTab.AiSourceBubbles.TranslationTestLabel</note>
<note>Label of the row showing a live translation-test result for one AI source bubbles engine (DeepL, Google Translate, or SIL Alpha2); this same label is reused once per engine.</note>
</trans-unit>
<trans-unit id="CollectionSettingsDialog.AndikaNewBasicUpdate1" sil:dynamic="true">
<source xml:lang="en">Bloom is switching the default font for "{0}" to the new "Andika New Basic".</source>
<note>ID: CollectionSettingsDialog.AndikaNewBasicUpdate1</note>
Expand Down Expand Up @@ -1344,6 +1402,31 @@
<source xml:lang="en">Add Page...</source>
<note>ID: EditTab.AddPageDialog.Title</note>
</trans-unit>
<trans-unit id="EditTab.AiTranslation.Cancelled" sil:dynamic="true" translate="no">
<source xml:lang="en">Cancelled.</source>
<note>ID: EditTab.AiTranslation.Cancelled</note>
<note>Status line in the AI translation progress dialog after the user clicks Cancel; the book is left unchanged.</note>
</trans-unit>
<trans-unit id="EditTab.AiTranslation.DialogTitle" sil:dynamic="true" translate="no">
<source xml:lang="en">Translating</source>
<note>ID: EditTab.AiTranslation.DialogTitle</note>
<note>Title of the progress dialog shown while AI engines translate the book's text before it opens for editing.</note>
</trans-unit>
<trans-unit id="EditTab.AiTranslation.EngineDone" sil:dynamic="true" translate="no">
<source xml:lang="en">{0}: done.</source>
<note>ID: EditTab.AiTranslation.EngineDone</note>
<note>Progress line in the AI translation dialog. {0} is replaced with the translation engine's name (e.g. "DeepL"), which must not be translated.</note>
</trans-unit>
<trans-unit id="EditTab.AiTranslation.EngineError" sil:dynamic="true" translate="no">
<source xml:lang="en">{0}: {1}</source>
<note>ID: EditTab.AiTranslation.EngineError</note>
<note>Error line in the AI translation dialog. {0} is the translation engine's name (e.g. "DeepL"); {1} is an error message. Only the separator punctuation can be localized.</note>
</trans-unit>
<trans-unit id="EditTab.AiTranslation.EngineTranslating" sil:dynamic="true" translate="no">
<source xml:lang="en">{0}: translating {1} text box(es)...</source>
<note>ID: EditTab.AiTranslation.EngineTranslating</note>
<note>Progress line in the AI translation dialog. {0} is the translation engine's name (e.g. "DeepL"), which must not be translated; {1} is the number of text boxes being translated.</note>
</trans-unit>
<trans-unit id="EditTab.BackMatter.InsideBackCoverTextPrompt" sil:dynamic="true">
<source xml:lang="en">If you need somewhere to put more information about the book, you can use this page, which is the inside of the back cover.</source>
<note>ID: EditTab.BackMatter.InsideBackCoverTextPrompt</note>
Expand Down
10 changes: 10 additions & 0 deletions DistFiles/localization/en/BloomLowPriority.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,16 @@
<note>ID: CollectionTab.BookMenu.MoveToCurrentCollection</note>
<note>{0} will be replaced with the name of the collection that the user currently has open for editing.</note>
</trans-unit>
<trans-unit id="CollectionTab.BookMenu.RemoveAiSourceTranslations" translate="no">
<source xml:lang="en">Remove AI Source Translations</source>
<note>ID: CollectionTab.BookMenu.RemoveAiSourceTranslations</note>
<note>A book right-click menu command in the Collection tab. It deletes the AI-generated translations that appear as source bubbles when editing. "AI" stands for artificial intelligence.</note>
</trans-unit>
<trans-unit id="CollectionTab.BookMenu.RemovedAiSourceTranslations" translate="no">
<source xml:lang="en">Bloom removed the AI source translations from this book.</source>
<note>ID: CollectionTab.BookMenu.RemovedAiSourceTranslations</note>
<note>Confirmation message shown after the "Remove AI Source Translations" menu command finishes. "Bloom" is the product name and must not be translated; "AI" stands for artificial intelligence.</note>
</trans-unit>
</body>
</file>
</xliff>
12 changes: 12 additions & 0 deletions PAPERCUTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Papercuts

Small dev/agent/tooling friction points captured mid-task (see the `papercut` skill).

- Running `dotnet test` (or any BloomExe build) while a `./go.sh` / `dotnet watch`
Bloom is live fails at the copy-to-output step: the running process locks both
`output/Debug/AnyCPU/Bloom.exe` (native apphost) and, once hot-reload deltas have
been applied, `Bloom.dll` too (MSB3026/MSB3027 "being used by another process").
Compilation itself succeeds; only the copy fails, so the tests never run.
Workaround that neither kills the running instance nor touches the locked output:
redirect the whole build to a scratch dir and skip the apphost, e.g.
`dotnet test src/BloomTests/BloomTests.csproj --filter ... -p:UseAppHost=false -p:OutDir=<abs-scratch-path-with-trailing-slash>`.
1 change: 1 addition & 0 deletions src/BloomBrowserUI/bookEdit/js/collectionSettings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface ICollectionSettings {
defaultSourceLanguage2: string;
currentCollectionLanguage2: string;
currentCollectionLanguage3: string;
allowAiSourceBubbles: boolean;
browserRoot: string;
topics: string[];
}
125 changes: 107 additions & 18 deletions src/BloomBrowserUI/bookEdit/sourceBubbles/BloomSourceBubbles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,25 @@
// The actual function is injected by C#.
/// <reference path="../js/collectionSettings.d.ts"/>
import { renderRoot } from "../../utils/reactRender";
import * as ReactDOM from "react-dom";
import * as React from "react";
import $ from "jquery";
import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
import theOneLocalizationManager from "../../lib/localizationManager/localizationManager";
import StyleEditor from "../StyleEditor/StyleEditor";
import bloomQtipUtils from "../js/bloomQtipUtils";
import "../../lib/jquery.easytabs.js"; //load into global space
import BloomHintBubbles from "../js/BloomHintBubbles";
import { postJson, postString } from "../../utils/bloomApi";
import CopyContentButton from "../../react_components/CopyContentButton";
import {
getAiProviderDisplayName,
getLanguageNameFromBrowser,
isAiLanguageTag,
removeAiTranslationDivsFromClone,
} from "./aiTranslationDisplay";

declare function GetSettings(): any; // C# (or test code) injects this
declare function GetSettings(): ICollectionSettings;

export default class BloomSourceBubbles {
//:empty is not quite enough... we don't want to show bubbles if all there is is an empty paragraph
Expand All @@ -29,6 +38,61 @@ export default class BloomSourceBubbles {
return $.trim($(obj).text()).length === 0;
}

private static maybeRememberSourceBubbleLanguage(langTag: string): void {
postString("editView/sourceTextTab", langTag);
Comment thread
hatton marked this conversation as resolved.
}

private static getLanguageDisplayName(langTag: string): string {
const aiSplitMarker = "-x-ai-";
const aiSplitIndex = langTag.indexOf(aiSplitMarker);
if (aiSplitIndex < 0) {
return (
theOneLocalizationManager.getLanguageName(langTag) || langTag
);
}

const targetLanguageTag = langTag.substring(0, aiSplitIndex);
const providerId = langTag.substring(
aiSplitIndex + aiSplitMarker.length,
);
// getLanguageName echoes the tag back when it doesn't know the language, so treat
// that as "unknown" and let the browser's Intl database have a try.
const bloomLanguageName =
theOneLocalizationManager.getLanguageName(targetLanguageTag);
const targetLanguageName =
(bloomLanguageName !== targetLanguageTag
? bloomLanguageName
: undefined) ||
getLanguageNameFromBrowser(targetLanguageTag) ||
targetLanguageTag;
const providerDisplayName = getAiProviderDisplayName(providerId);
return `AI ${targetLanguageName} (${providerDisplayName})`;
}

private static appendSourceTabLabel(
anchor: HTMLAnchorElement,
langTag: string,
localizedLanguageName: string,
): void {
if (!isAiLanguageTag(langTag)) {
anchor.textContent = localizedLanguageName;
return;
}

// TODO (BL-16549 follow-up): migrate to React 18 createRoot. Kept on the synchronous
// ReactDOM.render here deliberately for now -- these tab-label anchors are rebuilt each
// time bubbles are constructed, and createRoot's async render / one-root-per-container
// rules need their own change; behavior is unchanged in the meantime.
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
<>
<AutoAwesomeIcon fontSize="inherit" aria-hidden={true} />
<span> {localizedLanguageName}</span>
</>,
anchor,
);
}

// This is the method that should be called from bloomEditing to create tabbed source bubbles
// for translation.
// param 'group' is a .bloom-translationGroup DIV
Expand Down Expand Up @@ -94,16 +158,21 @@ export default class BloomSourceBubbles {
newLangTag?: string,
): JQuery {
if (group.classList.contains("bloom-no-source-bubble")) return $();
const liveGroup = $(group);

// Copy source texts out to their own div, where we can make a bubble with tabs out of them
// We do this because if we made a bubble out of the div, that would suck up the vernacular editable area, too,
const divForBubble = $(group).clone();
const divForBubble = liveGroup.clone();
divForBubble.removeAttr("style");
divForBubble.removeClass(); //remove them all
divForBubble.addClass("ui-sourceTextsForBubble");
// For now, we don't want labels (hints) in the source bubbles. BL-4295 discusses possibly changing this.
divForBubble.find("label.bubble").each((index, element) => {
$(element).remove();
});
if (!GetSettings().allowAiSourceBubbles) {
removeAiTranslationDivsFromClone(divForBubble);
}

//make the source texts in the bubble read-only and remove any user font size adjustments
divForBubble.find("textarea, div").each(function (): boolean {
Expand Down Expand Up @@ -172,31 +241,35 @@ export default class BloomSourceBubbles {
const langTag = sourceElement.getAttribute("lang");
if (langTag) {
const localizedLanguageName =
theOneLocalizationManager.getLanguageName(langTag) ||
langTag;
BloomSourceBubbles.getLanguageDisplayName(langTag);
// This is bizarre. The href ought to be referring to the element with the specified ID,
// which should be the tab CONTENT that should be shown for this language. But we have modified
// easytabs (see above) so that the target (main page content div) for a tab is the element whose
// lang attr is the value following the # in the href, rather than its id.
// Even more bizarrely, we make the id of the list item have that value also, so that
// the apparent target of the <a> is the <li> it resides inside. Not sure why this is
// helpful.
$(list).append(
'<li id="' +
langTag +
'" title="' +
langTag +
'"><a class="sourceTextTab" href="#' +
langTag +
'">' +
localizedLanguageName +
"</a></li>",
const liElement = document.createElement("li");
liElement.id = langTag;
liElement.title = langTag;
const anchor = document.createElement("a");
anchor.className = "sourceTextTab";
anchor.href = `#${langTag}`;
BloomSourceBubbles.appendSourceTabLabel(
anchor,
langTag,
localizedLanguageName,
);
liElement.append(anchor);
list.append(liElement);
(
list.get(0) as HTMLElement
).lastElementChild?.firstElementChild?.addEventListener(
"click",
() => postString("editView/sourceTextTab", langTag),
() =>
BloomSourceBubbles.maybeRememberSourceBubbleLanguage(
langTag,
),
);
// BL-8174: Add a tooltip with the language tag to the item
// BL-15212: we no longer want the tag here, just on the language-name label
Expand Down Expand Up @@ -304,7 +377,7 @@ export default class BloomSourceBubbles {
if (indexA >= 0) return -1;
if (indexB >= 0) return 1;
// Neither in preferred list - maintain alphabetical order
return langA < langB ? (langA > langB ? 1 : 0) : -1;
return langA < langB ? -1 : langA > langB ? 1 : 0;
});

return $(itemArray);
Expand Down Expand Up @@ -427,7 +500,23 @@ export default class BloomSourceBubbles {
}

private static styledSelectChangeHandler(event) {
const newLangTag = event.target.href.split("#")[1];
const anchor =
event.target instanceof Element
? event.target.closest("a.sourceTextTab")
: undefined;
const currentTarget =
event.currentTarget instanceof Element
? event.currentTarget
: undefined;
const href =
anchor?.getAttribute("href") ??
currentTarget
?.querySelector("a.sourceTextTab")
?.getAttribute("href");
if (!href) {
return;
}
const newLangTag = href.split("#")[1];

// Figure out which qtip we're in and go find the associated bloom-translationGroup
const qtip = $(event.target).closest(".qtip").attr("id");
Expand All @@ -442,7 +531,7 @@ export default class BloomSourceBubbles {
group[0],
newLangTag,
);
postString("editView/sourceTextTab", newLangTag);
BloomSourceBubbles.maybeRememberSourceBubbleLanguage(newLangTag);
if (divForBubble.length !== 0) {
BloomHintBubbles.addHintBubbles(
group.get(0),
Expand Down
Loading