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)
+```
+
+
+
+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.
+
+
+
+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".__
-
-
-
-A new Apps Script tab will open. It should look something like this:
-
-
+### 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.
-
+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.
-
+#### Updating
-__Go to the API keys & limits tab in your API account__
-
-
-
-__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.
-
-
-
-__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.__
-
-
-
-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.
-
-
-
-__Click on the "Save" icon in the Apps Script toolbar__
-
-
-
-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.
-
-
-
-
-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.
-
+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**.
-
+## 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.
-
-
-
-To translate our second cell of source text, you can copy cell B1 and paste it
-into B2.
-
-
-
-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)```
-
-
-
-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.
-
-
-
-### 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
-