From 6ea708b51c59a887cbaa6f55f45f8adb77df2fa9 Mon Sep 17 00:00:00 2001 From: pjaudiomv Date: Mon, 11 May 2026 23:59:07 -0400 Subject: [PATCH] add language --- crumb.php | 76 ++++++++++++++++++++++++++++++++++++-- readme.txt | 6 ++- tests/test-crumb.php | 88 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 165 insertions(+), 5 deletions(-) diff --git a/crumb.php b/crumb.php index 2224e63..6996f35 100644 --- a/crumb.php +++ b/crumb.php @@ -3,7 +3,7 @@ * Plugin Name: Crumb * Plugin URI: https://wordpress.org/plugins/crumb/ * Description: Embeds the Crumb meeting finder widget on any page or post using a shortcode. - * Version: 1.4.0 + * Version: 1.5.0 * Author: bmltenabled * Author URI: https://bmlt.app * License: GPL v2 or later @@ -15,19 +15,23 @@ exit; } -define( 'CRUMB_VERSION', '1.4.0' ); +define( 'CRUMB_VERSION', '1.5.0' ); class Crumb { private static ?self $instance = null; private static ?bool $shortcode_geolocation = null; private static ?int $shortcode_geolocation_radius = null; + private static ?string $shortcode_language = null; private static ?array $crouton_options = null; private static array $compat_tags = []; const DEFAULT_CDN_URL = 'https://cdn.aws.bmlt.app/crumb-widget.js'; const REWRITE_VERSION = '1'; + /** Languages the widget supports (kept in sync with src/stores/localization.ts). */ + const SUPPORTED_LANGUAGES = [ 'en', 'es', 'fr', 'de', 'pt', 'it', 'sv', 'da', 'el', 'fa', 'pl', 'ru', 'ja' ]; + /** * Tag → forced view for crouton-named shortcodes. * Map-flavored tags become "both" (map + list) so users don't lose the list view @@ -254,6 +258,7 @@ public static function setup_shortcode( array $atts ): string { 'geolocation_radius' => null, 'update_url' => null, 'columns' => null, + 'language' => null, ], $atts, 'crumb' @@ -271,6 +276,15 @@ public static function setup_shortcode( array $atts ): string { } } + // Language must be one of the codes the widget bundles; otherwise we drop it + // and let the widget auto-detect from navigator.language. + if ( null !== $atts['language'] ) { + $lang = strtolower( trim( (string) $atts['language'] ) ); + if ( in_array( $lang, self::SUPPORTED_LANGUAGES, true ) ) { + self::$shortcode_language = $lang; + } + } + // Shortcode attribute takes precedence; fall back to saved option only when not provided. // Crouton settings (bmlt_tabs_options) are used as a fallback when the crumb option is empty. $server = esc_url( trim( $atts['server'] ?? self::get_option_or_crouton( 'crumb_server', 'https://latest.aws.bmlt.app/main_server/' ) ) ); @@ -433,6 +447,17 @@ public static function localize_config(): void { $config['geolocationRadius'] = self::$shortcode_geolocation_radius; } + // Shortcode language overrides; otherwise fall back to the saved option. + // Empty string in either path means "let the widget auto-detect". + if ( null !== self::$shortcode_language ) { + $config['language'] = self::$shortcode_language; + } elseif ( ! isset( $config['language'] ) ) { + $saved_language = get_option( 'crumb_language', '' ); + if ( '' !== $saved_language && in_array( $saved_language, self::SUPPORTED_LANGUAGES, true ) ) { + $config['language'] = $saved_language; + } + } + if ( ! empty( $config ) ) { wp_add_inline_script( 'crumb', 'var CrumbWidgetConfig = ' . wp_json_encode( $config ) . ';', 'before' ); } @@ -477,6 +502,11 @@ public static function sanitize_geolocation_radius( string $input ): string { return ( 0 !== $val ) ? (string) $val : ''; } + public static function sanitize_language( string $input ): string { + $lang = strtolower( trim( $input ) ); + return in_array( $lang, self::SUPPORTED_LANGUAGES, true ) ? $lang : ''; + } + public static function sanitize_config( string $input ): string { $input = trim( $input ); @@ -528,6 +558,14 @@ public static function register_settings(): void { register_setting( $group, 'crumb_css_template', 'sanitize_text_field' ); register_setting( $group, 'crumb_view', 'sanitize_text_field' ); register_setting( $group, 'crumb_update_url', 'sanitize_text_field' ); + register_setting( + $group, + 'crumb_language', + [ + 'type' => 'string', + 'sanitize_callback' => [ static::class, 'sanitize_language' ], + ] + ); register_setting( $group, 'crumb_geolocation_radius', @@ -658,6 +696,38 @@ class="regular-text" placeholder="meetings" />

Optional. Sets the default view when the widget loads. Can be overridden at runtime via the ?view= query parameter, or per-page via the shortcode view attribute.

+ + + + 'English', + 'es' => 'Español', + 'fr' => 'Français', + 'de' => 'Deutsch', + 'pt' => 'Português', + 'it' => 'Italiano', + 'sv' => 'Svenska', + 'da' => 'Dansk', + 'el' => 'Ελληνικά', + 'fa' => 'فارسی', + 'pl' => 'Polski', + 'ru' => 'Русский', + 'ja' => '日本語', + ]; + ?> + +

Optional. Forces the widget UI language. Default behavior is to detect from the visitor's browser (navigator.language). Can be overridden per-page via the shortcode language attribute.

+ + @@ -719,7 +789,7 @@ class="large-text" placeholder="https://example.org/meeting-update-form/?meeting

[crumb]

- [crumb server="https://your-server/main_server" service_body="42" format_ids="17,54" view="map" geolocation="true" geolocation_radius="-50"] + [crumb server="https://your-server/main_server" service_body="42" format_ids="17,54" view="map" geolocation="true" geolocation_radius="-50" language="es"] diff --git a/readme.txt b/readme.txt index d4014c9..eb6f0ba 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: narcotics anonymous, na, meetings, bmlt, meeting finder Requires at least: 6.0 Tested up to: 7.0 Requires PHP: 8.1 -Stable tag: 1.4.0 +Stable tag: 1.5.0 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -42,6 +42,7 @@ Shortcode attributes: * `geolocation_radius` — Geolocation search radius. Positive integer = fixed radius in miles (or km per server settings). Negative integer = BMLT auto-radius: the server expands the search until it finds roughly that many meetings (e.g. `-50` finds ~50 nearby meetings). Overrides the Geolocation Radius setting and Widget Configuration. * `update_url` — URL template for the **Update Meeting Info** link shown at the bottom of the meeting detail panel. Supports tokens `{meeting_id}`, `{meeting_name}`, `{server_url}`, `{return_url}` (URL-encoded on substitution). Works with bmlt-workflow, hosted forms, or `mailto:` URLs. * `columns` — Comma-separated list of columns to show in list view (e.g. `time,name,location,address,service_body`). Omit a name to hide that column. Leave unset to use the widget default. +* `language` — Force the widget UI language for this page (e.g. `en`, `es`, `fr`, `de`, `pt`, `it`, `sv`, `da`, `el`, `fa`, `pl`, `ru`, `ja`). Leave unset to auto-detect from the visitor's browser. = Switching from Crouton = @@ -125,6 +126,9 @@ The widget fetches meeting data from a BMLT server whose URL you configure in Se == Changelog == += 1.5.0 = +* Added **Language** setting on the global settings page and matching `language` shortcode attribute (e.g. `[crumb language="es"]`). Forces the widget UI language; leave empty to auto-detect from the visitor's browser. Supported codes: `en`, `es`, `fr`, `de`, `pt`, `it`, `sv`, `da`, `el`, `fa`, `pl`, `ru`, `ja`. Per-shortcode value overrides the saved setting; `widget_config` JSON `language` key still wins over both. + = 1.4.0 = * Added `columns` shortcode attribute — comma-separated list of columns to show in list view (e.g. `[crumb columns="time,name,location,address,service_body"]`). Omit a name to hide that column. Pairs with the new `data-columns` reader in the Crumb widget; leave unset to use the widget default. * Crouton compatibility: `has_areas` and `has_regions` on `[bmlt_tabs]` / `[bmlt_map]` / `[crouton_tabs]` / `[crouton_map]` now add the `service_body` column to the widget output, preserving crouton's behavior of surfacing the originating service body in the listing. diff --git a/tests/test-crumb.php b/tests/test-crumb.php index 40a9410..10950ca 100644 --- a/tests/test-crumb.php +++ b/tests/test-crumb.php @@ -11,7 +11,7 @@ public function setUp(): void { $GLOBALS['wp_scripts'] = new WP_Scripts(); // Reset private statics on Crumb. $ref = new ReflectionClass( Crumb::class ); - foreach ( [ 'shortcode_geolocation', 'shortcode_geolocation_radius', 'crouton_options' ] as $prop ) { + foreach ( [ 'shortcode_geolocation', 'shortcode_geolocation_radius', 'shortcode_language', 'crouton_options' ] as $prop ) { $p = $ref->getProperty( $prop ); $p->setAccessible( true ); $p->setValue( null, null ); @@ -414,6 +414,92 @@ public function test_shortcode_geolocation_radius_negative_is_accepted() { $this->assertSame( -50, $config['geolocationRadius'] ); } + // ------------------------------------------------------------------------- + // language shortcode attribute + option + // ------------------------------------------------------------------------- + + public function test_shortcode_language_sets_config() { + $this->enqueue_crumb(); + do_shortcode( '[crumb language="es"]' ); + + Crumb::localize_config(); + + $config = $this->get_inline_config(); + $this->assertNotNull( $config ); + $this->assertSame( 'es', $config['language'] ); + } + + public function test_shortcode_language_invalid_is_dropped() { + $this->enqueue_crumb(); + do_shortcode( '[crumb language="banana"]' ); + + Crumb::localize_config(); + + $raw = wp_scripts()->get_data( 'crumb', 'before' ); + $this->assertEmpty( $raw ); + } + + public function test_saved_language_option_sets_config() { + $this->enqueue_crumb(); + update_option( 'crumb_language', 'fr' ); + + Crumb::localize_config(); + + $config = $this->get_inline_config(); + $this->assertNotNull( $config ); + $this->assertSame( 'fr', $config['language'] ); + + delete_option( 'crumb_language' ); + } + + public function test_shortcode_language_overrides_saved_option() { + $this->enqueue_crumb(); + update_option( 'crumb_language', 'fr' ); + do_shortcode( '[crumb language="de"]' ); + + Crumb::localize_config(); + + $config = $this->get_inline_config(); + $this->assertNotNull( $config ); + $this->assertSame( 'de', $config['language'] ); + + delete_option( 'crumb_language' ); + } + + public function test_widget_config_json_language_takes_precedence_over_option() { + // JSON widget_config is loaded first; saved option only fills in when language is unset. + $this->enqueue_crumb(); + update_option( 'crumb_widget_config', '{"language":"pt"}' ); + update_option( 'crumb_language', 'fr' ); + + Crumb::localize_config(); + + $config = $this->get_inline_config(); + $this->assertNotNull( $config ); + $this->assertSame( 'pt', $config['language'] ); + + delete_option( 'crumb_widget_config' ); + delete_option( 'crumb_language' ); + } + + public function test_sanitize_language_accepts_supported_code() { + $this->assertSame( 'es', Crumb::sanitize_language( 'es' ) ); + $this->assertSame( 'ja', Crumb::sanitize_language( 'ja' ) ); + } + + public function test_sanitize_language_rejects_unsupported() { + $this->assertSame( '', Crumb::sanitize_language( 'banana' ) ); + $this->assertSame( '', Crumb::sanitize_language( 'EN-US' ) ); + } + + public function test_sanitize_language_empty_returns_empty() { + $this->assertSame( '', Crumb::sanitize_language( '' ) ); + } + + public function test_sanitize_language_lowercases_and_trims() { + $this->assertSame( 'fr', Crumb::sanitize_language( ' FR ' ) ); + } + // ------------------------------------------------------------------------- // sanitize_geolocation_radius // -------------------------------------------------------------------------