From 14ead71098f4fb48671eb74640bfe4dce7171ab2 Mon Sep 17 00:00:00 2001 From: Jeffrey van Rossum Date: Fri, 28 Aug 2026 14:07:06 +0900 Subject: [PATCH 1/9] Prepare the plugin for a WordPress.org listing. Drop the GitHub update checker, add a license and dist ignore list, and tighten settings, notices, and logging for Plugin Check. Co-authored-by: Cursor --- .distignore | 12 +++++ .github/workflows/release.yml | 9 ++-- .gitignore | 1 + composer.json | 3 +- license.txt | 18 +++++++ resources/js/setup-notice.js | 28 ++++++++++ resources/views/settings.php | 97 +++++++++++++++++++---------------- src/Admin/Ajax.php | 21 ++++++-- src/Admin/SettingsPage.php | 32 +++++++++--- src/Api/Client.php | 16 +++--- src/Plugin.php | 47 +++++++++++++---- src/Search/Interceptor.php | 12 ++--- src/Support/UpdateChecker.php | 34 ------------ src/Sync/BulkSync.php | 5 +- src/Sync/ContentSync.php | 2 - uninstall.php | 5 +- 16 files changed, 218 insertions(+), 124 deletions(-) create mode 100644 .distignore create mode 100644 license.txt create mode 100644 resources/js/setup-notice.js delete mode 100644 src/Support/UpdateChecker.php diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..9bb3c44 --- /dev/null +++ b/.distignore @@ -0,0 +1,12 @@ +.git +.github +.gitignore +.distignore +.slimm +.phpunit.cache +tests +phpunit.xml +composer.lock +build +*.zip +.DS_Store diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a749ee7..76a7a35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,8 @@ name: Release # Build a distributable plugin zip and attach it to the GitHub release whenever -# a version tag is pushed. The Datalumo plugin checks for updates against these -# release assets (see src/Support/UpdateChecker.php), so the zip must contain the -# Composer-built vendor/ directory — GitHub's auto-generated source zip does not. +# a version tag is pushed. The zip must contain the Composer-built vendor/ +# directory — GitHub's auto-generated source archive does not. on: push: tags: @@ -52,12 +51,14 @@ jobs: --exclude '.git' \ --exclude '.github' \ --exclude '.gitignore' \ + --exclude '.distignore' \ --exclude '.slimm' \ --exclude 'build' \ --exclude '*.zip' \ --exclude 'tests' \ --exclude 'phpunit.xml' \ - --exclude '.phpunit.cache' + --exclude '.phpunit.cache' \ + --exclude '.DS_Store' (cd build && zip -rq datalumo.zip datalumo) - name: Create release and upload asset diff --git a/.gitignore b/.gitignore index 567402e..ac84e6a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build/ *.zip .slimm/ .phpunit.cache/ +.DS_Store diff --git a/composer.json b/composer.json index 74eb1b0..0a4f68b 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,7 @@ "require": { "php": ">=8.1", "automattic/jetpack-autoloader": "^3.0", - "woocommerce/action-scheduler": "^3.0", - "yahnis-elsts/plugin-update-checker": "^5.7" + "woocommerce/action-scheduler": "^3.0" }, "autoload": { "psr-4": { diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..5434d58 --- /dev/null +++ b/license.txt @@ -0,0 +1,18 @@ +Datalumo WordPress Plugin +Copyright (C) 2026 Jeffrey van Rossum + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, see . + +License: GPL-2.0-or-later +License URI: https://www.gnu.org/licenses/gpl-2.0.html diff --git a/resources/js/setup-notice.js b/resources/js/setup-notice.js new file mode 100644 index 0000000..48434b6 --- /dev/null +++ b/resources/js/setup-notice.js @@ -0,0 +1,28 @@ +/** + * Persist dismissal of the dashboard setup notice. + */ +(function () { + 'use strict'; + + var config = window.datalumoSetupNotice; + + if (! config) { + return; + } + + document.addEventListener('click', function (event) { + var dismiss = event.target && event.target.closest + ? event.target.closest('.datalumo-setup-notice .notice-dismiss') + : null; + + if (! dismiss) { + return; + } + + var body = new FormData(); + body.append('action', 'datalumo_dismiss_setup_notice'); + body.append('_ajax_nonce', config.nonce); + + fetch(config.ajaxUrl, { method: 'POST', body: body, credentials: 'same-origin' }); + }); +})(); diff --git a/resources/views/settings.php b/resources/views/settings.php index 8cc98c1..0cac4f9 100644 --- a/resources/views/settings.php +++ b/resources/views/settings.php @@ -1,47 +1,52 @@ true], 'objects'); -unset($postTypes['attachment']); +/** @var string $datalumo_tab (set by SettingsPage::render) */ +$datalumo_organisation = Options::get('organisation', []); +$datalumo_sources = (array) Options::get('sources', []); +$datalumo_syncs = (array) Options::get('syncs', []); +$datalumo_post_types = get_post_types(['public' => true], 'objects'); +unset($datalumo_post_types['attachment']); -$tabs = [ +$datalumo_tabs = [ 'connection' => __('Connection', 'datalumo'), 'content-sync' => __('Content sync', 'datalumo'), 'chatbot' => __('Chatbot', 'datalumo'), 'search-box' => __('Search box', 'datalumo'), 'enhanced-search' => __('Enhanced search', 'datalumo'), ]; + +$datalumo_updated = isset($_GET['updated'], $_GET['_wpnonce']) + && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'datalumo_settings_updated') + && sanitize_text_field(wp_unslash($_GET['updated'])) === '1'; ?>

Datalumo

- +

- + - + @@ -57,7 +62,7 @@ class="nav-tab "> @@ -81,11 +86,14 @@ class="nav-tab "> - + esc_html($datalumo_organisation['name'] ?? $datalumo_organisation['id'] ?? ''), + count($datalumo_sources), + ); + ?> @@ -94,24 +102,24 @@ class="nav-tab "> - +

- $sync) : ?> -
- + $datalumo_sync) : ?> +
+
- + - @@ -148,7 +156,7 @@ class="nav-tab ">

- + @@ -186,8 +194,11 @@ class="nav-tab ">

+ +

+ - + @@ -199,7 +210,7 @@ class="nav-tab "> - + @@ -221,11 +232,11 @@ class="nav-tab "> + + + +
+ +
+ + + + +

-

+ 1; + ?>
- $datalumo_sync) : ?> -
+ $datalumo_sync) : ?> +
@@ -117,12 +261,35 @@ class="nav-tab + +

+ +

+ @@ -153,7 +320,6 @@ class="nav-tab -

@@ -188,15 +354,11 @@ class="nav-tab -

- -

- @@ -268,4 +430,31 @@ class="nav-tab + + + + + + + diff --git a/src/Admin/Ajax.php b/src/Admin/Ajax.php index 6fef90d..86a0f8a 100644 --- a/src/Admin/Ajax.php +++ b/src/Admin/Ajax.php @@ -4,6 +4,7 @@ use Datalumo\Wp\Api\ApiException; use Datalumo\Wp\Api\Client; +use Datalumo\Wp\Support\Credentials; use Datalumo\Wp\Support\Options; use Datalumo\Wp\Sync\BulkSync; @@ -21,10 +22,10 @@ public function register(): void } /** - * Verify the pasted org id + token against /me; on success cache the - * organisation and its sources so the pickers render offline. + * Verify a pasted API token against GET /api/v1/me. The token identifies + * the organisation; no organisation ID is required. * - * An empty POST token falls back to the stored one — the password field + * An empty POST token falls back to the stored one. The password field * renders only a "saved" placeholder, never the secret. The base URL comes * from the form field (so an unsaved value works) and is persisted on success. */ @@ -33,35 +34,39 @@ public function connect(): void $this->authorise(); // phpcs:disable WordPress.Security.NonceVerification -- verified in authorise(). - $organisationId = sanitize_text_field(wp_unslash((string) ($_POST['organisation_id'] ?? ''))); $token = sanitize_text_field(wp_unslash((string) ($_POST['token'] ?? ''))); $apiUrl = array_key_exists('api_url', $_POST) ? (esc_url_raw(wp_unslash((string) $_POST['api_url'])) ?: 'https://datalumo.app') : null; // phpcs:enable WordPress.Security.NonceVerification - if ($organisationId === '') { - $organisationId = (string) Options::get('organisation.id', ''); + if ($token === '') { + $token = (string) Options::get('api_token', ''); } if ($token === '') { - $token = (string) Options::get('api_token', ''); + wp_send_json_error(['message' => __('An API token is required.', 'datalumo')]); + } + + if (Credentials::looksLikeWidgetKey($token)) { + wp_send_json_error(['message' => __('That looks like a widget key. Paste an API token from API keys instead.', 'datalumo')]); } - if ($organisationId === '' || $token === '') { - wp_send_json_error(['message' => __('Both the organisation ID and an API token are required.', 'datalumo')]); + if (Credentials::looksLikeSecret($token)) { + wp_send_json_error(['message' => __('That looks like a widget secret. Paste an API token from API keys instead.', 'datalumo')]); } try { - $me = (new Client())->me($organisationId, $token, $apiUrl); + $me = (new Client())->me('', $token, $apiUrl); } catch (ApiException $e) { wp_send_json_error(['message' => $e->getMessage()]); } $stored = [ 'api_token' => $token, - 'organisation' => $me['organisation'] ?? ['id' => $organisationId], + 'organisation' => $me['organisation'] ?? [], 'sources' => $me['sources'] ?? [], + 'connected_via' => 'token', ]; if ($apiUrl !== null) { diff --git a/src/Admin/SettingsPage.php b/src/Admin/SettingsPage.php index f53fc2e..efeab38 100644 --- a/src/Admin/SettingsPage.php +++ b/src/Admin/SettingsPage.php @@ -2,15 +2,22 @@ namespace Datalumo\Wp\Admin; +use Datalumo\Wp\Api\ApiException; +use Datalumo\Wp\Api\Client; use Datalumo\Wp\Support\Assets; +use Datalumo\Wp\Support\Credentials; use Datalumo\Wp\Support\Options; class SettingsPage { public const NONCE = 'datalumo_settings'; + public const ASK_MAX_LENGTH = 280; + private const TABS = ['connection', 'content-sync', 'chatbot', 'search-box', 'enhanced-search']; + private bool $widgetKeyRejected = false; + public function register(): void { add_action('admin_menu', function (): void { @@ -55,14 +62,68 @@ public function render(): void { $datalumo_tab = $this->requestedTab(); + if ($datalumo_tab === 'content-sync') { + $this->refreshSources(); + } + require DATALUMO_DIR . '/resources/views/settings.php'; } + /** + * Other tabs are empty until a connection exists and the post-connect + * checklist (if any) has been saved. + */ + public static function setupIsReady(): bool + { + return Options::isConnected() && ! Options::get('setup_pending'); + } + + public static function docsUrl(string $hash = ''): string + { + $url = Options::baseUrl().'/docs/wordpress'; + + return $hash !== '' ? $url.'#'.$hash : $url; + } + + /** + * Landing page for the settings "Need help?" form. Query `ask` is + * honoured on first-party docs only, as a composer prefill. + */ + public static function helpUrl(): string + { + return self::docsUrl(); + } + + /** + * Pull a fresh source list (including knowledge base names) so the + * picker stays current after a connect or a rename in Datalumo. + */ + private function refreshSources(): void + { + if (! Options::isConnected()) { + return; + } + + try { + $me = (new Client())->me('', (string) Options::get('api_token')); + } catch (ApiException) { + return; + } + + if (isset($me['sources']) && is_array($me['sources'])) { + Options::set('sources', $me['sources']); + } + } + private function requestedTab(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- settings tab is a navigation query arg. $tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : ''; + if (! self::setupIsReady()) { + return 'connection'; + } + return in_array($tab, self::TABS, true) ? $tab : 'connection'; } @@ -85,16 +146,16 @@ public function save(): void 'content-sync' => $this->saveSyncs($input), 'chatbot' => Options::merge(['chatbot' => [ 'enabled' => ! empty($input['chatbot_enabled']), - 'widget_key' => sanitize_text_field((string) ($input['chatbot_widget_key'] ?? '')), + 'widget_key' => $this->sanitizeWidgetKey((string) ($input['chatbot_widget_key'] ?? ''), (string) Options::get('chatbot.widget_key', '')), 'identity_enabled' => ! empty($input['chatbot_identity_enabled']), - 'signing_secret' => sanitize_text_field((string) ($input['chatbot_signing_secret'] ?? '')), + 'signing_secret' => $this->sanitizeSigningSecret((string) ($input['chatbot_signing_secret'] ?? '')), ]]), 'search-box' => Options::merge(['search_box' => [ - 'widget_key' => sanitize_text_field((string) ($input['search_box_widget_key'] ?? '')), + 'widget_key' => $this->sanitizeWidgetKey((string) ($input['search_box_widget_key'] ?? ''), (string) Options::get('search_box.widget_key', '')), ]]), 'enhanced-search' => Options::merge(['enhanced' => [ 'enabled' => ! empty($input['enhanced_enabled']), - 'widget_key' => sanitize_text_field((string) ($input['enhanced_widget_key'] ?? '')), + 'widget_key' => $this->sanitizeWidgetKey((string) ($input['enhanced_widget_key'] ?? ''), (string) Options::get('enhanced.widget_key', '')), 'post_types' => array_map('sanitize_key', (array) ($input['enhanced_post_types'] ?? [])), 'summary_enabled' => ! empty($input['enhanced_summary_enabled']), 'summary_selector' => sanitize_text_field((string) ($input['enhanced_summary_selector'] ?? '')), @@ -103,15 +164,20 @@ public function save(): void default => $this->saveConnection($input), }; - wp_safe_redirect(add_query_arg( - [ - 'page' => 'datalumo', - 'tab' => $tab, - 'updated' => '1', - '_wpnonce' => wp_create_nonce('datalumo_settings_updated'), - ], - admin_url('options-general.php'), - )); + $args = [ + 'page' => 'datalumo', + 'tab' => $tab, + '_wpnonce' => wp_create_nonce('datalumo_settings_updated'), + ]; + + if ($this->widgetKeyRejected) { + $args['datalumo_notice'] = 'invalid_widget_key'; + } else { + // options-head.php prints "Settings saved." when updated=1 on a Settings screen. + $args['updated'] = '1'; + } + + wp_safe_redirect(add_query_arg($args, admin_url('options-general.php'))); exit; } @@ -164,4 +230,24 @@ private function saveSyncs(array $input): void Options::set('syncs', $syncs); } + + private function sanitizeWidgetKey(string $value, string $current = ''): string + { + $value = sanitize_text_field($value); + + if ($value === '' || Credentials::looksLikeWidgetKey($value)) { + return $value; + } + + $this->widgetKeyRejected = true; + + return $current; + } + + private function sanitizeSigningSecret(string $value): string + { + $value = sanitize_text_field($value); + + return $value !== '' ? $value : (string) Options::get('chatbot.signing_secret', ''); + } } diff --git a/src/Admin/index.php b/src/Admin/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/src/Admin/index.php @@ -0,0 +1,2 @@ +request('GET', $this->rootUrl('api/v1/me', $baseUrl), token: $token); + } + return $this->request('GET', $this->orgUrl($organisationId, 'me', $baseUrl), token: $token); } + /** + * @return array + */ + public function exchangeWordPressGrant(string $code, string $state, ?string $baseUrl = null): array + { + return $this->request( + 'POST', + $this->rootUrl('api/integrations/wordpress/exchange', $baseUrl), + ['code' => $code, 'state' => $state], + anonymous: true, + ); + } + public function pushPage(string $sourceId, array $payload): array { return $this->request('POST', $this->sourceUrl($sourceId, 'pages'), $payload); @@ -70,13 +88,18 @@ public function search(string $widgetKey, string $query, int $limit = 50, ?strin ); } - private function orgUrl(string $organisationId, string $path, ?string $baseUrl = null): string + private function rootUrl(string $path, ?string $baseUrl = null): string { $base = $baseUrl !== null && $baseUrl !== '' ? Options::normaliseBaseUrl($baseUrl) : Options::baseUrl(); - return $base . '/api/v1/' . rawurlencode($organisationId) . '/' . $path; + return $base . '/' . ltrim($path, '/'); + } + + private function orgUrl(string $organisationId, string $path, ?string $baseUrl = null): string + { + return $this->rootUrl('api/v1/' . rawurlencode($organisationId) . '/' . $path, $baseUrl); } private function sourceUrl(string $sourceId, string $path): string @@ -100,7 +123,7 @@ public function parseWidgetKey(string $key): array return [$parts[0], $parts[1]]; } - private function request(string $method, string $url, array $body = [], ?string $token = null, ?string $origin = null): array + private function request(string $method, string $url, array $body = [], ?string $token = null, ?string $origin = null, bool $anonymous = false): array { $headers = [ 'Accept' => 'application/json', @@ -109,7 +132,7 @@ private function request(string $method, string $url, array $body = [], ?string if ($origin !== null) { $headers['Origin'] = $origin; - } else { + } elseif (! $anonymous) { $token ??= (string) Options::get('api_token'); if ($token !== '') { diff --git a/src/Api/index.php b/src/Api/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/src/Api/index.php @@ -0,0 +1,2 @@ +exchangeWordPressGrant($code, $state); + } catch (ApiException $e) { + self::redirectSettings('connection', 'grant_failed'); + } + + self::apply($payload); + self::redirectSettings('connection', 'connected'); + } + + public static function disconnect(): void + { + self::authorise(); + check_admin_referer(self::ACTION_DISCONNECT); + + $chatbot = (array) Options::get('chatbot', []); + $chatbot['widget_key'] = ''; + $chatbot['signing_secret'] = ''; + $chatbot['enabled'] = false; + $chatbot['identity_enabled'] = false; + + $searchBox = (array) Options::get('search_box', []); + $searchBox['widget_key'] = ''; + + $enhanced = (array) Options::get('enhanced', []); + $enhanced['widget_key'] = ''; + $enhanced['enabled'] = false; + + Options::merge([ + 'api_token' => '', + 'organisation' => [], + 'sources' => [], + 'setup_pending' => false, + 'setup_source_id' => '', + 'connected_via' => '', + 'chatbot' => $chatbot, + 'search_box' => $searchBox, + 'enhanced' => $enhanced, + ]); + + self::redirectSettings('connection', 'disconnected'); + } + + public static function saveSetup(): void + { + self::authorise(); + check_admin_referer(self::ACTION_SETUP); + + $input = wp_unslash($_POST); + $sourceId = (string) Options::get('setup_source_id', ''); + $postTypes = array_map('sanitize_key', (array) ($input['setup_post_types'] ?? [])); + + if ($sourceId !== '' && $postTypes !== []) { + $syncs = (array) Options::get('syncs', []); + $existing = false; + + foreach ($syncs as $index => $row) { + if (($row['source_id'] ?? '') === $sourceId) { + $syncs[$index]['post_types'] = $postTypes; + $existing = true; + break; + } + } + + if (! $existing) { + $syncs[] = [ + 'id' => wp_generate_uuid4(), + 'source_id' => $sourceId, + 'post_types' => $postTypes, + 'meta_mappings' => [], + ]; + } + + Options::set('syncs', $syncs); + } + + $chatbot = (array) Options::get('chatbot', []); + $chatbot['enabled'] = ! empty($input['setup_chat_enabled']); + $chatbot['identity_enabled'] = ! empty($input['setup_identity_enabled']); + Options::set('chatbot', $chatbot); + + $enhanced = (array) Options::get('enhanced', []); + $enhanced['enabled'] = ! empty($input['setup_enhanced_enabled']); + Options::set('enhanced', $enhanced); + + Options::set('setup_pending', false); + + $args = [ + 'page' => 'datalumo', + 'tab' => 'connection', + 'datalumo_notice' => 'setup_saved', + '_wpnonce' => wp_create_nonce('datalumo_settings_updated'), + ]; + + if (! empty($input['setup_sync_now']) && $sourceId !== '') { + $syncId = ''; + + foreach ((array) Options::get('syncs', []) as $row) { + if (($row['source_id'] ?? '') === $sourceId) { + $syncId = (string) ($row['id'] ?? ''); + break; + } + } + + if ($syncId !== '') { + $args['tab'] = 'content-sync'; + $args['datalumo_sync'] = $syncId; + } + } + + wp_safe_redirect(add_query_arg($args, admin_url('options-general.php'))); + exit; + } + + /** + * @param array $payload + */ + public static function apply(array $payload): void + { + $organisation = is_array($payload['organisation'] ?? null) ? $payload['organisation'] : []; + $sources = is_array($payload['sources'] ?? null) ? $payload['sources'] : []; + $source = is_array($payload['source'] ?? null) ? $payload['source'] : []; + $chatbot = is_array($payload['chatbot'] ?? null) ? $payload['chatbot'] : null; + $search = is_array($payload['search'] ?? null) ? $payload['search'] : null; + + $sourceId = (string) ($source['id'] ?? ''); + $stored = [ + 'api_token' => (string) ($payload['token'] ?? ''), + 'organisation' => $organisation, + 'sources' => $sources, + 'setup_pending' => true, + 'setup_source_id' => $sourceId, + 'connected_via' => 'grant', + ]; + + if ($sourceId !== '') { + $syncs = (array) Options::get('syncs', []); + $hasSource = false; + + foreach ($syncs as $row) { + if (($row['source_id'] ?? '') === $sourceId) { + $hasSource = true; + break; + } + } + + if (! $hasSource) { + $syncs[] = [ + 'id' => wp_generate_uuid4(), + 'source_id' => $sourceId, + 'post_types' => ['post', 'page'], + 'meta_mappings' => [], + ]; + $stored['syncs'] = $syncs; + } + } + + if (is_array($chatbot)) { + $stored['chatbot'] = array_merge((array) Options::get('chatbot', []), [ + 'widget_key' => (string) ($chatbot['widget_key'] ?? ''), + 'signing_secret' => (string) ($chatbot['signing_secret'] ?? ''), + ]); + } + + if (is_array($search)) { + $key = (string) ($search['widget_key'] ?? ''); + $stored['search_box'] = array_merge((array) Options::get('search_box', []), [ + 'widget_key' => $key, + ]); + $stored['enhanced'] = array_merge((array) Options::get('enhanced', []), [ + 'widget_key' => $key, + ]); + } + + Options::merge($stored); + } + + public static function startUrl(string $baseUrl, string $state): string + { + $base = Options::normaliseBaseUrl($baseUrl); + $home = home_url('/'); + $host = (string) wp_parse_url($home, PHP_URL_HOST); + + return $base.'/integrations/wordpress?'.http_build_query([ + 'site' => $host, + 'site_url' => $home, + 'return' => admin_url('admin-post.php?action='.self::ACTION_CALLBACK), + 'cancel' => admin_url('options-general.php').'?page=datalumo&tab=connection', + 'state' => $state, + ]); + } + + private static function authorise(): void + { + if (! current_user_can('manage_options')) { + wp_die(esc_html__('Not allowed.', 'datalumo')); + } + } + + private static function redirectSettings(string $tab, string $notice): void + { + wp_safe_redirect(add_query_arg( + [ + 'page' => 'datalumo', + 'tab' => $tab, + 'datalumo_notice' => $notice, + '_wpnonce' => wp_create_nonce('datalumo_settings_updated'), + ], + admin_url('options-general.php'), + )); + exit; + } +} diff --git a/src/Connect/index.php b/src/Connect/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/src/Connect/index.php @@ -0,0 +1,2 @@ +register(); } + /** + * Default product fields for search and chat: short description, + * product categories/tags, visible attributes, and SKU (including + * variation SKUs). A mapping that already set sku is left alone. + * + * @param array $payload + * @return array + */ + public function enrichProduct(array $payload, WP_Post $post): array + { + if (! in_array($post->post_type, ['product', 'product_variation'], true) + || ! function_exists('wc_get_product')) { + return $payload; + } + + $product = wc_get_product($post->ID); + + if (! $product) { + return $payload; + } + + $payload = $this->attachShortDescription($payload, $product); + $payload = $this->attachTaxonomies($payload, $post); + $payload = $this->attachAttributes($payload, $product); + + return $this->attachSku($payload, $product); + } + + /** + * @param array $payload + * @return array + */ + private function attachShortDescription(array $payload, object $product): array + { + if (! method_exists($product, 'get_short_description')) { + return $payload; + } + + $short = trim(wp_strip_all_tags((string) $product->get_short_description())); + + if ($short === '') { + return $payload; + } + + $html = '

'.esc_html($short).'

'; + $content = trim((string) ($payload['content'] ?? '')); + + if ($content === '') { + $payload['content'] = $html; + } elseif (! str_contains($content, $short)) { + $payload['content'] = $html."\n\n".$content; + } + + return $payload; + } + + /** + * @param array $payload + * @return array + */ + private function attachTaxonomies(array $payload, WP_Post $post): array + { + $categoryNames = $this->termField($post->ID, 'product_cat', 'name'); + $categorySlugs = $this->termField($post->ID, 'product_cat', 'slug'); + $tagNames = $this->termField($post->ID, 'product_tag', 'name'); + $tagSlugs = $this->termField($post->ID, 'product_tag', 'slug'); + $meta = is_array($payload['meta'] ?? null) ? $payload['meta'] : []; + $lines = []; + + if ($categorySlugs !== []) { + $meta['categories'] = $categorySlugs; + $lines[] = 'category: '.implode(', ', $categoryNames !== [] ? $categoryNames : $categorySlugs); + } + + if ($tagSlugs !== []) { + $meta['tags'] = $tagSlugs; + $lines[] = 'tag: '.implode(', ', $tagNames !== [] ? $tagNames : $tagSlugs); + } + + $payload['meta'] = $meta; + + return $this->appendSearchableLines($payload, $lines); + } + + /** + * @param array $payload + * @return array + */ + private function attachAttributes(array $payload, object $product): array + { + if (! method_exists($product, 'get_attributes')) { + return $payload; + } + + $attributes = []; + $lines = []; + + foreach ($product->get_attributes() as $attribute) { + if (! is_object($attribute)) { + continue; + } + + if (method_exists($attribute, 'get_visible') && ! $attribute->get_visible()) { + continue; + } + + $label = $this->attributeLabel($attribute); + $values = $this->attributeValues($product, $attribute); + + if ($label === '' || $values === []) { + continue; + } + + $attributes[$label] = $values; + $lines[] = $label.': '.implode(', ', $values); + } + + if ($attributes === []) { + return $payload; + } + + $meta = is_array($payload['meta'] ?? null) ? $payload['meta'] : []; + + if (! isset($meta['attributes'])) { + $meta['attributes'] = $attributes; + $payload['meta'] = $meta; + } + + return $this->appendSearchableLines($payload, $lines); + } + + /** + * @param array $payload + * @return array + */ + private function attachSku(array $payload, object $product): array + { + $skus = $this->productSkus($product); + + if ($skus === []) { + return $payload; + } + + $meta = is_array($payload['meta'] ?? null) ? $payload['meta'] : []; + + if (isset($meta['sku']) && $meta['sku'] !== '' && $meta['sku'] !== []) { + return $payload; + } + + $meta['sku'] = count($skus) === 1 ? $skus[0] : $skus; + $payload['meta'] = $meta; + + return $this->appendSearchableLines($payload, ['sku: '.implode(', ', $skus)]); + } + + /** + * @return array + */ + private function productSkus(object $product): array + { + $skus = []; + $own = trim((string) $product->get_sku()); + + if ($own !== '') { + $skus[] = $own; + } + + if (method_exists($product, 'is_type') && $product->is_type('variable') && method_exists($product, 'get_children')) { + foreach ($product->get_children() as $childId) { + $child = wc_get_product((int) $childId); + + if (! $child) { + continue; + } + + $sku = trim((string) $child->get_sku()); + + if ($sku !== '') { + $skus[] = $sku; + } + } + } + + return array_values(array_unique($skus)); + } + + /** + * @return array + */ + private function termField(int $postId, string $taxonomy, string $field): array + { + $terms = get_the_terms($postId, $taxonomy); + + if (! is_array($terms)) { + return []; + } + + $values = []; + + foreach ($terms as $term) { + $value = trim((string) (is_object($term) ? ($term->{$field} ?? '') : '')); + + if ($value !== '') { + $values[] = $value; + } + } + + return array_values(array_unique($values)); + } + + private function attributeLabel(object $attribute): string + { + $name = method_exists($attribute, 'get_name') ? (string) $attribute->get_name() : ''; + + if (function_exists('wc_attribute_label') && $name !== '') { + $label = trim((string) wc_attribute_label($name)); + + if ($label !== '') { + return $label; + } + } + + return $name; + } + + /** + * @return array + */ + private function attributeValues(object $product, object $attribute): array + { + if (method_exists($attribute, 'is_taxonomy') && $attribute->is_taxonomy() + && method_exists($attribute, 'get_name') + && function_exists('wc_get_product_terms')) { + $id = method_exists($product, 'get_id') ? (int) $product->get_id() : 0; + $terms = wc_get_product_terms($id, $attribute->get_name(), ['fields' => 'names']); + + return is_array($terms) + ? array_values(array_filter(array_map(strval(...), $terms))) + : []; + } + + $options = method_exists($attribute, 'get_options') ? $attribute->get_options() : []; + + return array_values(array_filter(array_map(strval(...), (array) $options))); + } + + /** + * @param array $payload + * @param array $lines + * @return array + */ + private function appendSearchableLines(array $payload, array $lines): array + { + if ($lines === []) { + return $payload; + } + + $payload['content'] = rtrim((string) ($payload['content'] ?? '')) + ."\n\n

".esc_html(implode("\n", $lines)).'

'; + + return $payload; + } + /** * WooCommerce catalog orderings, resolved against product meta. * diff --git a/src/Integration/index.php b/src/Integration/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/src/Integration/index.php @@ -0,0 +1,2 @@ +register(); (new Ajax())->register(); + Grant::register(); } (new ContentSync())->register(); diff --git a/src/Search/index.php b/src/Search/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/src/Search/index.php @@ -0,0 +1,2 @@ +cleanContent($post); - if ($content === '') { - return null; - } - $meta = [ 'post_type' => $post->post_type, 'categories' => $this->termSlugs($post->ID, 'category'), @@ -70,7 +66,18 @@ public function prepare(WP_Post $post, array $metaMappings = []): ?array 'meta' => array_filter($meta, fn ($value) => $value !== null && $value !== '' && $value !== []), ]; - return apply_filters('datalumo_page_payload', $payload, $post); + $payload = apply_filters('datalumo_page_payload', $payload, $post); + + if (! is_array($payload)) { + return null; + } + + // WooCommerce may fill an empty body (short description, SKU, attributes). + if (trim(wp_strip_all_tags((string) ($payload['content'] ?? ''))) === '') { + return null; + } + + return $payload; } /** @@ -80,17 +87,45 @@ public function prepare(WP_Post $post, array $metaMappings = []): ?array */ private function cleanContent(WP_Post $post): string { + $this->ensureWooCommerceNotices(); + $raw = (string) $post->post_content; $raw = apply_filters('datalumo_render_shortcodes', false) ? do_shortcode($raw) : strip_shortcodes($raw); - $html = wp_filter_content_tags(wptexturize(do_blocks(wpautop($raw)))); + $raw = wpautop($raw); + + try { + $html = wp_filter_content_tags(wptexturize(do_blocks($raw))); + } catch (\Throwable) { + // WooCommerce Blocks (cart, checkout) call frontend helpers that + // are not loaded under WP-Cron. Keep the rest of the batch going. + $html = wp_filter_content_tags(wptexturize($raw)); + } return trim(wp_strip_all_tags($html) === '' ? '' : $html); } + /** + * WooCommerce only loads notice helpers on the storefront. Action + * Scheduler still runs do_blocks(), and Hydration::cache_store_notices() + * fatals if wc_get_notices() is missing. + */ + private function ensureWooCommerceNotices(): void + { + if (function_exists('wc_get_notices') || ! defined('WC_ABSPATH')) { + return; + } + + $file = WC_ABSPATH.'includes/wc-notice-functions.php'; + + if (is_readable($file)) { + require_once $file; + } + } + /** * @return array */ diff --git a/src/Sync/index.php b/src/Sync/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/src/Sync/index.php @@ -0,0 +1,2 @@ +justReturn(false); + Functions\when('wp_json_encode')->alias(fn ($value) => json_encode($value)); + Functions\when('wp_remote_retrieve_response_code')->justReturn(200); + Functions\when('wp_remote_retrieve_body')->justReturn('{"organisation":{"id":"org-1"}}'); + Functions\when('wp_remote_retrieve_header')->justReturn(''); + + $seen = null; + Functions\when('wp_remote_request')->alias(function (string $url) use (&$seen) { + $seen = $url; + + return ['response' => ['code' => 200], 'body' => '{"organisation":{"id":"org-1"}}']; + }); + + $result = (new Client())->me('', 'plain-token'); + + expect($seen)->toBe('https://datalumo.app/api/v1/me') + ->and($result['organisation']['id'])->toBe('org-1'); +}); + +it('keeps the org-scoped /me path when an organisation id is given', function () { + Functions\when('is_wp_error')->justReturn(false); + Functions\when('wp_json_encode')->alias(fn ($value) => json_encode($value)); + Functions\when('wp_remote_retrieve_response_code')->justReturn(200); + Functions\when('wp_remote_retrieve_body')->justReturn('{"organisation":{"id":"org-1"}}'); + Functions\when('wp_remote_retrieve_header')->justReturn(''); + + $seen = null; + Functions\when('wp_remote_request')->alias(function (string $url) use (&$seen) { + $seen = $url; + + return ['response' => ['code' => 200], 'body' => '{"organisation":{"id":"org-1"}}']; + }); + + (new Client())->me('org-1', 'plain-token'); + + expect($seen)->toBe('https://datalumo.app/api/v1/org-1/me'); +}); + +it('parses a compound widget key and rejects a secret', function () { + Functions\when('esc_html__')->returnArg(1); + + expect((new Client())->parseWidgetKey('org-1/widget-2'))->toBe(['org-1', 'widget-2']) + ->and(fn () => (new Client())->parseWidgetKey('dl_secret')) + ->toThrow(ApiException::class); +}); diff --git a/tests/Unit/CredentialsTest.php b/tests/Unit/CredentialsTest.php new file mode 100644 index 0000000..ae95fe8 --- /dev/null +++ b/tests/Unit/CredentialsTest.php @@ -0,0 +1,20 @@ +toBeTrue(); +}); + +it('does not treat a secret as a widget key', function () { + expect(Credentials::looksLikeWidgetKey('dl_abc123'))->toBeFalse(); +}); + +it('does not treat a bare token as a widget key', function () { + expect(Credentials::looksLikeWidgetKey('1|plain-api-token'))->toBeFalse(); +}); + +it('recognises a dl_ secret', function () { + expect(Credentials::looksLikeSecret('dl_abc123'))->toBeTrue() + ->and(Credentials::looksLikeSecret('1|plain-api-token'))->toBeFalse(); +}); diff --git a/tests/Unit/GrantTest.php b/tests/Unit/GrantTest.php new file mode 100644 index 0000000..83fcf33 --- /dev/null +++ b/tests/Unit/GrantTest.php @@ -0,0 +1,104 @@ +justReturn('https://shop.example/'); + Functions\when('admin_url')->alias(fn (string $path) => 'https://shop.example/wp-admin/'.$path); + Functions\when('wp_parse_url')->alias(fn (string $url, int $component = -1) => parse_url($url, $component)); + + expect(Grant::startUrl('https://datalumo.app', 'state-1'))->toBe( + 'https://datalumo.app/integrations/wordpress?'.http_build_query([ + 'site' => 'shop.example', + 'site_url' => 'https://shop.example/', + 'return' => 'https://shop.example/wp-admin/admin-post.php?action=datalumo_oauth_callback', + 'cancel' => 'https://shop.example/wp-admin/options-general.php?page=datalumo&tab=connection', + 'state' => 'state-1', + ]), + ); +}); + +it('maps an exchange payload onto stored options', function () { + Grant::apply([ + 'token' => 'tok_1', + 'organisation' => ['id' => 'org_1', 'name' => 'Acme'], + 'sources' => [['id' => 'src_1', 'name' => 'shop.example']], + 'source' => ['id' => 'src_1', 'name' => 'shop.example'], + 'chatbot' => ['widget_key' => 'org_1/chat_1', 'signing_secret' => 'dl_secret'], + 'search' => ['widget_key' => 'org_1/search_1'], + ]); + + expect(Options::get('api_token'))->toBe('tok_1') + ->and(Options::get('organisation.id'))->toBe('org_1') + ->and(Options::get('setup_pending'))->toBeTrue() + ->and(Options::get('setup_source_id'))->toBe('src_1') + ->and(Options::get('connected_via'))->toBe('grant') + ->and(Options::get('chatbot.widget_key'))->toBe('org_1/chat_1') + ->and(Options::get('chatbot.signing_secret'))->toBe('dl_secret') + ->and(Options::get('search_box.widget_key'))->toBe('org_1/search_1') + ->and(Options::get('enhanced.widget_key'))->toBe('org_1/search_1') + ->and(Options::get('syncs.0.source_id'))->toBe('src_1') + ->and(Options::get('syncs.0.post_types'))->toBe(['post', 'page']); +}); + +it('sends the browser to Datalumo with wp_redirect so an off-site host is not rewritten', function () { + Functions\when('current_user_can')->justReturn(true); + Functions\when('check_admin_referer')->justReturn(true); + Functions\when('set_transient')->justReturn(true); + Functions\when('get_current_user_id')->justReturn(1); + Functions\when('esc_url_raw')->returnArg(1); + Functions\when('wp_unslash')->returnArg(1); + Functions\when('home_url')->justReturn('http://datalumo-wp.test/'); + Functions\when('admin_url')->alias(fn (string $path) => 'http://datalumo-wp.test/wp-admin/'.$path); + Functions\when('wp_parse_url')->alias(fn (string $url, int $component = -1) => parse_url($url, $component)); + + $_POST = ['api_url' => 'https://dl.test']; + + $redirected = null; + Functions\expect('wp_redirect')->once()->andReturnUsing(function (string $url) use (&$redirected): void { + $redirected = $url; + throw new RuntimeException('redirect'); + }); + Functions\expect('wp_safe_redirect')->never(); + + try { + Grant::start(); + expect(false)->toBeTrue(); + } catch (RuntimeException $e) { + expect($e->getMessage())->toBe('redirect'); + } + + expect($redirected)->toStartWith('https://dl.test/integrations/wordpress?'); +}); + +it('rejects a callback when the stored state does not match', function () { + Functions\when('current_user_can')->justReturn(true); + Functions\when('get_current_user_id')->justReturn(1); + Functions\when('get_transient')->justReturn('expected-state'); + Functions\when('delete_transient')->justReturn(true); + Functions\when('sanitize_text_field')->returnArg(1); + Functions\when('wp_unslash')->returnArg(1); + Functions\when('wp_create_nonce')->justReturn('nonce'); + Functions\when('admin_url')->justReturn('https://shop.example/wp-admin/options-general.php'); + Functions\when('add_query_arg')->alias(fn (array $args, string $url) => $url.'?'.http_build_query($args)); + + $redirected = null; + Functions\when('wp_safe_redirect')->alias(function (string $url) use (&$redirected): void { + $redirected = $url; + throw new RuntimeException('redirect'); + }); + + $_GET = ['state' => 'wrong-state', 'code' => 'abc']; + + try { + Grant::callback(); + expect(false)->toBeTrue(); + } catch (RuntimeException $e) { + expect($e->getMessage())->toBe('redirect'); + } + + expect($redirected)->toContain('grant_failed') + ->and($redirected)->not->toContain('connected'); +}); diff --git a/tests/Unit/IndexPhpTest.php b/tests/Unit/IndexPhpTest.php new file mode 100644 index 0000000..18dcb4e --- /dev/null +++ b/tests/Unit/IndexPhpTest.php @@ -0,0 +1,35 @@ +isDir() || ! in_array($current->getFilename(), $skip, true); + }, + ), + RecursiveIteratorIterator::SELF_FIRST, + ); + + $missing = []; + + foreach (array_merge([$root], iterator_to_array($iterator)) as $file) { + $dir = $file instanceof SplFileInfo + ? ($file->isDir() ? $file->getPathname() : null) + : $file; + + if ($dir === null) { + continue; + } + + $index = $dir.'/index.php'; + + if (! is_file($index) || ! str_contains((string) file_get_contents($index), 'Silence is golden')) { + $missing[] = $dir; + } + } + + expect($missing)->toBe([]); +}); diff --git a/tests/Unit/PagePreparerTest.php b/tests/Unit/PagePreparerTest.php index 31c9c5b..b413063 100644 --- a/tests/Unit/PagePreparerTest.php +++ b/tests/Unit/PagePreparerTest.php @@ -33,6 +33,18 @@ ->and($payload['content_mime'])->toBe('text/html'); }); +it('falls back when block rendering fatals', function () { + Functions\when('get_the_title')->justReturn('Product'); + Functions\when('do_blocks')->alias(function (): string { + throw new Error('Call to undefined function Automattic\WooCommerce\Blocks\Domain\Services\wc_get_notices()'); + }); + + $post = new WP_Post(); + $post->post_content = '

Ceramic mug

'; + + expect((new PagePreparer())->prepare($post)['content'])->toBe('

Ceramic mug

'); +}); + it('skips a post with no content', function () { Functions\when('get_the_title')->justReturn('Empty'); Functions\when('wp_strip_all_tags')->justReturn(''); @@ -42,3 +54,19 @@ expect((new PagePreparer())->prepare($post))->toBeNull(); }); + +it('keeps a payload the filter filled after an empty body', function () { + Functions\when('get_the_title')->justReturn('Mug'); + Functions\when('apply_filters')->alias(function (string $hook, mixed $value) { + if ($hook === 'datalumo_page_payload' && is_array($value)) { + $value['content'] = '

A ceramic mug.

'; + } + + return $value; + }); + + $post = new WP_Post(); + $post->post_content = ''; + + expect((new PagePreparer())->prepare($post)['content'])->toBe('

A ceramic mug.

'); +}); diff --git a/tests/Unit/SettingsPageTest.php b/tests/Unit/SettingsPageTest.php new file mode 100644 index 0000000..775570e --- /dev/null +++ b/tests/Unit/SettingsPageTest.php @@ -0,0 +1,65 @@ +returnArg(1); + + $page = new SettingsPage(); + $method = new ReflectionMethod(SettingsPage::class, 'sanitizeWidgetKey'); + $rejected = new ReflectionProperty(SettingsPage::class, 'widgetKeyRejected'); + + expect($method->invoke($page, $pasted, 'org-1/widget-2'))->toBe('org-1/widget-2') + ->and($rejected->getValue($page))->toBeTrue(); +})->with([ + 'dl_secret', + '1|plain-api-token', + 'not-a-key', +]); + +it('does not print a second Settings saved notice', function () { + $view = (string) file_get_contents(dirname(__DIR__, 2).'/resources/views/settings.php'); + + expect($view)->not->toContain("esc_html_e('Settings saved.'"); +}); + +it('points settings help at the WordPress plugin docs', function () { + expect(SettingsPage::docsUrl())->toBe('https://datalumo.app/docs/wordpress') + ->and(SettingsPage::docsUrl('chat-page-actions'))->toBe('https://datalumo.app/docs/wordpress#chat-page-actions') + ->and(SettingsPage::helpUrl())->toBe('https://datalumo.app/docs/wordpress') + ->and(SettingsPage::ASK_MAX_LENGTH)->toBe(280); +}); + +it('hides the settings sidebar until setup is ready', function () { + $view = (string) file_get_contents(dirname(__DIR__, 2).'/resources/views/settings.php'); + + expect($view)->toContain('datalumo-settings-aside') + ->and(substr_count($view, 'if (SettingsPage::setupIsReady())'))->toBe(2); +}); + +it('hides other tabs until the post-connect checklist is saved', function () { + Options::merge([ + 'api_token' => 'tok_1', + 'organisation' => ['id' => 'org_1'], + 'setup_pending' => true, + ]); + + expect(SettingsPage::setupIsReady())->toBeFalse(); + + Options::set('setup_pending', false); + + expect(SettingsPage::setupIsReady())->toBeTrue(); +}); + +it('accepts a compound widget key', function () { + Functions\when('sanitize_text_field')->returnArg(1); + + $page = new SettingsPage(); + $method = new ReflectionMethod(SettingsPage::class, 'sanitizeWidgetKey'); + $rejected = new ReflectionProperty(SettingsPage::class, 'widgetKeyRejected'); + + expect($method->invoke($page, 'org-1/widget-2', ''))->toBe('org-1/widget-2') + ->and($rejected->getValue($page))->toBeFalse(); +}); diff --git a/tests/Unit/WooCommerceSkuTest.php b/tests/Unit/WooCommerceSkuTest.php new file mode 100644 index 0000000..cad9da3 --- /dev/null +++ b/tests/Unit/WooCommerceSkuTest.php @@ -0,0 +1,230 @@ +id; + } + + public function get_sku(): string + { + return $this->sku; + } + + public function get_short_description(): string + { + return $this->shortDescription; + } + + public function get_attributes(): array + { + return $this->attributes; + } + + public function is_type(string $type): bool + { + return $this->type === $type; + } + + public function get_children(): array + { + return $this->children; + } + }; +} + +function wcAttribute(string $name, array $options, bool $visible = true, bool $taxonomy = false): object +{ + return new class($name, $options, $visible, $taxonomy) + { + public function __construct( + private string $name, + private array $options, + private bool $visible, + private bool $taxonomy, + ) {} + + public function get_name(): string + { + return $this->name; + } + + public function get_visible(): bool + { + return $this->visible; + } + + public function is_taxonomy(): bool + { + return $this->taxonomy; + } + + public function get_options(): array + { + return $this->options; + } + }; +} + +beforeEach(function () { + Functions\when('wp_strip_all_tags')->returnArg(1); + Functions\when('wc_attribute_label')->returnArg(1); + Functions\when('get_the_terms')->justReturn([]); +}); + +it('leaves non-product payloads alone', function () { + $post = new WP_Post(); + $post->post_type = 'page'; + + $payload = ['content' => '

About

', 'meta' => ['post_type' => 'page']]; + + expect((new WooCommerce())->enrichProduct($payload, $post))->toBe($payload); +}); + +it('adds a simple product sku to meta and searchable content', function () { + Functions\when('wc_get_product')->justReturn(wcProduct(sku: 'VNECK-BLU')); + + $post = new WP_Post(); + $post->ID = 12; + $post->post_type = 'product'; + + $payload = (new WooCommerce())->enrichProduct([ + 'content' => '

A blue v-neck.

', + 'meta' => ['post_type' => 'product'], + ], $post); + + expect($payload['meta']['sku'])->toBe('VNECK-BLU') + ->and($payload['content'])->toContain('sku: VNECK-BLU'); +}); + +it('includes variation skus on a variable product', function () { + Functions\when('wc_get_product')->alias(function (int $id) { + return match ($id) { + 10 => wcProduct(sku: 'jacket', type: 'variable', children: [11, 12], id: 10), + 11 => wcProduct(sku: 'jacket-blue-m', id: 11), + 12 => wcProduct(sku: 'jacket-red-l', id: 12), + default => null, + }; + }); + + $post = new WP_Post(); + $post->ID = 10; + $post->post_type = 'product'; + + $payload = (new WooCommerce())->enrichProduct([ + 'content' => '

A jacket.

', + 'meta' => ['post_type' => 'product'], + ], $post); + + expect($payload['meta']['sku'])->toBe(['jacket', 'jacket-blue-m', 'jacket-red-l']) + ->and($payload['content'])->toContain('sku: jacket, jacket-blue-m, jacket-red-l'); +}); + +it('does not overwrite an existing sku mapping', function () { + Functions\when('wc_get_product')->justReturn(wcProduct(sku: 'FROM-WC')); + + $post = new WP_Post(); + $post->ID = 3; + $post->post_type = 'product'; + + $payload = (new WooCommerce())->enrichProduct([ + 'content' => '

Mapped.

', + 'meta' => ['sku' => 'FROM-MAP'], + ], $post); + + expect($payload['meta']['sku'])->toBe('FROM-MAP') + ->and($payload['content'])->toBe('

Mapped.

'); +}); + +it('prepends the short description when the body is empty', function () { + Functions\when('wc_get_product')->justReturn(wcProduct( + sku: 'MUG-1', + shortDescription: 'A ceramic mug.', + )); + + $post = new WP_Post(); + $post->ID = 4; + $post->post_type = 'product'; + + $payload = (new WooCommerce())->enrichProduct([ + 'content' => '', + 'meta' => ['post_type' => 'product'], + ], $post); + + expect($payload['content'])->toStartWith('

A ceramic mug.

') + ->and($payload['content'])->toContain('sku: MUG-1'); +}); + +it('adds product categories and tags as searchable meta', function () { + Functions\when('wc_get_product')->justReturn(wcProduct(sku: 'SHOE-1')); + Functions\when('get_the_terms')->alias(function (int $id, string $taxonomy) { + return match ($taxonomy) { + 'product_cat' => [ + (object) ['name' => 'Running', 'slug' => 'running'], + (object) ['name' => 'Mens', 'slug' => 'mens'], + ], + 'product_tag' => [ + (object) ['name' => 'Sale', 'slug' => 'sale'], + ], + default => [], + }; + }); + + $post = new WP_Post(); + $post->ID = 8; + $post->post_type = 'product'; + + $payload = (new WooCommerce())->enrichProduct([ + 'content' => '

Trail shoe.

', + 'meta' => ['post_type' => 'product'], + ], $post); + + expect($payload['meta']['categories'])->toBe(['running', 'mens']) + ->and($payload['meta']['tags'])->toBe(['sale']) + ->and($payload['content'])->toContain('category: Running, Mens') + ->and($payload['content'])->toContain('tag: Sale'); +}); + +it('adds visible attributes and skips hidden ones', function () { + Functions\when('wc_get_product')->justReturn(wcProduct( + sku: 'JKT-1', + attributes: [ + wcAttribute('Color', ['Blue', 'Red']), + wcAttribute('Internal', ['secret'], visible: false), + ], + )); + + $post = new WP_Post(); + $post->ID = 9; + $post->post_type = 'product'; + + $payload = (new WooCommerce())->enrichProduct([ + 'content' => '

A jacket.

', + 'meta' => ['post_type' => 'product'], + ], $post); + + expect($payload['meta']['attributes'])->toBe(['Color' => ['Blue', 'Red']]) + ->and($payload['content'])->toContain('Color: Blue, Red') + ->and($payload['content'])->not->toContain('Internal'); +}); diff --git a/tests/Unit/index.php b/tests/Unit/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/tests/Unit/index.php @@ -0,0 +1,2 @@ + Date: Sat, 29 Aug 2026 00:20:48 +0900 Subject: [PATCH 3/9] Hide the self-hosted Datalumo URL behind a disclosure. Connect with Datalumo stays the main action. The URL field is still there for self-hosted installs, and opens when a non-cloud URL is already stored. Co-authored-by: Cursor --- readme.txt | 2 +- resources/css/admin.css | 23 +++++++++++++++++++++++ resources/views/settings.php | 19 +++++++++++++------ tests/Unit/SettingsPageTest.php | 8 ++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/readme.txt b/readme.txt index 7045cb9..1b1b3e3 100644 --- a/readme.txt +++ b/readme.txt @@ -65,7 +65,7 @@ Only after you connect and enable a feature. Content sync sends the published po = Can I use a self-hosted Datalumo instance? = -Yes. Set the Datalumo URL on the Connection tab before you connect. +Yes. On the Connection tab, open Using a self-hosted Datalumo? and set the URL before you connect. Manual setup has the same field. = Can the chatbot add products to a WooCommerce cart? = diff --git a/resources/css/admin.css b/resources/css/admin.css index 4558221..74885da 100644 --- a/resources/css/admin.css +++ b/resources/css/admin.css @@ -184,6 +184,29 @@ padding: 0; } +.datalumo-self-host { + margin-top: 1rem; + color: #646970; +} + +.datalumo-self-host summary { + cursor: pointer; + font-size: 13px; +} + +.datalumo-self-host[open] summary { + margin-bottom: 0.5rem; +} + +.datalumo-self-host label { + display: block; + margin-bottom: 0.25rem; +} + +.datalumo-self-host p { + margin: 0; +} + .datalumo-settings-layout { display: flex; flex-wrap: wrap; diff --git a/resources/views/settings.php b/resources/views/settings.php index 4fb7a6c..a0047d5 100644 --- a/resources/views/settings.php +++ b/resources/views/settings.php @@ -169,14 +169,21 @@ class="nav-tab "> + -

- - -

+ +
> + +

+ + +

+
-

diff --git a/tests/Unit/SettingsPageTest.php b/tests/Unit/SettingsPageTest.php index 775570e..ada4439 100644 --- a/tests/Unit/SettingsPageTest.php +++ b/tests/Unit/SettingsPageTest.php @@ -32,6 +32,14 @@ ->and(SettingsPage::ASK_MAX_LENGTH)->toBe(280); }); +it('hides the self-hosted URL behind a disclosure on connect', function () { + $view = (string) file_get_contents(dirname(__DIR__, 2).'/resources/views/settings.php'); + + expect($view)->toContain('datalumo-self-host') + ->and($view)->toContain("Using a self-hosted Datalumo?") + ->and($view)->toContain('datalumo-grant-api-url'); +}); + it('hides the settings sidebar until setup is ready', function () { $view = (string) file_get_contents(dirname(__DIR__, 2).'/resources/views/settings.php'); From c73acb13a66eb5bf1561cf3e7746c82d70560cb4 Mon Sep 17 00:00:00 2001 From: Jeffrey van Rossum Date: Sat, 29 Aug 2026 00:40:09 +0900 Subject: [PATCH 4/9] Keep bin out of the GitHub release zip. fresh-site and ensure-index-php are maintainer tools, not something a WordPress site should receive. Co-authored-by: Cursor --- .distignore | 1 + .github/workflows/release.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.distignore b/.distignore index 9bb3c44..6d9d17a 100644 --- a/.distignore +++ b/.distignore @@ -5,6 +5,7 @@ .slimm .phpunit.cache tests +bin phpunit.xml composer.lock build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 76a7a35..94d00ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,6 +56,7 @@ jobs: --exclude 'build' \ --exclude '*.zip' \ --exclude 'tests' \ + --exclude 'bin' \ --exclude 'phpunit.xml' \ --exclude '.phpunit.cache' \ --exclude '.DS_Store' From cafed64d13772cc4acea43814afe028d72ae10f4 Mon Sep 17 00:00:00 2001 From: Jeffrey van Rossum Date: Sun, 30 Aug 2026 16:32:40 +0900 Subject: [PATCH 5/9] Clear Plugin Check errors from the merged Connect and Woo code. Add translators comments, use wp_parse_url, and document the remaining scanner false positives. Co-authored-by: Cursor --- src/Connect/Grant.php | 3 +++ src/Integration/AddToCart.php | 6 ++++++ src/Integration/HostNavigation.php | 8 ++++++-- tests/Unit/HostNavigationTest.php | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Connect/Grant.php b/src/Connect/Grant.php index f776426..294a842 100644 --- a/src/Connect/Grant.php +++ b/src/Connect/Grant.php @@ -51,6 +51,7 @@ public static function start(): void // Off-site on purpose. wp_safe_redirect would rewrite dl.test / // datalumo.app back to this site's admin. + // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Datalumo grant URL is off-site. wp_redirect($url); exit; } @@ -60,8 +61,10 @@ public static function callback(): void self::authorise(); $expected = (string) get_transient(self::TRANSIENT.'_'.get_current_user_id()); + // phpcs:disable WordPress.Security.NonceVerification.Recommended -- OAuth callback; CSRF is the hashed state. $state = sanitize_text_field(wp_unslash((string) ($_GET['state'] ?? ''))); $code = sanitize_text_field(wp_unslash((string) ($_GET['code'] ?? ''))); + // phpcs:enable WordPress.Security.NonceVerification.Recommended delete_transient(self::TRANSIENT.'_'.get_current_user_id()); diff --git a/src/Integration/AddToCart.php b/src/Integration/AddToCart.php index 12471a7..b75cc87 100644 --- a/src/Integration/AddToCart.php +++ b/src/Integration/AddToCart.php @@ -285,6 +285,7 @@ public function add(int $productId, int $quantity, int $variationId = 0, array $ return [ 'ok' => true, 'message' => $name !== '' + /* translators: %s: product name */ ? sprintf(__('%s added to your cart.', 'datalumo'), $name) : __('Added to your cart.', 'datalumo'), 'fragments' => $this->cartFragments(), @@ -371,6 +372,7 @@ private function elicitAttributes(object $product, array $rows, array $keys, arr return [ 'ok' => false, 'status' => 'choices', + /* translators: %s: product attribute name, such as colour or size */ 'message' => sprintf(__('Which %s?', 'datalumo'), self::attributeLabel($keys[0])), 'layout' => 'options', 'choices' => $choices, @@ -519,6 +521,7 @@ public static function choicesTree(array $rows, array $keys, array $selected, ob return $options; } + /* translators: %s: product attribute name, such as colour or size */ $nextMessage = sprintf(__('Which %s?', 'datalumo'), self::attributeLabel($rest[0])); $tree = []; @@ -761,7 +764,9 @@ private static function unprefixedAttributeKey(string $key): string private function requestPayload(): array { // phpcs:disable WordPress.Security.NonceVerification.Missing -- verified in handle(). + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded below. $raw = wp_unslash($_POST['payload'] ?? ''); + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // phpcs:enable WordPress.Security.NonceVerification.Missing if (is_string($raw) && $raw !== '') { @@ -788,6 +793,7 @@ private function cartFragments(): array woocommerce_mini_cart(); $miniCart = (string) ob_get_clean(); + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce cart fragment API. $fragments = apply_filters('woocommerce_add_to_cart_fragments', [ 'div.widget_shopping_cart_content' => '

' . $miniCart . '
', ]); diff --git a/src/Integration/HostNavigation.php b/src/Integration/HostNavigation.php index 8f918b8..e067e51 100644 --- a/src/Integration/HostNavigation.php +++ b/src/Integration/HostNavigation.php @@ -127,7 +127,7 @@ public function publishedUrlFromPayload(array $payload): ?string public function isSameSiteUrl(string $url): bool { - $parts = parse_url($url); + $parts = wp_parse_url($url); if (! is_array($parts) || ! isset($parts['scheme'], $parts['host'])) { return false; @@ -172,7 +172,7 @@ public function checkoutUrl(): string private function homeHost(): string { $home = function_exists('home_url') ? home_url('/') : ''; - $host = parse_url($home, PHP_URL_HOST); + $host = wp_parse_url($home, PHP_URL_HOST); return is_string($host) ? strtolower($host) : ''; } @@ -210,7 +210,11 @@ private function positiveId(mixed $value): ?int */ private function requestPayload(): array { + // phpcs:disable WordPress.Security.NonceVerification.Missing -- verified in handle(). + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded below. $raw = isset($_POST['payload']) ? wp_unslash($_POST['payload']) : ''; + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + // phpcs:enable WordPress.Security.NonceVerification.Missing if (! is_string($raw) || $raw === '') { return []; diff --git a/tests/Unit/HostNavigationTest.php b/tests/Unit/HostNavigationTest.php index a4b9b6a..9e6df3d 100644 --- a/tests/Unit/HostNavigationTest.php +++ b/tests/Unit/HostNavigationTest.php @@ -15,6 +15,7 @@ function hostNav(): HostNavigation { Functions\when('home_url')->justReturn('https://shop.test/'); + Functions\when('wp_parse_url')->alias(fn (string $url, int $component = -1) => parse_url($url, $component)); return new HostNavigation(); } From 911f7ea803795c6936a118163cc7880409896d96 Mon Sep 17 00:00:00 2001 From: Jeffrey van Rossum Date: Sun, 30 Aug 2026 16:53:57 +0900 Subject: [PATCH 6/9] Tighten the directory readme and stop promising conversation resume. List jeffreyvr as the only contributor, document the remaining data sent, and treat visitor identity as an optional hook. Co-authored-by: Cursor --- readme.txt | 20 ++++++++++---------- resources/views/settings.php | 5 +++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/readme.txt b/readme.txt index 53b3ac3..8717bc1 100644 --- a/readme.txt +++ b/readme.txt @@ -1,5 +1,5 @@ === Datalumo === -Contributors: datalumo, jeffreyvr +Contributors: jeffreyvr Tags: search, ai, chatbot, rag Requires at least: 6.0 Tested up to: 7.1 @@ -18,8 +18,7 @@ Datalumo keeps a searchable, AI-ready copy of your WordPress content and gives y * **Chat widget** — a floating assistant that answers from your content, with sources. * **Search box** — drop-in search via the `[datalumo_search]` shortcode. * **Enhanced search** — serve WordPress' native search results from Datalumo's ranking, with an optional streamed AI summary above the results. Falls back to native search automatically. -* **Visitor identity** — logged-in users can continue their chat conversations across visits, verified with a server-side signature. -* **Chat page actions**: the official plugin listens for confirmed chat events (`add_to_cart`, `view_cart`, `open_checkout`, `open_page`). Add those actions in Datalumo under Reply → What it can do. +* **Chat actions** — add to cart, view cart, checkout, and open a page. Add them in Datalumo under Reply → What it can do. This plugin is an interface to a [Datalumo](https://datalumo.app) instance. You need a Datalumo account (or a self-hosted instance). Connect with Datalumo from Settings, or use Manual setup to paste an API token. Nothing is sent until an administrator connects the site and turns a feature on. @@ -35,10 +34,12 @@ The plugin talks to the Datalumo instance you configure. The default host is `ht When a feature is enabled, the plugin may: * Load the widget script from `{your Datalumo URL}/widget/v1/datalumo.js` (chat, search box, AI summary, and click tracking). -* Send published post content to the Datalumo API so it can be indexed (title, HTML, permalink, featured image URL, author display name, categories, tags, dates, and any custom field mappings you add). WooCommerce products also send short description, product categories and tags, visible attributes, SKU, and the product image when there is no featured image. -* Send visitor search queries to Datalumo when enhanced search is on. +* Send published content to the Datalumo API: title, HTML, permalink, featured image URL, author display name, categories, tags, dates, and any custom field mappings. WooCommerce products also send short description, product categories and tags, visible attributes, SKU, and the product image when there is no featured image. +* On Connect with Datalumo, send this site's host and URL so the grant can return to this admin. +* Send the current page id as chat context, plus product id and SKU on product pages. +* Send visitor search queries when enhanced search is on. * Send result-click events (URL, rank, search session) when enhanced search is on. -* Send a signed visitor id (`wp-user-{id}` HMAC) when visitor identity is enabled on the Chatbot tab. +* Send a signed visitor id (`wp-user-{id}` HMAC) if you enable identity on the Chatbot tab. Optional, for custom implementations. The plugin does not resume conversations. = Development = @@ -61,7 +62,7 @@ Yes. The plugin does not search or chat on its own. It connects WordPress to a D = What data is sent to Datalumo? = -Only after you connect and enable a feature. Content sync sends the published posts you choose. Enhanced search sends visitor queries and optional result-click analytics. The chat and search widgets load Datalumo's script and talk to that instance. Visitor identity is off unless you turn it on. +Only after you connect and turn a feature on. The list is under External services above. = Can I use a self-hosted Datalumo instance? = @@ -69,7 +70,7 @@ Yes. On the Connection tab, open Using a self-hosted Datalumo? and set the URL b = Can the chatbot add products to a WooCommerce cart? = -Yes, if WooCommerce is active. In Datalumo, add the WordPress Add to cart action (event add_to_cart, field product_id). The visitor confirms in chat. Variable products send colour, then that colour's sizes, as nested choice steps (up to 8 options per step); more than that opens the product page. +Yes, if WooCommerce is active. In Datalumo, add the WordPress Add to cart action. The visitor confirms in chat. Variable products can ask for options first. = What other chat actions does the plugin handle? = @@ -113,6 +114,5 @@ View cart and Open checkout (WooCommerce), and Open this page (a post or page by = 0.0.1 = * Initial public release. -* Content sync, chat/search widgets, enhanced search, and visitor identity. -* GitHub auto-updates via Plugin Update Checker (removed ahead of the WordPress.org release). +* Content sync, chat/search widgets, and enhanced search. * Dutch and Chinese translations. diff --git a/resources/views/settings.php b/resources/views/settings.php index a0047d5..c859022 100644 --- a/resources/views/settings.php +++ b/resources/views/settings.php @@ -77,7 +77,7 @@ class="nav-tab __('Custom fields, filters, and sync details.', 'datalumo'), - 'chatbot' => __('Shortcodes, visitor identity, and chat page actions.', 'datalumo'), + 'chatbot' => __('Shortcodes and chat page actions.', 'datalumo'), 'search-box', 'enhanced-search' => __('Search box shortcode and enhanced search.', 'datalumo'), default => __('Connect, sync, and widgets.', 'datalumo'), }; @@ -353,8 +353,9 @@ class="nav-tab +

From c5398cf092c24b1906cdc086b735ccfff81e8ed5 Mon Sep 17 00:00:00 2001 From: Jeffrey van Rossum Date: Sun, 30 Aug 2026 16:55:27 +0900 Subject: [PATCH 7/9] Add WordPress.org plugin icons as full-bleed squares. Keep them in .wordpress-org so the release zip does not ship directory assets. Co-authored-by: Cursor --- .distignore | 1 + .github/workflows/release.yml | 1 + .wordpress-org/icon-128x128.png | Bin 0 -> 4500 bytes .wordpress-org/icon-256x256.png | Bin 0 -> 9957 bytes 4 files changed, 2 insertions(+) create mode 100644 .wordpress-org/icon-128x128.png create mode 100644 .wordpress-org/icon-256x256.png diff --git a/.distignore b/.distignore index 6d9d17a..0ab376d 100644 --- a/.distignore +++ b/.distignore @@ -2,6 +2,7 @@ .github .gitignore .distignore +.wordpress-org .slimm .phpunit.cache tests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fdaa4f..32f0197 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,6 +52,7 @@ jobs: --exclude '.github' \ --exclude '.gitignore' \ --exclude '.distignore' \ + --exclude '.wordpress-org' \ --exclude '.slimm' \ --exclude 'build' \ --exclude '*.zip' \ diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..e9970223f413f8d04bbcfb19b4ac22e6a3d4b598 GIT binary patch literal 4500 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 bNad|utp>F{jrKqPwgCFt#&Eo*!>j)RGM2AK literal 0 HcmV?d00001 diff --git a/.wordpress-org/icon-256x256.png b/.wordpress-org/icon-256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..1f35e1420d2bbea03f11e095f5d07b84a7d5f371 GIT binary patch literal 9957 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 literal 0 HcmV?d00001 From 65ea8bc592f9f0defc10f52bbc7724013b09fe40 Mon Sep 17 00:00:00 2001 From: Jeffrey van Rossum Date: Sun, 30 Aug 2026 17:14:45 +0900 Subject: [PATCH 8/9] Release 1.0.0 and complete the locale files for the WordPress.org branch. Co-authored-by: Cursor --- bin/ensure-index-php | 2 +- datalumo.php | 4 +- languages/datalumo-nl_NL.mo | Bin 6642 -> 9613 bytes languages/datalumo-nl_NL.po | 435 ++++++++++++++++++++++++++--------- languages/datalumo-zh_CN.mo | Bin 6208 -> 9015 bytes languages/datalumo-zh_CN.po | 435 ++++++++++++++++++++++++++--------- languages/datalumo-zh_HK.mo | Bin 6216 -> 9038 bytes languages/datalumo-zh_HK.po | 435 ++++++++++++++++++++++++++--------- languages/datalumo-zh_TW.mo | Bin 6219 -> 9041 bytes languages/datalumo-zh_TW.po | 435 ++++++++++++++++++++++++++--------- languages/datalumo.pot | 445 +++++++++++++++++++++++++++--------- readme.txt | 5 +- tests/Unit/IndexPhpTest.php | 2 +- 13 files changed, 1639 insertions(+), 559 deletions(-) diff --git a/bin/ensure-index-php b/bin/ensure-index-php index cfd748b..abb7332 100755 --- a/bin/ensure-index-php +++ b/bin/ensure-index-php @@ -7,7 +7,7 @@ */ $root = dirname(__DIR__); -$skip = ['.git', '.github', '.slimm', '.phpunit.cache']; +$skip = ['.git', '.github', '.slimm', '.phpunit.cache', '.wordpress-org', 'build']; $contents = "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 E3*?#18~^|S diff --git a/languages/datalumo-nl_NL.po b/languages/datalumo-nl_NL.po index db7e4c7..bbc1945 100644 --- a/languages/datalumo-nl_NL.po +++ b/languages/datalumo-nl_NL.po @@ -1,275 +1,490 @@ -# Dutch (Netherlands) translation for Datalumo -# Copyright (C) 2026 Jeffrey van Rossum -# This file is distributed under the same license as the Datalumo plugin. msgid "" msgstr "" "Project-Id-Version: Datalumo 1.0.0\n" "Report-Msgid-Bugs-To: https://datalumo.app\n" -"POT-Creation-Date: 2026-07-18 12:00+0000\n" -"PO-Revision-Date: 2026-07-18 12:00+0000\n" "Last-Translator: Jeffrey van Rossum\n" "Language-Team: Dutch\n" -"Language: nl_NL\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2026-08-30T08:09:50+00:00\n" +"PO-Revision-Date: 2026-08-30 16:00+0000\n" +"Language: nl_NL\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Domain: datalumo\n" +#. Description of the plugin #: datalumo.php msgid "Sync your WordPress content to Datalumo and add AI-powered search and chat to your site." msgstr "Synchroniseer je WordPress-content met Datalumo en voeg AI-gestuurde zoekfunctie en chat toe aan je site." -#: src/Plugin.php +#: src/Plugin.php:88 msgid "Datalumo is almost ready — connect your account to start syncing content." msgstr "Datalumo is bijna klaar — koppel je account om content te synchroniseren." -#: src/Plugin.php +#: src/Plugin.php:90 msgid "Open settings" msgstr "Instellingen openen" -#: src/Admin/SettingsPage.php -#: resources/views/settings.php +#. Plugin Name of the plugin +#. Author of the plugin +#: datalumo.php +#: src/Admin/SettingsPage.php:25 +#: src/Admin/SettingsPage.php:26 msgid "Datalumo" msgstr "Datalumo" -#: src/Admin/SettingsPage.php -msgid "Connected to %s — %d source(s) available." -msgstr "Verbonden met %s — %d bron(nen) beschikbaar." - -#: src/Admin/SettingsPage.php +#: src/Admin/SettingsPage.php:52 msgid "Connection failed:" msgstr "Verbinding mislukt:" -#: src/Admin/SettingsPage.php +#. translators: 1: number of posts already synced, 2: total posts +#: src/Admin/SettingsPage.php:55 +#, php-format msgid "%1$d of %2$d posts synced…" msgstr "%1$d van %2$d berichten gesynchroniseerd…" -#: src/Admin/SettingsPage.php +#: src/Admin/SettingsPage.php:56 msgid "Sync complete." msgstr "Synchronisatie voltooid." -#: src/Admin/SettingsPage.php -#: src/Admin/Ajax.php +#: src/Admin/Ajax.php:127 +#: src/Admin/SettingsPage.php:133 +#: src/Connect/Grant.php:270 msgid "Not allowed." msgstr "Niet toegestaan." -#: src/Admin/Ajax.php -msgid "Both the organisation ID and an API token are required." -msgstr "Zowel het organisatienummer als een API-token zijn verplicht." - -#: src/Api/Client.php +#: src/Api/Client.php:120 msgid "Invalid widget key — copy it from the widget editor." msgstr "Ongeldige widget-sleutel — kopieer deze uit de widget-editor." -#: src/Api/Client.php +#. translators: %d: HTTP status code +#: src/Api/Client.php:163 +#, php-format msgid "Datalumo request failed (%d)." msgstr "Datalumo-verzoek mislukt (%d)." -#: src/Sync/BulkSync.php +#: src/Sync/BulkSync.php:48 msgid "Unknown sync configuration." msgstr "Onbekende synchronisatie-instelling." -#: src/Search/Summary.php +#: src/Search/Summary.php:72 msgid "AI Summary" msgstr "AI-samenvatting" -#: src/Search/Summary.php +#: src/Search/Summary.php:73 msgid "Show more" msgstr "Meer tonen" -#: src/Search/Summary.php +#: src/Search/Summary.php:74 msgid "Show less" msgstr "Minder tonen" -#: src/Search/Summary.php +#: src/Search/Summary.php:75 msgid "Summarising the best results…" msgstr "De beste resultaten worden samengevat…" -#: resources/views/settings.php +#: resources/views/settings.php:19 msgid "Connection" msgstr "Verbinding" -#: resources/views/settings.php +#: resources/views/settings.php:20 msgid "Content sync" msgstr "Content synchroniseren" -#: resources/views/settings.php +#: resources/views/settings.php:21 msgid "Chatbot" msgstr "Chatbot" -#: resources/views/settings.php +#: resources/views/settings.php:22 msgid "Search box" msgstr "Zoekveld" -#: resources/views/settings.php +#: resources/views/settings.php:23 +#: resources/views/settings.php:386 msgid "Enhanced search" msgstr "Verbeterd zoeken" -#: resources/views/settings.php -msgid "Settings saved." -msgstr "Instellingen opgeslagen." - -#: resources/views/settings.php +#: resources/views/settings.php:181 +#: resources/views/settings.php:213 msgid "Datalumo URL" msgstr "Datalumo-URL" -#: resources/views/settings.php +#: resources/views/settings.php:217 msgid "Leave as-is unless you were told otherwise." msgstr "Laat dit staan, tenzij je iets anders hebt gehoord." -#: resources/views/settings.php -msgid "Organisation ID" -msgstr "Organisatienummer" - -#: resources/views/settings.php -msgid "Find it next to your API keys in the Datalumo dashboard." -msgstr "Je vindt dit naast je API-sleutels in het Datalumo-dashboard." - -#: resources/views/settings.php +#: resources/views/settings.php:222 msgid "API token" msgstr "API-token" -#: resources/views/settings.php +#: resources/views/settings.php:225 +#: resources/views/settings.php:365 msgid "•••••••• (saved)" msgstr "•••••••• (opgeslagen)" -#: resources/views/settings.php -msgid "Leave blank to re-test, or paste a new token to replace it." -msgstr "Laat leeg om opnieuw te testen, of plak een nieuw token om het te vervangen." - -#: resources/views/settings.php -msgid "Create a token under API keys in your Datalumo dashboard." -msgstr "Maak een token onder API-sleutels in je Datalumo-dashboard." - -#: resources/views/settings.php +#: resources/views/settings.php:232 msgid "Connect & test" msgstr "Verbinden en testen" -#: resources/views/settings.php -msgid "Connected to %1$s (%2$d sources)." -msgstr "Verbonden met %1$s (%2$d bronnen)." - -#: resources/views/settings.php +#: resources/views/settings.php:255 msgid "Connect your account first." msgstr "Koppel eerst je account." -#: resources/views/settings.php -msgid "Each sync pushes the chosen post types into one Datalumo source. Create a source for this WordPress-site in your dashboard first." -msgstr "Elke synchronisatie stuurt de gekozen berichttypen naar één Datalumo-bron. Maak eerst een bron voor deze WordPress-site aan in je dashboard." - -#: resources/views/settings.php +#: resources/views/settings.php:267 msgid "Source" msgstr "Bron" -#: resources/views/settings.php +#: resources/views/settings.php:270 msgid "— choose a source —" msgstr "— kies een bron —" -#: resources/views/settings.php +#: resources/views/settings.php:303 msgid "Post types" msgstr "Berichttypen" -#: resources/views/settings.php +#: resources/views/settings.php:317 msgid "Full sync" msgstr "Volledige synchronisatie" -#: resources/views/settings.php +#: resources/views/settings.php:319 msgid "Sync now" msgstr "Nu synchroniseren" -#: resources/views/settings.php +#: resources/views/settings.php:320 msgid "Cancel" msgstr "Annuleren" -#: resources/views/settings.php -msgid "Custom field and taxonomy mappings can be added per sync with the datalumo_page_payload filter for now." -msgstr "Koppelingen van aangepaste velden en taxonomieën kun je voorlopig per synchronisatie toevoegen met het filter datalumo_page_payload." - -#: resources/views/settings.php +#: resources/views/settings.php:330 msgid "Save syncs" msgstr "Synchronisaties opslaan" -#: resources/views/settings.php +#: resources/views/settings.php:335 msgid "Chat widget" msgstr "Chatwidget" -#: resources/views/settings.php +#: resources/views/settings.php:134 +#: resources/views/settings.php:339 msgid "Show the floating chat on every page" msgstr "Toon de zwevende chat op elke pagina" -#: resources/views/settings.php +#: resources/views/settings.php:344 +#: resources/views/settings.php:374 +#: resources/views/settings.php:395 msgid "Widget key" msgstr "Widget-sleutel" -#: resources/views/settings.php +#: resources/views/settings.php:348 msgid "Copy it from the widget editor (\"For developers\"). Add this site to the widget's websites list." msgstr "Kopieer deze uit de widget-editor (\"For developers\"). Voeg deze site toe aan de websites-lijst van de widget." -#: resources/views/settings.php +#: resources/views/settings.php:352 msgid "Visitor identity" msgstr "Bezoekersidentiteit" -#: resources/views/settings.php -msgid "Let logged-in users continue their conversations across visits" -msgstr "Laat ingelogde gebruikers hun gesprekken over bezoeken heen voortzetten" - -#: resources/views/settings.php +#: resources/views/settings.php:362 msgid "Identity signing secret" msgstr "Geheim voor identiteitsondertekening" -#: resources/views/settings.php -msgid "From the same \"For developers\" section. Never exposed to the browser — used to sign user ids server-side." -msgstr "Uit hetzelfde gedeelte \"For developers\". Wordt nooit naar de browser gestuurd — alleen server-side gebruikt om gebruikers-id's te ondertekenen." - -#: resources/views/settings.php -msgid "Tip: use the [datalumo_chat] shortcode to embed the chat inline on a page." -msgstr "Tip: gebruik de shortcode [datalumo_chat] om de chat in een pagina te plaatsen." - -#: resources/views/settings.php +#: resources/views/settings.php:378 msgid "A search-type widget key. Place the box anywhere with the [datalumo_search] shortcode." msgstr "Een widget-sleutel van het type zoeken. Plaats het zoekveld overal met de shortcode [datalumo_search]." -#: resources/views/settings.php +#: resources/views/settings.php:390 msgid "Serve WordPress search results from Datalumo (falls back to native search on any error)" msgstr "Toon WordPress-zoekresultaten via Datalumo (valt bij een fout terug op de standaard zoekfunctie)" -#: resources/views/settings.php +#: resources/views/settings.php:399 msgid "A search-type widget key; this site must be in its websites list." msgstr "Een widget-sleutel van het type zoeken; deze site moet in de websites-lijst staan." -#: resources/views/settings.php +#: resources/views/settings.php:403 msgid "Limit to post types" msgstr "Beperk tot berichttypen" -#: resources/views/settings.php +#: resources/views/settings.php:412 msgid "Leave all unchecked to enhance every search." msgstr "Laat alles uit om elke zoekopdracht te verbeteren." -#: resources/views/settings.php +#: resources/views/settings.php:416 msgid "AI summary" msgstr "AI-samenvatting" -#: resources/views/settings.php +#: resources/views/settings.php:420 msgid "Show a streamed AI answer above the results" msgstr "Toon een gestreamd AI-antwoord boven de resultaten" -#: resources/views/settings.php +#: resources/views/settings.php:425 msgid "Summary placement" msgstr "Plaatsing van de samenvatting" -#: resources/views/settings.php +#: resources/views/settings.php:428 msgid "auto" msgstr "auto" -#: resources/views/settings.php +#: resources/views/settings.php:431 msgid "Before results" msgstr "Voor de resultaten" -#: resources/views/settings.php +#: resources/views/settings.php:432 msgid "After results" msgstr "Na de resultaten" -#: resources/views/settings.php +#: resources/views/settings.php:434 msgid "Optional CSS selector of the results container; detected automatically when empty." msgstr "Optionele CSS-selector van de resultatencontainer; wordt automatisch bepaald als dit leeg is." + +#. Plugin URI of the plugin +#: datalumo.php +msgid "https://github.com/datalumo/wordpress" +msgstr "https://github.com/datalumo/wordpress" + +#. Author URI of the plugin +#: datalumo.php +msgid "https://datalumo.app" +msgstr "https://datalumo.app" + +#: resources/views/settings.php:32 +msgid "Connected to Datalumo." +msgstr "Verbonden met Datalumo." + +#: resources/views/settings.php:33 +msgid "Connect did not finish. Start again from this page." +msgstr "Verbinden is niet gelukt. Begin opnieuw vanaf deze pagina." + +#: resources/views/settings.php:34 +msgid "Disconnected. The API key remains in Datalumo under API keys until you revoke it there." +msgstr "Verbinding verbroken. De API-sleutel blijft in Datalumo onder API-sleutels staan totdat je die daar intrekt." + +#: resources/views/settings.php:35 +msgid "Setup saved." +msgstr "Installatie opgeslagen." + +#: resources/views/settings.php:36 +msgid "Widget keys look like org-id/widget-id. An API token or secret will not work there." +msgstr "Widget-sleutels zien eruit als org-id/widget-id. Een API-token of geheim werkt daar niet." + +#: resources/views/settings.php:79 +msgid "Custom fields, filters, and sync details." +msgstr "Aangepaste velden, filters en synchronisatiedetails." + +#: resources/views/settings.php:80 +msgid "Shortcodes and chat page actions." +msgstr "Shortcodes en chatacties." + +#: resources/views/settings.php:81 +msgid "Search box shortcode and enhanced search." +msgstr "Zoekveld-shortcode en verbeterd zoeken." + +#: resources/views/settings.php:82 +msgid "Connect, sync, and widgets." +msgstr "Verbinden, synchroniseren en widgets." + +#: resources/views/settings.php:104 +msgid "Finish setup" +msgstr "Installatie afronden" + +#: resources/views/settings.php:105 +msgid "Pick a starting setup. You can change these later." +msgstr "Kies een startinstelling. Je kunt dit later wijzigen." + +#: resources/views/settings.php:111 +msgid "What to sync" +msgstr "Wat synchroniseren" + +#: resources/views/settings.php:129 +msgid "Chat" +msgstr "Chat" + +#: resources/views/settings.php:144 +msgid "Search" +msgstr "Zoeken" + +#: resources/views/settings.php:149 +msgid "Use Datalumo for WordPress search results" +msgstr "Gebruik Datalumo voor WordPress-zoekresultaten" + +#: resources/views/settings.php:156 +msgid "Save and sync now" +msgstr "Opslaan en nu synchroniseren" + +#: resources/views/settings.php:157 +msgid "Save setup" +msgstr "Installatie opslaan" + +#: resources/views/settings.php:163 +#: resources/views/settings.php:206 +msgid "Disconnect" +msgstr "Verbinding verbreken" + +#: resources/views/settings.php:168 +msgid "Connect this site to Datalumo. You will pick or create a knowledge base and widgets, then we create a source and API key for you." +msgstr "Koppel deze site aan Datalumo. Je kiest of maakt een knowledge base en widgets, daarna maken we een bron en API-sleutel voor je." + +#: resources/views/settings.php:172 +#: resources/views/settings.php:239 +msgid "Connect with Datalumo" +msgstr "Verbinden met Datalumo" + +#: resources/views/settings.php:179 +msgid "Using a self-hosted Datalumo?" +msgstr "Gebruik je een zelf-gehoste Datalumo?" + +#: resources/views/settings.php:189 +msgid "Manual setup" +msgstr "Handmatige instelling" + +#: resources/views/settings.php:202 +msgid "Organisation" +msgstr "Organisatie" + +#: resources/views/settings.php:226 +msgid "Paste a token from API keys. The organisation is inferred from the token." +msgstr "Plak een token van API-sleutels. De organisatie volgt uit het token." + +#: resources/views/settings.php:234 +msgid "Save URL" +msgstr "URL opslaan" + +#. translators: %s: knowledge base name +#: resources/views/settings.php:294 +#, php-format +msgid "Knowledge base: %s" +msgstr "Knowledge base: %s" + +#: resources/views/settings.php:356 +msgid "Pass a signed id for logged-in users to the chat widget" +msgstr "Stuur een ondertekend id van ingelogde gebruikers naar de chatwidget" + +#: resources/views/settings.php:358 +msgid "Optional, for custom implementations. The plugin does not resume conversations on its own." +msgstr "Optioneel, voor eigen implementaties. De plugin hervat gesprekken niet zelf." + +#: resources/views/settings.php:366 +msgid "From the same \"For developers\" section. Never exposed to the browser. Used to sign user ids server-side." +msgstr "Uit hetzelfde gedeelte \"For developers\". Wordt nooit naar de browser gestuurd. Alleen server-side gebruikt om gebruikers-id's te ondertekenen." + +#: resources/views/settings.php:447 +msgid "Need help?" +msgstr "Hulp nodig?" + +#: resources/views/settings.php:449 +msgid "Ask a question" +msgstr "Stel een vraag" + +#: resources/views/settings.php:450 +msgid "How do I sync products?" +msgstr "Hoe synchroniseer ik producten?" + +#: resources/views/settings.php:452 +msgid "Start chat" +msgstr "Chat starten" + +#: resources/views/settings.php:461 +msgid "WordPress plugin docs" +msgstr "WordPress-plugin documentatie" + +#: src/Admin/Ajax.php:48 +msgid "An API token is required." +msgstr "Een API-token is verplicht." + +#: src/Admin/Ajax.php:52 +msgid "That looks like a widget key. Paste an API token from API keys instead." +msgstr "Dat lijkt op een widget-sleutel. Plak een API-token van API-sleutels." + +#: src/Admin/Ajax.php:56 +msgid "That looks like a widget secret. Paste an API token from API keys instead." +msgstr "Dat lijkt op een widget-geheim. Plak een API-token van API-sleutels." + +#. translators: 1: organisation name, 2: number of sources +#: src/Admin/SettingsPage.php:51 +#, php-format +msgid "Connected to %1$s — %2$d source(s) available." +msgstr "Verbonden met %1$s — %2$d bron(nen) beschikbaar." + +#: src/Admin/SettingsPage.php:53 +msgid "Starting sync…" +msgstr "Synchronisatie starten…" + +#: src/Integration/AddToCart.php:44 +#: src/Integration/AddToCart.php:99 +msgid "No product was specified." +msgstr "Er is geen product opgegeven." + +#: src/Integration/AddToCart.php:45 +#: src/Integration/AddToCart.php:266 +#: src/Integration/AddToCart.php:820 +msgid "Could not add that to the cart." +msgstr "Kon dat niet aan de winkelwagen toevoegen." + +#: src/Integration/AddToCart.php:211 +msgid "WooCommerce is not available." +msgstr "WooCommerce is niet beschikbaar." + +#: src/Integration/AddToCart.php:220 +#: src/Integration/AddToCart.php:240 +msgid "That product could not be found." +msgstr "Dat product is niet gevonden." + +#: src/Integration/AddToCart.php:247 +msgid "That product cannot be purchased." +msgstr "Dat product kan niet worden gekocht." + +#. translators: %s: product name +#: src/Integration/AddToCart.php:289 +#, php-format +msgid "%s added to your cart." +msgstr "%s is toegevoegd aan je winkelwagen." + +#: src/Integration/AddToCart.php:290 +msgid "Added to your cart." +msgstr "Toegevoegd aan je winkelwagen." + +#: src/Integration/AddToCart.php:332 +#: src/Integration/AddToCart.php:359 +msgid "This product needs a variation." +msgstr "Dit product heeft een variant nodig." + +#: src/Integration/AddToCart.php:367 +msgid "This product has many options. Open the product to pick one." +msgstr "Dit product heeft veel opties. Open het product om er een te kiezen." + +#. translators: %s: product attribute name, such as colour or size +#: src/Integration/AddToCart.php:376 +#: src/Integration/AddToCart.php:525 +#, php-format +msgid "Which %s?" +msgstr "Welke %s?" + +#: src/Integration/HostNavigation.php:44 +msgid "Could not open that page." +msgstr "Kon die pagina niet openen." + +#: src/Integration/HostNavigation.php:45 +#: src/Integration/HostNavigation.php:66 +msgid "No page was specified." +msgstr "Er is geen pagina opgegeven." + +#: src/Integration/HostNavigation.php:46 +msgid "The cart is not available." +msgstr "De winkelwagen is niet beschikbaar." + +#: src/Integration/HostNavigation.php:47 +msgid "Checkout is not available." +msgstr "Afrekenen is niet beschikbaar." + +#: src/Integration/HostNavigation.php:60 +msgid "Unknown action." +msgstr "Onbekende actie." + +#. translators: 1: item offset, 2: error message +#: src/Sync/BulkSync.php:186 +#, php-format +msgid "Sync stopped at item %1$d: %2$s" +msgstr "Synchronisatie gestopt bij item %1$d: %2$s" + +#. translators: 1: item offset, 2: attempt count, 3: error message +#: src/Sync/BulkSync.php:199 +#, php-format +msgid "Sync stopped at item %1$d after %2$d attempts: %3$s" +msgstr "Synchronisatie gestopt bij item %1$d na %2$d pogingen: %3$s" diff --git a/languages/datalumo-zh_CN.mo b/languages/datalumo-zh_CN.mo index b59d8a66fe6bc3e6ffe6c6ade356b86962d30c44..0b3ca8c890bde0f9070ab79fc74898decb470acf 100644 GIT binary patch literal 9015 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 Si(a>tWK<+)`t+~XKJ#xQ2lVLx diff --git a/languages/datalumo-zh_CN.po b/languages/datalumo-zh_CN.po index 3de7ee7..f8d0c4d 100644 --- a/languages/datalumo-zh_CN.po +++ b/languages/datalumo-zh_CN.po @@ -1,275 +1,490 @@ -# Simplified Chinese translation for Datalumo -# Copyright (C) 2026 Jeffrey van Rossum -# This file is distributed under the same license as the Datalumo plugin. msgid "" msgstr "" "Project-Id-Version: Datalumo 1.0.0\n" "Report-Msgid-Bugs-To: https://datalumo.app\n" -"POT-Creation-Date: 2026-07-18 12:00+0000\n" -"PO-Revision-Date: 2026-07-18 12:00+0000\n" "Last-Translator: Jeffrey van Rossum\n" "Language-Team: Chinese (Simplified)\n" -"Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2026-08-30T08:09:50+00:00\n" +"PO-Revision-Date: 2026-08-30 16:00+0000\n" +"Language: zh_CN\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Domain: datalumo\n" +#. Description of the plugin #: datalumo.php msgid "Sync your WordPress content to Datalumo and add AI-powered search and chat to your site." msgstr "将 WordPress 内容同步到 Datalumo,并为网站添加 AI 搜索与聊天。" -#: src/Plugin.php +#: src/Plugin.php:88 msgid "Datalumo is almost ready — connect your account to start syncing content." msgstr "Datalumo 即将就绪 — 连接你的账户以开始同步内容。" -#: src/Plugin.php +#: src/Plugin.php:90 msgid "Open settings" msgstr "打开设置" -#: src/Admin/SettingsPage.php -#: resources/views/settings.php +#. Plugin Name of the plugin +#. Author of the plugin +#: datalumo.php +#: src/Admin/SettingsPage.php:25 +#: src/Admin/SettingsPage.php:26 msgid "Datalumo" msgstr "Datalumo" -#: src/Admin/SettingsPage.php -msgid "Connected to %s — %d source(s) available." -msgstr "已连接到 %s — 可用 %d 个来源。" - -#: src/Admin/SettingsPage.php +#: src/Admin/SettingsPage.php:52 msgid "Connection failed:" msgstr "连接失败:" -#: src/Admin/SettingsPage.php +#. translators: 1: number of posts already synced, 2: total posts +#: src/Admin/SettingsPage.php:55 +#, php-format msgid "%1$d of %2$d posts synced…" msgstr "已同步 %1$d / %2$d 篇文章…" -#: src/Admin/SettingsPage.php +#: src/Admin/SettingsPage.php:56 msgid "Sync complete." msgstr "同步完成。" -#: src/Admin/SettingsPage.php -#: src/Admin/Ajax.php +#: src/Admin/Ajax.php:127 +#: src/Admin/SettingsPage.php:133 +#: src/Connect/Grant.php:270 msgid "Not allowed." msgstr "无权操作。" -#: src/Admin/Ajax.php -msgid "Both the organisation ID and an API token are required." -msgstr "必须同时填写组织 ID 和 API 令牌。" - -#: src/Api/Client.php +#: src/Api/Client.php:120 msgid "Invalid widget key — copy it from the widget editor." msgstr "无效的小组件密钥 — 请从小组件编辑器复制。" -#: src/Api/Client.php +#. translators: %d: HTTP status code +#: src/Api/Client.php:163 +#, php-format msgid "Datalumo request failed (%d)." msgstr "Datalumo 请求失败(%d)。" -#: src/Sync/BulkSync.php +#: src/Sync/BulkSync.php:48 msgid "Unknown sync configuration." msgstr "未知的同步配置。" -#: src/Search/Summary.php +#: src/Search/Summary.php:72 msgid "AI Summary" msgstr "AI 摘要" -#: src/Search/Summary.php +#: src/Search/Summary.php:73 msgid "Show more" msgstr "显示更多" -#: src/Search/Summary.php +#: src/Search/Summary.php:74 msgid "Show less" msgstr "显示更少" -#: src/Search/Summary.php +#: src/Search/Summary.php:75 msgid "Summarising the best results…" msgstr "正在汇总最佳结果…" -#: resources/views/settings.php +#: resources/views/settings.php:19 msgid "Connection" msgstr "连接" -#: resources/views/settings.php +#: resources/views/settings.php:20 msgid "Content sync" msgstr "内容同步" -#: resources/views/settings.php +#: resources/views/settings.php:21 msgid "Chatbot" msgstr "聊天机器人" -#: resources/views/settings.php +#: resources/views/settings.php:22 msgid "Search box" msgstr "搜索框" -#: resources/views/settings.php +#: resources/views/settings.php:23 +#: resources/views/settings.php:386 msgid "Enhanced search" msgstr "增强搜索" -#: resources/views/settings.php -msgid "Settings saved." -msgstr "设置已保存。" - -#: resources/views/settings.php +#: resources/views/settings.php:181 +#: resources/views/settings.php:213 msgid "Datalumo URL" msgstr "Datalumo 网址" -#: resources/views/settings.php +#: resources/views/settings.php:217 msgid "Leave as-is unless you were told otherwise." msgstr "除非另有说明,否则请保持默认。" -#: resources/views/settings.php -msgid "Organisation ID" -msgstr "组织 ID" - -#: resources/views/settings.php -msgid "Find it next to your API keys in the Datalumo dashboard." -msgstr "可在 Datalumo 控制台的 API 密钥旁找到。" - -#: resources/views/settings.php +#: resources/views/settings.php:222 msgid "API token" msgstr "API 令牌" -#: resources/views/settings.php +#: resources/views/settings.php:225 +#: resources/views/settings.php:365 msgid "•••••••• (saved)" msgstr "••••••••(已保存)" -#: resources/views/settings.php -msgid "Leave blank to re-test, or paste a new token to replace it." -msgstr "留空可重新测试,或粘贴新令牌以替换。" - -#: resources/views/settings.php -msgid "Create a token under API keys in your Datalumo dashboard." -msgstr "在 Datalumo 控制台的「API 密钥」中创建令牌。" - -#: resources/views/settings.php +#: resources/views/settings.php:232 msgid "Connect & test" msgstr "连接并测试" -#: resources/views/settings.php -msgid "Connected to %1$s (%2$d sources)." -msgstr "已连接到 %1$s(%2$d 个来源)。" - -#: resources/views/settings.php +#: resources/views/settings.php:255 msgid "Connect your account first." msgstr "请先连接你的账户。" -#: resources/views/settings.php -msgid "Each sync pushes the chosen post types into one Datalumo source. Create a source of type \"API\" in your dashboard first." -msgstr "每条同步会将所选文章类型推送到一个 Datalumo 来源。请先在控制台创建一个类型为「API」的来源。" - -#: resources/views/settings.php +#: resources/views/settings.php:267 msgid "Source" msgstr "来源" -#: resources/views/settings.php +#: resources/views/settings.php:270 msgid "— choose a source —" msgstr "— 选择来源 —" -#: resources/views/settings.php +#: resources/views/settings.php:303 msgid "Post types" msgstr "文章类型" -#: resources/views/settings.php +#: resources/views/settings.php:317 msgid "Full sync" msgstr "完整同步" -#: resources/views/settings.php +#: resources/views/settings.php:319 msgid "Sync now" msgstr "立即同步" -#: resources/views/settings.php +#: resources/views/settings.php:320 msgid "Cancel" msgstr "取消" -#: resources/views/settings.php -msgid "Custom field and taxonomy mappings can be added per sync with the datalumo_page_payload filter for now." -msgstr "目前可通过 datalumo_page_payload 过滤器为每条同步添加自定义字段和分类映射。" - -#: resources/views/settings.php +#: resources/views/settings.php:330 msgid "Save syncs" msgstr "保存同步" -#: resources/views/settings.php +#: resources/views/settings.php:335 msgid "Chat widget" msgstr "聊天小组件" -#: resources/views/settings.php +#: resources/views/settings.php:134 +#: resources/views/settings.php:339 msgid "Show the floating chat on every page" msgstr "在每个页面显示浮动聊天" -#: resources/views/settings.php +#: resources/views/settings.php:344 +#: resources/views/settings.php:374 +#: resources/views/settings.php:395 msgid "Widget key" msgstr "小组件密钥" -#: resources/views/settings.php +#: resources/views/settings.php:348 msgid "Copy it from the widget editor (\"For developers\"). Add this site to the widget's websites list." msgstr "从小组件编辑器(「For developers」)复制。请将本站加入该小组件的网站列表。" -#: resources/views/settings.php +#: resources/views/settings.php:352 msgid "Visitor identity" msgstr "访客身份" -#: resources/views/settings.php -msgid "Let logged-in users continue their conversations across visits" -msgstr "允许已登录用户在多次访问间继续对话" - -#: resources/views/settings.php +#: resources/views/settings.php:362 msgid "Identity signing secret" msgstr "身份签名密钥" -#: resources/views/settings.php -msgid "From the same \"For developers\" section. Never exposed to the browser — used to sign user ids server-side." -msgstr "同样在「For developers」部分。不会暴露给浏览器 — 仅在服务器端用于签署用户 ID。" - -#: resources/views/settings.php -msgid "Tip: use the [datalumo_chat] shortcode to embed the chat inline on a page." -msgstr "提示:使用 [datalumo_chat] 短代码可在页面中嵌入聊天。" - -#: resources/views/settings.php +#: resources/views/settings.php:378 msgid "A search-type widget key. Place the box anywhere with the [datalumo_search] shortcode." msgstr "搜索类型小组件密钥。使用 [datalumo_search] 短代码可在任意位置放置搜索框。" -#: resources/views/settings.php +#: resources/views/settings.php:390 msgid "Serve WordPress search results from Datalumo (falls back to native search on any error)" msgstr "通过 Datalumo 提供 WordPress 搜索结果(出错时回退到原生搜索)" -#: resources/views/settings.php +#: resources/views/settings.php:399 msgid "A search-type widget key; this site must be in its websites list." msgstr "搜索类型小组件密钥;本站必须在其网站列表中。" -#: resources/views/settings.php +#: resources/views/settings.php:403 msgid "Limit to post types" msgstr "限制文章类型" -#: resources/views/settings.php +#: resources/views/settings.php:412 msgid "Leave all unchecked to enhance every search." msgstr "全部不勾选则增强所有搜索。" -#: resources/views/settings.php +#: resources/views/settings.php:416 msgid "AI summary" msgstr "AI 摘要" -#: resources/views/settings.php +#: resources/views/settings.php:420 msgid "Show a streamed AI answer above the results" msgstr "在结果上方显示流式 AI 回答" -#: resources/views/settings.php +#: resources/views/settings.php:425 msgid "Summary placement" msgstr "摘要位置" -#: resources/views/settings.php +#: resources/views/settings.php:428 msgid "auto" msgstr "自动" -#: resources/views/settings.php +#: resources/views/settings.php:431 msgid "Before results" msgstr "结果之前" -#: resources/views/settings.php +#: resources/views/settings.php:432 msgid "After results" msgstr "结果之后" -#: resources/views/settings.php +#: resources/views/settings.php:434 msgid "Optional CSS selector of the results container; detected automatically when empty." msgstr "结果容器的可选 CSS 选择器;为空时自动检测。" + +#. Plugin URI of the plugin +#: datalumo.php +msgid "https://github.com/datalumo/wordpress" +msgstr "https://github.com/datalumo/wordpress" + +#. Author URI of the plugin +#: datalumo.php +msgid "https://datalumo.app" +msgstr "https://datalumo.app" + +#: resources/views/settings.php:32 +msgid "Connected to Datalumo." +msgstr "已连接到 Datalumo。" + +#: resources/views/settings.php:33 +msgid "Connect did not finish. Start again from this page." +msgstr "连接未完成。请从此页面重新开始。" + +#: resources/views/settings.php:34 +msgid "Disconnected. The API key remains in Datalumo under API keys until you revoke it there." +msgstr "已断开连接。API 密钥仍保留在 Datalumo 的「API 密钥」中,直到你在那里撤销。" + +#: resources/views/settings.php:35 +msgid "Setup saved." +msgstr "设置已保存。" + +#: resources/views/settings.php:36 +msgid "Widget keys look like org-id/widget-id. An API token or secret will not work there." +msgstr "小组件密钥的格式为 org-id/widget-id。API 令牌或密钥不能用在这里。" + +#: resources/views/settings.php:79 +msgid "Custom fields, filters, and sync details." +msgstr "自定义字段、过滤器和同步详情。" + +#: resources/views/settings.php:80 +msgid "Shortcodes and chat page actions." +msgstr "短代码和聊天页面操作。" + +#: resources/views/settings.php:81 +msgid "Search box shortcode and enhanced search." +msgstr "搜索框短代码与增强搜索。" + +#: resources/views/settings.php:82 +msgid "Connect, sync, and widgets." +msgstr "连接、同步与小组件。" + +#: resources/views/settings.php:104 +msgid "Finish setup" +msgstr "完成设置" + +#: resources/views/settings.php:105 +msgid "Pick a starting setup. You can change these later." +msgstr "选择初始设置。之后可以再改。" + +#: resources/views/settings.php:111 +msgid "What to sync" +msgstr "同步内容" + +#: resources/views/settings.php:129 +msgid "Chat" +msgstr "聊天" + +#: resources/views/settings.php:144 +msgid "Search" +msgstr "搜索" + +#: resources/views/settings.php:149 +msgid "Use Datalumo for WordPress search results" +msgstr "使用 Datalumo 提供 WordPress 搜索结果" + +#: resources/views/settings.php:156 +msgid "Save and sync now" +msgstr "保存并立即同步" + +#: resources/views/settings.php:157 +msgid "Save setup" +msgstr "保存设置" + +#: resources/views/settings.php:163 +#: resources/views/settings.php:206 +msgid "Disconnect" +msgstr "断开连接" + +#: resources/views/settings.php:168 +msgid "Connect this site to Datalumo. You will pick or create a knowledge base and widgets, then we create a source and API key for you." +msgstr "将本站点连接到 Datalumo。你将选择或创建知识库和小组件,然后我们会为你创建来源和 API 密钥。" + +#: resources/views/settings.php:172 +#: resources/views/settings.php:239 +msgid "Connect with Datalumo" +msgstr "通过 Datalumo 连接" + +#: resources/views/settings.php:179 +msgid "Using a self-hosted Datalumo?" +msgstr "使用自托管的 Datalumo?" + +#: resources/views/settings.php:189 +msgid "Manual setup" +msgstr "手动设置" + +#: resources/views/settings.php:202 +msgid "Organisation" +msgstr "组织" + +#: resources/views/settings.php:226 +msgid "Paste a token from API keys. The organisation is inferred from the token." +msgstr "粘贴「API 密钥」中的令牌。组织会从令牌中识别。" + +#: resources/views/settings.php:234 +msgid "Save URL" +msgstr "保存网址" + +#. translators: %s: knowledge base name +#: resources/views/settings.php:294 +#, php-format +msgid "Knowledge base: %s" +msgstr "知识库:%s" + +#: resources/views/settings.php:356 +msgid "Pass a signed id for logged-in users to the chat widget" +msgstr "向聊天小组件传递已登录用户的签名 ID" + +#: resources/views/settings.php:358 +msgid "Optional, for custom implementations. The plugin does not resume conversations on its own." +msgstr "可选,用于自定义实现。插件不会自行恢复对话。" + +#: resources/views/settings.php:366 +msgid "From the same \"For developers\" section. Never exposed to the browser. Used to sign user ids server-side." +msgstr "同样在「For developers」部分。不会暴露给浏览器。仅在服务器端用于签署用户 ID。" + +#: resources/views/settings.php:447 +msgid "Need help?" +msgstr "需要帮助?" + +#: resources/views/settings.php:449 +msgid "Ask a question" +msgstr "提问" + +#: resources/views/settings.php:450 +msgid "How do I sync products?" +msgstr "如何同步产品?" + +#: resources/views/settings.php:452 +msgid "Start chat" +msgstr "开始聊天" + +#: resources/views/settings.php:461 +msgid "WordPress plugin docs" +msgstr "WordPress 插件文档" + +#: src/Admin/Ajax.php:48 +msgid "An API token is required." +msgstr "必须填写 API 令牌。" + +#: src/Admin/Ajax.php:52 +msgid "That looks like a widget key. Paste an API token from API keys instead." +msgstr "这看起来像小组件密钥。请改用「API 密钥」中的 API 令牌。" + +#: src/Admin/Ajax.php:56 +msgid "That looks like a widget secret. Paste an API token from API keys instead." +msgstr "这看起来像小组件密钥。请改用「API 密钥」中的 API 令牌。" + +#. translators: 1: organisation name, 2: number of sources +#: src/Admin/SettingsPage.php:51 +#, php-format +msgid "Connected to %1$s — %2$d source(s) available." +msgstr "已连接到 %1$s — 可用 %2$d 个来源。" + +#: src/Admin/SettingsPage.php:53 +msgid "Starting sync…" +msgstr "正在开始同步…" + +#: src/Integration/AddToCart.php:44 +#: src/Integration/AddToCart.php:99 +msgid "No product was specified." +msgstr "未指定产品。" + +#: src/Integration/AddToCart.php:45 +#: src/Integration/AddToCart.php:266 +#: src/Integration/AddToCart.php:820 +msgid "Could not add that to the cart." +msgstr "无法加入购物车。" + +#: src/Integration/AddToCart.php:211 +msgid "WooCommerce is not available." +msgstr "WooCommerce 不可用。" + +#: src/Integration/AddToCart.php:220 +#: src/Integration/AddToCart.php:240 +msgid "That product could not be found." +msgstr "找不到该产品。" + +#: src/Integration/AddToCart.php:247 +msgid "That product cannot be purchased." +msgstr "该产品无法购买。" + +#. translators: %s: product name +#: src/Integration/AddToCart.php:289 +#, php-format +msgid "%s added to your cart." +msgstr "%s 已加入购物车。" + +#: src/Integration/AddToCart.php:290 +msgid "Added to your cart." +msgstr "已加入购物车。" + +#: src/Integration/AddToCart.php:332 +#: src/Integration/AddToCart.php:359 +msgid "This product needs a variation." +msgstr "此产品需要选择变体。" + +#: src/Integration/AddToCart.php:367 +msgid "This product has many options. Open the product to pick one." +msgstr "此产品选项较多。请打开产品页选择。" + +#. translators: %s: product attribute name, such as colour or size +#: src/Integration/AddToCart.php:376 +#: src/Integration/AddToCart.php:525 +#, php-format +msgid "Which %s?" +msgstr "哪个%s?" + +#: src/Integration/HostNavigation.php:44 +msgid "Could not open that page." +msgstr "无法打开该页面。" + +#: src/Integration/HostNavigation.php:45 +#: src/Integration/HostNavigation.php:66 +msgid "No page was specified." +msgstr "未指定页面。" + +#: src/Integration/HostNavigation.php:46 +msgid "The cart is not available." +msgstr "购物车不可用。" + +#: src/Integration/HostNavigation.php:47 +msgid "Checkout is not available." +msgstr "结账不可用。" + +#: src/Integration/HostNavigation.php:60 +msgid "Unknown action." +msgstr "未知操作。" + +#. translators: 1: item offset, 2: error message +#: src/Sync/BulkSync.php:186 +#, php-format +msgid "Sync stopped at item %1$d: %2$s" +msgstr "同步在第 %1$d 项停止:%2$s" + +#. translators: 1: item offset, 2: attempt count, 3: error message +#: src/Sync/BulkSync.php:199 +#, php-format +msgid "Sync stopped at item %1$d after %2$d attempts: %3$s" +msgstr "同步在第 %1$d 项停止,已尝试 %2$d 次:%3$s" diff --git a/languages/datalumo-zh_HK.mo b/languages/datalumo-zh_HK.mo index 62ddd62dbd8bbe130ee8d549b079d51335fe1ec2..a9fe20d0a427ac75c973591600de1332f832d255 100644 GIT binary patch literal 9038 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 diff --git a/languages/datalumo-zh_HK.po b/languages/datalumo-zh_HK.po index 2f0df00..620b623 100644 --- a/languages/datalumo-zh_HK.po +++ b/languages/datalumo-zh_HK.po @@ -1,275 +1,490 @@ -# Traditional Chinese (Hong Kong) translation for Datalumo -# Copyright (C) 2026 Jeffrey van Rossum -# This file is distributed under the same license as the Datalumo plugin. msgid "" msgstr "" "Project-Id-Version: Datalumo 1.0.0\n" "Report-Msgid-Bugs-To: https://datalumo.app\n" -"POT-Creation-Date: 2026-07-18 12:00+0000\n" -"PO-Revision-Date: 2026-07-18 12:00+0000\n" "Last-Translator: Jeffrey van Rossum\n" "Language-Team: Chinese (Hong Kong)\n" -"Language: zh_HK\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2026-08-30T08:09:50+00:00\n" +"PO-Revision-Date: 2026-08-30 16:00+0000\n" +"Language: zh_HK\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Domain: datalumo\n" +#. Description of the plugin #: datalumo.php msgid "Sync your WordPress content to Datalumo and add AI-powered search and chat to your site." msgstr "將 WordPress 內容同步至 Datalumo,並為網站加入 AI 搜尋與聊天。" -#: src/Plugin.php +#: src/Plugin.php:88 msgid "Datalumo is almost ready — connect your account to start syncing content." msgstr "Datalumo 即將準備就緒 — 連接你的帳戶以開始同步內容。" -#: src/Plugin.php +#: src/Plugin.php:90 msgid "Open settings" msgstr "開啟設定" -#: src/Admin/SettingsPage.php -#: resources/views/settings.php +#. Plugin Name of the plugin +#. Author of the plugin +#: datalumo.php +#: src/Admin/SettingsPage.php:25 +#: src/Admin/SettingsPage.php:26 msgid "Datalumo" msgstr "Datalumo" -#: src/Admin/SettingsPage.php -msgid "Connected to %s — %d source(s) available." -msgstr "已連接至 %s — 有 %d 個來源可用。" - -#: src/Admin/SettingsPage.php +#: src/Admin/SettingsPage.php:52 msgid "Connection failed:" msgstr "連接失敗:" -#: src/Admin/SettingsPage.php +#. translators: 1: number of posts already synced, 2: total posts +#: src/Admin/SettingsPage.php:55 +#, php-format msgid "%1$d of %2$d posts synced…" msgstr "已同步 %1$d / %2$d 篇文章…" -#: src/Admin/SettingsPage.php +#: src/Admin/SettingsPage.php:56 msgid "Sync complete." msgstr "同步完成。" -#: src/Admin/SettingsPage.php -#: src/Admin/Ajax.php +#: src/Admin/Ajax.php:127 +#: src/Admin/SettingsPage.php:133 +#: src/Connect/Grant.php:270 msgid "Not allowed." msgstr "無權操作。" -#: src/Admin/Ajax.php -msgid "Both the organisation ID and an API token are required." -msgstr "必須同時填寫機構 ID 及 API 權杖。" - -#: src/Api/Client.php +#: src/Api/Client.php:120 msgid "Invalid widget key — copy it from the widget editor." msgstr "無效的小工具密鑰 — 請從小工具編輯器複製。" -#: src/Api/Client.php +#. translators: %d: HTTP status code +#: src/Api/Client.php:163 +#, php-format msgid "Datalumo request failed (%d)." msgstr "Datalumo 請求失敗(%d)。" -#: src/Sync/BulkSync.php +#: src/Sync/BulkSync.php:48 msgid "Unknown sync configuration." msgstr "未知的同步設定。" -#: src/Search/Summary.php +#: src/Search/Summary.php:72 msgid "AI Summary" msgstr "AI 摘要" -#: src/Search/Summary.php +#: src/Search/Summary.php:73 msgid "Show more" msgstr "顯示更多" -#: src/Search/Summary.php +#: src/Search/Summary.php:74 msgid "Show less" msgstr "顯示較少" -#: src/Search/Summary.php +#: src/Search/Summary.php:75 msgid "Summarising the best results…" msgstr "正在匯總最佳結果…" -#: resources/views/settings.php +#: resources/views/settings.php:19 msgid "Connection" msgstr "連接" -#: resources/views/settings.php +#: resources/views/settings.php:20 msgid "Content sync" msgstr "內容同步" -#: resources/views/settings.php +#: resources/views/settings.php:21 msgid "Chatbot" msgstr "聊天機械人" -#: resources/views/settings.php +#: resources/views/settings.php:22 msgid "Search box" msgstr "搜尋框" -#: resources/views/settings.php +#: resources/views/settings.php:23 +#: resources/views/settings.php:386 msgid "Enhanced search" msgstr "強化搜尋" -#: resources/views/settings.php -msgid "Settings saved." -msgstr "設定已儲存。" - -#: resources/views/settings.php +#: resources/views/settings.php:181 +#: resources/views/settings.php:213 msgid "Datalumo URL" msgstr "Datalumo 網址" -#: resources/views/settings.php +#: resources/views/settings.php:217 msgid "Leave as-is unless you were told otherwise." msgstr "除非另有指示,否則請維持預設。" -#: resources/views/settings.php -msgid "Organisation ID" -msgstr "機構 ID" - -#: resources/views/settings.php -msgid "Find it next to your API keys in the Datalumo dashboard." -msgstr "可在 Datalumo 控制台的 API 密鑰旁找到。" - -#: resources/views/settings.php +#: resources/views/settings.php:222 msgid "API token" msgstr "API 權杖" -#: resources/views/settings.php +#: resources/views/settings.php:225 +#: resources/views/settings.php:365 msgid "•••••••• (saved)" msgstr "••••••••(已儲存)" -#: resources/views/settings.php -msgid "Leave blank to re-test, or paste a new token to replace it." -msgstr "留空可重新測試,或貼上新權杖以取代。" - -#: resources/views/settings.php -msgid "Create a token under API keys in your Datalumo dashboard." -msgstr "在 Datalumo 控制台的「API 密鑰」建立權杖。" - -#: resources/views/settings.php +#: resources/views/settings.php:232 msgid "Connect & test" msgstr "連接並測試" -#: resources/views/settings.php -msgid "Connected to %1$s (%2$d sources)." -msgstr "已連接至 %1$s(%2$d 個來源)。" - -#: resources/views/settings.php +#: resources/views/settings.php:255 msgid "Connect your account first." msgstr "請先連接你的帳戶。" -#: resources/views/settings.php -msgid "Each sync pushes the chosen post types into one Datalumo source. Create a source of type \"API\" in your dashboard first." -msgstr "每項同步會將所選文章類型推送至一個 Datalumo 來源。請先在控制台建立類型為「API」的來源。" - -#: resources/views/settings.php +#: resources/views/settings.php:267 msgid "Source" msgstr "來源" -#: resources/views/settings.php +#: resources/views/settings.php:270 msgid "— choose a source —" msgstr "— 選擇來源 —" -#: resources/views/settings.php +#: resources/views/settings.php:303 msgid "Post types" msgstr "文章類型" -#: resources/views/settings.php +#: resources/views/settings.php:317 msgid "Full sync" msgstr "完整同步" -#: resources/views/settings.php +#: resources/views/settings.php:319 msgid "Sync now" msgstr "立即同步" -#: resources/views/settings.php +#: resources/views/settings.php:320 msgid "Cancel" msgstr "取消" -#: resources/views/settings.php -msgid "Custom field and taxonomy mappings can be added per sync with the datalumo_page_payload filter for now." -msgstr "目前可透過 datalumo_page_payload 過濾器為每項同步加入自訂欄位及分類法對應。" - -#: resources/views/settings.php +#: resources/views/settings.php:330 msgid "Save syncs" msgstr "儲存同步" -#: resources/views/settings.php +#: resources/views/settings.php:335 msgid "Chat widget" msgstr "聊天小工具" -#: resources/views/settings.php +#: resources/views/settings.php:134 +#: resources/views/settings.php:339 msgid "Show the floating chat on every page" msgstr "在每個頁面顯示浮動聊天" -#: resources/views/settings.php +#: resources/views/settings.php:344 +#: resources/views/settings.php:374 +#: resources/views/settings.php:395 msgid "Widget key" msgstr "小工具密鑰" -#: resources/views/settings.php +#: resources/views/settings.php:348 msgid "Copy it from the widget editor (\"For developers\"). Add this site to the widget's websites list." msgstr "從小工具編輯器(「For developers」)複製。請將本網站加入該小工具的網站列表。" -#: resources/views/settings.php +#: resources/views/settings.php:352 msgid "Visitor identity" msgstr "訪客身分" -#: resources/views/settings.php -msgid "Let logged-in users continue their conversations across visits" -msgstr "讓已登入用戶在多次造訪之間繼續對話" - -#: resources/views/settings.php +#: resources/views/settings.php:362 msgid "Identity signing secret" msgstr "身分簽署密鑰" -#: resources/views/settings.php -msgid "From the same \"For developers\" section. Never exposed to the browser — used to sign user ids server-side." -msgstr "同樣在「For developers」部分。不會向瀏覽器暴露 — 只在伺服器端用作簽署用戶 ID。" - -#: resources/views/settings.php -msgid "Tip: use the [datalumo_chat] shortcode to embed the chat inline on a page." -msgstr "提示:使用 [datalumo_chat] 短代碼可在頁面中嵌入聊天。" - -#: resources/views/settings.php +#: resources/views/settings.php:378 msgid "A search-type widget key. Place the box anywhere with the [datalumo_search] shortcode." msgstr "搜尋類型小工具密鑰。使用 [datalumo_search] 短代碼可在任何位置放置搜尋框。" -#: resources/views/settings.php +#: resources/views/settings.php:390 msgid "Serve WordPress search results from Datalumo (falls back to native search on any error)" msgstr "透過 Datalumo 提供 WordPress 搜尋結果(出錯時會改用原生搜尋)" -#: resources/views/settings.php +#: resources/views/settings.php:399 msgid "A search-type widget key; this site must be in its websites list." msgstr "搜尋類型小工具密鑰;本網站必須在其網站列表中。" -#: resources/views/settings.php +#: resources/views/settings.php:403 msgid "Limit to post types" msgstr "限制文章類型" -#: resources/views/settings.php +#: resources/views/settings.php:412 msgid "Leave all unchecked to enhance every search." msgstr "全部不勾選則強化所有搜尋。" -#: resources/views/settings.php +#: resources/views/settings.php:416 msgid "AI summary" msgstr "AI 摘要" -#: resources/views/settings.php +#: resources/views/settings.php:420 msgid "Show a streamed AI answer above the results" msgstr "在結果上方顯示串流 AI 回答" -#: resources/views/settings.php +#: resources/views/settings.php:425 msgid "Summary placement" msgstr "摘要位置" -#: resources/views/settings.php +#: resources/views/settings.php:428 msgid "auto" msgstr "自動" -#: resources/views/settings.php +#: resources/views/settings.php:431 msgid "Before results" msgstr "結果之前" -#: resources/views/settings.php +#: resources/views/settings.php:432 msgid "After results" msgstr "結果之後" -#: resources/views/settings.php +#: resources/views/settings.php:434 msgid "Optional CSS selector of the results container; detected automatically when empty." msgstr "結果容器的可選 CSS 選擇器;留空時會自動偵測。" + +#. Plugin URI of the plugin +#: datalumo.php +msgid "https://github.com/datalumo/wordpress" +msgstr "https://github.com/datalumo/wordpress" + +#. Author URI of the plugin +#: datalumo.php +msgid "https://datalumo.app" +msgstr "https://datalumo.app" + +#: resources/views/settings.php:32 +msgid "Connected to Datalumo." +msgstr "已連接至 Datalumo。" + +#: resources/views/settings.php:33 +msgid "Connect did not finish. Start again from this page." +msgstr "連接未完成。請從此頁面重新開始。" + +#: resources/views/settings.php:34 +msgid "Disconnected. The API key remains in Datalumo under API keys until you revoke it there." +msgstr "已中斷連接。API 密鑰仍保留在 Datalumo 的「API 密鑰」中,直到你在那裡撤銷。" + +#: resources/views/settings.php:35 +msgid "Setup saved." +msgstr "設定已儲存。" + +#: resources/views/settings.php:36 +msgid "Widget keys look like org-id/widget-id. An API token or secret will not work there." +msgstr "小工具密鑰的格式為 org-id/widget-id。API 權杖或密鑰不能用在這裡。" + +#: resources/views/settings.php:79 +msgid "Custom fields, filters, and sync details." +msgstr "自訂欄位、過濾器及同步詳情。" + +#: resources/views/settings.php:80 +msgid "Shortcodes and chat page actions." +msgstr "短代碼及聊天頁面操作。" + +#: resources/views/settings.php:81 +msgid "Search box shortcode and enhanced search." +msgstr "搜尋框短代碼與強化搜尋。" + +#: resources/views/settings.php:82 +msgid "Connect, sync, and widgets." +msgstr "連接、同步與小工具。" + +#: resources/views/settings.php:104 +msgid "Finish setup" +msgstr "完成設定" + +#: resources/views/settings.php:105 +msgid "Pick a starting setup. You can change these later." +msgstr "選擇初始設定。之後可以再改。" + +#: resources/views/settings.php:111 +msgid "What to sync" +msgstr "同步內容" + +#: resources/views/settings.php:129 +msgid "Chat" +msgstr "聊天" + +#: resources/views/settings.php:144 +msgid "Search" +msgstr "搜尋" + +#: resources/views/settings.php:149 +msgid "Use Datalumo for WordPress search results" +msgstr "使用 Datalumo 提供 WordPress 搜尋結果" + +#: resources/views/settings.php:156 +msgid "Save and sync now" +msgstr "儲存並立即同步" + +#: resources/views/settings.php:157 +msgid "Save setup" +msgstr "儲存設定" + +#: resources/views/settings.php:163 +#: resources/views/settings.php:206 +msgid "Disconnect" +msgstr "中斷連接" + +#: resources/views/settings.php:168 +msgid "Connect this site to Datalumo. You will pick or create a knowledge base and widgets, then we create a source and API key for you." +msgstr "將本網站連接至 Datalumo。你將選擇或建立知識庫及小工具,然後我們會為你建立來源及 API 密鑰。" + +#: resources/views/settings.php:172 +#: resources/views/settings.php:239 +msgid "Connect with Datalumo" +msgstr "透過 Datalumo 連接" + +#: resources/views/settings.php:179 +msgid "Using a self-hosted Datalumo?" +msgstr "使用自行託管的 Datalumo?" + +#: resources/views/settings.php:189 +msgid "Manual setup" +msgstr "手動設定" + +#: resources/views/settings.php:202 +msgid "Organisation" +msgstr "機構" + +#: resources/views/settings.php:226 +msgid "Paste a token from API keys. The organisation is inferred from the token." +msgstr "貼上「API 密鑰」中的權杖。機構會從權杖識別。" + +#: resources/views/settings.php:234 +msgid "Save URL" +msgstr "儲存網址" + +#. translators: %s: knowledge base name +#: resources/views/settings.php:294 +#, php-format +msgid "Knowledge base: %s" +msgstr "知識庫:%s" + +#: resources/views/settings.php:356 +msgid "Pass a signed id for logged-in users to the chat widget" +msgstr "向聊天小工具傳遞已登入用戶的簽署 ID" + +#: resources/views/settings.php:358 +msgid "Optional, for custom implementations. The plugin does not resume conversations on its own." +msgstr "選用,供自訂實作。外掛不會自行恢復對話。" + +#: resources/views/settings.php:366 +msgid "From the same \"For developers\" section. Never exposed to the browser. Used to sign user ids server-side." +msgstr "同樣在「For developers」部分。不會向瀏覽器暴露。只在伺服器端用作簽署用戶 ID。" + +#: resources/views/settings.php:447 +msgid "Need help?" +msgstr "需要協助?" + +#: resources/views/settings.php:449 +msgid "Ask a question" +msgstr "提出問題" + +#: resources/views/settings.php:450 +msgid "How do I sync products?" +msgstr "如何同步產品?" + +#: resources/views/settings.php:452 +msgid "Start chat" +msgstr "開始聊天" + +#: resources/views/settings.php:461 +msgid "WordPress plugin docs" +msgstr "WordPress 外掛文件" + +#: src/Admin/Ajax.php:48 +msgid "An API token is required." +msgstr "必須填寫 API 權杖。" + +#: src/Admin/Ajax.php:52 +msgid "That looks like a widget key. Paste an API token from API keys instead." +msgstr "這看起來像小工具密鑰。請改用「API 密鑰」中的 API 權杖。" + +#: src/Admin/Ajax.php:56 +msgid "That looks like a widget secret. Paste an API token from API keys instead." +msgstr "這看起來像小工具秘密。請改用「API 密鑰」中的 API 權杖。" + +#. translators: 1: organisation name, 2: number of sources +#: src/Admin/SettingsPage.php:51 +#, php-format +msgid "Connected to %1$s — %2$d source(s) available." +msgstr "已連接至 %1$s — 有 %2$d 個來源可用。" + +#: src/Admin/SettingsPage.php:53 +msgid "Starting sync…" +msgstr "正在開始同步…" + +#: src/Integration/AddToCart.php:44 +#: src/Integration/AddToCart.php:99 +msgid "No product was specified." +msgstr "未指定產品。" + +#: src/Integration/AddToCart.php:45 +#: src/Integration/AddToCart.php:266 +#: src/Integration/AddToCart.php:820 +msgid "Could not add that to the cart." +msgstr "無法加入購物車。" + +#: src/Integration/AddToCart.php:211 +msgid "WooCommerce is not available." +msgstr "WooCommerce 無法使用。" + +#: src/Integration/AddToCart.php:220 +#: src/Integration/AddToCart.php:240 +msgid "That product could not be found." +msgstr "找不到該產品。" + +#: src/Integration/AddToCart.php:247 +msgid "That product cannot be purchased." +msgstr "該產品無法購買。" + +#. translators: %s: product name +#: src/Integration/AddToCart.php:289 +#, php-format +msgid "%s added to your cart." +msgstr "%s 已加入購物車。" + +#: src/Integration/AddToCart.php:290 +msgid "Added to your cart." +msgstr "已加入購物車。" + +#: src/Integration/AddToCart.php:332 +#: src/Integration/AddToCart.php:359 +msgid "This product needs a variation." +msgstr "此產品需要選擇款式。" + +#: src/Integration/AddToCart.php:367 +msgid "This product has many options. Open the product to pick one." +msgstr "此產品選項較多。請開啟產品頁選擇。" + +#. translators: %s: product attribute name, such as colour or size +#: src/Integration/AddToCart.php:376 +#: src/Integration/AddToCart.php:525 +#, php-format +msgid "Which %s?" +msgstr "哪個%s?" + +#: src/Integration/HostNavigation.php:44 +msgid "Could not open that page." +msgstr "無法開啟該頁面。" + +#: src/Integration/HostNavigation.php:45 +#: src/Integration/HostNavigation.php:66 +msgid "No page was specified." +msgstr "未指定頁面。" + +#: src/Integration/HostNavigation.php:46 +msgid "The cart is not available." +msgstr "購物車無法使用。" + +#: src/Integration/HostNavigation.php:47 +msgid "Checkout is not available." +msgstr "結帳無法使用。" + +#: src/Integration/HostNavigation.php:60 +msgid "Unknown action." +msgstr "未知操作。" + +#. translators: 1: item offset, 2: error message +#: src/Sync/BulkSync.php:186 +#, php-format +msgid "Sync stopped at item %1$d: %2$s" +msgstr "同步在第 %1$d 項停止:%2$s" + +#. translators: 1: item offset, 2: attempt count, 3: error message +#: src/Sync/BulkSync.php:199 +#, php-format +msgid "Sync stopped at item %1$d after %2$d attempts: %3$s" +msgstr "同步在第 %1$d 項停止,已嘗試 %2$d 次:%3$s" diff --git a/languages/datalumo-zh_TW.mo b/languages/datalumo-zh_TW.mo index 3b29a31437429bca1be6d236a2fc3ccaccb6b046..dd9c3fcbc0e2ba9ed7d5dcac36c75ca1b6601b4a 100644 GIT binary patch literal 9041 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 BWoQ5Z delta 3303 zcmZ{kdr(x@9mh{nA3;!1(P%JuP%uUXfhN&no75y|(mK&-tEnb6m)#3{%X05_?_HI) z=`P4)5fJP_8Z058Vv(0rhk!tEI-NeI?M$2LADO<=Oy%C)^&i@4JDsM}rr+PaSI|y- zX3zbc_xb(4zu(#S_WUeA{K1L^=NZaw)OD!Y4>I;8aN7cWP+Au{ z9ROE=$G{X&1&@PY2R{Y=7Ayl-J;WG(>IZzQsNYN5Uc}J!C!;e z!V*N>&;IHTd<-5ze+hQe!V+*R7zD|nE|+~^Hu~2=h{fIq*MlE`%fWwxq-Yv$hm&kQ z_zJig{2W*XlF~m1!)a*5(IAWd0aDl(U^jW51adG7JoW%%&w?A!?}Bw+@cUpfxCw`l zfqs`KKsql1!WP!(@=Y)o{a%m^|5Y0FCr^Ka0pcB(AAw}y61cM&+yv6b{UB|81Elpe zAYIS~mV-SYg>oqpMF#hPq`)9Z>qkLS;D;aw(^f+NZ=g|tz|ld&;3vUJFaXZF@6asU=C}XQ{?nI?OC*&tvi+Z=2z*##kMef8C;3rTK75k~V!*()Kd%HVeD}*-Xz~iX+^R)8-V9%h^^QE#2 z^+{Cv11dnJLN6(qyrH<_WzCg5yCpta#6`j4iKC^49OOY+5BUw9Ju8=LL3zHb89r|K zWUd9vBvsKRL(x?J;`3Zmy{LTmffu=|I|CDM8tr$DWf^kL(xwA6hLUs3l1m zq9Z9eXLC+rwwLQ#DCm*($FWRPRoP?6UL3-6bpFRk8_y8~Ntxoqq}p&$mJFFo?gb&$ z3p?q;a=B9Hipnc-4u4)UB!8$v<6cSkm1t6sZV%~(R>4aZ*-uvDbm^3)Y892dLJ9;F zwM^$87+-?Jyft99*Ih48<#g=vfK(>qQ|Z?vFHZE6SdfUT+DY-FCDq|# z$>Vc22SU0})@`*sK23)v0Zli!Q5nFfqQXN>mFMksRLJAc-F4F$;99A@g>QhX8|K`c z^A0uybt8`zt3J|rH{_J1pvTAdC~#CUxGJB*rJAG8y>d|)|A$)Q$H~QuY~@2DHD#MP zl(JuprW_5+(g~TD_$9TRP7BJphO8UgkO({=>2_RH`J@{gdqKc2d1P3S$M(qv_iJTk zvNsoD3h8nXf@`Xws3Dp3QG(PxfnhtJbS`;2xRKoMDHhwB)H z+m5!!CmFAC-KPZ&kLJa!#^s6<%1?+#lL&}kK^XB0BnVL^i(jtDY#0&EapUTbOkz25n7Zeud=WoqN5o77a zsrw|gEQHgG_}6^Lzi>qSGyPRDwd$zYzWS#x#YS7LuFjd8jplR{o9{FlgHw;zu+c*c6G{dT09nJw+sJ5llFhqF=>kxSOu-kI?$ z*7#uj>J?G3=Fvmu*mY+(UVnq<6lCkOQ&Ic*nvuqt={jqob#|)3KIguL&Lg%u8+eYF z&n-1uuEpB=#Glr*h0V^s`82f7e%Fk?W44UMF4WwPG*P-H>dz(4jogkjo0Aihn}s^m z_9Y@!P-bR40w?Dka+HBA@qtFOHVS9wTy*?#_QN>GUHA#t=XNo3>V+}0soiSrG)Ec{ zqHF(GE<2izSk||ztcGbb3e^x+xZ8IL+eH4_bg?`mYoYzP#giGkL|w)~5z9Cnj-Bs_ zU7a8)>zl3i5hv=iQ%zR1J$`*^X8a5d+KDnV*^4}xnY=_t$A<;7BGxepC1>8fg^0!@ zwYbT=e8C#*NkqEief`MGM7xOHoQh2}LDcxw@3BNjD}sEFd=E5=Z>@VhcP=^EJ#%on z=DIobrV};h2x0ZqiB~d9#fO=S@UP!1mhqRvR!eK_^2F@a1;-Np-;X%<_7KdDU7T`6 zOmtTz-nuw5K4gwIQ6f~GF)v>wr>xHY*xS7bf^%cMZ^#_JV0+uwZw_CKPYfVQm^sp7 Xj`f\n" "Language-Team: LANGUAGE \n" -"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2026-08-30T08:09:50+00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"X-Generator: WP-CLI 2.12.0\n" "X-Domain: datalumo\n" +#. Plugin Name of the plugin +#. Author of the plugin +#: datalumo.php +#: src/Admin/SettingsPage.php:25 +#: src/Admin/SettingsPage.php:26 +msgid "Datalumo" +msgstr "" + +#. Plugin URI of the plugin +#: datalumo.php +msgid "https://github.com/datalumo/wordpress" +msgstr "" + +#. Description of the plugin #: datalumo.php msgid "Sync your WordPress content to Datalumo and add AI-powered search and chat to your site." msgstr "" -#: src/Plugin.php -msgid "Datalumo is almost ready — connect your account to start syncing content." +#. Author URI of the plugin +#: datalumo.php +msgid "https://datalumo.app" msgstr "" -#: src/Plugin.php -msgid "Open settings" +#: resources/views/settings.php:19 +msgid "Connection" msgstr "" -#: src/Admin/SettingsPage.php -#: resources/views/settings.php -msgid "Datalumo" +#: resources/views/settings.php:20 +msgid "Content sync" msgstr "" -#: src/Admin/SettingsPage.php -msgid "Connected to %s — %d source(s) available." +#: resources/views/settings.php:21 +msgid "Chatbot" msgstr "" -#: src/Admin/SettingsPage.php -msgid "Connection failed:" +#: resources/views/settings.php:22 +msgid "Search box" msgstr "" -#: src/Admin/SettingsPage.php -msgid "%1$d of %2$d posts synced…" +#: resources/views/settings.php:23 +#: resources/views/settings.php:386 +msgid "Enhanced search" msgstr "" -#: src/Admin/SettingsPage.php -msgid "Sync complete." +#: resources/views/settings.php:32 +msgid "Connected to Datalumo." msgstr "" -#: src/Admin/SettingsPage.php -#: src/Admin/Ajax.php -msgid "Not allowed." +#: resources/views/settings.php:33 +msgid "Connect did not finish. Start again from this page." msgstr "" -#: src/Admin/Ajax.php -msgid "Both the organisation ID and an API token are required." +#: resources/views/settings.php:34 +msgid "Disconnected. The API key remains in Datalumo under API keys until you revoke it there." msgstr "" -#: src/Api/Client.php -msgid "Invalid widget key — copy it from the widget editor." +#: resources/views/settings.php:35 +msgid "Setup saved." msgstr "" -#: src/Api/Client.php -msgid "Datalumo request failed (%d)." +#: resources/views/settings.php:36 +msgid "Widget keys look like org-id/widget-id. An API token or secret will not work there." msgstr "" -#: src/Sync/BulkSync.php -msgid "Unknown sync configuration." +#: resources/views/settings.php:79 +msgid "Custom fields, filters, and sync details." msgstr "" -#: src/Search/Summary.php -msgid "AI Summary" +#: resources/views/settings.php:80 +msgid "Shortcodes and chat page actions." msgstr "" -#: src/Search/Summary.php -msgid "Show more" +#: resources/views/settings.php:81 +msgid "Search box shortcode and enhanced search." msgstr "" -#: src/Search/Summary.php -msgid "Show less" +#: resources/views/settings.php:82 +msgid "Connect, sync, and widgets." msgstr "" -#: src/Search/Summary.php -msgid "Summarising the best results…" +#: resources/views/settings.php:104 +msgid "Finish setup" msgstr "" -#: resources/views/settings.php -msgid "Connection" +#: resources/views/settings.php:105 +msgid "Pick a starting setup. You can change these later." msgstr "" -#: resources/views/settings.php -msgid "Content sync" +#: resources/views/settings.php:111 +msgid "What to sync" msgstr "" -#: resources/views/settings.php -msgid "Chatbot" +#: resources/views/settings.php:129 +msgid "Chat" msgstr "" -#: resources/views/settings.php -msgid "Search box" +#: resources/views/settings.php:134 +#: resources/views/settings.php:339 +msgid "Show the floating chat on every page" msgstr "" -#: resources/views/settings.php -msgid "Enhanced search" +#: resources/views/settings.php:144 +msgid "Search" +msgstr "" + +#: resources/views/settings.php:149 +msgid "Use Datalumo for WordPress search results" +msgstr "" + +#: resources/views/settings.php:156 +msgid "Save and sync now" msgstr "" -#: resources/views/settings.php -msgid "Settings saved." +#: resources/views/settings.php:157 +msgid "Save setup" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:163 +#: resources/views/settings.php:206 +msgid "Disconnect" +msgstr "" + +#: resources/views/settings.php:168 +msgid "Connect this site to Datalumo. You will pick or create a knowledge base and widgets, then we create a source and API key for you." +msgstr "" + +#: resources/views/settings.php:172 +#: resources/views/settings.php:239 +msgid "Connect with Datalumo" +msgstr "" + +#: resources/views/settings.php:179 +msgid "Using a self-hosted Datalumo?" +msgstr "" + +#: resources/views/settings.php:181 +#: resources/views/settings.php:213 msgid "Datalumo URL" msgstr "" -#: resources/views/settings.php -msgid "Leave as-is unless you were told otherwise." +#: resources/views/settings.php:189 +msgid "Manual setup" msgstr "" -#: resources/views/settings.php -msgid "Organisation ID" +#: resources/views/settings.php:202 +msgid "Organisation" msgstr "" -#: resources/views/settings.php -msgid "Find it next to your API keys in the Datalumo dashboard." +#: resources/views/settings.php:217 +msgid "Leave as-is unless you were told otherwise." msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:222 msgid "API token" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:225 +#: resources/views/settings.php:365 msgid "•••••••• (saved)" msgstr "" -#: resources/views/settings.php -msgid "Leave blank to re-test, or paste a new token to replace it." +#: resources/views/settings.php:226 +msgid "Paste a token from API keys. The organisation is inferred from the token." msgstr "" -#: resources/views/settings.php -msgid "Create a token under API keys in your Datalumo dashboard." -msgstr "" - -#: resources/views/settings.php +#: resources/views/settings.php:232 msgid "Connect & test" msgstr "" -#: resources/views/settings.php -msgid "Connected to %1$s (%2$d sources)." +#: resources/views/settings.php:234 +msgid "Save URL" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:255 msgid "Connect your account first." msgstr "" -#: resources/views/settings.php -msgid "Each sync pushes the chosen post types into one Datalumo source. Create a source of type \"API\" in your dashboard first." -msgstr "" - -#: resources/views/settings.php +#: resources/views/settings.php:267 msgid "Source" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:270 msgid "— choose a source —" msgstr "" -#: resources/views/settings.php +#. translators: %s: knowledge base name +#: resources/views/settings.php:294 +#, php-format +msgid "Knowledge base: %s" +msgstr "" + +#: resources/views/settings.php:303 msgid "Post types" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:317 msgid "Full sync" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:319 msgid "Sync now" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:320 msgid "Cancel" msgstr "" -#: resources/views/settings.php -msgid "Custom field and taxonomy mappings can be added per sync with the datalumo_page_payload filter for now." -msgstr "" - -#: resources/views/settings.php +#: resources/views/settings.php:330 msgid "Save syncs" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:335 msgid "Chat widget" msgstr "" -#: resources/views/settings.php -msgid "Show the floating chat on every page" -msgstr "" - -#: resources/views/settings.php +#: resources/views/settings.php:344 +#: resources/views/settings.php:374 +#: resources/views/settings.php:395 msgid "Widget key" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:348 msgid "Copy it from the widget editor (\"For developers\"). Add this site to the widget's websites list." msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:352 msgid "Visitor identity" msgstr "" -#: resources/views/settings.php -msgid "Let logged-in users continue their conversations across visits" +#: resources/views/settings.php:356 +msgid "Pass a signed id for logged-in users to the chat widget" msgstr "" -#: resources/views/settings.php -msgid "Identity signing secret" +#: resources/views/settings.php:358 +msgid "Optional, for custom implementations. The plugin does not resume conversations on its own." msgstr "" -#: resources/views/settings.php -msgid "From the same \"For developers\" section. Never exposed to the browser — used to sign user ids server-side." +#: resources/views/settings.php:362 +msgid "Identity signing secret" msgstr "" -#: resources/views/settings.php -msgid "Tip: use the [datalumo_chat] shortcode to embed the chat inline on a page." +#: resources/views/settings.php:366 +msgid "From the same \"For developers\" section. Never exposed to the browser. Used to sign user ids server-side." msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:378 msgid "A search-type widget key. Place the box anywhere with the [datalumo_search] shortcode." msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:390 msgid "Serve WordPress search results from Datalumo (falls back to native search on any error)" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:399 msgid "A search-type widget key; this site must be in its websites list." msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:403 msgid "Limit to post types" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:412 msgid "Leave all unchecked to enhance every search." msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:416 msgid "AI summary" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:420 msgid "Show a streamed AI answer above the results" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:425 msgid "Summary placement" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:428 msgid "auto" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:431 msgid "Before results" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:432 msgid "After results" msgstr "" -#: resources/views/settings.php +#: resources/views/settings.php:434 msgid "Optional CSS selector of the results container; detected automatically when empty." msgstr "" +#: resources/views/settings.php:447 +msgid "Need help?" +msgstr "" + +#: resources/views/settings.php:449 +msgid "Ask a question" +msgstr "" + +#: resources/views/settings.php:450 +msgid "How do I sync products?" +msgstr "" + +#: resources/views/settings.php:452 +msgid "Start chat" +msgstr "" + +#: resources/views/settings.php:461 +msgid "WordPress plugin docs" +msgstr "" + +#: src/Admin/Ajax.php:48 +msgid "An API token is required." +msgstr "" + +#: src/Admin/Ajax.php:52 +msgid "That looks like a widget key. Paste an API token from API keys instead." +msgstr "" + +#: src/Admin/Ajax.php:56 +msgid "That looks like a widget secret. Paste an API token from API keys instead." +msgstr "" + +#: src/Admin/Ajax.php:127 +#: src/Admin/SettingsPage.php:133 +#: src/Connect/Grant.php:270 +msgid "Not allowed." +msgstr "" + +#. translators: 1: organisation name, 2: number of sources +#: src/Admin/SettingsPage.php:51 +#, php-format +msgid "Connected to %1$s — %2$d source(s) available." +msgstr "" + +#: src/Admin/SettingsPage.php:52 +msgid "Connection failed:" +msgstr "" + +#: src/Admin/SettingsPage.php:53 +msgid "Starting sync…" +msgstr "" + +#. translators: 1: number of posts already synced, 2: total posts +#: src/Admin/SettingsPage.php:55 +#, php-format +msgid "%1$d of %2$d posts synced…" +msgstr "" + +#: src/Admin/SettingsPage.php:56 +msgid "Sync complete." +msgstr "" + +#: src/Api/Client.php:120 +msgid "Invalid widget key — copy it from the widget editor." +msgstr "" + +#. translators: %d: HTTP status code +#: src/Api/Client.php:163 +#, php-format +msgid "Datalumo request failed (%d)." +msgstr "" + +#: src/Integration/AddToCart.php:44 +#: src/Integration/AddToCart.php:99 +msgid "No product was specified." +msgstr "" + +#: src/Integration/AddToCart.php:45 +#: src/Integration/AddToCart.php:266 +#: src/Integration/AddToCart.php:820 +msgid "Could not add that to the cart." +msgstr "" + +#: src/Integration/AddToCart.php:211 +msgid "WooCommerce is not available." +msgstr "" + +#: src/Integration/AddToCart.php:220 +#: src/Integration/AddToCart.php:240 +msgid "That product could not be found." +msgstr "" + +#: src/Integration/AddToCart.php:247 +msgid "That product cannot be purchased." +msgstr "" + +#. translators: %s: product name +#: src/Integration/AddToCart.php:289 +#, php-format +msgid "%s added to your cart." +msgstr "" + +#: src/Integration/AddToCart.php:290 +msgid "Added to your cart." +msgstr "" + +#: src/Integration/AddToCart.php:332 +#: src/Integration/AddToCart.php:359 +msgid "This product needs a variation." +msgstr "" + +#: src/Integration/AddToCart.php:367 +msgid "This product has many options. Open the product to pick one." +msgstr "" + +#. translators: %s: product attribute name, such as colour or size +#: src/Integration/AddToCart.php:376 +#: src/Integration/AddToCart.php:525 +#, php-format +msgid "Which %s?" +msgstr "" + +#: src/Integration/HostNavigation.php:44 +msgid "Could not open that page." +msgstr "" + +#: src/Integration/HostNavigation.php:45 +#: src/Integration/HostNavigation.php:66 +msgid "No page was specified." +msgstr "" + +#: src/Integration/HostNavigation.php:46 +msgid "The cart is not available." +msgstr "" + +#: src/Integration/HostNavigation.php:47 +msgid "Checkout is not available." +msgstr "" + +#: src/Integration/HostNavigation.php:60 +msgid "Unknown action." +msgstr "" + +#: src/Plugin.php:88 +msgid "Datalumo is almost ready — connect your account to start syncing content." +msgstr "" + +#: src/Plugin.php:90 +msgid "Open settings" +msgstr "" + +#: src/Search/Summary.php:72 +msgid "AI Summary" +msgstr "" + +#: src/Search/Summary.php:73 +msgid "Show more" +msgstr "" + +#: src/Search/Summary.php:74 +msgid "Show less" +msgstr "" + +#: src/Search/Summary.php:75 +msgid "Summarising the best results…" +msgstr "" + +#: src/Sync/BulkSync.php:48 +msgid "Unknown sync configuration." +msgstr "" + +#. translators: 1: item offset, 2: error message +#: src/Sync/BulkSync.php:186 +#, php-format +msgid "Sync stopped at item %1$d: %2$s" +msgstr "" + +#. translators: 1: item offset, 2: attempt count, 3: error message +#: src/Sync/BulkSync.php:199 +#, php-format +msgid "Sync stopped at item %1$d after %2$d attempts: %3$s" +msgstr "" diff --git a/readme.txt b/readme.txt index 8717bc1..a91a98e 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: search, ai, chatbot, rag Requires at least: 6.0 Tested up to: 7.1 Requires PHP: 8.1 -Stable tag: 0.2.0 +Stable tag: 1.0.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -79,6 +79,9 @@ View cart and Open checkout (WooCommerce), and Open this page (a post or page by == Changelog == += 1.0.0 = +* First WordPress.org release. + = 0.2.0 = * Featured images sync with the page. WooCommerce products use the product image when the post has none. Removing the image and syncing again clears it in Datalumo. diff --git a/tests/Unit/IndexPhpTest.php b/tests/Unit/IndexPhpTest.php index 18dcb4e..bc1c36c 100644 --- a/tests/Unit/IndexPhpTest.php +++ b/tests/Unit/IndexPhpTest.php @@ -2,7 +2,7 @@ it('has a silence is golden index.php in every first-party folder', function () { $root = dirname(__DIR__, 2); - $skip = ['.git', '.github', '.slimm', '.phpunit.cache', 'vendor']; + $skip = ['.git', '.github', '.slimm', '.phpunit.cache', '.wordpress-org', 'vendor', 'build']; $iterator = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), From 3019e8629cd2058dc9368900fbd53b797acdf042 Mon Sep 17 00:00:00 2001 From: Jeffrey van Rossum Date: Sun, 30 Aug 2026 17:44:23 +0900 Subject: [PATCH 9/9] Write the directory readme for a WordPress site owner, not a Datalumo operator. Co-authored-by: Cursor --- readme.txt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/readme.txt b/readme.txt index a91a98e..8657bd1 100644 --- a/readme.txt +++ b/readme.txt @@ -20,11 +20,11 @@ Datalumo keeps a searchable, AI-ready copy of your WordPress content and gives y * **Enhanced search** — serve WordPress' native search results from Datalumo's ranking, with an optional streamed AI summary above the results. Falls back to native search automatically. * **Chat actions** — add to cart, view cart, checkout, and open a page. Add them in Datalumo under Reply → What it can do. -This plugin is an interface to a [Datalumo](https://datalumo.app) instance. You need a Datalumo account (or a self-hosted instance). Connect with Datalumo from Settings, or use Manual setup to paste an API token. Nothing is sent until an administrator connects the site and turns a feature on. +Connect with Datalumo from Settings, or use Manual setup to paste an API token. = External services = -The plugin talks to the Datalumo instance you configure. The default host is `https://datalumo.app`; you can point it at your own instance instead. +The plugin talks to the Datalumo service. * Service: [https://datalumo.app](https://datalumo.app) * Documentation: [https://datalumo.app/docs](https://datalumo.app/docs) @@ -33,7 +33,7 @@ The plugin talks to the Datalumo instance you configure. The default host is `ht When a feature is enabled, the plugin may: -* Load the widget script from `{your Datalumo URL}/widget/v1/datalumo.js` (chat, search box, AI summary, and click tracking). +* Load the widget script from `https://datalumo.app/widget/v1/datalumo.js` (chat, search box, AI summary, and click tracking). * Send published content to the Datalumo API: title, HTML, permalink, featured image URL, author display name, categories, tags, dates, and any custom field mappings. WooCommerce products also send short description, product categories and tags, visible attributes, SKU, and the product image when there is no featured image. * On Connect with Datalumo, send this site's host and URL so the grant can return to this admin. * Send the current page id as chat context, plus product id and SKU on product pages. @@ -58,16 +58,12 @@ Production dependencies are installed with `composer install --no-dev`. = Do I need a Datalumo account? = -Yes. The plugin does not search or chat on its own. It connects WordPress to a Datalumo instance — the hosted service at datalumo.app or one you run yourself. +Yes. The plugin does not search or chat on its own. It connects WordPress to Datalumo. = What data is sent to Datalumo? = Only after you connect and turn a feature on. The list is under External services above. -= Can I use a self-hosted Datalumo instance? = - -Yes. On the Connection tab, open Using a self-hosted Datalumo? and set the URL before you connect. Manual setup has the same field. - = Can the chatbot add products to a WooCommerce cart? = Yes, if WooCommerce is active. In Datalumo, add the WordPress Add to cart action. The visitor confirms in chat. Variable products can ask for options first. @@ -76,7 +72,6 @@ Yes, if WooCommerce is active. In Datalumo, add the WordPress Add to cart action View cart and Open checkout (WooCommerce), and Open this page (a post or page by id, slug, or same-site URL). Add them from Reply → Plugin actions → WordPress. - == Changelog == = 1.0.0 =