Blame view

docs/店匠相关资料/SHOPLAZZA_I18N_AND_FRONTEND_GUIDE.md 18.4 KB
39e63ad1   tangwang   docs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
  # Shoplazza 应用开发指南:多语言支持与前端优化
  
  ## 目录
  
  1. [多语言支持](#多语言支持)
  2. [前端优化与 Lessjs 集成](#前端优化与-lessjs-集成)
  3. [与店匠主题的适配](#与店匠主题的适配)
  
  ---
  
  ## 多语言支持
  
  ### 1. 概述
  
  Shoplazza 应用支持 16 种语言,通过 Liquid 模板的 `t` 过滤器和 `shop.locale` 对象实现自动语言适配。
  
  ### 2. 支持的语言列表
  
  根据 [Lessjs 文档](https://lessjs.shoplazza.com/latest/docs/introduction/),Shoplazza 支持以下语言:
  
  | 语言代码 | 语言名称 | 示例文本 |
  |---------|---------|---------|
  | `ar-SA` | 阿拉伯语(沙特阿拉伯) | أضف إلى السلة |
  | `de-DE` | 德语 | In den Warenkorb legen |
  | `en-US` | 英语 | Add to cart |
  | `es-ES` | 西班牙语 | Añadir a la cesta |
  | `fr-FR` | 法语 | Ajouter au panier |
  | `id-ID` | 印度尼西亚语 | Masukkan ke keranjang |
  | `it-IT` | 意大利语 | Aggiungi al carrello |
  | `ja-JP` | 日语 | カートに追加 |
  | `ko-KR` | 韩语 | 카트에 추가하십시오 |
  | `nl-NL` | 荷兰语 | Voeg toe aan winkelkar |
  | `pl-PL` | 波兰语 | Dodaj do koszyka |
  | `pt-PT` | 葡萄牙语 | Adicionar ao carrinho |
  | `ru-RU` | 俄语 | Добавить в корзину |
  | `th-TH` | 泰语 | เพิ่มลงในรถเข็น |
  | `zh-CN` | 中文(简体中文) | 加入购物车 |
  | `zh-TW` | 中文(繁体中文) | 加入購物車 |
  
  ### 3. 实现方式
  
  #### 3.1 目录结构
  
  ```
  extensions/your-extension/
  ├── blocks/
  │   └── your-block.liquid
  ├── assets/
  │   └── your-block.css
  └── locales/
      ├── zh-CN.json
      ├── en-US.json
      ├── ko-KR.json
      ├── ja-JP.json
      └── ... (其他语言文件)
  ```
  
  #### 3.2 语言文件格式
  
  每个语言文件(如 `locales/zh-CN.json`)应包含完整的翻译键值对:
  
  ```json
  {
    "search": {
      "placeholder": "搜索商品...",
      "loading": "搜索中,请稍候...",
      "no_results": "未找到相关商品",
      "error": "搜索失败,请重试",
      "results_count": "找到 {count} 个结果",
      "sort_label": "排序:",
      "sort_default": "默认排序",
      "sort_price_asc": "价格:低到高",
      "sort_price_desc": "价格:高到低",
      "sort_newest": "最新上架",
      "facet_category": "类目",
      "facet_brand": "品牌",
      "facet_tag": "标签",
      "pagination_prev": "上一页",
      "pagination_next": "下一页",
      "out_of_stock": "缺货",
      "unknown_product": "未知商品",
      "enter_keyword": "请输入搜索关键词",
      "searching": "搜索中..."
    }
  }
  ```
  
  #### 3.3 Liquid 模板中的使用
  
  ##### 3.3.1 获取店铺语言
  
  ```liquid
  {% comment %} 获取店铺当前语言 {% endcomment %}
  {{ shop.locale }}
  ```
  
  ##### 3.3.2 使用翻译过滤器
  
  在 HTML 属性中使用:
  
  ```liquid
  <input 
    type="text" 
    placeholder="{{ 'search.placeholder' | t }}"
    aria-label="{{ 'search.placeholder' | t }}"
  />
  ```
  
  在 HTML 内容中使用:
  
  ```liquid
  <label>{{ 'search.sort_label' | t }}</label>
  ```
  
  `<option>` 标签中使用(需要配合 JavaScript 确保正确显示):
  
  ```liquid
  <select id="sort-select">
    <option value="" data-translation-key="sortDefault">
      {{ 'search.sort_default' | t }}
    </option>
    <option value="min_price:asc" data-translation-key="sortPriceAsc">
      {{ 'search.sort_price_asc' | t }}
    </option>
  </select>
  ```
  
  #### 3.4 JavaScript 中的多语言支持
  
  ##### 3.4.1 将翻译传递给 JavaScript
  
  `<script>` 标签中,通过 Liquid 将翻译文本传递给 JavaScript:
  
  ```javascript
  <script>
  (function() {
    // 获取店铺语言
    const shopLocale = "{{ shop.locale }}" || "zh-CN";
    
    // 翻译文本(通过 Liquid 的 t 过滤器获取)
    const translations = {
      placeholder: "{{ 'search.placeholder' | t }}",
      loading: "{{ 'search.loading' | t }}",
      noResults: "{{ 'search.no_results' | t }}",
      error: "{{ 'search.error' | t }}",
      resultsCount: "{{ 'search.results_count' | t }}",
      // ... 更多翻译键
    };
    
    // 工具函数:替换占位符
    function translate(key, params) {
      let text = translations[key] || key;
      if (params) {
        Object.keys(params).forEach(param => {
          text = text.replace(`{${param}}`, params[param]);
        });
      }
      return text;
    }
    
    // 使用翻译
    function showResults(total) {
      const message = translate('resultsCount', { count: total });
      document.getElementById('status').textContent = message;
    }
  })();
  </script>
  ```
  
  ##### 3.4.2 处理动态内容翻译
  
  对于动态生成的内容(如排序下拉框),需要确保翻译正确显示:
  
  ```javascript
  // 初始化排序下拉框文本(确保翻译正确显示)
  function initSortSelect() {
    const sortSelect = document.getElementById('sort-select');
    if (sortSelect) {
      // 遍历所有选项,使用 JavaScript 设置文本
      Array.from(sortSelect.options).forEach((option, index) => {
        const translationKey = option.getAttribute('data-translation-key');
        if (translationKey && translations[translationKey]) {
          option.text = translations[translationKey];
        }
      });
    }
  }
  
  // 在 DOM 加载完成后初始化
  document.addEventListener('DOMContentLoaded', function() {
    initSortSelect();
  });
  ```
  
  ##### 3.4.3 翻译回退机制(可选)
  
  如果 Liquid 翻译失败,可以实现 JavaScript 回退机制:
  
  ```javascript
  function checkAndLoadTranslations() {
    const firstTranslation = translations.placeholder;
    const isLikelyEnglish = firstTranslation === 'Search products...' || 
                           firstTranslation === 'search.placeholder' ||
                           firstTranslation === '';
    
    // 如果检测到可能是英语,且店铺语言不是英语,尝试加载正确的翻译
    if (isLikelyEnglish && shopLocale && shopLocale !== 'en-US' && shopLocale !== 'en') {
      const localeFile = `/extensions/your-extension/locales/${shopLocale}.json`;
      fetch(localeFile)
        .then(response => response.json())
        .then(data => {
          if (data && data.search) {
            // 更新翻译对象
            translations = {
              ...translations,
              placeholder: data.search.placeholder || translations.placeholder,
              loading: data.search.loading || translations.loading,
              // ... 更多翻译
            };
            
            // 重新初始化UI
            initSortSelect();
          }
        })
        .catch(error => {
          console.warn('Failed to load translation file:', error);
        });
    }
  }
  ```
  
  ### 4. 最佳实践
  
  1. **完整性**:确保所有语言文件包含相同的翻译键,避免缺失导致显示键名
  2. **占位符**:使用 `{count}` 等占位符支持动态内容,如 `"找到 {count} 个结果"`
  3. **命名规范**:使用有意义的键名,如 `search.placeholder` 而不是 `text1`
  4. **测试**:在不同语言环境下测试应用,确保所有文本正确显示
  5. **回退机制**:为关键翻译提供默认值,避免显示空白或键名
  
  ---
  
  ## 前端优化与 Lessjs 集成
  
  ### 1. Lessjs 简介
  
  **Lessjs** 是 Shoplazza(店匠)官方提供的前端组件库,基于 **Web Components** 开发。它提供了:
  
  - 高性能页面构建能力
  - 现成的组件(轮播图、视频、瀑布流等)
  - 自定义组件支持
  - 与 Shoplazza 主题的深度集成
  
  **官方文档**
  - [Lessjs 简介](https://lessjs.shoplazza.com/latest/docs/introduction/)
  - [自定义组件](https://lessjs.shoplazza.com/latest/docs/custom-component/)
  
  ### 2. 引入 Lessjs
  
  #### 2.1 通过 CDN 引入
  
  在 Liquid 模板的 `<head>` 标签中引入 Lessjs:
  
  ```liquid
  <head>
    <meta charset="UTF-8">
    <title>Your App</title>
    
    {% comment %} 引入 Lessjs {% endcomment %}
    <script async crossorigin="anonymous" src="https://static.staticdj.com/cuttlefish/v1/spz.min.js"></script>
    
    {% comment %} Lessjs 加载前的样式(必须){% endcomment %}
    <style>
      body {
        -webkit-animation: -spz-start 8s steps(1,end) 0s 1 normal both;
        -moz-animation: -spz-start 8s steps(1,end) 0s 1 normal both;
        -ms-animation: -spz-start 8s steps(1,end) 0s 1 normal both;
        animation: -spz-start 8s steps(1,end) 0s 1 normal both;
      }
      @-webkit-keyframes -spz-start {
        from { visibility: hidden; }
        to { visibility: visible; }
      }
      @-moz-keyframes -spz-start {
        from { visibility: hidden; }
        to { visibility: visible; }
      }
      @-ms-keyframes -spz-start {
        from { visibility: hidden; }
        to { visibility: visible; }
      }
      @-o-keyframes -spz-start {
        from { visibility: hidden; }
        to { visibility: visible; }
      }
      @keyframes -spz-start {
        from { visibility: hidden; }
        to { visibility: visible; }
      }
    </style>
    <noscript>
      <style>
        body {
          -webkit-animation: none;
          -moz-animation: none;
          -ms-animation: none;
          animation: none;
        }
      </style>
    </noscript>
  </head>
  ```
  
  **重要要求**
  1. 脚本必须带有 `async` 属性
  2. 必须包含上述 CSS 样式,用于在 Lessjs 加载前隐藏页面内容
  3. 必须包含 `<noscript>` 标签的样式回退
  
  #### 2.2 HTML lang 属性
  
  确保 HTML 的 `lang` 属性设置为支持的语言代码之一(见[支持的语言列表](#2-支持的语言列表)):
  
  ```liquid
  <html lang="{{ shop.locale | default: 'zh-CN' }}">
  ```
  
  ### 3. 创建自定义 Lessjs 组件
  
  #### 3.1 基础结构
  
  Lessjs 自定义组件基于 ES6 class,继承 `SPZ.BaseElement`
  
  ```liquid
  {% comment %} 使用自定义组件 {% endcomment %}
  <spz-custom-ai-search layout="container"></spz-custom-ai-search>
  
  {% comment %} 定义自定义组件 {% endcomment %}
  <spz-script layout="logic" type="application/javascript">
    class SpzCustomAiSearch extends SPZ.BaseElement {
      constructor(element) {
        super(element);
        // 初始化变量
        this.searchQuery = '';
        this.currentPage = 1;
      }
  
      isLayoutSupported(layout) {
        // 支持的布局类型
        return layout == SPZCore.Layout.CONTAINER;
      }
  
      buildCallback() {
        // 构建回调:组件开始构建时触发,用于初始化
        console.log('Component building...');
      }
  
      mountCallback() {
        // 挂载回调:组件挂载完成后触发,用于数据请求
        console.log('Component mounted, fetching data...');
        this.performSearch();
      }
  
      unmountCallback() {
        // 卸载回调:组件卸载前触发,用于清理
        console.log('Component unmounting, cleaning up...');
        // 移除事件监听器等
      }
  
      layoutCallback() {
        // 布局回调:元素及子节点完全挂载完成,用于 DOM 操作
        console.log('Layout complete, ready for DOM operations');
      }
    }
  
    // 注册自定义组件
    SPZ.defineElement('spz-custom-ai-search', SpzCustomAiSearch);
  </spz-script>
  ```
  
  #### 3.2 命名规范
  
  - **组件名称**:必须以 `spz-custom-` 开头,如 `spz-custom-ai-search`
  - **私有变量/函数**:以 `_` 结尾,如 `this.searchQuery_`
  - **公共 API 方法**:不以 `_` 结尾,如 `this.performSearch()`
  
  #### 3.3 生命周期函数
  
  | 函数名 | 触发时机 | 用途 |
  |--------|---------|------|
  | `constructor` | 组件创建时 | 初始化变量,必须调用 `super(element)` |
  | `isLayoutSupported` | 布局检查时 | 返回组件支持的布局类型 |
  | `buildCallback` | 组件开始构建时 | 初始化工作 |
  | `mountCallback` | 组件挂载完成后 | 数据请求、API 调用 |
  | `unmountCallback` | 组件卸载前 | 清理事件监听、重置变量 |
  | `layoutCallback` | DOM 完全挂载后 | DOM 操作、事件绑定 |
  
  #### 3.4 this 对象
  
  自定义组件中的 `this` 对象提供以下属性和方法:
  
  | 属性/方法 | 说明 |
  |----------|------|
  | `this.element` | 当前组件 DOM 元素 |
  | `this.win` | window 对象 |
  | `this.getViewport()` | 获取 Viewport 服务(参考 [SPZServices 文档](https://lessjs.shoplazza.com/latest/docs/spz-services/)) |
  
  #### 3.5 完整示例:AI 搜索组件
  
  ```liquid
  {% use "ai-search.css" %}
  
  <div id="ai-search" class="ai-search-container">
    <spz-custom-ai-search layout="container">
      <!-- 搜索框 -->
      <form class="search-form" id="search-form">
        <div class="search-input-wrapper">
          <input 
            type="text" 
            id="search-input"
            class="search-input" 
            placeholder="{{ 'search.placeholder' | t }}"
            autocomplete="off"
          />
          <button type="submit" class="search-button" id="search-button">
            <svg class="search-icon" viewBox="0 0 24 24" width="20" height="20">
              <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
            </svg>
          </button>
        </div>
      </form>
      
      <!-- 搜索结果容器 -->
      <div class="search-results-container" id="search-results-container"></div>
    </spz-custom-ai-search>
  </div>
  
  <spz-script layout="logic" type="application/javascript">
    class SpzCustomAiSearch extends SPZ.BaseElement {
      constructor(element) {
        super(element);
        this.apiEndpoint = 'https://saas-ai-api.essa.top/app-api/app/shoplazza/search/';
        this.state = {
          query: '',
          currentPage: 1,
          pageSize: 20
        };
      }
  
      isLayoutSupported(layout) {
        return layout == SPZCore.Layout.CONTAINER;
      }
  
      buildCallback() {
        // 初始化翻译
        this.shopLocale = "{{ shop.locale }}" || "zh-CN";
        this.translations = {
          placeholder: "{{ 'search.placeholder' | t }}",
          loading: "{{ 'search.loading' | t }}",
          noResults: "{{ 'search.no_results' | t }}",
          // ... 更多翻译
        };
      }
  
      mountCallback() {
        // 绑定搜索表单事件
        const form = this.element.querySelector('#search-form');
        if (form) {
          form.addEventListener('submit', (e) => this.handleSearch(e));
        }
      }
  
      async handleSearch(event) {
        event.preventDefault();
        
        const searchInput = this.element.querySelector('#search-input');
        const query = searchInput.value.trim();
        
        if (!query) {
          alert(this.translations.enterKeyword);
          return;
        }
        
        this.state.query = query;
        this.state.currentPage = 1;
        
        await this.performSearch();
      }
  
      async performSearch() {
        const resultsContainer = this.element.querySelector('#search-results-container');
        resultsContainer.innerHTML = `<div class="loading">${this.translations.loading}</div>`;
        
        try {
          const response = await fetch(this.apiEndpoint, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              query: this.state.query,
              size: this.state.pageSize,
              from: (this.state.currentPage - 1) * this.state.pageSize
            })
          });
          
          const data = await response.json();
          this.renderResults(data);
        } catch (error) {
          resultsContainer.innerHTML = `<div class="error">${this.translations.error}</div>`;
        }
      }
  
      renderResults(data) {
        const resultsContainer = this.element.querySelector('#search-results-container');
        // 渲染搜索结果...
      }
  
      unmountCallback() {
        // 清理工作
      }
    }
  
    SPZ.defineElement('spz-custom-ai-search', SpzCustomAiSearch);
  </spz-script>
  ```
  
  ### 4. 使用 Lessjs 的优势
  
  1. **性能优化**:Lessjs 基于 Web Components,提供更好的性能
  2. **主题集成**:与 Shoplazza 主题深度集成,样式更统一
  3. **组件复用**:可以使用 Lessjs 提供的现成组件(轮播图、视频等)
  4. **标准化**:遵循 Shoplazza 的开发规范,减少兼容性问题
  
  ---
  
  ## 与店匠主题的适配
  
  ### 1. 样式适配
  
  #### 1.1 使用主题变量
  
  尽可能使用 Shoplazza 主题提供的 CSS 变量:
  
  ```css
  /* 使用主题颜色变量 */
  .ai-search-container {
    color: var(--color-text, #333);
    background: var(--color-background, #fff);
    border-color: var(--color-border, #e0e0e0);
  }
  
  /* 使用主题字体变量 */
  .ai-search-container {
    font-family: var(--font-body-family, sans-serif);
    font-size: var(--font-body-size, 14px);
  }
  ```
  
  #### 1.2 响应式设计
  
  遵循 Shoplazza 的响应式断点:
  
  ```css
  /* 移动端 */
  @media (max-width: 749px) {
    .ai-search-container {
      padding: 10px;
    }
  }
  
  /* 平板 */
  @media (min-width: 750px) and (max-width: 989px) {
    .ai-search-container {
      padding: 20px;
    }
  }
  
  /* 桌面端 */
  @media (min-width: 990px) {
    .ai-search-container {
      padding: 30px;
    }
  }
  ```
  
  ### 2. 功能适配
  
  #### 2.1 使用 Shoplazza 全局对象
  
  在 JavaScript 中,可以使用 Shoplazza 提供的全局对象:
  
  ```javascript
  // 访问店铺信息
  const shopDomain = window.Shopify?.shop || "{{ shop.domain }}";
  const shopLocale = window.Shopify?.locale || "{{ shop.locale }}";
  
  // 使用 Shoplazza 的工具函数(如果可用)
  if (window.Shoplazza?.utils) {
    // 使用工具函数
  }
  ```
  
  #### 2.2 事件处理
  
  遵循 Shoplazza 的事件处理规范:
  
  ```javascript
  // 使用事件委托,提高性能
  document.addEventListener('click', (e) => {
    if (e.target.closest('.search-button')) {
      handleSearch(e);
    }
  });
  
  // 避免直接操作 DOM,使用 Lessjs 组件生命周期
  ```
  
  ### 3. 性能优化建议
  
  1. **延迟加载**:非关键资源使用 `defer` 或 `async`
  2. **图片优化**:使用 Shoplazza 的图片 CDN 和响应式图片
  3. **代码分割**:将大型 JavaScript 拆分为多个模块
  4. **缓存策略**:合理使用浏览器缓存和 CDN 缓存
  
  ### 4. 测试清单
  
  - [ ] 在不同语言环境下测试多语言显示
  - [ ] 在不同设备上测试响应式布局
  - [ ] 测试与不同 Shoplazza 主题的兼容性
  - [ ] 测试 Lessjs 组件的加载和渲染
  - [ ] 测试 API 调用的错误处理
  - [ ] 测试页面加载性能
  
  ---
  
  ## 总结
  
  1. **多语言支持**:使用 Liquid 的 `t` 过滤器和 `shop.locale`,确保所有语言文件完整
  2. **Lessjs 集成**:使用 Lessjs 自定义组件提升性能和主题适配
  3. **主题适配**:使用主题变量和响应式设计,确保与店匠主题良好集成
  
  遵循以上指南,可以开发出高质量、高性能、多语言支持的 Shoplazza 应用。
  
  ---
  
  ## 参考资源
  
  - [Lessjs 官方文档](https://lessjs.shoplazza.com/latest/docs/introduction/)
  - [Lessjs 自定义组件文档](https://lessjs.shoplazza.com/latest/docs/custom-component/)
  - [Shoplazza 开发者文档](https://www.shoplazza.dev/)