# 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/)

