SHOPLAZZA_I18N_AND_FRONTEND_GUIDE.md
18.4 KB
Shoplazza 应用开发指南:多语言支持与前端优化
目录
多语言支持
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. 最佳实践
- 完整性:确保所有语言文件包含相同的翻译键,避免缺失导致显示键名
- 占位符:使用
{count}等占位符支持动态内容,如"找到 {count} 个结果" - 命名规范:使用有意义的键名,如
search.placeholder而不是text1 - 测试:在不同语言环境下测试应用,确保所有文本正确显示
- 回退机制:为关键翻译提供默认值,避免显示空白或键名
前端优化与 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>
重要要求:
- 脚本必须带有
async属性 - 必须包含上述 CSS 样式,用于在 Lessjs 加载前隐藏页面内容
- 必须包含
<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 的优势
- 性能优化:Lessjs 基于 Web Components,提供更好的性能
- 主题集成:与 Shoplazza 主题深度集成,样式更统一
- 组件复用:可以使用 Lessjs 提供的现成组件(轮播图、视频等)
- 标准化:遵循 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. 性能优化建议
- 延迟加载:非关键资源使用
defer或async - 图片优化:使用 Shoplazza 的图片 CDN 和响应式图片
- 代码分割:将大型 JavaScript 拆分为多个模块
- 缓存策略:合理使用浏览器缓存和 CDN 缓存
4. 测试清单
- [ ] 在不同语言环境下测试多语言显示
- [ ] 在不同设备上测试响应式布局
- [ ] 测试与不同 Shoplazza 主题的兼容性
- [ ] 测试 Lessjs 组件的加载和渲染
- [ ] 测试 API 调用的错误处理
- [ ] 测试页面加载性能
总结
- 多语言支持:使用 Liquid 的
t过滤器和shop.locale,确保所有语言文件完整 - Lessjs 集成:使用 Lessjs 自定义组件提升性能和主题适配
- 主题适配:使用主题变量和响应式设计,确保与店匠主题良好集成
遵循以上指南,可以开发出高质量、高性能、多语言支持的 Shoplazza 应用。