Auto-translate on copy (consolidated #106 + downstream UI fixes, rebased on main) - #115
Conversation
better errors using, select instead of radio group fix the richeditor Fixed mediafinder issue, updated config, updated the way whitelist is defined
I wasn't using proper popups before and this resulted in the copy button submitting forms that it was inside, which would close the file upload popup and relation popup, and just generally cause issues.
Co-authored-by: Meindert <89913092+AIC-BV@users.noreply.github.com>
…king The .ml-btn had a fixed 44px width sized for two-letter locale codes; codes like nl-BE overflowed. The button now shrink-wraps its label (min-width 44px, so two-letter codes render identically) and multilingual.js publishes the rendered width as a --ml-btn-width CSS variable via a ResizeObserver, which the copy dropdown uses to position itself against the button. The .ml-dropdown/.ml-copy-dropdown wrappers span the full field height (height: 100%), so on tall fields (e.g. mediafinder) an invisible strip swallowed clicks on the underlying UI whenever the dropdown was open. The wrappers are now pointer-events: none with the button and menu re-enabled, which also supersedes the height: 3px closed-state hacks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Locale pair as chips in the modal title, a real label on the provider select, and a cancel button (reusing backend::lang.form.cancel). Moves the modal-footer out of modal-body where it was accidentally nested. Drops two dead rules: .lang-code-display (scoped under .field-multilingual, which never matches the body-level popup) and .ml-modal label (class applied nowhere). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The flag shipped with the copy feature (#99) but was only discoverable by reading _locale_copy.htm. No behavior change; adds a TRANSLATE_DISABLE_COPY env hook consistent with the other entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
✅ Status update — most of this is now implementedThe items below have been addressed on the branch (all commits CI-green on PHP 8.1/8.2/8.3). This header supersedes the original review that follows. Implemented
Corrected — false positives, verified working live
Deferred (needs your call)
On "disabled by default" (Marc / Isaiah): the auto-hide is the better realization of Marc's clutter concern — nothing new renders until a provider key is configured, and plain copy is untouched. Original review (for history)# Maintainer review — path to mergeReviewed the consolidated feature across five lenses (functionality, the two production regressions, security, UX, and structure/staleness). Overall: the feature is sound and production-proven, and rebasing onto ✅ Resolved by the rebase (was flagged on #106, gone here)
🔴 Blockers (must fix before merge)1. Reflected XSS in the translation-method popup. 2. Confirm the fileupload-caption regression is actually fixed. The Jan-2026 "can't save fileupload 🟡 UX — the "keep it easy, un-clutter the default" decisionToday, copying a locale went from a 1-click action to dropdown → item → modal → select → Copy (a modal + AJAX round-trip), and the provider selector renders even when no API key is configured (default config ships Google with an empty key → a non-functional option). This is the "clunky by default" concern. Recommendation — auto-hide the auto-translate UI when no usable provider is configured:
Net: a stock install sees zero new clutter and the original copy behaviour (satisfies "disabled by default"); the moment a key is set, the picker lights up (satisfies "easy to reach"). Optionally add an explicit Current vs. recommended (visuals):
Current (clunky) state — attach: Recommended state — to be captured after the auto-hide UX is built 🟢 Quality / maintainability (before or shortly after merge)
Verification checklist
This is the concrete "revisions needed" list (per @IsaiahPaget's question to @mjauvin) and the cleanup pass @LukeTowers offered @AIC-BV in December — now scoped against a clean base. Additional findings — consolidated from Copilot + CodeRabbitTheir inline comments on this PR have been triaged and resolved/hidden; the worthwhile items are folded in here so this stays the single active review. Functional (High)
Correctness (Medium)
Minor
Corroborated (already covered above, now double-confirmed): the XSS on Flagged but not actioned:
Provider-key setup is undiscoverable (new — UX/onboarding)To use this feature a user must know it exists, obtain a Google Cloud / DeepL key with no in-product link or instructions, SSH in to edit Recommendation — do both:
Screenshots for this are a TODO — they can only be captured once the setup screen (b) exists.
|
…d guzzle dependency The providers use Laravel's Http client which requires guzzlehttp/guzzle (not shipped by a stock Winter/Laravel install), so guzzle is a genuine dependency. Adding it forces a full composer re-resolve in CI (testing against Winter develop), which the stable-only >=1.2.8 constraint could not satisfy against the dev-develop modules. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Escape the provider name and the copy-from locale in the method popup, and validate posted locales against Locale::listAvailable() before use (fixes a reflected-XSS via _copy_from_locale). - GoogleTranslateProvider: drop urldecode() (it corrupted literal percent sequences; Google returns HTML-entity-encoded text) and guard the response shape. - Treat a literal "0" as translatable content instead of empty. - Cast the posted _provider to string so a missing provider is treated as "none" instead of passing null to a string-typed translate(). - Allow guzzlehttp/guzzle ^8.0 in addition to ^7.10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Guard the (optional) copy controls in MLTextarea.updateLayout so the widget still initializes when disable_copy is set (was throwing 'Cannot read properties of null'). - Validate the auto-translate response before indexing translatedValue[0]. - Declare the previous plugin value so mlText.noConflict() doesn't ReferenceError. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a shared $.wn.translate.applyAutoTranslateResponse() helper in multilingual.js and route mltext/mltextarea/mlmarkdowneditor/mlricheditor through it, removing the repeated success-handling and fixing the markdown/ rich editors that indexed translatedValue[0] before validating the response. - Fix mlnestedform's noConflict typos (mlNEstedForm / $.fn.MLNestedForm). Scoped intentionally: the remaining per-widget code (froala/ace/mediafinder/ repeater init, dispose and updateLayout) is genuinely widget-specific and the plugin-registration scaffolding is standard Winter widget boilerplate, so a full base-class rewrite would add risk without meaningful dedup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The happy medium between 'disabled by default' and 'easy to reach': - MLControl only offers providers that actually have an API key, and the copy dropdown omits the method-popup handler when none are usable, so the frontend falls back to the original one-click copy (no modal, no clutter) on a stock install. Configure a key and the method picker appears. - Fall back to the 'none' default when the configured default provider has no key. - autoTranslateArray no longer throws when nothing is whitelisted; it keeps the plain copy (nested translation stays opt-in via autoTranslateWhiteList). - Escape the copy-locale code and event handler in the partial. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g-driven factory - Add AbstractTranslationProvider with shared construction and automatic request batching (Google 100/req, DeepL 50/req) so large repeater/block collections no longer fail wholesale. - Add per-request HTTP timeouts and guard the DeepL response shape. - DeepL: normalise locales to the provider's codes (base code for source, region only for supported targets like EN-GB/PT-BR) so nl-BE/fr-BE work. - Provider exceptions report the HTTP status instead of echoing the raw upstream body. - ProviderFactory is now config-driven (providers.<name>.class) with a built-in fallback for google/deepl; no request data reaches instantiation. - Enable the DeepL provider in config (auto-hidden until DEEPL_API_KEY is set) and document the class-based provider setup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a README section covering provider setup (Google/DeepL env vars + how to obtain keys), the opt-in/auto-hidden behaviour, the nested-field whitelist, and how to add a custom provider. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore the descendant-combinator spacing, blank lines and calc() spacing that an editor auto-formatter had rewritten, leaving only the functional PR changes in the diff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Validate each provider batch returns exactly one string per input segment before merging, so a partial/malformed response fails loudly instead of silently keeping source values via flatten/expand. - Google/DeepL: require a well-formed translations array (and string translatedText) rather than defaulting a missing value to an empty string. - Bump guzzle floor to patched releases (^7.15.2 || ^8.0.1). - README: tag the env code fence as dotenv. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add array/string parameter and array return types (they were already the effective contract via flatten() and translate()), and cast the posted locales to string in the composite-widget callers so a missing post value can't reach the typed boundary as null. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@AIC-BV can you start using / testing this branch in production and we'll get the rest of the modifications you needed included into this and merged? |
The defaultProvider / TRANSLATE_PROVIDER config only pre-selected an option in the translation-method popup; it did nothing else. It earned its keep only when 2+ providers were configured AND the admin had a standing preference, and pre-selecting a paid provider by default is a mild footgun. Replace it with a zero-config rule: when exactly one provider is usable it is pre-selected (an unambiguous default that saves a click); with several, the picker stays on "None" so translation remains a deliberate choice. Removes the config key, the TRANSLATE_PROVIDER env var, the validation branch, and the vestigial test setup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the global autoTranslateWhiteList config with a per-field opt-in: a composite widget's nested sub-field is machine-translated on copy only when its field definition declares translatable: true; everything else is copied verbatim. This fixes the global whitelist's structural problems (one list bleeding across every model/widget, and collisions between same-named fields) by declaring translatability where it belongs — at the field. It also aligns with the translatable: true convention from #76 / issue #21, so translatability is expressed once and consistently. getAutoTranslatableFields() now collects translatable field names by walking the widget's field definitions: group mode (repeater groups / blocks) and single form mode (repeater / nestedform), descending into nested forms and stripping @context suffixes. The autoTranslateWhiteList config key is removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface which locales are translated directly in each ML field's locale switcher, so editors can see what still needs translating without opening every field one by one. - Each locale in the switcher shows a status dot: default/source (grey), translated (green), or empty/falls-back-to-default (dashed outline). - The field's locale button gains an amber marker whenever any non-default locale is still untranslated. - Driven entirely client-side from the per-locale values the ML controls already render (no backend/model changes); refreshes live on edit, locale switch, copy, and auto-translate. Applies to the scalar ML controls (text/textarea/richeditor/markdown/url/ mediafinder) that use the shared locale selector; composite widgets (repeater/nestedform/blocks) can follow in a later pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Provides a reusable way to answer "how translated is this record": for every enabled non-default locale it counts how many translatable attributes hold a value, returning an overall percentage, a fully-complete locale count, and a per-locale breakdown. Intended for surfacing translation status outside the edit form — e.g. a backend list column or badge — complementing the per-field switcher indicators. Read-only; disabled locales are excluded (they aren't part of the target set). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eteness" This reverts commit bb7711f.
What do I have to do to be able to translate repeater fields now? Ok much easier to setup & maintain without the whitelist and with |
|
Has this been tested on tailwindui-plugin with dark theme? |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
The per-locale status dots and the untranslated marker were added to the compiled multilingual.css only, not the multilingual.less source, so recompiling the bundle would silently drop them. Move them into the .less and regenerate the compiled .css from source — which also reconciles drift that had accumulated in the committed CSS (a stale height:100% on the dropdown wrappers and colour-case differences). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes for multilingual controls under the fancy form layout, where the tabless header inputs are transparent and taller than a normal input: - Hide the locale/copy controls while the field is focused. Raising the input's z-index only masks them when the input is opaque; the fancy layout's transparent input left them showing over the text. The CSS fix here is @mjauvin's (Marc Jauvin). - Size and restyle the controls for the taller fancy inputs: fill the height and use light text on a translucent frosted background so they blend into the header instead of floating as short pale boxes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the separate always-visible copy button + its own dropdown with a small copy button on each locale row of the language selector. The button copies that row's locale value into the currently-active locale (the reverse of switching), is disabled on the current row, and carries a "Copy from :locale" tooltip. Benefits: one control instead of two floating over the input (removing the whole class of overlap/positioning issues), the copy action sits contextually next to each language, and it pairs with the per-locale status dots — dot + name + copy per row. Also adds a browser confirmation before the destructive overwrite: copying only prompts when the active locale already has content that would be replaced. Prototype for review. Follow-ups: retire the now-dead .ml-copy-btn/.ml-copy-dropdown CSS, verify/polish across composite widgets, and consider a styled confirm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Disable a locale row's copy button when that locale has no value to copy from (in addition to the active-locale row), so you can't trigger an empty copy. - Make the copy button a full-height hit area with the padding on the button around the icon, rather than a small button surrounded by dead space. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Faintly highlight (subtle background + bold) the row of the locale currently being edited so it's obvious which one is active in the list. Also switch the row-targeting rules to descendant selectors: the backend's dropdown widget rewraps the list (adding li.dropdown-container / first-item wrappers), which silently broke the direct-child (> li > a) selectors — including the copy-button spacing rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With the copy action merged into the language selector, the separate .ml-copy-btn and .ml-copy-dropdown no longer render. Remove all their now-dead CSS (and simplify the .ml-dropdown rules that special-cased them), and drop the ResizeObserver / updateActiveButtonWidth --ml-btn-width tracking that only existed to position the old copy dropdown next to the locale button. Verified the merged selector still renders and copies correctly across the scalar and composite (repeater / nested form) widgets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The copy-from action is now a lightweight, contextual control inside the language selector, so there's no need to hide it. Drop the disable_copy config option (and TRANSLATE_DISABLE_COPY env) and the per-widget guards it required, including the now-dead .ml-copy-btn/.ml-copy-dropdown positioning in the textarea, rich editor, and markdown editor widgets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the native window.confirm() with $.wn.confirm() (the backend's sweet-alert dialog) for the destructive copy-overwrite warning, falling back to the native prompt if it's unavailable. Splits copyLocale() into the confirm gate and an applyCopyLocale() step so the async dialog can proceed on confirm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed nested forms (#118) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Adds a backend Settings screen (System → Settings → Translation Providers) for configuring the Google Cloud Translation and DeepL API credentials, so keys no longer have to be set via env/config only. - Winter\Translate\Models\Setting (SettingsModel): stores the per-provider key/endpoint and the DeepL plan, and on boot pushes them into winter.translate::providers.* so the existing ProviderFactory and getUsableTranslateProviders() resolve them unchanged. Empty fields fall back to the file/env config, so an env-configured key keeps working. - Guided setup UX: a tab per provider pairing step-by-step instructions (with direct console links) against masked (sensitive) key inputs, a Free/Pro plan selector for DeepL, and an honest status callout that reflects when a provider is already configured via the environment. - Secrets are never pulled into the form: an env key shows as "configured via environment" with a blank field rather than being copied into the settings record. - Config is only applied on backend requests (the feature is backend-only) and guarded so install/migrate before the settings table exists is a no-op. - New winter.translate.manage_settings permission gating the screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- README: lead the provider setup with the new backend Translation Providers screen, keeping env/config as the deployment alternative. - version.yaml: add 2.4.0 covering the auto-translate-on-copy feature, the settings page, the inline copy selector, status indicators and nested-field translation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Summary
Consolidates the auto-translate on copy work into a single branch rebased cleanly onto the latest
main, so it can be reviewed as a coherent unit. This supersedes:All original commits and authorship are preserved. The branch is
main+ feature commits, with no unrelated changes.What it does
Adds optional machine translation when copying content from one locale to another on a multilingual backend field:
providers/): aTranslationProviderinterface +ProviderFactory, with Google Cloud Translation and DeepL implementations.MLAutoTranslatetrait + per-widget wiring).Provider keys are read server-side only. The providers call Laravel's
Httpclient, which requiresguzzlehttp/guzzle(not shipped by stock Winter) — hence the added dependency.Translation Providers settings page (new)
Keys no longer have to live only in env/config. Settings → Translation Providers provides a guided screen:
winter.translate.manage_settingspermission.Behavior notes
UI: inline copy in the language selector
The copy action was moved into the language selector rather than living in a separate always-visible button/dropdown:
Translation status indicators
Editors previously had no way to see which locales of a field were already translated versus still empty — a long-standing gap in both this plugin and its RainLab upstream (cf. rainlab/translate-plugin#301, #462, #542). This adds an at-a-glance indicator, driven entirely client-side from the per-locale values the ML controls already render (no backend or model changes):
Applies to all ML widgets, since they share the same locale selector. Live-as-you-type on scalar fields; composite widgets reflect the saved state (refreshed on copy / switch / save).
Testing
tests/unit/traits/MLAutoTranslateTest.phpcovers provider resolution and the translation / whitelist helpers (including repeater-group and tab-organized nested-form field collection).main; the settings page verified end-to-end in the backend (guided tabs render, keys save and round-trip, env-configured keys surface as configured via environment, config bridge resolves saved keys with env fallback, frontend/console boots unaffected).Credits
Feature by @IsaiahPaget (#106), funded and production-tested by @AIC-BV, with UI fixes from IsaiahPaget#1. Consolidated/rebased, with the settings page, inline-copy redesign and status indicators added on top.