SHOPLAZZA_I18N_AND_FRONTEND_GUIDE.md 18.4 KB

Shoplazza 应用开发指南:多语言支持与前端优化

目录

  1. 多语言支持
  2. 前端优化与 Lessjs 集成
  3. 与店匠主题的适配

多语言支持

1. 概述

Shoplazza 应用支持 16 种语言,通过 Liquid 模板的 t 过滤器和 shop.locale 对象实现自动语言适配。

2. 支持的语言列表

根据 Lessjs 文档,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)应包含完整的翻译键值对:

{
  "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 获取店铺语言
{% comment %} 获取店铺当前语言 {% endcomment %}
{{ shop.locale }}
3.3.2 使用翻译过滤器

在 HTML 属性中使用:

<input 
  type="text" 
  placeholder="{{ 'search.placeholder' | t }}"
  aria-label="{{ 'search.placeholder' | t }}"
/>

在 HTML 内容中使用:

<label>{{ 'search.sort_label' | t }}</label>

<option> 标签中使用(需要配合 JavaScript 确保正确显示):

<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:

<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 处理动态内容翻译

对于动态生成的内容(如排序下拉框),需要确保翻译正确显示:

// 初始化排序下拉框文本(确保翻译正确显示)
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 回退机制:

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 主题的深度集成

官方文档

2. 引入 Lessjs

2.1 通过 CDN 引入

在 Liquid 模板的 <head> 标签中引入 Lessjs:

<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 属性设置为支持的语言代码之一(见支持的语言列表):

<html lang="{{ shop.locale | default: 'zh-CN' }}">

3. 创建自定义 Lessjs 组件

3.1 基础结构

Lessjs 自定义组件基于 ES6 class,继承 SPZ.BaseElement

{% 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 文档

3.5 完整示例:AI 搜索组件

{% 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 变量:

/* 使用主题颜色变量 */
.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 的响应式断点:

/* 移动端 */
@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 提供的全局对象:

// 访问店铺信息
const shopDomain = window.Shopify?.shop || "{{ shop.domain }}";
const shopLocale = window.Shopify?.locale || "{{ shop.locale }}";

// 使用 Shoplazza 的工具函数(如果可用)
if (window.Shoplazza?.utils) {
  // 使用工具函数
}

2.2 事件处理

遵循 Shoplazza 的事件处理规范:

// 使用事件委托,提高性能
document.addEventListener('click', (e) => {
  if (e.target.closest('.search-button')) {
    handleSearch(e);
  }
});

// 避免直接操作 DOM,使用 Lessjs 组件生命周期

3. 性能优化建议

  1. 延迟加载:非关键资源使用 deferasync
  2. 图片优化:使用 Shoplazza 的图片 CDN 和响应式图片
  3. 代码分割:将大型 JavaScript 拆分为多个模块
  4. 缓存策略:合理使用浏览器缓存和 CDN 缓存

4. 测试清单

  • [ ] 在不同语言环境下测试多语言显示
  • [ ] 在不同设备上测试响应式布局
  • [ ] 测试与不同 Shoplazza 主题的兼容性
  • [ ] 测试 Lessjs 组件的加载和渲染
  • [ ] 测试 API 调用的错误处理
  • [ ] 测试页面加载性能

总结

  1. 多语言支持:使用 Liquid 的 t 过滤器和 shop.locale,确保所有语言文件完整
  2. Lessjs 集成:使用 Lessjs 自定义组件提升性能和主题适配
  3. 主题适配:使用主题变量和响应式设计,确保与店匠主题良好集成

遵循以上指南,可以开发出高质量、高性能、多语言支持的 Shoplazza 应用。


参考资源