diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ce90a1..d1d931a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.0] - 2026-03-20 +### Added +* Style rule ID field in the sidebar (Advanced section). +* Custom instructions field — up to 10 instructions, one per line. +* Model selection: Default, Quality optimized, Prefer quality, Speed optimized. +* All new options are saved and restored automatically between sessions. + +### Changed +* Sidebar options reorganised into collapsible sections: **Options** + (formality, context, custom instructions) and **Advanced** (glossary ID, + style rule ID, model), keeping the main translation controls uncluttered. +* All translation options are now passed to the API as a single JSON object. + +## [0.3.0] - 2026-03-17 +### Added +* **Sidebar UI for translation**. Select cells, set options, and click + **Translate** — results are written as static values and never + re-translated when the sheet is reopened. +* Translation options: source/target language, formality + (Default / Formal / Informal), context hint, and glossary ID. All + options are saved and restored automatically between sessions. +* API key management in the sidebar: enter, validate, and clear the key + without opening Apps Script settings. +* Usage bar showing character consumption for the current billing period, + updated after each translation. Billed characters for the current + translation are shown after translation is complete. + +### Changed +* The `DeepLTranslate()` and `DeepLUsage()` functions are maintained for + backwards compatibility, but disabled by default, see + `FORMULA_FUNCTIONS.md` for instructions. + ## [0.2.0] - 2025-09-01 ### Changed * Renamed `freeze` variable to `disableTranslations`. @@ -20,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial release. -[Unreleased]: https://github.com/DeepLcom/google-sheets-example/compare/v0.2.0...HEAD +[Unreleased]: https://github.com/DeepLcom/google-sheets-example/compare/v0.4.0...HEAD +[0.4.0]: https://github.com/DeepLcom/google-sheets-example/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/DeepLcom/google-sheets-example/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/DeepLcom/google-sheets-example/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/DeepLcom/google-sheets-example/releases/tag/v0.1.0 diff --git a/DeepL.gs b/DeepL.gs index 2f3ce00..0a917cb 100644 --- a/DeepL.gs +++ b/DeepL.gs @@ -22,6 +22,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/* Set to true to enable the DeepLTranslate() and DeepLUsage() formula functions. + Read FORMULA_FUNCTIONS.md for usage details and cost implications before enabling. */ +const enableFormulaFunctions = false; + /* Change the line below to disable all translations. */ const disableTranslations = false; // Set to true to stop translations. @@ -30,10 +34,235 @@ const activateAutoDetect = false; // Set to true to enable auto-detection of re- /* You shouldn't need to modify the lines below here */ -const deeplApiKey = PropertiesService.getScriptProperties().getProperty('DEEPL_API_KEY'); - /* Version of this script from https://github.com/DeepLcom/google-sheet-example, included in logs. */ -const scriptVersion = "0.2.0"; +const scriptVersion = "0.4.0"; + +/** + * Creates the DeepL menu when the spreadsheet is opened. + */ +function onOpen() { + SpreadsheetApp.getUi() + .createMenu('DeepL') + .addItem('Open sidebar', 'showSidebar') + .addSeparator() + .addItem('About (v' + scriptVersion + ')', 'showAbout_') + .addToUi(); +} + +/** + * Shows a dialog with the current script version and a link to the changelog. + */ +function showAbout_() { + const ui = SpreadsheetApp.getUi(); + ui.alert( + 'DeepL for Google Sheets', + 'Version ' + scriptVersion + '\n\n' + + 'To check for updates, visit:\n' + + 'https://github.com/DeepLcom/google-sheets-example/blob/main/CHANGELOG.md', + ui.ButtonSet.OK + ); +} + +/** + * Returns the current script version. Called from the sidebar on load. + * @return {string} + */ +function getScriptVersion() { + return scriptVersion; +} + +/** + * Opens the DeepL translation sidebar. + */ +function showSidebar() { + const html = HtmlService.createHtmlOutputFromFile('DeepLSidebar') + .setTitle('DeepL Translate') + .setWidth(300); + SpreadsheetApp.getUi().showSidebar(html); +} + +/** + * Translates the currently selected cells and writes results back as static values. + * Called from the sidebar via google.script.run. + * + * @param {string|null} sourceLang Source language code, or null for auto-detect. + * @param {string} targetLang Target language code. + * @param {{glossaryId, formality, context, styleId, customInstructions, modelType}} options + * @return {{translated: number, skipped: number, failed: number, error: string, billedCharacters: number}} + */ +function translateSelectionFromSidebar(sourceLang, targetLang, options) { + const range = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveRange(); + if (!range) throw new Error('No cells selected.'); + + options = options || {}; + + const flatCells = []; + for (let r = 0; r < range.getNumRows(); r++) { + for (let c = 0; c < range.getNumColumns(); c++) { + flatCells.push(range.getCell(r + 1, c + 1)); + } + } + + PropertiesService.getScriptProperties().setProperties({ + 'DEEPL_LAST_SOURCE_LANG': sourceLang || '', + 'DEEPL_LAST_TARGET_LANG': targetLang, + 'DEEPL_LAST_FORMALITY': options.formality || '', + 'DEEPL_LAST_CONTEXT': options.context || '', + 'DEEPL_LAST_GLOSSARY_ID': options.glossaryId || '', + 'DEEPL_LAST_STYLE_ID': options.styleId || '', + 'DEEPL_LAST_CUSTOM_INSTRUCTIONS': options.customInstructions + ? options.customInstructions.join('\n') : '', + 'DEEPL_LAST_MODEL_TYPE': options.modelType || '', + 'DEEPL_LAST_EXTRA_OPTIONS': options.extraOptions || '', + }); + + const cellsToTranslate = flatCells.filter(cell => { + const text = cell.getDisplayValue(); + return text && text.trim() !== ''; + }); + const skipped = flatCells.length - cellsToTranslate.length; + + if (cellsToTranslate.length === 0) { + return { translated: 0, skipped, failed: 0, error: '', billedCharacters: 0 }; + } + + try { + const texts = cellsToTranslate.map(cell => cell.getDisplayValue()); + const results = callDeeplTranslateApi_(texts, sourceLang, targetLang, options); + const billedCharacters = results.reduce((sum, r) => sum + r.billedCharacters, 0); + for (let i = 0; i < cellsToTranslate.length; i++) { + cellsToTranslate[i].setValue(results[i].text); + } + return { translated: cellsToTranslate.length, skipped, failed: 0, error: '', billedCharacters }; + } catch (e) { + const lastError = e.message || String(e); + Logger.log(`DeepLcom/google-sheets-example/${scriptVersion}: translateSelectionFromSidebar error: ${lastError}`); + return { translated: 0, skipped, failed: cellsToTranslate.length, error: lastError, billedCharacters: 0 }; + } +} + +/** + * Returns API usage as a structured object for display in the sidebar. + * Called from the sidebar via google.script.run. + * @return {{charCount: number, charLimit: number}} + */ +function getUsageForSidebar() { + const response = httpRequestWithRetries_('get', '/v2/usage'); + checkResponse_(response); + const obj = JSON.parse(response.getContentText()); + if (obj.character_count === undefined || obj.character_limit === undefined) + throw new Error('Character usage not found in API response.'); + return { charCount: obj.character_count, charLimit: obj.character_limit }; +} + +/** + * Returns all saved sidebar options, or null if none have been saved yet. + * Called from the sidebar on load. + * @return {{sourceLang, targetLang, formality, context, glossaryId, styleId, customInstructions, modelType}|null} + */ +function getSavedOptions() { + const props = PropertiesService.getScriptProperties(); + const targetLang = props.getProperty('DEEPL_LAST_TARGET_LANG'); + if (!targetLang) return null; + return { + sourceLang: props.getProperty('DEEPL_LAST_SOURCE_LANG') || '', + targetLang, + formality: props.getProperty('DEEPL_LAST_FORMALITY') || '', + context: props.getProperty('DEEPL_LAST_CONTEXT') || '', + glossaryId: props.getProperty('DEEPL_LAST_GLOSSARY_ID') || '', + styleId: props.getProperty('DEEPL_LAST_STYLE_ID') || '', + customInstructions: props.getProperty('DEEPL_LAST_CUSTOM_INSTRUCTIONS') || '', + modelType: props.getProperty('DEEPL_LAST_MODEL_TYPE') || '', + extraOptions: props.getProperty('DEEPL_LAST_EXTRA_OPTIONS') || '', + }; +} + +/** + * Returns the last 4 characters of the saved API key, for display in the sidebar. + * @return {string|null} + */ +function getApiKeySuffix() { + const key = getApiKey_(); + return key ? key.slice(-4) : null; +} + +/** + * Returns whether an API key is currently saved in Script Properties. + * Called from the sidebar on load. + * @return {boolean} + */ +function hasApiKey() { + return !!PropertiesService.getScriptProperties().getProperty('DEEPL_API_KEY'); +} + +/** + * Saves the given API key to Script Properties and reloads the cached constant. + * Called from the sidebar via google.script.run. + * @param {string} key + */ +function saveApiKey(key) { + if (!key || !key.trim()) throw new Error('API key must not be empty.'); + PropertiesService.getScriptProperties().setProperty('DEEPL_API_KEY', key.trim()); +} + +/** + * Deletes the saved API key from Script Properties. + * Called from the sidebar via google.script.run. + */ +function clearApiKey() { + PropertiesService.getScriptProperties().deleteProperty('DEEPL_API_KEY'); +} + +/** + * Calls the DeepL translate API for a single text string. + * Shared by the formula function and the sidebar/menu actions. + * + * @param {string} text The text to translate. + * @param {string|null} sourceLang Source language code, or null/falsy for auto-detect. + * @param {string} targetLang Target language code. + * @param {string|null} glossaryId Glossary ID, or null to omit. + * @return {string} Translated text. + */ +function callDeeplTranslateApi_(texts, sourceLang, targetLang, options) { + options = options || {}; + const body = { text: texts, target_lang: targetLang, show_billed_characters: true }; + if (sourceLang) body.source_lang = sourceLang; + if (options.glossaryId) body.glossary_id = options.glossaryId; + if (options.formality) body.formality = options.formality; + if (options.context) body.context = options.context; + if (options.styleId) body.style_id = options.styleId; + if (options.customInstructions && options.customInstructions.length) + body.custom_instructions = options.customInstructions; + if (options.modelType) body.model_type = options.modelType; + if (options.extraOptions) { + const raw = options.extraOptions.trim(); + if (raw.startsWith('{')) { + let parsed; + try { parsed = JSON.parse(raw); } catch (e) { + throw new Error('Extra options: invalid JSON — ' + e.message); + } + Object.assign(body, parsed); + } else { + for (const line of raw.split('\n')) { + const idx = line.indexOf('='); + if (idx > 0) { + const key = line.slice(0, idx).trim(); + const val = line.slice(idx + 1).trim(); + if (key) body[key] = val; + } + } + } + } + const totalChars = texts.reduce((sum, t) => sum + t.length, 0); + const response = httpRequestWithRetries_('post', '/v2/translate', body, totalChars, true); + checkResponse_(response); + return JSON.parse(response.getContentText()).translations + .map(t => ({ text: t.text, billedCharacters: t.billed_characters || 0 })); +} + +function getApiKey_() { + return PropertiesService.getScriptProperties().getProperty('DEEPL_API_KEY'); +} /** * Translates from one language to another using the DeepL Translation API. @@ -56,6 +285,9 @@ function DeepLTranslate(input, glossaryId, options ) { + if (!enableFormulaFunctions) { + throw new Error('Formula functions are disabled. Set enableFormulaFunctions = true in DeepL.gs to enable them. See FORMULA_FUNCTIONS.md for details and cost implications.'); + } if (input === undefined) { throw new Error("input field is undefined, please specify the text to translate."); } else if (typeof input === "number") { @@ -118,6 +350,9 @@ function DeepLTranslate(input, * @customfunction */ function DeepLUsage(type) { + if (!enableFormulaFunctions) { + throw new Error('Formula functions are disabled. Set enableFormulaFunctions = true in DeepL.gs to enable them. See FORMULA_FUNCTIONS.md for details and cost implications.'); + } const response = httpRequestWithRetries_('get', '/v2/usage'); checkResponse_(response); const responseObject = JSON.parse(response.getContentText()); @@ -201,8 +436,12 @@ function checkResponse_(response) { /** * Helper function to execute HTTP requests and retry failed requests. */ -function httpRequestWithRetries_(method, relativeUrl, formData = null, charCount = 0) { - const baseUrl = deeplApiKey.endsWith(':fx') +function httpRequestWithRetries_(method, relativeUrl, formData = null, charCount = 0, useJson = false) { + const apiKey = getApiKey_(); + if (!apiKey) { + throw new Error('DeepL API key not set. Use the DeepL sidebar to add your API key.'); + } + const baseUrl = apiKey.endsWith(':fx') ? 'https://api-free.deepl.com' : 'https://api.deepl.com'; const url = baseUrl + relativeUrl; @@ -210,10 +449,17 @@ function httpRequestWithRetries_(method, relativeUrl, formData = null, charCount method: method, muteHttpExceptions: true, headers: { - 'Authorization': 'DeepL-Auth-Key ' + deeplApiKey, + 'Authorization': 'DeepL-Auth-Key ' + apiKey, }, }; - if (formData) params.payload = formData; + if (formData) { + if (useJson) { + params.contentType = 'application/json'; + params.payload = JSON.stringify(formData); + } else { + params.payload = formData; + } + } let response = null; for (let numRetries = 0; numRetries < 5; numRetries++) { const lastRequestTime = Date.now(); diff --git a/DeepLSidebar.html b/DeepLSidebar.html new file mode 100644 index 0000000..d3729ba --- /dev/null +++ b/DeepLSidebar.html @@ -0,0 +1,724 @@ + + + + + + + + +
Loading…
+ + +
+

Connect your DeepL account

+

+ Enter your DeepL API key to get started. You can find it in your + DeepL account. +

+ +
+ + +
+ +
+
+ + +
+ + + + + + + + + +
+ Options +
+ + + + + Formality is not supported for this language and will be ignored. + + + + + +
+
+ +
+ Customization +
+ + + + + + + + + + Up to 10 instructions, 300 characters each. + +
+
+ +
+ Advanced +
+ + + + + + + Accepts key=value lines or a JSON object. Sent directly to the DeepL API. + +
+
+ + +
+ +
+ +
+ +
+
+
+
+ Loading… + +
+
+ +
+ +
+ Settings +
+
Active key: ·······
+ +
+ + +
+ +
+ +
+
+ +
+ + + + + + diff --git a/FORMULA_FUNCTIONS.md b/FORMULA_FUNCTIONS.md new file mode 100644 index 0000000..7b3b452 --- /dev/null +++ b/FORMULA_FUNCTIONS.md @@ -0,0 +1,155 @@ +# DeepL Formula Functions + +The script includes two spreadsheet formula functions — `DeepLTranslate()` and +`DeepLUsage()` — that let you translate cell values directly using formulas. + +These functions are included for backward-compatibility with the original +version of the script, where they are replaced by the sidebar. +They are not required for the latest version. + +These functions are **disabled by default**. Before enabling them, read the cost +warning below. + +## Enabling formula functions + +Open **Extensions → Apps Script**, find this line near the top of `DeepL.gs`, +and change `false` to `true`: + +```js +const enableFormulaFunctions = false; +``` + +Save the script and reload your sheet. The functions will then be available. + +## Cost warning + +Formula cells are recalculated every time the sheet is reopened. If your sheet +contains `DeepLTranslate()` formulas, **reopening it will re-translate every +formula cell and charge those characters against your quota**, even if the source +text has not changed. + +DeepL API Pro subscribers can set a monthly cost control limit to cap unexpected +charges. [Instructions are in the DeepL help center][cost-control]. Free tier +accounts are limited to 500,000 characters per month. + +See [Re-translation workarounds](#re-translation-workarounds) below for ways to +mitigate this. + +The sidebar approach (described in the main [README](README.md)) does not have +this problem — it writes static values that are never recalculated — so it is +recommended for most users. + +## DeepLTranslate + +Translates text from one language to another. + +``` +=DeepLTranslate(input, [sourceLang], [targetLang], [glossaryId], [options]) +``` + +| Parameter | Description | +|---|---| +| `input` | The text to translate (required). | +| `sourceLang` | Source language code, e.g. `"EN"`. Use `"auto"` or omit to auto-detect. | +| `targetLang` | Target language code, e.g. `"DE"`. Defaults to your system language if omitted. | +| `glossaryId` | ID of a DeepL glossary to apply. Requires `sourceLang` to also be set. | +| `options` | A two-column range or inline array of additional API options (see below). | + +### Examples + +``` +=DeepLTranslate("Bonjour!") + → "Hello!" (or equivalent in your system language) + +=DeepLTranslate("Guten Tag", "auto", "FR") + → "Bonjour" + +=DeepLTranslate("Hello", "EN", "DE", "61a74456-b47c-48a2-8271-bbfd5e8152af") + → "Moin" (using a glossary) +``` + +### Additional options + +Pass extra DeepL API parameters via a two-column range where the first column +is the option name and the second is the value: + +``` +=DeepLTranslate(A1,,"DE",,C2:D4) +``` + +![Additional options example](docs/DeepL_Function_Call_Additional_Options.png) + +When translating multiple cells, make the options reference absolute: + +``` +=DeepLTranslate(A1,,"DE",,$C$2:$D$4) +``` + +Options can also be passed inline: + +``` +=DeepLTranslate(A1,,"DE",,{"tag_handling","xml";"ignore_tags","ignore,a,b,c"}) +``` + +## DeepLUsage + +Returns API character usage for the current billing period. + +``` +=DeepLUsage([type]) +``` + +| Parameter | Description | +|---|---| +| `type` | Omit for a summary string. Pass `"count"` for the number used, `"limit"` for the monthly limit. | + +### Examples + +``` +=DeepLUsage() + → "106691 of 500000 characters used." + +=DeepLUsage("count") + → 106691 + +=DeepLUsage("limit") + → 500000 +``` + +## Re-translation workarounds + +### Paste special — Values only + +After translating with `DeepLTranslate`, copy the cell and use +**Edit → Paste special → Values only** to replace the formula with plain text. +The cell will no longer recalculate. + +![Using Paste special -> Values only](docs/Google_Paste_Values.png) + +This also works on a range: select all translated cells, copy, then paste +values only. + +### Set up Cost Control + +DeepL API Pro subscribers can cap monthly spend in their account settings. +[Instructions are in the DeepL help center][cost-control]. + +### disableTranslations flag + +Set `disableTranslations = true` in `DeepL.gs` to prevent all formula +translations without removing the formulas from cells. Existing translated +values will be returned as-is. + +### activateAutoDetect flag + +Set `activateAutoDetect = true` in `DeepL.gs` to enable automatic detection of +cell recalculations. When active, cells that already have a translated value +will not be re-translated on reopen. This is disabled by default because it is +not fully reliable. + +### Remove the API key + +Removing `DEEPL_API_KEY` from Script Properties will cause all formula +functions to error, preventing any translations — and any charges. + +[cost-control]: https://support.deepl.com/hc/en-us/articles/360020685580-Cost-control diff --git a/README.md b/README.md index 280aa0e..92f3719 100644 --- a/README.md +++ b/README.md @@ -1,306 +1,120 @@ -# DeepL API - Google Sheets Example +# DeepL API - Google Sheets Example (Sidebar UI) -In the past few months, we've gotten a lot of requests for code samples or -example projects for the DeepL API. We think this is a great idea! This Google -Sheets example is the first such code sample that we've released, and we hope it -can serve as inspiration or help you as you work through your own project. +This branch adds a **sidebar UI** to the Google Sheets DeepL integration. Select +cells in your sheet, open the DeepL sidebar, choose your translation options, and +click **Translate** — results are written back as plain text values, so they are +never re-translated when the sheet is reopened. **Disclaimer**: the DeepL support team does *not* provide support for this -example project. Please keep this in mind when deciding whether to use it. - -Instructions for getting started are below. If you have any questions or -comments, please [create a GitHub issue][issues] to let us know. We'd be happy -to hear your feedback. +example project. If you have questions or feedback, please +[open a GitHub issue][issues]. ## Requirements -### DeepL API Authentication Key - -To use this plugin, you'll need a DeepL API authentication key. To get a key, -[please create an account here][pro-account]. With a DeepL API Free account, you -can translate up to 500,000 characters/month for free. - -### Google Account - -You'll also need a Google account to use Google Sheets. Please ensure you comply -with all applicable terms of service when using third-party products such as -Google Sheets. - -## Cost Control and API Consumption Disclaimer - -While DeepL's Free API accounts allow you to translate up to 500,000 characters -per month for free, our Pro API accounts include a monthly base price + -pay-as-you-go usage pricing. [You can see pricing details here][pro-account]. - -**Important note:** there's a known issue with the add-on where re-opening an -existing Sheet that contains DeepL API add-on formulas will "re-translate" all -cells, and these re-translations will count against your API character -consumption. - -We've built a couple of workarounds into the script (trying to "detect" when -cells have already been translated, adding a flag a user can set in the script -to disable re-translation altogether), but we haven't yet figured out an ideal -solution. Ideas are welcome! - -We also know that in Google Sheets, because a formula can be copied and pasted -with just a few keystrokes, it can be easy to translate a lot of characters -very quickly — and maybe translate more than you intended. - -In the [Re-translation Workarounds](#re-translation-workarounds) section below, -we explain some methods to avoid this. - -**Please review these guidelines** if you plan to use the add-on! We don't want -anyone to unintentionally translate more than they'd planned. +- A **DeepL API authentication key**. [Create a free account here][pro-account] + — the Free tier allows up to 500,000 characters/month. +- A **Google account** to use Google Sheets. ## Setup -1. In your Google Sheet, from the "Extensions" menu select "Apps Script" to open - the Apps Script editor. -2. Create a script file named `DeepL.gs`, copy the contents of the - [DeepL.gs][deepl-gs-raw] file in this repo into it, and save the script file. - Note you do not need to modify the file. -3. Open the Project settings (the gear icon in the left panel) and scroll down to - the "Script properties" section. -4. Edit the script properties to add a new property `DEEPL_API_KEY` with the value - containing your DeepL API authentication key, and save the script properties. -5. Close the Apps Script settings and return to your sheet. -6. Use the `DeepLTranslate` and `DeepLUsage` functions as explained in - [Usage](#usage). - -You should review the [Re-translation Workarounds](#re-translation-workarounds) -to avoid translating more than you intend. - -### Setup tutorial - -These instructions were written so that non-developers can also use the add-on -🙂. - -This guide walks you through setup using a new, blank Google Sheet. But you can -also use the add-on with an existing Sheet (including if that Sheet already has -App Scripts). In the case of a sheet that already has App Scripts, you'd simply -need to add a new Apps Script file (e.g. named "DeepL.gs") and add the code -provided below. - -__Create a new Google Sheet. In the top toolbar, click on "Extensions" then "Apps Script".__ - -![Extensions menu -> App Script button](docs/Select_Extensions_AppScript.png) - -A new Apps Script tab will open. It should look something like this: - -![Apps Script tab](docs/AppScript_Page_With_Placeholder.png) +### Option A — Copy the template sheet (recommended) -__Delete the function myFunction()... placeholder code so that this "Code.gs"__ -__section on the Apps Script tab is completely empty.__ +DeepL maintains a ready-to-use template Google Sheet with the script already +embedded. No coding required. -![Deleting the placeholder code](docs/AppScript_Page_Deleted_Placeholder.png) +1. Open the [DeepL for Google Sheets template][template-sheet] *(link to be + added when the template is published)*. +2. Click **File → Make a copy**. Give it a name and save it to your Drive. +3. Open your copy and click **DeepL → Open sidebar** in the toolbar. +4. Enter your DeepL API key when prompted. The sidebar verifies the key and + takes you straight to the translation UI. -Replace the "Code.gs" section in the Apps Script tab with the contents of the -"DeepL.gs" file in this git repository. -[Click here][deepl-gs-raw] to get the raw contents from GitHub, and copy and -paste the contents into the Apps Script tab. +### Option B — Manual installation -__Go to deepl.com and sign in to your DeepL API account__ +Use this if you want to add DeepL to an existing sheet, or prefer to install +from source. -If you don't yet have a DeepL API account, [please create one here][pro-account]. +1. In your Google Sheet, go to **Extensions → Apps Script**. +2. Click **+** next to "Files", choose **Script**, name it `DeepL` and paste in the contents of + [DeepL.gs][deepl-gs-raw]. +3. Add another file, choose **HTML**, name it `DeepLSidebar`, and paste in the contents of + [DeepLSidebar.html][deepl-sidebar-html-raw]. **Save the changes**. +4. Close the Apps Script tab and reload your sheet. A **DeepL** menu will + appear in the toolbar. +5. Click **DeepL → Open sidebar** and enter your API key when prompted. -![Login to DeepL API account](docs/DeepL_API_Login.png) +#### Updating -__Go to the API keys & limits tab in your API account__ - -![DeepL API key tab](docs/DeepL_API_Key_Tab.png) - -__Click on `Create key` to generate a new key.__ - -Name your key to note that this key is for Google Sheets translation. - -After the key is created, copy your authentication key. - -![Create API key](docs/DeepL_Create_API_Key.png) - -__Go back to the Apps Script tab. Open the project settings (the gear icon in -the left panel) and scroll down to the "Script properties" section.__ - -![Set script properties](docs/Google_Set_ScriptProperties.png) - -Edit the script properties to add a new property named `DEEPL_API_KEY` and paste -the copied DeepL API key into the value box. Then click `Save script properties`. - -__Rename your Apps Script project__ - -Click on the "Untitled project" title and give the project a new name. You can -use any name you like. - -![Rename the Apps Script project](docs/AppScript_Rename_Project.png) - -__Click on the "Save" icon in the Apps Script toolbar__ - -![Save the Apps Script project](docs/AppScript_Save_Project.png) - -You can now close the Apps Script tab and navigate back to the Sheet you created -at the start of setup. Let's get translating! +To check for updates, see the [CHANGELOG][changelog]. To update, open +**Extensions → Apps Script** and replace the contents of `DeepL.gs` and +`DeepLSidebar.html` with the latest versions (links above). Save and reload +your sheet. Your API key and saved options are stored in Script Properties and +are unaffected by updates. ## Usage -The example includes two functions: `DeepLTranslate` and `DeepLUsage`. - -Each function has "pop-up" documentation that you'll see when you start typing -it into a cell in your sheet. - -![Popup documentation for DeepLTranslate function](docs/DeepL_Translate_Popup_Full.png) -![Popup documentation for DeepLUsage function](docs/DeepL_Usage_Popup_Full.png) - -Note that you cannot create glossaries using this Google Sheets add-on. You can -only reference glossary IDs of glossaries that were already created with the -DeepL API. - -In addition, here are some examples that might help you get started. +1. **Select** one or more cells containing the text you want to translate. +2. Click **DeepL → Open sidebar**. +3. Set your options: -``` -=DeepLTranslate("Bonjour!") - “Hello!” (or equivalent in your system language) + **Options** (always visible) -DeepLTranslate("Guten Tag", "auto", "FR") - “Bonjour” + | Option | Description | + |---|---| + | Source language | Language of the input text. Leave as Auto-detect if unsure. Choose *Other* to enter any BCP-47 code. | + | Target language | Language to translate into. Choose *Other* to enter any BCP-47 code. | + | Formality | Default / Formal / Informal. A note appears when the target language does not support this setting. | + | Context | Optional hint to disambiguate the text (e.g. *"product listing for a luxury watch"*). Not translated. | -=DeepLTranslate("Hello", "en", "de", "61a74456-b47c-48a2-8271-bbfd5e8152af") - “Moin” (translating using a glossary) + **Customization** (collapsible) -=DeepLUsage() - “106691 of 500000 characters used.” + | Option | Description | + |---|---| + | Glossary ID | ID of a DeepL glossary to apply. | + | Style rule ID | ID of a DeepL style rule list to apply. | + | Custom instructions | Up to 10 plain-language instructions, one per line (e.g. *"Use gender-neutral language"*). | -=DeepLUsage("count") - 106691 -``` + **Advanced** (collapsible) -### Usage Tutorial + | Option | Description | + |---|---| + | Model | Default, Quality optimized, Prefer quality, or Speed optimized. | + | Extra API options | Additional DeepL API parameters as `key=value` lines or a JSON object. | -Type some sample source text into cells A1 and A2. +4. Click **Translate**. Results are written back as plain text and will not be + re-translated when the sheet is reopened. -I'll use the following sentences: -* "The weather sure is nice today." -* "I wonder if it's supposed to rain later this week." +All settings are saved and restored automatically between sessions. -![Example sentences for using DeepL plugin](docs/DeepL_Plugin_Example_Sentences.png) +The sidebar shows a **usage bar** for the current billing period, updated after +each translation. Your API key can be replaced or cleared at any time from the +**Settings** section at the bottom of the sidebar. -In cell B1, type `=DeepLTranslate(` to start using the DeepL function we created. +The installed version is shown in the sidebar footer and under **DeepL → About**. -![Typing in the DeepLTranslate formula](docs/DeepL_Translate_Function_Start.png) +## Formula functions -We'll use the following parameters: -* `input`: A1 (cell A1—but you can also type in your own text) -* `source_lang`: "auto" (DeepL will auto-detect the source language) -* `target_lang`: "DE" (German—or feel free to select a 2-letter language code of - your choice from the [target_lang section on this page][api-languages]) -* `glossary_id`: We'll skip this parameter, as we aren't using a glossary in - this example. +The script still includes the former `DeepLTranslate()` and `DeepLUsage()` spreadsheet +formula functions, for backward compatibility. These are **disabled by default** — read +[FORMULA_FUNCTIONS.md][formula-functions] for usage details, cost implications, +and instructions to enable them. -The resulting function call will look like this: +## Contributing -```=DeepLTranslate(A1, "auto", "DE")``` +We welcome feedback and contributions. Please [open an issue][issues] or +[submit a pull request][pull-requests]. -Press enter to run the function. - -Success! Cell A1 was translated into German. - -![Translating the first cell](docs/DeepL_Function_Call_1.png) - -To translate our second cell of source text, you can copy cell B1 and paste it -into B2. - -![Translating the second cell](docs/DeepL_Function_Call_2.png) - -Congrats! You've reached the end of this tutorial. Happy translating! - -### Additional options - -The `DeepLTranslate()` function allows you to specify additional DeepL API -options to include in your translation requests. This allows you to specify tag -handling options, sentence-splitting, and so on. - -The fifth argument to `DeepLTranslate()` accepts the options specified as a -range with two columns: the option name and option values. You can specify the -options somewhere in the sheet and refer to them in your translations, as shown -in the following example: - -```=DeepLTranslate(A1,,"de",,C2:D4)``` - -![Translating the second cell](docs/DeepL_Function_Call_Additional_Options.png) - -Note that the `source_lang` and `glossary_id` parameters are not used in this -example, so they are empty. - -If you are translating multiple cells, you may want to make the reference to the -options absolute (`$C$2:$D$4`). - -#### Inline formula options - -You can also pass the options to the DeepLTranslate function directly using the -`{opt1, val1; opt2, val2; ..}` syntax, for example: - -```=DeepLTranslate(A1,,"de",,{"tag_handling", "xml"; "ignore_tags", "ignore,a,b,c"})``` - -## Re-translation Workarounds - -### Set up Cost Control - -DeepL API Pro subscribers can activate Cost Control in their account. -[Instructions for activating cost control are available in the DeepL help center][cost-control]. - -If you're a DeepL API Pro subscriber, we recommend setting a cost control limit -if you have a firm monthly budget for your DeepL API usage. - -Cost Control is not available to DeepL API Free users; they are limited to -500,000 characters per month. - -### Copy-Paste "Values only" - -After you have used the `DeepLTranslate` function to get a translation in a -cell, you can use this workaround to "freeze" the result, so that it will not -be re-translated. - -Copy the cell you want to freeze, then use "Paste special", "Values only" on -the same cell. You can also do this with multiple cells in a range. - -For example, after translating into cells B1 and B2, we can freeze their results. -Copying cells B1 and B2, then in the "Edit" menu, go to "Paste special", then -click on "Values only". -You can also use the keyboard shortcut applicable to your operating system. - -![Using Paste special -> Values only](docs/Google_Paste_Values.png) - -### Remove DeepL API key from script properties - -To eliminate the possibility of re-translating cells, you can remove the -DeepL API key from the script properties. - -### Script `disableTranslations` Flag - -At the top of the provided script ([DeepL.gs](DeepL.gs)), there is a -`disableTranslations` variable to disable all translations. If you set it to -`true` and save the script, the `DeepLTranslate` function will be disabled and -already-translated cells will not be re-translated. - -### Built-in re-translation detection - -Automatic re-translation detection is built-in to the script, however it is -disabled by default because unfortunately tests have shown the detection -technique used is not fully reliable. - -There is an `activateAutoDetect` variable at the top of the provided script -([DeepL.gs](DeepL.gs)) to activate the automatic re-translation detection. Set -it to `true` to enable this feature. +[api-languages]: https://www.deepl.com/docs-api/translating-text?utm_source=github&utm_content=google-sheets-plugin-readme&utm_medium=readme -## Contributing feedback and improvements +[template-sheet]: https://docs.google.com/spreadsheets/d/1VVMDPYV7oL7ZM51RFUBDmmXnXRjYcMeiPx4gw5zAOgc/edit?usp=sharing -We welcome your feedback and questions about the project. Please feel free -to [create a GitHub issue][issues] to get in touch with us. +[deepl-gs-raw]: https://raw.githubusercontent.com/DeepLcom/google-sheets-example/feat/sidebar-ui-addon/DeepL.gs -If you'd like to contribute to the project, please open a [pull request][pull-requests]. -Our contributing guidelines apply to all contributions. +[deepl-sidebar-html-raw]: https://raw.githubusercontent.com/DeepLcom/google-sheets-example/feat/sidebar-ui-addon/DeepLSidebar.html -[api-languages]: https://www.deepl.com/docs-api/translating-text?utm_source=github&utm_content=google-sheets-plugin-readme&utm_medium=readme +[formula-functions]: FORMULA_FUNCTIONS.md -[deepl-gs-raw]: https://raw.githubusercontent.com/DeepLcom/google-sheets-example/main/DeepL.gs +[changelog]: https://github.com/DeepLcom/google-sheets-example/blob/main/CHANGELOG.md [issues]: https://github.com/DeepLcom/google-sheets-example/issues @@ -309,4 +123,3 @@ Our contributing guidelines apply to all contributions. [pro-account]: https://www.deepl.com/pro?utm_source=github&utm_content=google-sheets-plugin-readme&utm_medium=readme#developer [cost-control]: https://support.deepl.com/hc/en-us/articles/360020685580-Cost-control -