@spree/docs 0.1.181 → 0.1.183
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-reference/seller-api/errors.md +1 -1
- package/dist/api-reference/store.yaml +78 -91
- package/dist/developer/core-concepts/addresses.md +106 -198
- package/dist/developer/core-concepts/architecture.md +97 -126
- package/dist/developer/core-concepts/calculators.md +75 -252
- package/dist/developer/core-concepts/carts.md +1 -1
- package/dist/developer/core-concepts/channels.md +0 -4
- package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
- package/dist/developer/core-concepts/customers.md +0 -3
- package/dist/developer/core-concepts/discounts.md +133 -0
- package/dist/developer/core-concepts/events.md +83 -576
- package/dist/developer/core-concepts/fees.md +144 -0
- package/dist/developer/core-concepts/imports-exports.md +105 -679
- package/dist/developer/core-concepts/inventory.md +114 -248
- package/dist/developer/core-concepts/markets.md +9 -12
- package/dist/developer/core-concepts/media.md +9 -11
- package/dist/developer/core-concepts/metafields.md +123 -200
- package/dist/developer/core-concepts/order-totals.md +110 -0
- package/dist/developer/core-concepts/orders.md +1 -1
- package/dist/developer/core-concepts/payments.md +11 -14
- package/dist/developer/core-concepts/pricing.md +11 -13
- package/dist/developer/core-concepts/products.md +173 -19
- package/dist/developer/core-concepts/promotions.md +12 -11
- package/dist/developer/core-concepts/search-filtering.md +2 -4
- package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
- package/dist/developer/core-concepts/taxes.md +125 -113
- package/dist/developer/core-concepts/translations.md +61 -68
- package/dist/developer/core-concepts/webhooks.md +25 -59
- package/dist/developer/how-to/custom-promotion.md +3 -3
- package/package.json +1 -1
- package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
|
@@ -1,163 +1,175 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Taxes
|
|
3
|
-
description:
|
|
3
|
+
description: How Spree works out tax — tax categories and rates, inclusive versus added-on pricing, and connecting an external tax service.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Tax is the part of commerce most likely to differ from what you assumed. A US shopper expects `$17.99` on the shelf and a little more at the till. A German shopper expects `€17.99` to be the whole story, VAT already inside it. Both are correct, and a store selling to both has to do both.
|
|
9
|
+
|
|
10
|
+
Spree handles this by keeping three things separate:
|
|
11
|
+
|
|
12
|
+
- **What is being sold** — a [tax category](#tax-categories) groups products taxed the same way
|
|
13
|
+
- **Where the customer is** — their [market](markets.md) and shipping address
|
|
14
|
+
- **Who works out the number** — Spree's own rates, or an external tax service
|
|
15
|
+
|
|
16
|
+
The result of all this is a set of **tax lines** on the order — one per charge, recording what was taxed and at what rate. This page is about how they get their numbers.
|
|
9
17
|
|
|
10
18
|
```mermaid
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
TaxCategory {
|
|
19
|
-
string name
|
|
20
|
-
boolean is_default
|
|
21
|
-
string tax_code
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
TaxRate {
|
|
25
|
-
string name
|
|
26
|
-
decimal amount
|
|
27
|
-
boolean included_in_price
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
Zone {
|
|
31
|
-
string name
|
|
32
|
-
string kind
|
|
33
|
-
}
|
|
19
|
+
flowchart LR
|
|
20
|
+
Product -->|"has a"| TaxCategory
|
|
21
|
+
Address -->|"determines"| Market
|
|
22
|
+
Market -->|"selects"| Provider["Tax provider"]
|
|
23
|
+
TaxCategory --> Provider
|
|
24
|
+
Provider -->|"writes"| TaxLine["Tax lines on the order"]
|
|
34
25
|
```
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
- **Tax Category** groups products for tax purposes (e.g., "Clothing", "Food", "Digital")
|
|
38
|
-
- **Tax Rate** defines the percentage and rules for a Tax Category within a [Zone](addresses.md#zones)
|
|
39
|
-
- **Zone** defines geographic regions (countries or states) where taxes apply
|
|
40
|
-
- **[Adjustments](taxes-discounts-fees.md)** are created on orders/line items to apply taxes
|
|
27
|
+
## Tax categories
|
|
41
28
|
|
|
42
|
-
|
|
29
|
+
A tax category is how you say "these things are taxed alike". Children's clothing, books and hot food are treated differently from general goods in many countries, and a tax category is where that distinction lives.
|
|
43
30
|
|
|
44
|
-
|
|
31
|
+
Every product carries one. If you don't choose, the default is used.
|
|
45
32
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
| Field | Description | Example |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `name` | What it's called | `Standard`, `Reduced`, `Digital` |
|
|
36
|
+
| `is_default` | Used by products that don't pick one | `true` |
|
|
37
|
+
| `tax_code` | The code your tax service knows it by | `1257L` |
|
|
49
38
|
|
|
50
|
-
|
|
39
|
+
```typescript Admin SDK
|
|
40
|
+
const reduced = await adminClient.taxCategories.create({
|
|
41
|
+
name: 'Reduced rate',
|
|
42
|
+
tax_code: '1257L',
|
|
43
|
+
})
|
|
44
|
+
```
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|-----------|-------------|---------|
|
|
54
|
-
| `name` | Category name | `Clothing` |
|
|
55
|
-
| `is_default` | Whether this is the default category | `true` |
|
|
56
|
-
| `tax_code` | Code from your tax provider (e.g., Stripe, Avalara) | `1257L` |
|
|
46
|
+
`tax_code` matters once you connect an external service — it is how you tell that service what kind of thing this is, in the vocabulary it already uses.
|
|
57
47
|
|
|
58
|
-
|
|
48
|
+
## Tax rates
|
|
59
49
|
|
|
50
|
+
A tax rate is the number itself, plus where it applies and whether it's already in the price.
|
|
60
51
|
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
| Field | Description | Example |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `name` | What appears on the invoice | `VAT 20%` |
|
|
55
|
+
| `amount` | The rate as a decimal | `0.2` for 20% |
|
|
56
|
+
| `included_in_price` | Whether the price already contains it | `true` in the EU |
|
|
57
|
+
| `country_code` / `state_code` | Where it applies | `DE`, or `US` + `CA` |
|
|
58
|
+
| `tax_category` | What it applies to | `Standard` |
|
|
63
59
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
```typescript Admin SDK
|
|
61
|
+
const { data: rates } = await adminClient.taxRates.list()
|
|
62
|
+
|
|
63
|
+
await adminClient.taxRates.create({
|
|
64
|
+
name: 'VAT 20%',
|
|
65
|
+
amount: '0.2',
|
|
66
|
+
included_in_price: true,
|
|
67
|
+
country_code: 'GB',
|
|
68
|
+
tax_category_id: 'taxcat_xxx',
|
|
67
69
|
})
|
|
70
|
+
```
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
name: 'Clothing',
|
|
71
|
-
tax_code: '1257L',
|
|
72
|
-
is_default: true,
|
|
73
|
-
})
|
|
72
|
+
Rates name their country and state by **ISO code**, not by an ID — `DE`, `US` + `CA`. There's no separate region record to create first.
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
await client.taxCategories.delete('taxcat_xxx')
|
|
77
|
-
```
|
|
74
|
+
## Included or added on
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
spree api post /tax_categories -d '{"name": "Clothing", "tax_code": "1257L", "is_default": true}'
|
|
81
|
-
spree api get /tax_categories
|
|
82
|
-
```
|
|
76
|
+
`included_in_price` is the single most consequential setting here, so it's worth being precise about what it does.
|
|
83
77
|
|
|
78
|
+
**Added on (US style):**
|
|
84
79
|
|
|
85
|
-
|
|
80
|
+
The shelf price is what the product costs. Tax is worked out at checkout, once the address is known, and added.
|
|
86
81
|
|
|
87
|
-
|
|
82
|
+
A $17.99 item at 5%:
|
|
88
83
|
|
|
89
|
-
|
|
|
90
|
-
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
| `zone` | Geographic region where this rate applies | `US` |
|
|
95
|
-
| `tax_category` | Products this rate applies to | `Clothing` |
|
|
84
|
+
| | |
|
|
85
|
+
|---|---|
|
|
86
|
+
| Price shown | **$17.99** |
|
|
87
|
+
| Tax added | $0.90 |
|
|
88
|
+
| Customer pays | **$18.89** |
|
|
96
89
|
|
|
97
|
-
|
|
90
|
+
Set `included_in_price: false`.
|
|
98
91
|
|
|
99
|
-
|
|
92
|
+
**Included (EU style):**
|
|
100
93
|
|
|
101
|
-
|
|
94
|
+
The shelf price is the whole price. The tax inside it is shown for information — the customer pays the number they saw.
|
|
102
95
|
|
|
103
|
-
|
|
104
|
-
- Displayed price: **$17.99**
|
|
105
|
-
- Tax at checkout: **$0.90**
|
|
106
|
-
- Order total: **$18.89**
|
|
96
|
+
A £17.99 item at 20% VAT:
|
|
107
97
|
|
|
108
|
-
|
|
98
|
+
| | |
|
|
99
|
+
|---|---|
|
|
100
|
+
| Price shown | **£17.99** |
|
|
101
|
+
| VAT inside it | £3.00 |
|
|
102
|
+
| Customer pays | **£17.99** |
|
|
109
103
|
|
|
110
|
-
|
|
104
|
+
Set `included_in_price: true`.
|
|
111
105
|
|
|
112
|
-
**Example:** A £17.99 item with 5% VAT included:
|
|
113
|
-
- Displayed price: **£17.99** (includes £0.86 VAT)
|
|
114
|
-
- If shipped outside VAT zone: price reduces to **£17.13**
|
|
115
106
|
|
|
116
|
-
> **
|
|
107
|
+
> **WARNING:** When building an order summary, add `additional_tax_total` only. `included_tax_total` is **already inside** the item prices — adding it counts the tax twice. This is the most common bug in a European storefront.
|
|
117
108
|
|
|
118
|
-
|
|
109
|
+
Because prices are shown before anyone knows where the shopper lives, each [Market](markets.md) declares whether its prices are quoted with tax included. That's what lets a catalogue page show honest prices to a German visitor and a US visitor at the same time.
|
|
119
110
|
|
|
120
|
-
|
|
111
|
+
## Tax lines
|
|
121
112
|
|
|
122
|
-
|
|
113
|
+
Whatever works out the tax, the record is the same: a tax line per charge, saying what was taxed and how.
|
|
123
114
|
|
|
124
|
-
|
|
115
|
+
| Attribute | Description |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `label` | What the customer sees — `VAT 20%` |
|
|
118
|
+
| `rate` | The rate applied — `"0.2"` |
|
|
119
|
+
| `included` | Whether the tax was already inside the displayed price |
|
|
120
|
+
| `amount` / `display_amount` | The tax charged |
|
|
121
|
+
| `line_item_id` / `fulfillment_id` / `fee_id` | What was taxed — exactly one is set |
|
|
125
122
|
|
|
126
|
-
|
|
123
|
+
```typescript Admin SDK
|
|
124
|
+
const { data: taxLines } = await adminClient.orders.taxLines.list('or_xxx')
|
|
125
|
+
```
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
2. Spree finds matching tax rates for that zone and the product's tax category
|
|
130
|
-
3. Tax [Adjustments](taxes-discounts-fees.md) are created on line items
|
|
131
|
-
4. The order total is updated
|
|
127
|
+
Tax lines attach to a line item, a [fulfillment](fulfillments.md), or a [fee](fees.md) — so tax on goods and tax on delivery stay separately visible, which is what a tax return needs.
|
|
132
128
|
|
|
133
|
-
|
|
129
|
+
Each line also keeps its own copy of the rate and label. If someone edits that tax rate next year, the order still says what the customer was actually charged.
|
|
134
130
|
|
|
135
|
-
|
|
131
|
+
You never create tax lines yourself; they're written whenever the total is worked out.
|
|
132
|
+
|
|
133
|
+
## When tax is worked out
|
|
134
|
+
|
|
135
|
+
Tax is recalculated whenever the answer could have changed — an item added, an address entered, a delivery option chosen. Every one of those requests returns the updated cart, so a storefront never needs a separate "refresh the tax" call.
|
|
136
|
+
|
|
137
|
+
**Step 1: Before an address is known**
|
|
138
|
+
|
|
139
|
+
Prices are shown using the market's tax treatment. For an inclusive market the displayed price is the real one; for an added-on market tax simply hasn't been worked out yet.
|
|
140
|
+
|
|
141
|
+
**Step 2: Once the shipping address is entered**
|
|
142
|
+
|
|
143
|
+
The address decides the real rate. Tax lines are written against each item, the delivery charge, and any taxable fee.
|
|
144
|
+
|
|
145
|
+
**Step 3: At completion**
|
|
146
|
+
|
|
147
|
+
Whatever the tax is at that moment is what's charged, and the order keeps it. A rate change next month does not rewrite an order from last month.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
## Connecting a tax service
|
|
151
|
+
|
|
152
|
+
Spree's built-in rate tables are fine when the rules are simple — one country, or a handful of clearly-defined rates. They become a liability where the rules are not: US sales tax varies by city and changes constantly, and cross-border EU VAT has thresholds that shift with your turnover.
|
|
153
|
+
|
|
154
|
+
For those, connect a real tax service. **The provider is chosen per [Market](markets.md)**, which means one store can use its own rates in a simple market and an external service in a complicated one.
|
|
155
|
+
|
|
156
|
+
```typescript Admin SDK
|
|
157
|
+
const { data: providers } = await adminClient.taxProviders.list()
|
|
158
|
+
```
|
|
136
159
|
|
|
137
|
-
|
|
160
|
+
A provider declares up front what it cannot do — no US local tax, no reverse charge — so the dashboard can warn a merchant who pairs it with a market that needs it, rather than quietly under-collecting.
|
|
138
161
|
|
|
139
|
-
|
|
162
|
+
Whichever provider is in use, the result is the same: tax lines on the order, in the same shape. Your storefront code doesn't change.
|
|
140
163
|
|
|
141
|
-
|
|
142
|
-
- Create zones for each taxable state
|
|
143
|
-
- Create tax rates per zone (e.g., 8.25% for California, 6.25% for Texas)
|
|
144
|
-
- Set `included_in_price: false`
|
|
164
|
+
## Tax-exempt customers
|
|
145
165
|
|
|
146
|
-
**
|
|
147
|
-
- Create a zone for EU countries
|
|
148
|
-
- Create tax rates per country (e.g., 19% Germany, 20% France)
|
|
149
|
-
- Set `included_in_price: true`
|
|
150
|
-
- Set the EU zone as the default tax zone
|
|
166
|
+
Business customers are often exempt, and the paperwork is real. Spree records a customer's or company's **tax identifier** — a VAT number, an ABN — and can validate it. Companies can also hold exemption certificates, scoped to the country or state that issued them.
|
|
151
167
|
|
|
152
|
-
|
|
153
|
-
- Create a "Digital" tax category
|
|
154
|
-
- Add tax rates for each EU country using the customer's country VAT rate
|
|
155
|
-
- Physical products use the seller's country VAT rate
|
|
168
|
+
Exemption is decided when tax is worked out, not stored as a flag on the customer, so an expired certificate stops applying by itself. See [Companies & Catalogs](companies-and-catalogs.md).
|
|
156
169
|
|
|
157
|
-
## Related
|
|
170
|
+
## Related
|
|
158
171
|
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
161
|
-
- [
|
|
162
|
-
- [
|
|
163
|
-
- [Avalara integration](../../integrations/tax/avalara.md) — Automated tax calculation for complex jurisdictions
|
|
172
|
+
- [Order totals](order-totals.md) — how tax rolls into what a customer pays
|
|
173
|
+
- [Markets](markets.md) — where tax treatment and provider are chosen
|
|
174
|
+
- [Addresses](addresses.md) — what determines the rate
|
|
175
|
+
- [Companies & Catalogs](companies-and-catalogs.md) — B2B tax identifiers and exemptions
|
|
@@ -1,134 +1,127 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Translations
|
|
3
|
-
description:
|
|
3
|
+
description: Selling in more than one language — translating product content, and translating the interface around it.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Two different things need translating, and they're handled separately because they belong to different people.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
2. **UI Translations** — interface strings used in the admin panel (e.g., button labels, flash messages)
|
|
10
|
+
**Your content** — product names, descriptions, category names — is written by whoever runs the store. Spree stores a version of each field per language.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
**The interface** — button labels, error messages, email wording — is written by Spree, and is already translated into dozens of languages by the community.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
> **INFO:** Which languages a region gets is decided by its [Market](markets.md). A market sets the default locale alongside the currency, so a visitor from France lands on French prices and French copy without choosing anything.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
## Translating content
|
|
17
|
+
|
|
18
|
+
Every translatable record keeps one row per language, so a product genuinely has a French name rather than a French name pasted over the English one.
|
|
18
19
|
|
|
19
20
|
```mermaid
|
|
20
21
|
erDiagram
|
|
21
|
-
Product ||--o{ ProductTranslation : "
|
|
22
|
-
Taxon ||--o{ TaxonTranslation : "has many"
|
|
23
|
-
Store ||--o{ StoreTranslation : "has many"
|
|
22
|
+
Product ||--o{ ProductTranslation : "one per locale"
|
|
24
23
|
|
|
25
24
|
Product {
|
|
26
|
-
string id PK
|
|
27
25
|
string status
|
|
26
|
+
string sku
|
|
28
27
|
}
|
|
29
|
-
|
|
30
28
|
ProductTranslation {
|
|
31
|
-
string id PK
|
|
32
|
-
string product_id FK
|
|
33
29
|
string locale
|
|
34
30
|
string name
|
|
35
|
-
|
|
31
|
+
string description
|
|
36
32
|
string slug
|
|
37
33
|
string meta_title
|
|
38
|
-
string meta_description
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
TaxonTranslation {
|
|
42
|
-
string id PK
|
|
43
|
-
string taxon_id FK
|
|
44
|
-
string locale
|
|
45
|
-
string name
|
|
46
|
-
text description
|
|
47
|
-
string permalink
|
|
48
34
|
}
|
|
49
35
|
```
|
|
50
36
|
|
|
51
|
-
###
|
|
37
|
+
### What can be translated
|
|
52
38
|
|
|
53
|
-
| Resource
|
|
54
|
-
|
|
55
|
-
| Product
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
| Option
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
39
|
+
| Resource | Fields |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Product | `name`, `description`, `slug`, `meta_title`, `meta_description` |
|
|
42
|
+
| Category | `name`, `description`, `permalink` |
|
|
43
|
+
| Collection | `name`, `description`, `permalink` |
|
|
44
|
+
| Product type | `name` |
|
|
45
|
+
| Option type | `presentation` |
|
|
46
|
+
| Option value | `presentation` |
|
|
47
|
+
| Store | `name`, `seo_title`, `meta_description`, `meta_keywords`, and support contact details |
|
|
48
|
+
| Policy | `name`, `body` |
|
|
49
|
+
| Seller | `name`, `about` |
|
|
63
50
|
|
|
64
|
-
|
|
51
|
+
Note that **slugs are translated too**. A French page can live at `/produits/sac-spree` rather than at the English slug with French words on it — which is what search engines actually reward. See [Slugs](slugs.md#internationalization).
|
|
65
52
|
|
|
66
|
-
|
|
53
|
+
## Reading translated content
|
|
54
|
+
|
|
55
|
+
Ask for a locale and you get that language back. Nothing else about the request changes:
|
|
67
56
|
|
|
68
57
|
|
|
69
58
|
```typescript Store SDK
|
|
70
|
-
|
|
71
|
-
const product = await client.products.get('spree-tote', {}, {
|
|
72
|
-
locale: 'fr',
|
|
73
|
-
})
|
|
59
|
+
const product = await client.products.get('spree-tote', {}, { locale: 'fr' })
|
|
74
60
|
|
|
75
61
|
product.name // "Sac Spree"
|
|
76
|
-
product.description // "Un sac fourre-tout élégant
|
|
62
|
+
product.description // "Un sac fourre-tout élégant…"
|
|
77
63
|
product.slug // "sac-spree"
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
const { data: categories } = await client.categories.list({}, {
|
|
81
|
-
locale: 'de',
|
|
82
|
-
})
|
|
65
|
+
const { data: categories } = await client.categories.list({}, { locale: 'de' })
|
|
83
66
|
```
|
|
84
67
|
|
|
85
68
|
```typescript Admin SDK
|
|
86
|
-
//
|
|
87
|
-
// The Admin API resolves by prefixed ID only — slugs are not accepted.
|
|
69
|
+
// The Admin API resolves by ID only — slugs are not accepted here
|
|
88
70
|
const product = await adminClient.products.get('prod_86Rf07xd4z', {}, { locale: 'fr' })
|
|
89
|
-
|
|
90
|
-
const { data: categories } = await adminClient.categories.list({}, { locale: 'de' })
|
|
91
71
|
```
|
|
92
72
|
|
|
93
73
|
```bash cURL
|
|
94
|
-
# Fetch product in French
|
|
95
74
|
curl 'https://api.mystore.com/api/v3/store/products/spree-tote' \
|
|
96
75
|
-H 'X-Spree-API-Key: pk_xxx' \
|
|
97
76
|
-H 'X-Spree-Locale: fr'
|
|
98
77
|
```
|
|
99
78
|
|
|
100
79
|
|
|
101
|
-
|
|
80
|
+
If a field has no translation for the requested locale, you get the default-language value rather than an empty string — a half-translated catalogue still renders as a usable page.
|
|
81
|
+
|
|
82
|
+
> **NOTE:** Responses vary by locale, so if you cache them, include the locale in the cache key. Spree sets `Vary` headers for you, which is enough for a CDN but not for a hand-rolled in-memory cache.
|
|
83
|
+
|
|
84
|
+
## Writing translations
|
|
102
85
|
|
|
103
|
-
|
|
86
|
+
Merchants translate in the dashboard, switching locale on the record they're editing. For bulk work — handing a catalogue to a translation agency — export to CSV, translate, and import it back.
|
|
104
87
|
|
|
105
|
-
Translations
|
|
88
|
+
Translations can also be written through the Admin API, which is what you'd use to sync from an external translation service:
|
|
106
89
|
|
|
107
|
-
|
|
90
|
+
```typescript Admin SDK
|
|
91
|
+
await adminClient.products.update('prod_xxx', {
|
|
92
|
+
translations: {
|
|
93
|
+
fr: { name: 'Sac Spree', description: 'Un sac fourre-tout élégant…' },
|
|
94
|
+
de: { name: 'Spree Tasche' },
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Which resources and fields accept translations
|
|
99
|
+
const { data: resources } = await adminClient.translatableResources.list()
|
|
100
|
+
```
|
|
108
101
|
|
|
109
|
-
|
|
102
|
+
Asking the API which resources are translatable means a translation tool doesn't need a hardcoded list that goes stale.
|
|
110
103
|
|
|
111
|
-
|
|
104
|
+
## Translating the interface
|
|
112
105
|
|
|
113
|
-
|
|
106
|
+
Spree's own strings — everything in the dashboard and in transactional emails — come from a community-maintained project covering 40+ languages.
|
|
114
107
|
|
|
115
108
|
|
|
116
|
-
```bash Spree CLI
|
|
109
|
+
```bash Spree CLI
|
|
117
110
|
spree bundle add spree_i18n
|
|
118
111
|
```
|
|
119
112
|
|
|
120
|
-
```bash
|
|
113
|
+
```bash Bundler
|
|
121
114
|
bundle add spree_i18n
|
|
122
115
|
```
|
|
123
116
|
|
|
124
117
|
|
|
125
|
-
|
|
118
|
+
That's the whole installation. Locales are picked up automatically; nothing needs copying into your app.
|
|
126
119
|
|
|
127
|
-
> **INFO:**
|
|
120
|
+
> **INFO:** See the [supported locales](https://github.com/spree-contrib/spree_i18n/tree/main/config/locales) in the Spree I18n repository. Contributions are welcome if yours is incomplete.
|
|
128
121
|
|
|
129
|
-
## Related
|
|
122
|
+
## Related
|
|
130
123
|
|
|
131
|
-
- [Markets](markets.md) —
|
|
132
|
-
- [
|
|
133
|
-
- [
|
|
134
|
-
- [
|
|
124
|
+
- [Markets](markets.md) — which locale and currency a region gets
|
|
125
|
+
- [Slugs](slugs.md) — localized URLs
|
|
126
|
+
- [Custom Fields](metafields.md) — translating your own fields
|
|
127
|
+
- [Localization](../../api-reference/store-api/localization.md) — the locale, currency and country headers
|