> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sitestorepro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Translation Service: AI-Powered Content Localization

> Understand how Site Store Pro's TranslationService uses OpenAI to bulk-translate content entities, protect shortcodes, and deliver multilingual email templates.

The `TranslationService` (`App\Services\TranslationService`) powers Site Store Pro's automated multilingual content system. It connects to the OpenAI API to translate content across your entire store — products, CMS pages, email templates, navigation items, and more — while protecting platform-specific shortcode markup from being corrupted or mistranslated during the process.

<Warning>
  You must set `OPENAI_API_KEY` in your `.env` file before any AI-powered translation feature will function. Without a valid key, all bulk translation jobs will fail silently or throw an authentication error from the OpenAI client. Obtain your key from [platform.openai.com](https://platform.openai.com) and add it as:

  ```bash theme={null}
  OPENAI_API_KEY=sk-...
  ```
</Warning>

## OpenAI Client Requirement

The service uses the `openai-php/client` package at `^0.20.0`. Confirm this dependency is present in your `composer.json` and installed before running translations.

```bash theme={null}
composer require openai-php/client:"^0.20.0"
```

***

## Shortcode Protection Engine

Shortcodes are the bracketed plugin tags embedded in CMS content, such as:

```text theme={null}
[plugin:slideshow-2026 id=2]
[plugin:contact-form id=5 theme=dark]
```

If you send these strings to OpenAI as-is, the model may rephrase, reorder, or omit parts of the tag — breaking the plugin entirely on translated pages. The Shortcode Protection Engine prevents this with a two-step wrap-and-restore process.

<Steps>
  <Step title="Strip — Before sending to OpenAI">
    The service scans the content string for any bracketed shortcode tags and replaces each one with a neutral, indexed placeholder that OpenAI will treat as an opaque token:

    **Input content:**

    ```text theme={null}
    Welcome to our store. [plugin:slideshow-2026 id=2] Browse our latest products.
    ```

    **Sent to OpenAI:**

    ```text theme={null}
    Welcome to our store. {{PLUGIN_0}} Browse our latest products.
    ```

    The original shortcodes are stored in an internal map keyed by their placeholder index (`PLUGIN_0`, `PLUGIN_1`, etc.).
  </Step>

  <Step title="Restore — After translation returns">
    Once OpenAI returns the translated string, the service replaces every `{{PLUGIN_N}}` placeholder with the corresponding original shortcode from the map:

    **Returned from OpenAI (French):**

    ```text theme={null}
    Bienvenue dans notre boutique. {{PLUGIN_0}} Parcourez nos derniers produits.
    ```

    **Final stored translation:**

    ```text theme={null}
    Bienvenue dans notre boutique. [plugin:slideshow-2026 id=2] Parcourez nos derniers produits.
    ```
  </Step>
</Steps>

<Tip>
  Monitor active and failed translation jobs in real time by navigating to **Admin → CMS → Queue Monitor**. Each bulk translation run dispatches multiple queued jobs — one per entity batch. The Queue Monitor shows job status, failure reasons, and retry controls without requiring direct server access.
</Tip>

***

## Bulk Translation Pipeline

When an admin clicks **Bulk Translate All** on a language card, the service dispatches queued translation jobs for all 11 core content entity groups. Each group translates the parent record fields and writes results into the corresponding `*_translations` child table for the target language.

<Accordion title="View all 11 translated entity groups">
  | #  | Parent Table          | Translation Table                 |
  | -- | --------------------- | --------------------------------- |
  | 1  | `cms_pages`           | `cms_page_translations`           |
  | 2  | `products`            | `product_translations`            |
  | 3  | `product_variants`    | `product_variant_translations`    |
  | 4  | `kb_articles`         | `kb_article_translations`         |
  | 5  | `testimonials`        | `testimonial_translations`        |
  | 6  | `nav_items`           | `nav_item_translations`           |
  | 7  | `cms_list_menu_items` | `cms_list_menu_item_translations` |
  | 8  | `product_categories`  | `category_translations`           |
  | 9  | `site_labels`         | `site_label_translations`         |
  | 10 | `email_templates`     | `email_template_translations`     |
  | 11 | `plugin_settings`     | `plugin_setting_translations`     |
</Accordion>

Each job applies the Shortcode Protection Engine before sending content to OpenAI and restores shortcodes before writing the translated result to the database.

***

## EmailTemplateService

`EmailTemplateService::sendEmail()` is the standard method for dispatching transactional emails throughout Site Store Pro. It supports full translation lookup, `{{variable}}` substitution, and dynamic template rendering.

### Signature

```php theme={null}
EmailTemplateService::sendEmail(
    string $slug,        // Template identifier, e.g. 'order_confirmation'
    string $toEmail,     // Recipient email address
    string $toName,      // Recipient display name
    array  $vars,        // Key-value map of {{variable}} replacements
    int    $languageId   // Language ID for translation lookup
): void;
```

### Parameters

<ParamField path="slug" type="string" required>
  The unique identifier for the email template record in the `email_templates` table. Examples: `order_confirmation`, `password_reset`, `download_reminder`.
</ParamField>

<ParamField path="toEmail" type="string" required>
  The recipient's email address.
</ParamField>

<ParamField path="toName" type="string" required>
  The recipient's display name, used in the email greeting and headers.
</ParamField>

<ParamField path="vars" type="array" required>
  An associative array of variable replacements. Keys correspond to `{{variable}}` tokens present in the template body and subject. For example:

  ```php theme={null}
  [
      'order_number' => '#10042',
      'customer_name' => 'Jane Smith',
      'total'        => '$89.99',
  ]
  ```
</ParamField>

<ParamField path="languageId" type="int" required>
  The ID of the target language. The service eager-loads the matching `email_template_translations` record for this language ID and falls back to the default template content if no translation exists.
</ParamField>

### Dispatch Example

```php theme={null}
use App\Services\EmailTemplateService;

EmailTemplateService::sendEmail(
    'order_confirmation',
    $order->customer_email,
    $order->customer_name,
    [
        'order_number' => $order->reference,
        'total'        => $order->formatted_total,
        'items'        => $order->items_summary,
    ],
    $order->language_id
);
```

The method fetches the active profile for the given `$slug`, applies translations for `$languageId`, substitutes all `{{variable}}` tokens, and dispatches an `App\Mail\DynamicTemplateMail` mailable to the queue.

***

## Child-Table Translation Pattern

All translatable entities in Site Store Pro follow a consistent parent/child schema pattern. The parent table holds canonical (default language) content, and each language's translated version of that content lives in a dedicated `*_translations` child table.

```text theme={null}
languages
  ├── cms_page_translations            (entity_id → cms_pages.id)
  ├── product_translations             (entity_id → products.id)
  ├── product_variant_translations     (entity_id → product_variants.id)
  ├── kb_article_translations          (entity_id → kb_articles.id)
  ├── testimonial_translations         (entity_id → testimonials.id)
  ├── nav_item_translations            (entity_id → nav_items.id)
  ├── cms_list_menu_item_translations  (entity_id → cms_list_menu_items.id)
  ├── category_translations            (entity_id → product_categories.id)
  ├── site_label_translations          (entity_id → site_labels.id)
  ├── email_template_translations      (entity_id → email_templates.id)
  └── plugin_setting_translations      (entity_id → plugin_settings.id)
```

Every child table contains at minimum:

| Column            | Type              | Description                      |
| ----------------- | ----------------- | -------------------------------- |
| `entity_id`       | `unsignedBigInt`  | Foreign key to the parent record |
| `language_id`     | `unsignedBigInt`  | Foreign key to `languages.id`    |
| translated fields | `text` / `string` | The translated column values     |

When querying translated content, eager-load the appropriate `*_translations` relationship filtered by `language_id` to avoid N+1 queries:

```php theme={null}
$pages = CmsPage::with(['translations' => function ($query) use ($languageId) {
    $query->where('language_id', $languageId);
}])->get();
```
