Kita udah nyinggung “URL sadar locale” berkali-kali sejak 38 Search dan Predictive Search. Bab ini tempat semuanya dijelasin: gimana Shopify ngelayanin pembeli dari banyak negara dan bahasa, dan apa yang harus theme lakuin biar gak ngerusaknya.
Shopify bantu merchant jualan ke banyak negara dan skala internasional, yang berarti banyak merchant butuh jualan dalam banyak bahasa dan mata uang. Fitur payungnya Shopify Markets: merchant bikin market (misalnya “Eropa”, “Amerika Utara”), tiap market punya negara, mata uang, bahasa, dan bisa punya domain atau subfolder sendiri.
Theme nyediain dua hal: pemilih negara dan bahasa (lewat object localization dan {% form 'localization' %}), dan URL yang bener buat tiap konteks. Merchant Shopify Plus juga dapat deteksi negara otomatis lewat geolokasi. Dan merchant bisa nyesuain konten theme per market lewat store contextualization di theme editor, yang datanya tersimpan di markets.json dari 32 Config settings_schema.json, settings_data.json, markets.json.
Object yang Dipakai
Udah dibedah di 13 Objects Global > Object Localization:
localization:available_countries,available_languages,country,language,market.country:name,iso_code,currency,popular?, dan trikimage_urlbuat bendera.shop_locale:name,endonym_name,iso_code,primary,root_url,direction.formdari{% form 'localization' %}.
Pemilih Negara
Buat pembeli milih mata uang lewat negara. Kalau bahasa yang lagi dipilih gak didukung negara baru, bahasanya otomatis diganti ke default negara itu.
Tiga syarat:
- Ada di dalam
{% form 'localization' %}. - Ada input
name="country_code"yang nilainya kode negara yang dipilih. - Cuma ditampilin kalau
available_countries.size > 1, gak ada gunanya pemilih dengan satu opsi.
Contoh tombol plus popover berisi tiap negara:
{% if localization.available_countries.size > 1 %}
<localization-form>
{% form 'localization' %}
<div class="disclosure">
<button type="button" class="disclosure__button" aria-expanded="false" aria-controls="CountryList">
{{ localization.country.name }} ({{ localization.country.currency.iso_code }} {{ localization.country.currency.symbol }})
<svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-caret" viewBox="0 0 10 6">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.354.646a.5.5 0 00-.708 0L5 4.293 1.354.646a.5.5 0 00-.708.708l4 4a.5.5 0 00.708 0l4-4a.5.5 0 000-.708z" fill="currentColor">
</svg>
</button>
<ul id="CountryList" role="list" class="disclosure__list" hidden>
{% for country in localization.available_countries %}
<li class="disclosure__item" tabindex="-1">
<a href="#"{% if country.iso_code == localization.country.iso_code %} aria-current="true"{% endif %} data-value="{{ country.iso_code }}">
{{ country.name }} ({{ country.currency.iso_code }} {{ country.currency.symbol }})
</a>
</li>
{% endfor %}
</ul>
<input type="hidden" name="country_code" value="{{ localization.country.iso_code }}" />
</div>
{% endform %}
</localization-form>
{% endif %}Perhatiin formatnya: nama negara lengkap + kode mata uang + simbol, “United States (USD $)”, sesuai pedoman UX di bawah. aria-current="true" nandain yang lagi aktif, data-value dibaca JavaScript. Contoh Dawn di sections/footer.liquid.
Pemilih Bahasa
Sama persis, dengan input name="language_code", data dari available_languages dan language, cuma kalau lebih dari satu bahasa:
{% if localization.available_languages.size > 1 %}
<localization-form>
{% form 'localization' %}
<div class="disclosure">
<button type="button" class="disclosure__button" aria-expanded="false" aria-controls="LanguageList">
{{ localization.language.endonym_name | capitalize }}
<svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-caret" viewBox="0 0 10 6">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.354.646a.5.5 0 00-.708 0L5 4.293 1.354.646a.5.5 0 00-.708.708l4 4a.5.5 0 00.708 0l4-4a.5.5 0 000-.708z" fill="currentColor">
</svg>
</button>
<ul id="LanguageList" role="list" class="disclosure__list" hidden>
{% for language in localization.available_languages %}
<li class="disclosure__item" tabindex="-1">
<a href="#"{% if language.iso_code == localization.language.iso_code %} aria-current="true"{% endif %} hreflang="{{ language.iso_code }}" lang="{{ language.iso_code }}" data-value="{{ language.iso_code }}">
{{ language.endonym_name | capitalize }}
</a>
</li>
{% endfor %}
</ul>
<input type="hidden" name="language_code" value="{{ localization.language.iso_code }}" />
</div>
{% endform %}
</localization-form>
{% endif %}Dua detail penting:
endonym_name, nama bahasa dalam bahasa itu sendiri (“Deutsch”, bukan “German”). Pembeli Jerman yang nyasar di toko berbahasa Inggris tetap bisa nemuin bahasanya.hreflangdanlangdi tiap link, biar screen reader ngucapin nama bahasanya dengan pelafalan yang bener.
❌ pembeli asing gak ngenalin bahasanya
{{ language.name }}✅ nama dalam bahasanya sendiri
{{ language.endonym_name | capitalize }}JavaScript Buat Submit Form
Karena selector-nya custom element dan gak ada tombol submit di form, submit-nya lewat JavaScript:
class LocalizationForm extends HTMLElement {
constructor() {
super();
this.elements = {
input: this.querySelector('input[name="language_code"], input[name="country_code"]'),
button: this.querySelector('button'),
panel: this.querySelector('ul'),
};
this.elements.button.addEventListener('click', this.openSelector.bind(this));
this.elements.button.addEventListener('focusout', this.closeSelector.bind(this));
this.addEventListener('keyup', this.onContainerKeyUp.bind(this));
this.querySelectorAll('a').forEach(item => item.addEventListener('click', this.onItemClick.bind(this)));
}
hidePanel() {
this.elements.button.setAttribute('aria-expanded', 'false');
this.elements.panel.setAttribute('hidden', true);
}
onContainerKeyUp(event) {
if (event.code.toUpperCase() !== 'ESCAPE') return;
this.hidePanel();
this.elements.button.focus();
}
onItemClick(event) {
event.preventDefault();
const form = this.querySelector('form');
this.elements.input.value = event.currentTarget.dataset.value;
if (form) form.submit();
}
openSelector() {
this.elements.button.focus();
this.elements.panel.toggleAttribute('hidden');
this.elements.button.setAttribute('aria-expanded', (this.elements.button.getAttribute('aria-expanded') === 'false').toString());
}
closeSelector(event) {
const shouldClose = event.relatedTarget && event.relatedTarget.nodeName === 'BUTTON';
if (event.relatedTarget === null || shouldClose) {
this.hidePanel();
}
}
}
customElements.define('localization-form', LocalizationForm);Alurnya: klik tombol → toggle hidden di <ul> dan aria-expanded → klik item → isi hidden input dari data-value → form.submit() → Shopify ngeset cookie localization dan redirect ke URL konteks yang baru. Escape nutup panel dan balikin fokus ke tombol. Satu class dipakai buat dua selector karena struktur markup-nya sama.
Catatan Shopify: custom element dengan JavaScript itu default yang disarankan buat aksesibilitas, tapi sediain fallback kalau JavaScript mati, misalnya <noscript> berisi <select name="country_code"> dengan tombol submit biasa. (Ini beda dari pemilih varian di 39 Produk Variants dan High-variant Products yang fallback-nya udah gak wajib.)
URL Sadar Locale
Ini bagian yang paling sering bikin bug di theme multi bahasa. Toko dapat URL dinamis waktu jualan internasional:
- Waktu locale tambahan dipublikasi di domain, Shopify otomatis bikin path buat itu:
shop.comdengan locale Prancis →shop.com/fr. - Waktu merchant bikin market baru, Shopify bikin path buat market itu: market utama AS berbahasa Inggris di
shop.com, market Kanada →shop.com/en-ca.
Jadi struktur URL bisa berubah kapan aja tergantung setting merchant. Kalau kamu hardcode /cart di link atau request Ajax, pengunjung yang lagi di /fr atau /en-ca dilempar balik ke default domain: bahasa dan mata uangnya berubah tanpa mereka minta. Ini bug yang nyata dan sering.
1. Di Liquid
Dua cara:
- Atribut
urlobject manapun (product.url,collection.url,link.url), yang udah nyertain prefix locale. - Object
routesdari 13 Objects Global > routes.
<a href="{{ routes.cart_url }}">{{ 'templates.cart.go_to_cart' | t }}</a>Hasilnya /cart di locale utama, /fr/cart di Prancis.
2. Di JavaScript
Bangun URL dari window.Shopify.routes.root, root dinamis yang selalu diakhiri /. Di market Kanada nilainya "/en-ca/":
fetch(window.Shopify.routes.root + 'cart.js')
.then(response => response.json())
.then(data => showCartContents(data));Request ke /en-ca/cart.js ngasih harga market Kanada, request ke /cart.js ngasih harga default. Ini kenapa semua contoh Ajax di 38 Search dan Predictive Search, 41 Produk Recommendations, Gift Card, dan Bundles, dan 51 Ajax API dan Section Rendering API pakai window.Shopify.routes.root.
❌ pembeli di /fr dilempar ke bahasa default
fetch('/cart/add.js', { ... })✅ tetap di konteksnya
fetch(window.Shopify.routes.root + 'cart/add.js', { ... })SEO Multi Bahasa
Selain pemilih, mesin pencari harus ngembaliin konten dan harga yang terlokalisasi. Dua hal:
- hreflang: Shopify otomatis nyertain tag hreflang lewat
content_for_header, jadi kamu gak perlu ngerakit sendiri. Detailnya di 48 SEO Metadata, hreflang, robots.txt. - Structured data: waktu output structured data produk,
priceCurrencyharus daricart.currency, bukan mata uang toko, biar Google nampilin harga di mata uang market itu:
priceCurrency: {{ cart.currency.iso_code }},Shopify ngingetin: info SEO di sini cuma umum, kalau ada masalah hasil pencarian dan mata uang, konsultasi ke ahli SEO.
Deteksi Localization Optimal
Kalau toko punya banyak versi lokal, tiap pengunjung sebaiknya dapat versi yang paling cocok. Mesin pencari bisa pre-localize, dan Shopify (Plus) bisa redirect otomatis, tapi pengunjung tetap bisa nyasar ke konteks market yang bukan lokasinya. Ajax API punya endpoint buat nyaranin localization yang lebih baik.
Prinsip pentingnya: jangan maksa pengunjung ke konteks yang terdeteksi. GeoIP gak sempurna (VPN), dan kadang orang emang mau kirim ke negara lain. Checkout Shopify tetap mastiin harga dan ketersediaan sesuai alamat pengiriman yang akhirnya dipilih. Jadi yang tepat: banner saran (“Sepertinya kamu di Kanada, mau belanja dalam CAD?”), bukan redirect paksa.
1. Query Saran
Endpoint browsing_context_suggestions.json pakai setting browser dan IP pengunjung. Bisa minta saran negara, bahasa, atau dua-duanya, dan ngecualiin nilai sekarang biar gak nyaranin yang udah aktif:
await fetch(
window.Shopify.routes.root
+ 'browsing_context_suggestions.json'
+ '?country[enabled]=true'
+ `&country[exclude]=${window.Shopify.country}`
+ '&language[enabled]=true'
+ `&language[exclude]=${window.Shopify.language}`
).then(r => r.json());window.Shopify.country dan window.Shopify.language itu konteks sekarang. Responsnya:
{
"detected_values": {
"country": {
"handle": "CA",
"name": "Canada"
}
},
"suggestions": [
{
"parts": {
"country": {
"handle": "CA",
"name": "Canada",
"confidence": 1,
"options": {
"US": "États-Unis",
"JP": "Japon",
"CA": "Canada"
}
},
"language": {
"handle": "fr",
"name": "français",
"confidence": 1,
"options": {
"en": "anglais",
"es": "espagnol",
"fr": "français",
"ja": "japonais"
}
}
},
"confidence": 1
}
]
}detected_values yang terdeteksi, suggestions saran lengkap dengan confidence (0–1) dan options (semua pilihan, namanya dalam bahasa yang disarankan, makanya “États-Unis”). Kalau gak ada saran (pengunjung udah di konteks optimal), suggestions kosong.
2. Terapin Saran
Saran diterapin dengan submit form localization. Versi minimal di JavaScript, bikin form tersembunyi dan submit:
function updateLocalization({country, language}) {
const formId = crypto.randomUUID();
const formHtml = `
<form id="${formId}" action="/localization" method="POST" hidden>
<input name="_method" value="PUT" />
<input name="country_code" value="${country}" />
<input name="language_code" value="${language}" />
</form>
`;
document.body.insertAdjacentHTML("beforeend", formHtml);
document.getElementById(formId).submit();
}
updateLocalization({country: "CA", language: "fr"});Ini ngungkap apa yang {% form 'localization' %} bikin di balik layar: POST ke /localization dengan _method=PUT, country_code, dan language_code. Panggil ini dari tombol “Ya” di banner saran.
Pedoman UX Selector
1. Penempatan
Empat lokasi utama, dan kalau ada dua selector, selalu ditaruh bareng:
- Footer: di atas konten sub-footer, terpisah dari link navigasi footer.
- Dekat link akun pelanggan: kalau link akun dekat cart, selector juga dekat cart, dengan gaya yang mirip link akun.
- Dekat cart: kalau link akun jauh dari cart, taruh selector dekat cart biar kelihatan. Selector harus melengkapi, bukan bersaing sama cart: kalau cart di kanan, selector di kiri cart biar cart tetap gampang dijangkau.
- Di dalam navigation drawer: perlakukan sebagai link utilitas/footer, bukan link navigasi.
2. Styling
- Pakai styling dropdown theme sebanyak mungkin: tipografi, warna, tinggi, state hover.
- Boleh bikin komponen kustom (bukan
<select>native), tapi sebagai popover, bukan modal, karena modal berlebihan buat beberapa opsi. - State hover, focus, selected, pressed ngikutin pola link theme.
- Lebar menu ngikutin isinya biar string panjang gak wrap, tapi gak melewati viewport.
- Format: nama negara lengkap + kode mata uang + simbol: “United States (USD $)”, “United Kingdom (GBP £)”.
Ringkasan
- Shopify Markets = market dengan negara, mata uang, bahasa, dan URL sendiri. Theme nyediain pemilih dan URL yang bener, Plus dapat geolokasi otomatis, konten per market lewat store contextualization.
- Pemilih negara:
{% form 'localization' %}+ inputcountry_code, darilocalization.available_countries, cuma kalau > 1, format “Nama (KODE simbol)”. - Pemilih bahasa: input
language_code, dariavailable_languages,endonym_name,hreflangdanlangdi link, cuma kalau > 1. - JavaScript custom element: toggle panel, isi hidden input dari
data-value,form.submit(), Escape nutup, plus fallback tanpa JavaScript. - URL sadar locale: Shopify bikin
/fr,/en-caotomatis. Jangan hardcode, pakaiobject.url/routesdi Liquid danwindow.Shopify.routes.root(selalu diakhiri/) di JavaScript. - SEO: hreflang otomatis lewat
content_for_header, structured data pakaipriceCurrencydaricart.currency.iso_code. - Deteksi optimal:
browsing_context_suggestions.jsondengancountry[enabled]/[exclude]danlanguage[enabled]/[exclude], terapin lewat form POST/localizationdengan_method=PUT. Saranin, jangan paksa. - UX: taruh di footer (atas sub-footer), dekat akun, atau kiri cart, atau sebagai utilitas di drawer; dua selector selalu bareng; popover bergaya dropdown theme, lebar ngikutin isi.
Bab selanjutnya kita bahas SEO: meta title dan description dari object page_title dan page_description, canonical URL, hreflang otomatis, Open Graph, structured data, metafield seo.hidden, dan kustomisasi robots.txt.