diff --git a/.gitattributes b/.gitattributes index 9ebfaf5..f60804a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ /.gitignore export-ignore /build.sh export-ignore /builds/ export-ignore +/.github/ export-ignore diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..27f1ad0 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,38 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + +jobs: + php: + runs-on: ubuntu-latest + strategy: + matrix: + # Oldest supported (README: PHP 7.0+) and current. + php: ['7.0', '8.4'] + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: none + - name: Syntax check + run: find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l + + js: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Syntax check + run: for f in admin/js/*.js; do node --check "$f"; done + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build ZIP (checks versions match and bundles the update checker) + run: ./build.sh + - name: Confirm update checker is bundled + run: unzip -l builds/everyalt-*.zip | grep -q 'everyalt/vendor/plugin-update-checker/plugin-update-checker.php' diff --git a/README.md b/README.md index 8adc703..499fd9d 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ # 🖼️ EveryAlt — Alt text for every image. **Zero limits. Totally free.**
- 🆓 100% free · ♾️ No limits from us · 🔑 Your OpenAI key + 🆓 100% free · ♾️ No limits from us · 🔑 Your AI key: OpenAI, Gemini, or DeepInfra
-> **EveryAlt 1.0 is 100% free.** Bring your own [OpenAI API key](https://platform.openai.com/api-keys), and generate as much alt text as you want. We don’t meter you, cap you, or bill you. **Ever.** +> **EveryAlt 1.0 is 100% free.** Bring your own API key from **OpenAI**, **Google Gemini**, or **DeepInfra**, and generate as much alt text as you want. We don’t meter you, cap you, or bill you. **Ever.** --- @@ -28,10 +28,10 @@ No more blank alt fields. No more guessing. **You upload. EveryAlt writes.** | Before | **EveryAlt 1.0** | |--------|------------------| | Limits, tiers, or paywalls | **Unlimited** — your key, your usage | -| Extra service to sign up for | **Just OpenAI** — one key, one place | -| Opaque pricing | **Transparent** — you pay OpenAI only | +| Extra service to sign up for | **Your choice of provider** — one key, no EveryAlt account | +| Opaque pricing | **Transparent** — you pay your AI provider only | -We don’t charge. We don’t sit in the middle. You use **your** OpenAI key. EveryAlt stays **free**, and you have **no limits** from us. +We don’t charge. We don’t sit in the middle. You use **your** own key. EveryAlt stays **free**, and you have **no limits** from us. --- @@ -55,19 +55,61 @@ Works on localhost, behind HTTP auth, and with your existing workflow. 1. **Upload** the plugin to `wp-content/plugins/everyalt` (or install via WordPress admin). 2. **Activate** the plugin (Plugins → EveryAlt → Activate). -3. **Open Media → EveryAlt** and add your [OpenAI API key](https://platform.openai.com/api-keys) in **Settings**. +3. **Open Media → EveryAlt → Settings**, pick an **AI model**, and paste the API key for that provider (see [Models & keys](#-models--keys)). 4. Turn on **Auto-generate on upload** if you want, or use **Bulk** / **Review** to handle existing images. That’s it. No account with us. No caps. No “upgrade to unlock more.” --- -## 🔑 Your key, your usage +## 🤖 Models & keys -- Alt text is generated by **OpenAI** (image is sent as base64; works on localhost and behind HTTP auth). -- Your API key is **stored encrypted** in your WordPress database. -- **You** are billed by OpenAI for your usage; EveryAlt does not charge you. -- We use a small, efficient model so cost per image stays low. +Choose a model in **Settings**. Every model reads the image itself and writes the alt text (and titles). You only need a key for the provider you pick, and you can store keys for several providers and switch any time. + +| Model | Provider | Input / 1M tokens | Output / 1M tokens | +|-------|----------|------------------:|-------------------:| +| **GPT-5.4 nano** (default) | OpenAI | $0.20 | $1.25 | +| **Gemini 3.1 Flash-Lite** | Google Gemini | $0.25 | $1.50 | +| **DeepSeek V4.1 Flash** | DeepInfra | $0.20 | $0.60 | +| **GLM-5.3-Flash** | DeepInfra | $0.15 | $0.50 | + +Prices are each provider’s published regular rates as of September 2026 and can change: [OpenAI pricing](https://openai.com/api/pricing/) · [Gemini pricing](https://ai.google.dev/gemini-api/docs/pricing) · [DeepInfra pricing](https://deepinfra.com/pricing). The actual cost of every image is recorded on the **Logs** tab. + +> **Upgrading from 1.0.x?** Earlier versions used `gpt-5-nano`, which OpenAI is retiring. Existing installs switch to **GPT-5.4 nano** automatically. Your OpenAI key keeps working. + +### Getting a key + +**OpenAI** +1. Sign in at [platform.openai.com/api-keys](https://platform.openai.com/api-keys). +2. Click **Create new secret key**, name it (e.g. “EveryAlt”), and copy it. It is only shown once. +3. Add a payment method or prepaid credits under billing; generation fails until the account has credit. + +OpenAI does not train on API data by default ([details](https://openai.com/enterprise-privacy/)). + +**Google Gemini** +1. Sign in to Google AI Studio at [aistudio.google.com/apikey](https://aistudio.google.com/apikey). +2. Click **Create API key**, pick or create a Google Cloud project, and copy the key. +3. Optional: enable billing on the project to use the paid tier. + +Gemini has a free tier, but Google may use free-tier content to improve its products; paid-tier content is not used that way ([Gemini API terms](https://ai.google.dev/gemini-api/terms)). + +**DeepInfra** (one key covers both DeepSeek V4.1 Flash and GLM-5.3-Flash) +1. Sign in or sign up at [deepinfra.com/dash/api_keys](https://deepinfra.com/dash/api_keys). +2. Click **New API key**, name it, and copy it. +3. Add a payment method or credits in your DeepInfra billing settings. + +🔒 **Your images stay private with DeepInfra.** DeepInfra runs these models on its own infrastructure in data centers in the **US and Canada**, with **zero data retention**: images and generated text are processed in memory and not stored, request content is not logged, and your data is never used for training. See [DeepInfra data privacy](https://docs.deepinfra.com/account/data-privacy), the [privacy policy](https://deepinfra.com/privacy), and the [trust center](https://trust.deepinfra.com/) (SOC 2 and ISO 27001). + +### Your key, your usage + +- The image is sent to your chosen provider as base64, so it works on localhost and behind HTTP auth. +- API keys are **stored encrypted** in your WordPress database. +- **You** are billed by your provider for usage; EveryAlt does not charge you. + +### For developers + +- `everyalt_model` filters the model ID sent to the provider: `( string $model_id, string $model_slug )`. This replaces `everyalt_openai_model`. +- `everyalt_input_token_price_per_million` / `everyalt_output_token_price_per_million` override the prices used for cost estimates: `( float $price, string $model_slug )`. --- @@ -75,7 +117,7 @@ That’s it. No account with us. No caps. No “upgrade to unlock more.” - WordPress 5.5+ - PHP 7.0+ -- An [OpenAI API key](https://platform.openai.com/api-keys) with API access +- An API key from [OpenAI](https://platform.openai.com/api-keys), [Google Gemini](https://aistudio.google.com/apikey), or [DeepInfra](https://deepinfra.com/dash/api_keys) --- @@ -99,6 +141,23 @@ The plugin uses the `everyalt` text domain and ships with a `.pot` in `languages ## 📝 Changelog +### Unreleased +- **New: choose your AI model.** Pick from **GPT-5.4 nano** (OpenAI), **Gemini 3.1 Flash-Lite** (Google), or **DeepSeek V4.1 Flash** / **GLM-5.3-Flash** (DeepInfra) in Settings. Keys are stored per provider, so you can switch without re-entering them. Settings shows each model's published pricing, step-by-step key instructions, and privacy details, including DeepInfra's US/Canada hosting and zero data retention. +- **Changed** OpenAI requests now use **GPT-5.4 nano**; `gpt-5-nano` is being retired. Existing installs switch automatically. +- **Added** a Model column to the Logs tab and CSV export. +- **Developer:** the `everyalt_openai_model` filter is replaced by `everyalt_model`; the price filters now also receive the model slug. +- **Fixed** automatic updates: release ZIPs now bundle the update checker library (it was missing from ZIPs built with `build.sh`). +- **Fixed** deactivating the plugin no longer deletes your API key and settings. Uninstalling now removes all EveryAlt data (settings, key, logs) instead of leaving it behind. +- **Fixed** "Auto-generate on upload" now starts checked on fresh installs, as intended. +- **Fixed** Editors and Authors can now use the block-editor and media-screen "Generate alt text" buttons on images they can edit, and failures now show a message instead of doing nothing. +- **Fixed** auto-generation on upload now runs exactly once per image, after all image sizes exist (it could previously retry once per image size after a failure). +- **Fixed** the 4MB limit now applies to the resized image actually sent to OpenAI, not the original upload, so large phone photos are no longer skipped. +- **Improved** the Bulk and Review tabs are paginated and much faster on large media libraries. +- **Improved** block-editor and media-screen button text is now translatable. +- **Added** a warning when the saved API key can no longer be decrypted (e.g. after the security keys in `wp-config.php` change). +- **Security** CSV log exports are protected against spreadsheet formula injection; the unused HTTP-auth username/password settings (the password was stored in plain text) have been removed and are deleted on update. +- **Removed** the unused `every_alt_logs` database table (dropped on uninstall) and the legacy `/get_tokens` REST endpoint. + ### 1.0.2 - **New — AI image titles.** EveryAlt now writes descriptive WordPress image **titles**, not just alt text: - Optional **Automatically generate image titles when images are uploaded** toggle in Settings. diff --git a/admin/class-everyalt-admin.php b/admin/class-everyalt-admin.php index 469bcdd..d5bb320 100644 --- a/admin/class-everyalt-admin.php +++ b/admin/class-everyalt-admin.php @@ -44,7 +44,7 @@ class Every_Alt_Admin { * @param string $message Short message * @param string $detail Optional longer detail (e.g. API error body) * @param string $usage Optional token usage (e.g. "Input Tokens: 193, Output Tokens: 12, Total: 205") - * @param string $cost Optional estimated cost in cents (e.g. "0.0123¢" for gpt-5-nano) + * @param string $cost Optional estimated cost in cents (e.g. "0.0123¢") */ private function every_alt_add_generation_log( $attachment_id, $status, $message, $detail = '', $usage = '', $cost = '' ) { $log = get_option( self::GENERATION_LOG_OPTION, array() ); @@ -56,6 +56,7 @@ private function every_alt_add_generation_log( $attachment_id, $status, $message 'detail' => $detail, 'usage' => $usage, 'cost' => $cost, + 'model' => $this->every_alt_selected_model_label(), ); array_unshift( $log, $entry ); $log = array_slice( $log, 0, self::GENERATION_LOG_MAX ); @@ -112,6 +113,7 @@ public function every_alt_export_logs_csv() { __( 'Time', 'everyalt' ), __( 'Attachment ID', 'everyalt' ), __( 'Status', 'everyalt' ), + __( 'Model', 'everyalt' ), __( 'Message / Alt text', 'everyalt' ), __( 'Cost (USD)', 'everyalt' ), __( 'Details', 'everyalt' ), @@ -134,48 +136,127 @@ public function every_alt_export_logs_csv() { isset( $entry['time'] ) ? $entry['time'] : '', isset( $entry['attachment_id'] ) ? $entry['attachment_id'] : '', $status_label, + isset( $entry['model'] ) ? $entry['model'] : '', isset( $entry['message'] ) ? $entry['message'] : '', $cost_usd, $detail, ); - fputcsv( $out, $row ); + fputcsv( $out, array_map( array( $this, 'every_alt_csv_safe_cell' ), $row ) ); } fclose( $out ); exit; } /** - * AJAX: Validate OpenAI API key (key from POST or stored key if empty). + * Neutralize spreadsheet formulas: model output or API errors starting with =, +, -, @ (or a tab/CR) + * would otherwise be evaluated when the CSV is opened in Excel or Sheets. + * + * @param mixed $value + * @return mixed + */ + private function every_alt_csv_safe_cell( $value ) { + if ( is_string( $value ) && $value !== '' && strpos( "=+-@\t\r", $value[0] ) !== false ) { + return "'" . $value; + } + return $value; + } + + /** + * One-time cleanup when the plugin version changes (updates do not re-run the activation hook). + */ + public function every_alt_maybe_upgrade() { + if ( get_option( 'every_alt_version' ) === $this->version ) { + return; + } + // Settings from earlier versions that are no longer used. The HTTP auth password was stored in plain text. + delete_option( 'every_alt_secret' ); + delete_option( 'every_alt_fulltext' ); + delete_option( 'every_alt_httpuser' ); + delete_option( 'every_alt_httpassword' ); + // Sites that already have a key have already chosen their auto-generate setting. + foreach ( Every_Alt_Providers::providers() as $provider ) { + if ( get_option( $provider['key_option'], '' ) !== '' ) { + delete_option( 'every_alt_do_auto_default' ); + break; + } + } + // gpt-5-nano is being retired; installs from before model selection move to GPT-5.4 nano. + add_option( Every_Alt_Providers::MODEL_OPTION, Every_Alt_Providers::DEFAULT_MODEL ); + update_option( 'every_alt_version', $this->version ); + } + + /** + * Warn admins when a key is stored but can no longer be decrypted (e.g. AUTH_KEY in wp-config.php changed). + */ + public function every_alt_key_decrypt_notice() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + $unreadable = array(); + foreach ( Every_Alt_Providers::providers() as $slug => $provider ) { + if ( get_option( $provider['key_option'], '' ) !== '' && Every_Alt_Providers::get_key( $slug ) === '' ) { + $unreadable[] = $provider['label']; + } + } + if ( ! $unreadable ) { + return; + } + $url = admin_url( 'upload.php?page=everyalt&tab=settings' ); + echo '' . wp_kses( + sprintf( + /* translators: 1: provider name(s), e.g. "OpenAI", 2: URL of the EveryAlt settings tab */ + __( 'EveryAlt can no longer read your saved %1$s API key, usually because the security keys in wp-config.php changed. Generation with that provider is paused until you re-enter the key.', 'everyalt' ), + esc_html( implode( ', ', $unreadable ) ), + esc_url( $url ) + ), + array( 'a' => array( 'href' => true ) ) + ) . '
Request failed.
'; + showResult(false, t('requestFailed', 'Request failed')); }); }); - } + }); // Bulk: Select all / Select none var selectAllBtn = document.getElementById('everyalt-bulk-select-all'); @@ -127,13 +146,13 @@ var success = data && data.success; var msg = success ? (data.alt_text || '') : (data && data.message) ? data.message : ''; if (statusEl) { - statusEl.textContent = success ? '\u2713 ' + (data.alt_text || '') : '\u2717 ' + (data.message || 'Error'); + statusEl.textContent = success ? '\u2713 ' + (data.alt_text || '') : '\u2717 ' + (data.message || t('error', 'Error')); statusEl.className = 'everyalt-bulk-item-status ' + (success ? 'success' : 'error'); } if (progressLog) { var li = document.createElement('li'); li.className = success ? 'success' : 'error'; - li.textContent = '#' + id + ': ' + (success ? (data.alt_text || '') : (data.message || 'Error')); + li.textContent = '#' + id + ': ' + (success ? (data.alt_text || '') : (data.message || t('error', 'Error'))); progressLog.appendChild(li); } if (success && itemEl) { @@ -146,7 +165,7 @@ .catch(function(err) { done++; updateProgress(); - var errMsg = err && err.message ? err.message : 'Request failed'; + var errMsg = err && err.message ? err.message : t('requestFailed', 'Request failed'); if (statusEl) { statusEl.textContent = '\u2717 ' + errMsg; statusEl.className = 'everyalt-bulk-item-status error'; @@ -177,13 +196,12 @@ btn.disabled = true; request('POST', '/everyalt-api/v1/save_alt', { media_id: mediaId, - log_id: 0, alt_text: altText }) .then(function() { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Saved!'; + statusEl.textContent = t('saved', 'Saved!'); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } @@ -191,7 +209,7 @@ .catch(function(err) { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Error: ' + (err && err.message ? err.message : 'Save failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('saveFailed', 'Save failed')); statusEl.className = 'everyalt-review-status error'; } }); @@ -213,13 +231,13 @@ if (data && data.success && data.alt_text !== undefined) { if (textarea) textarea.value = data.alt_text; if (statusEl) { - statusEl.textContent = 'Regenerated!'; + statusEl.textContent = t('regenerated', 'Regenerated!'); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } } else { if (statusEl) { - statusEl.textContent = 'Error: ' + (data && data.message ? data.message : 'Regenerate failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (data && data.message ? data.message : t('regenerateFailed', 'Regenerate failed')); statusEl.className = 'everyalt-review-status error'; } } @@ -227,7 +245,7 @@ .catch(function(err) { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Error: ' + (err && err.message ? err.message : 'Request failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('requestFailed', 'Request failed')); statusEl.className = 'everyalt-review-status error'; } }); @@ -293,13 +311,13 @@ updateProgress(); var success = data && data.success; if (statusEl) { - statusEl.textContent = success ? '✓ ' + (data.title || '') : '✗ ' + (data.message || 'Error'); + statusEl.textContent = success ? '✓ ' + (data.title || '') : '✗ ' + (data.message || t('error', 'Error')); statusEl.className = 'everyalt-bulk-item-status ' + (success ? 'success' : 'error'); } if (titleProgressLog) { var li = document.createElement('li'); li.className = success ? 'success' : 'error'; - li.textContent = '#' + id + ': ' + (success ? (data.title || '') : (data.message || 'Error')); + li.textContent = '#' + id + ': ' + (success ? (data.title || '') : (data.message || t('error', 'Error'))); titleProgressLog.appendChild(li); } if (success && itemEl) { @@ -312,7 +330,7 @@ .catch(function(err) { done++; updateProgress(); - var errMsg = err && err.message ? err.message : 'Request failed'; + var errMsg = err && err.message ? err.message : t('requestFailed', 'Request failed'); if (statusEl) { statusEl.textContent = '✗ ' + errMsg; statusEl.className = 'everyalt-bulk-item-status error'; @@ -348,7 +366,7 @@ .then(function() { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Saved!'; + statusEl.textContent = t('saved', 'Saved!'); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } @@ -356,7 +374,7 @@ .catch(function(err) { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Error: ' + (err && err.message ? err.message : 'Save failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('saveFailed', 'Save failed')); statusEl.className = 'everyalt-review-status error'; } }); @@ -378,13 +396,13 @@ if (data && data.success && data.title !== undefined) { if (textarea) textarea.value = data.title; if (statusEl) { - statusEl.textContent = 'Regenerated!'; + statusEl.textContent = t('regenerated', 'Regenerated!'); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } } else { if (statusEl) { - statusEl.textContent = 'Error: ' + (data && data.message ? data.message : 'Regenerate failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (data && data.message ? data.message : t('regenerateFailed', 'Regenerate failed')); statusEl.className = 'everyalt-review-status error'; } } @@ -392,7 +410,7 @@ .catch(function(err) { btn.disabled = false; if (statusEl) { - statusEl.textContent = 'Error: ' + (err && err.message ? err.message : 'Request failed'); + statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('requestFailed', 'Request failed')); statusEl.className = 'everyalt-review-status error'; } }); diff --git a/admin/js/everyalt-gutenberg-button.js b/admin/js/everyalt-gutenberg-button.js index fbf800f..fde3ab3 100644 --- a/admin/js/everyalt-gutenberg-button.js +++ b/admin/js/everyalt-gutenberg-button.js @@ -14,6 +14,18 @@ var PanelBody = wp.components.PanelBody; var Button = wp.components.Button; + var i18n = (typeof everyaltBlock !== 'undefined' && everyaltBlock.i18n) || {}; + function t(key, fallback) { return i18n[key] || fallback; } + + // Show a dismissible snackbar/notice in the editor; failures used to be swallowed silently. + function notify(type, message) { + if (!wp.data || !wp.data.dispatch) return; + var notices = wp.data.dispatch('core/notices'); + if (!notices) return; + var create = type === 'error' ? notices.createErrorNotice : notices.createSuccessNotice; + create(message, { id: 'everyalt-generate', type: 'snackbar', isDismissible: true }); + } + function EveryAltImageEdit(BlockEdit) { return function (props) { if (props.name !== 'core/image') { @@ -33,15 +45,18 @@ wp.apiFetch({ path: 'everyalt-api/v1/bulk_generate_alt', method: 'POST', - data: { media_id: attachmentId }, - headers: { 'Content-Type': 'application/json' } + data: { media_id: attachmentId } }).then(function (res) { setIsBusy(false); if (res && res.success && res.alt_text) { setAttributes({ alt: res.alt_text }); + notify('success', t('generated', 'Alt text generated.')); + } else { + notify('error', (res && res.message) ? res.message : t('failed', 'Could not generate alt text.')); } - }).catch(function () { + }).catch(function (err) { setIsBusy(false); + notify('error', t('errorPrefix', 'Error:') + ' ' + ((err && err.message) ? err.message : t('failed', 'Could not generate alt text.'))); }); } @@ -49,7 +64,7 @@ el(BlockEdit, props), el(InspectorControls, { key: 'everyalt' }, el(PanelBody, { - title: 'EveryAlt', + title: t('panelTitle', 'EveryAlt'), initialOpen: true, className: 'everyalt-inspector-panel' }, @@ -62,9 +77,9 @@ isBusy: isBusy, disabled: isBusy, style: { marginTop: '8px' } - }, 'Generate alt text with EveryAlt') + }, t('button', 'Generate alt text with EveryAlt')) : el('p', { className: 'everyalt-gutenberg-help', style: { margin: 0, fontSize: '12px', color: '#757575' } }, - 'Select or upload an image to generate alt text.') + t('selectImage', 'Select or upload an image to generate alt text.')) ) ) ); diff --git a/admin/js/everyalt-media-simple.js b/admin/js/everyalt-media-simple.js index d5ece2e..761ce1a 100644 --- a/admin/js/everyalt-media-simple.js +++ b/admin/js/everyalt-media-simple.js @@ -8,6 +8,8 @@ var restUrl = (config.restUrl || '').replace(/\/$/, ''); var restNonce = config.restNonce || ''; var mediaId = config.mediaId || 0; + var i18n = config.i18n || {}; + function t(key, fallback) { return i18n[key] || fallback; } if (!restUrl || !mediaId) return; @@ -17,7 +19,7 @@ var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'button'; - btn.textContent = 'Generate alt text with EveryAlt'; + btn.textContent = t('button', 'Generate alt text with EveryAlt'); wrap.appendChild(btn); var msg = document.createElement('p'); @@ -27,7 +29,7 @@ btn.addEventListener('click', function() { btn.disabled = true; - msg.textContent = 'Generating…'; + msg.textContent = t('generating', 'Generating…'); msg.className = 'everyalt-media-message'; fetch(restUrl + '/everyalt-api/v1/bulk_generate_alt', { @@ -46,20 +48,20 @@ btn.disabled = false; var success = data && data.success && data.alt_text; if (success) { - msg.textContent = 'Alt text generated.'; + msg.textContent = t('generated', 'Alt text generated.'); msg.className = 'everyalt-media-message notice notice-success'; var altField = document.getElementById('attachment_alt') || document.querySelector('textarea[name*="_wp_attachment_image_alt"]') || document.querySelector('input[name*="_wp_attachment_image_alt"]'); if (altField) { altField.value = data.alt_text; } } else { - msg.textContent = (data && data.message) ? data.message : 'Could not generate alt text.'; + msg.textContent = (data && data.message) ? data.message : t('failed', 'Could not generate alt text.'); msg.className = 'everyalt-media-message notice notice-error'; } }) .catch(function(err) { btn.disabled = false; - msg.textContent = 'Error: ' + (err.message || 'Request failed'); + msg.textContent = t('errorPrefix', 'Error:') + ' ' + (err.message || t('failed', 'Could not generate alt text.')); msg.className = 'everyalt-media-message notice notice-error'; }); }); diff --git a/admin/partials/everyalt-options-display.php b/admin/partials/everyalt-options-display.php index 9959a2f..c522b3a 100644 --- a/admin/partials/everyalt-options-display.php +++ b/admin/partials/everyalt-options-display.php @@ -41,7 +41,10 @@