i18n(th): batch 2 — map (mapUi + mapPage) + AGENTS note
This commit is contained in:
parent
39572998a0
commit
b8c306125c
15 changed files with 591 additions and 18 deletions
17
AGENTS.md
17
AGENTS.md
|
|
@ -2141,6 +2141,23 @@ similar are internal endpoints/hook names, not credits — leave them.)
|
|||
|
||||
## Internationalisation (EN / ES-Castellano) — decisions (2026-10-01)
|
||||
|
||||
### Thai (ไทย) — third locale (2026-10-06)
|
||||
- Added `th` alongside en/es on the **same framework**; core plumbing unchanged.
|
||||
Translations live in **`src/i18n/th/`** as fragments composed in `index.ts`
|
||||
(so parallel batches don't edit one giant file). Batch 1 = app shell + static
|
||||
pages (`common`, `nav`, `chromeUi`, `desktopNav`, `navSheet`, `pwa`, `home`,
|
||||
`about`, `faq`, `onboarding`); more namespaces land in later batches.
|
||||
- **Partial by design:** a missing `th` key falls back to English. `check:i18n`
|
||||
is now **3-way** — en/es stay key-for-key identical (unchanged), `th` may be
|
||||
partial and is reported as **coverage %**; a `th` key absent from `en` fails.
|
||||
- **Font:** `@fontsource/kanit` (Kanit) imported in `main.tsx` and added to the
|
||||
CSS + Tailwind font stacks — ⚠️ Outfit/Inter/Bebas/Barlow have **no Thai
|
||||
glyphs**, so without it Thai silently falls back to a system font.
|
||||
- **Detection:** a Thai-device browser (`th*`) starts in `th` (`detectLanguage`).
|
||||
- **The trick glossary (`src/lib/glossary.ts`) stays English.**
|
||||
- **Do-not-translate is broader than tricks for Thai:** core skate vocabulary
|
||||
stays English (spot, park, bowl, DIY, vert, crew, line, Stash, The Lab, Nostr,
|
||||
Lightning, Zap, Relay, sats) because Thai skaters use the English terms.
|
||||
- The app ships **English + Spanish (Castellano, Spain)**. The framework already
|
||||
exists: `src/contexts/language.ts` (context/types/hooks) + `LanguageContext.tsx`
|
||||
(provider), `src/i18n/en.ts` + `src/i18n/es.ts` (one default-export object
|
||||
|
|
|
|||
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -20,6 +20,7 @@
|
|||
"@fontsource-variable/outfit": "^5.2.8",
|
||||
"@fontsource/barlow-condensed": "^5.2.8",
|
||||
"@fontsource/bebas-neue": "^5.2.7",
|
||||
"@fontsource/kanit": "^5.3.0",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
|
|
@ -1501,6 +1502,15 @@
|
|||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/kanit": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/kanit/-/kanit-5.3.0.tgz",
|
||||
"integrity": "sha512-XrVxf5NWb8QLAEbQS2GYir3AY0WbOLr5Q80fWurnoFWXyc7CWvioGNPC2dxRGZKocMUXbwGhuK+sJqWkRLAWNA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@getalby/lightning-tools": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@
|
|||
"@fontsource-variable/outfit": "^5.2.8",
|
||||
"@fontsource/barlow-condensed": "^5.2.8",
|
||||
"@fontsource/bebas-neue": "^5.2.7",
|
||||
"@fontsource/kanit": "^5.3.0",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@netlify/blobs": "^11.1.1",
|
||||
"@noble/curves": "^2.0.1",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"@scure/base": "^1.1.1",
|
||||
"@nostrify/nostrify": "^0.47.1",
|
||||
"@nostrify/react": "^0.2.17",
|
||||
"@nostrify/types": "^0.36.7",
|
||||
|
|
@ -77,6 +77,7 @@
|
|||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@scure/base": "^1.1.1",
|
||||
"@scure/bip32": "^2.0.1",
|
||||
"@scure/bip39": "1.6.0",
|
||||
"@std/encoding": "npm:@jsr/std__encoding@^0.224.3",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
* 1. Every `t('…')` key used anywhere in `src/` MUST exist in BOTH `en` and
|
||||
* `es` (a missing key otherwise degrades to the raw key + a console warning).
|
||||
* 2. `en` and `es` must be key-for-key identical, so neither language can drift.
|
||||
* 3. Thai (`th`) may be PARTIAL while it's being translated — a gap falls back
|
||||
* to English and is reported as coverage %, not a failure. A `th` key that
|
||||
* doesn't exist in `en` IS drift and fails.
|
||||
*
|
||||
* Translations are loaded with esbuild (already a dependency) so the TypeScript
|
||||
* source files can be imported directly without a build step.
|
||||
|
|
@ -98,6 +101,7 @@ function collectUsedKeys(files) {
|
|||
async function main() {
|
||||
const en = flatten(await loadTranslations(join(SRC, 'i18n/en.ts')));
|
||||
const es = flatten(await loadTranslations(join(SRC, 'i18n/es.ts')));
|
||||
const th = flatten(await loadTranslations(join(SRC, 'i18n/th/index.ts')));
|
||||
|
||||
const problems = [];
|
||||
|
||||
|
|
@ -112,6 +116,13 @@ async function main() {
|
|||
if (!es.has(key)) problems.push(`used but missing in es: ${key} (${files[0]})`);
|
||||
}
|
||||
|
||||
// 3. Thai may be PARTIAL while it's being translated (missing keys fall back
|
||||
// to English), so a gap is reported as coverage, not a failure. But a Thai
|
||||
// key that doesn't exist in `en` IS drift → fail.
|
||||
const missingTh = [...en].filter((k) => !th.has(k)).length;
|
||||
for (const key of th) if (!en.has(key)) problems.push(`th has a key not in en: ${key}`);
|
||||
const thCoverage = en.size ? Math.round(((en.size - missingTh) / en.size) * 100) : 100;
|
||||
|
||||
if (problems.length) {
|
||||
console.error(`\n✖ i18n check failed (${problems.length}):`);
|
||||
for (const p of problems.slice(0, 200)) console.error(` • ${p}`);
|
||||
|
|
@ -119,7 +130,7 @@ async function main() {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✓ i18n check passed — ${en.size} keys in en/es, ${used.size} used`);
|
||||
console.log(`✓ i18n check passed — ${en.size} keys in en/es, ${used.size} used · th coverage ${thCoverage}% (${en.size - missingTh}/${en.size})`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ interface LanguageOption {
|
|||
const languages: LanguageOption[] = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
|
||||
];
|
||||
|
||||
interface LanguageToggleProps {
|
||||
|
|
|
|||
|
|
@ -1113,7 +1113,9 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
|
|||
// the app's current language, keeping skate trick names in English.
|
||||
const languageDirective = language === 'es'
|
||||
? '\n\nAlways reply in Spanish (Castellano). Keep skateboarding trick names in English (kickflip, ollie, nollie, heelflip, etc.) and never translate them.'
|
||||
: '';
|
||||
: language === 'th'
|
||||
? '\n\nAlways reply in Thai (ไทย) with a friendly, casual skate tone. Keep skateboarding trick names in English (kickflip, ollie, nollie, heelflip, etc.) and core skate vocabulary (spot, park, bowl, DIY, vert, crew, line) in English — never translate those.'
|
||||
: '';
|
||||
const systemPrompt = (personal.available && isPro
|
||||
? `${SYSTEM_PROMPT}${PERSONAL_TOOLS_PROMPT}`
|
||||
: SYSTEM_PROMPT) + languageDirective;
|
||||
|
|
|
|||
|
|
@ -15,14 +15,16 @@ interface LanguageProviderProps {
|
|||
|
||||
/**
|
||||
* Pick the starting language from the visitor's browser on their FIRST visit.
|
||||
* A device set to Spanish (`es-ES`, `es-MX`, …) lands in Castellano; anything
|
||||
* else falls back to English. Once the user taps the language toggle their
|
||||
* choice is stored and always wins.
|
||||
* A device set to Thai (`th`, `th-TH`) lands in Thai; Spanish (`es-*`) in
|
||||
* Castellano; anything else falls back to English. Once the user taps the
|
||||
* language toggle their choice is stored and always wins.
|
||||
*/
|
||||
function detectLanguage(): Language {
|
||||
if (typeof navigator === 'undefined') return 'en';
|
||||
const tags = [navigator.language, ...(navigator.languages ?? [])];
|
||||
return tags.some((tag) => tag?.toLowerCase().startsWith('es')) ? 'es' : 'en';
|
||||
const tags = [navigator.language, ...(navigator.languages ?? [])].map((t) => t?.toLowerCase() ?? '');
|
||||
if (tags.some((t) => t.startsWith('th'))) return 'th';
|
||||
if (tags.some((t) => t.startsWith('es'))) return 'es';
|
||||
return 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
import en from '@/i18n/en';
|
||||
import es from '@/i18n/es';
|
||||
import th from '@/i18n/th';
|
||||
|
||||
/**
|
||||
* Language context + types + hooks. Split from the `LanguageProvider` component
|
||||
|
|
@ -8,7 +9,7 @@ import es from '@/i18n/es';
|
|||
* `react-refresh/only-export-components` rule.
|
||||
*/
|
||||
|
||||
export type Language = 'en' | 'es';
|
||||
export type Language = 'en' | 'es' | 'th';
|
||||
|
||||
type TranslationKey = string;
|
||||
type TranslationValue = string | { [key: string]: TranslationValue };
|
||||
|
|
@ -30,7 +31,7 @@ export interface LanguageContextType {
|
|||
|
||||
export const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
export const translations: Record<Language, Translations> = { en, es };
|
||||
export const translations: Record<Language, Translations> = { en, es, th };
|
||||
|
||||
/** Get a nested value from an object using dot notation (`nav.home`). */
|
||||
export function getNestedValue(obj: Translations, path: string): string | undefined {
|
||||
|
|
|
|||
297
src/i18n/th/batch1.ts
Normal file
297
src/i18n/th/batch1.ts
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
/**
|
||||
* Thai (ไทย) — batch 1: app shell + static pages.
|
||||
*
|
||||
* Policy (same as Spanish): skate terminology stays English (spot, park, bowl,
|
||||
* DIY, vert, Street/Transition/Plaza, Zap, Crew, Stash, The Lab, Nostr,
|
||||
* Lightning, Bitcoin, Relay, sats). Everything else is Thai.
|
||||
*
|
||||
* Keys MUST match the `en` namespaces exactly; missing keys fall back to English
|
||||
* (the `check:i18n` script reports Thai coverage).
|
||||
*/
|
||||
const thBatch1 = {
|
||||
common: {
|
||||
loading: 'กำลังโหลด...',
|
||||
error: 'ผิดพลาด',
|
||||
save: 'บันทึก',
|
||||
cancel: 'ยกเลิก',
|
||||
delete: 'ลบ',
|
||||
edit: 'แก้ไข',
|
||||
add: 'เพิ่ม',
|
||||
remove: 'เอาออก',
|
||||
create: 'สร้าง',
|
||||
submit: 'ส่ง',
|
||||
close: 'ปิด',
|
||||
back: 'ย้อนกลับ',
|
||||
next: 'ถัดไป',
|
||||
done: 'เสร็จ',
|
||||
search: 'ค้นหา',
|
||||
filter: 'ตัวกรอง',
|
||||
sort: 'เรียงลำดับ',
|
||||
share: 'แชร์',
|
||||
more: 'เพิ่มเติม',
|
||||
copy: 'คัดลอก',
|
||||
copied: 'คัดลอกแล้ว!',
|
||||
viewAll: 'ดูทั้งหมด',
|
||||
seeMore: 'ดูเพิ่มเติม',
|
||||
showMore: 'แสดงเพิ่มเติม',
|
||||
showLess: 'แสดงน้อยลง',
|
||||
noResults: 'ไม่พบผลลัพธ์',
|
||||
tryAgain: 'ลองอีกครั้ง',
|
||||
learnMore: 'เรียนรู้เพิ่มเติม',
|
||||
getStarted: 'เริ่มต้น',
|
||||
yes: 'ใช่',
|
||||
no: 'ไม่',
|
||||
or: 'หรือ',
|
||||
and: 'และ',
|
||||
free: 'ฟรี',
|
||||
new: 'ใหม่',
|
||||
popular: 'ยอดนิยม',
|
||||
featured: 'แนะนำ',
|
||||
verified: 'ยืนยันแล้ว',
|
||||
sats: 'sats',
|
||||
clear: 'ล้าง',
|
||||
clearFilters: 'ล้างตัวกรอง',
|
||||
clearAll: 'ล้างทั้งหมด',
|
||||
},
|
||||
nav: {
|
||||
home: 'หน้าแรก',
|
||||
map: 'Spots',
|
||||
lab: 'The Lab',
|
||||
events: 'อีเวนต์',
|
||||
crews: 'Crews',
|
||||
wallet: 'กระเป๋า',
|
||||
settings: 'ตั้งค่า',
|
||||
more: 'เพิ่มเติม',
|
||||
rewards: 'รางวัล',
|
||||
stats: 'สถิติชุมชน',
|
||||
about: 'เกี่ยวกับ',
|
||||
food: 'อาหาร',
|
||||
marketplace: 'มาร์เก็ตเพลส',
|
||||
mints: 'Mints',
|
||||
appShop: 'ร้านค้าแอป',
|
||||
diy: 'DIY',
|
||||
trades: 'มาร์เก็ตเพลส',
|
||||
stash: 'Stash',
|
||||
collections: 'คอลเลกชัน',
|
||||
contacts: 'รายชื่อ',
|
||||
messages: 'ข้อความ',
|
||||
skaters: 'นักสเก็ต',
|
||||
glossary: 'คำศัพท์',
|
||||
riderCard: 'Rider Card',
|
||||
supporters: 'ผู้สนับสนุน ⚡',
|
||||
newsletter: 'จดหมายข่าว',
|
||||
viewProfile: 'ดูโปรไฟล์',
|
||||
leaderboards: 'ลีดเดอร์บอร์ด',
|
||||
},
|
||||
chromeUi: {
|
||||
skipToContent: 'ข้ามไปที่เนื้อหา',
|
||||
menu: 'เมนู',
|
||||
inBadge: 'เข้า',
|
||||
loggedIn: 'เข้าสู่ระบบแล้ว',
|
||||
profile: 'โปรไฟล์',
|
||||
logIn: 'เข้าสู่ระบบ',
|
||||
logOut: 'ออกจากระบบ',
|
||||
notLoggedIn: 'ยังไม่ได้เข้าสู่ระบบ',
|
||||
loggedOutPrompt: 'เข้าสู่ระบบเพื่อเพิ่มสปอต ให้ Zap นักสเก็ต และบันทึกรายการโปรด',
|
||||
accounts: 'บัญชี',
|
||||
addAnotherAccount: 'เพิ่มบัญชี',
|
||||
setStatus: 'ตั้งสถานะ',
|
||||
setYourStatus: 'ตั้งสถานะของคุณ',
|
||||
status: 'สถานะ',
|
||||
addSpot: 'เพิ่มสปอต',
|
||||
myData: 'ข้อมูลของฉัน',
|
||||
myStash: 'Stash ของฉัน',
|
||||
myCollections: 'คอลเลกชันของฉัน',
|
||||
viewMyProfile: 'ดูโปรไฟล์ของฉัน',
|
||||
customizeNavBar: 'ปรับแต่งแถบนำทาง',
|
||||
ecash: 'Ecash',
|
||||
madeWith: 'สร้างด้วย',
|
||||
forSkateCommunity: 'เพื่อชุมชนสเก็ตบอร์ด',
|
||||
beta: 'Beta',
|
||||
license: 'สัญญาอนุญาต',
|
||||
privacy: 'ความเป็นส่วนตัว',
|
||||
collaborators: 'ผู้ร่วมงานและเพื่อน',
|
||||
notSponsoredBy: 'ไม่ได้รับการสนับสนุนจาก',
|
||||
errorTitle: 'โหลดไม่สำเร็จ',
|
||||
errorDescription: 'คำขอล้มเหลว ตรวจสอบการเชื่อมต่อแล้วลองอีกครั้ง',
|
||||
tryAgain: 'ลองอีกครั้ง',
|
||||
weatherOn: 'เปิดสภาพอากาศ',
|
||||
weatherOff: 'ปิดสภาพอากาศ',
|
||||
trustFilter: 'ตัวกรองความน่าเชื่อถือ',
|
||||
filterByTrustLevel: 'กรองตามระดับความน่าเชื่อถือ',
|
||||
trust: {
|
||||
all: 'ผู้ใช้ทั้งหมด',
|
||||
allDesc: 'แสดงเนื้อหาจากทุกคน',
|
||||
verified: 'ยืนยันแล้ว',
|
||||
verifiedDesc: 'คุณติดตามหรือยืนยันแล้ว (trust ≥90)',
|
||||
trusted: 'น่าเชื่อถือ',
|
||||
trustedDesc: 'เพื่อนของเพื่อนหรือยืนยัน NIP-05 (trust ≥60)',
|
||||
following: 'เฉพาะที่ติดตาม',
|
||||
followingDesc: 'แสดงเฉพาะเนื้อหาจากคนที่คุณติดตาม',
|
||||
followingShort: 'กำลังติดตาม',
|
||||
},
|
||||
sort: {
|
||||
recent: 'ล่าสุด',
|
||||
recentDesc: 'ใหม่สุดก่อน',
|
||||
trust: 'ความน่าเชื่อถือสูงสุด',
|
||||
trustDesc: 'ครีเอเตอร์ที่น่าเชื่อถือที่สุด',
|
||||
proximity: 'ใกล้ฉันที่สุด',
|
||||
proximityDesc: 'ตำแหน่งที่ใกล้ที่สุด',
|
||||
popular: 'ยอดนิยม',
|
||||
popularDesc: 'คอมเมนต์/รีแอ็กชันมากที่สุด',
|
||||
zaps: 'ถูก Zap มากที่สุด',
|
||||
zapsDesc: 'มูลค่าที่ได้รับสูงสุด',
|
||||
},
|
||||
spotType: {
|
||||
street: 'Street',
|
||||
transition: 'Transition',
|
||||
park: 'Park',
|
||||
plaza: 'Plaza',
|
||||
diy: 'DIY',
|
||||
bowl: 'Bowl',
|
||||
vert: 'Vert',
|
||||
indoor: 'Indoor',
|
||||
skatepark: 'Skatepark',
|
||||
miniramp: 'Mini Ramp',
|
||||
},
|
||||
},
|
||||
desktopNav: {
|
||||
title: 'แถบนำทางเดสก์ท็อป',
|
||||
desc: 'เลือกว่าแท็บใดจะแสดงบนแถบบนและลำดับ แท็บที่ซ่อนยังอยู่ในเมนู “เพิ่มเติม”',
|
||||
fixed: 'คงที่',
|
||||
shownInTopBar: 'แสดงบนแถบบน ({{count}})',
|
||||
noExtraTabs: 'ไม่มีแท็บเพิ่มเติม — ทุกอย่างอยู่ในเมนู “เพิ่มเติม” เพิ่มด้านล่างได้เลย',
|
||||
inMoreMenu: 'อยู่ในเมนู “เพิ่มเติม” ({{count}})',
|
||||
moveUp: 'เลื่อน {{name}} ขึ้น',
|
||||
moveDown: 'เลื่อน {{name}} ลง',
|
||||
hide: 'ซ่อน {{name}}',
|
||||
moveToMore: 'ย้ายไปเมนูเพิ่มเติม',
|
||||
show: 'แสดง',
|
||||
showInTopBar: 'แสดง {{name}} บนแถบบน',
|
||||
showInTopBarTitle: 'แสดงบนแถบบน',
|
||||
reset: 'รีเซ็ตเป็นค่าเริ่มต้น',
|
||||
},
|
||||
navSheet: {
|
||||
title: 'ปรับแต่งแถบของคุณ',
|
||||
desc: 'เลือก {{count}} แท็บที่คุณใช้บ่อย ที่เหลือจะอยู่ในเมนู',
|
||||
preview: 'ตัวอย่าง',
|
||||
empty: 'ว่าง',
|
||||
order: 'ลำดับ ({{done}}/{{total}})',
|
||||
available: 'แท็บที่ใช้ได้',
|
||||
full: 'แถบเต็ม — เอาออกหนึ่งอันเพื่อสลับ',
|
||||
moveUp: 'เลื่อน {{name}} ขึ้น',
|
||||
moveDown: 'เลื่อน {{name}} ลง',
|
||||
login: 'เข้าสู่ระบบ',
|
||||
profile: 'โปรไฟล์',
|
||||
reset: 'รีเซ็ต',
|
||||
done: 'เสร็จ',
|
||||
},
|
||||
pwa: {
|
||||
installTitle: 'ติดตั้ง SkateSpots',
|
||||
installSubtitle: 'รับประสบการณ์แอปเต็มรูปแบบ',
|
||||
addToHomeScreen: 'เพิ่มไปยังหน้าจอหลัก',
|
||||
installApp: 'ติดตั้งแอป',
|
||||
maybeLater: 'ไว้ทีหลัง',
|
||||
gotIt: 'เข้าใจแล้ว!',
|
||||
benefits: {
|
||||
fast: 'เร็วสุดขีด',
|
||||
offline: 'ใช้แบบออฟไลน์ได้',
|
||||
fullAccess: 'เข้าถึงเต็มรูปแบบ',
|
||||
},
|
||||
iosStep1: 'แตะปุ่มแชร์',
|
||||
iosStep1Hint: 'ที่ด้านล่างของ Safari',
|
||||
iosStep2: 'เลื่อนแล้วแตะ “เพิ่มไปยังหน้าจอหลัก”',
|
||||
iosStep2Hint: 'ในเมนูแชร์',
|
||||
iosStep3: 'แตะ “เพิ่ม” เพื่อติดตั้ง',
|
||||
iosStep3Hint: 'แอปจะปรากฏบนหน้าจอหลักของคุณ',
|
||||
androidStep1: 'แตะปุ่มเมนู',
|
||||
androidStep1Hint: '(สามจุด) ในเบราว์เซอร์',
|
||||
androidStep2: 'เลือก “เพิ่มไปยังหน้าจอหลัก”',
|
||||
androidStep2Hint: 'หรือ “ติดตั้งแอป”',
|
||||
androidStep3: 'ยืนยันการติดตั้ง',
|
||||
androidStep3Hint: 'แอปจะปรากฏบนหน้าจอหลักของคุณ',
|
||||
tagline: 'ฟรี • ใช้แบบออฟไลน์ได้ • เร็วและเชื่อถือได้',
|
||||
},
|
||||
home: {
|
||||
tagline: 'ขับเคลื่อนโดยชุมชน • บน Relays',
|
||||
title: 'SKATE SPOTS',
|
||||
subtitle: 'ค้นหาสปอต • นัดเจอกัน • เรียนทริก • ซัพพอร์ตท้องถิ่น',
|
||||
description: 'การสเก็ตจริง ๆ เกิดนอกจอ — เครื่องมือนี้ช่วยให้คุณเจอมัน',
|
||||
betaWarning: 'BETA - อยู่ระหว่างพัฒนา • ใช้งานด้วยความระมัดระวัง',
|
||||
pwaHint: 'Progressive Web App - ติดตั้งบนอุปกรณ์ของคุณ',
|
||||
exploreMap: 'สำรวจแผนที่',
|
||||
addSpot: 'เพิ่มสปอต',
|
||||
globalSpots: 'สปอตทั่วโลก',
|
||||
openCommunity: 'ชุมชนเปิด',
|
||||
lightningZaps: 'Lightning Zaps',
|
||||
featuredSpots: 'สปอตแนะนำ',
|
||||
featuredSpotsDesc: 'สปอตสเก็ตที่ชุมชนยืนยันแล้วจากทั่วโลก',
|
||||
upcomingEvents: 'อีเวนต์ที่กำลังจะมา',
|
||||
upcomingEventsDesc: 'การแข่ง มีตติ้ง และเซสชันที่กำลังจะเกิดขึ้น',
|
||||
noSpotsYet: 'ยังไม่พบสปอต',
|
||||
noSpotsHint: 'เป็นคนแรกที่เพิ่มสปอตสเก็ต!',
|
||||
noEventsYet: 'ยังไม่มีอีเวนต์ที่กำลังจะมา',
|
||||
noEventsHint: 'กลับมาดูใหม่หรือสร้างขึ้นเอง!',
|
||||
theLab: 'The Lab',
|
||||
theLabDesc: 'เรียนทริก หาโค้ช และอัปสกิลการเล่นของคุณ',
|
||||
coaches: 'โค้ช',
|
||||
coachesDesc: 'เรียนจากนักสเก็ตที่มีประสบการณ์ช่วยให้คุณพัฒนา',
|
||||
filmers: 'ช่างถ่าย',
|
||||
filmersDesc: 'เชื่อมต่อกับช่างถ่ายเพื่อเก็บคลิปที่ดีที่สุด',
|
||||
findCoach: 'หาโค้ช',
|
||||
becomeCoach: 'เป็นโค้ช',
|
||||
findFilmer: 'หาช่างถ่าย',
|
||||
becomeFilmer: 'เป็นช่างถ่าย',
|
||||
viewAllSpots: 'ดูสปอตทั้งหมด',
|
||||
viewAllEvents: 'ดูอีเวนต์ทั้งหมด',
|
||||
createEvent: 'สร้างอีเวนต์',
|
||||
readyToShare: 'พร้อมแชร์แล้วหรือยัง?',
|
||||
readyToShareDesc: 'ช่วยให้ชุมชนสเก็ตเติบโตด้วยการเพิ่มสปอตโปรดของคุณ ทุกตำแหน่งที่แชร์ช่วยให้นักสเก็ตคนอื่นค้นพบที่ใหม่ ๆ',
|
||||
},
|
||||
about: {
|
||||
title: 'เกี่ยวกับ SkateSpots',
|
||||
subtitle: 'ชุมชนสเก็ตบอร์ดแบบกระจายศูนย์',
|
||||
whatIs: 'SkateSpots คืออะไร?',
|
||||
whatIsDesc: 'SkateSpots คือแพลตฟอร์มที่ขับเคลื่อนโดยชุมชนสำหรับค้นหาสถานที่สเก็ตทั่วโลก สร้างบน Nostr ให้นักสเก็ตเป็นเจ้าของข้อมูลของตัวเองอย่างแท้จริง',
|
||||
features: 'ฟีเจอร์',
|
||||
featuresDesc: 'ค้นหาสปอต สร้างอีเวนต์ เชื่อมต่อกับโค้ช และสนับสนุนร้านสเก็ตท้องถิ่น — ครบในที่เดียว',
|
||||
nostr: 'สร้างบน Nostr',
|
||||
nostrDesc: 'Nostr คือโปรโตคอลเปิดที่เรียบง่าย ทำให้การสื่อสารทั่วโลกต้านทานการเซ็นเซอร์ได้จริง บัญชีและข้อมูลเป็นของคุณ',
|
||||
openSource: 'โอเพนซอร์ส',
|
||||
openSourceDesc: 'SkateSpots เป็นโอเพนซอร์สเต็มรูปแบบ ร่วมพัฒนา ฟอร์ก หรือรันอินสแตนซ์ของคุณเองได้',
|
||||
lightning: 'การจ่ายด้วย Lightning',
|
||||
lightningDesc: 'Zap นักสเก็ต โค้ช และครีเอเตอร์ที่คุณชอบด้วยการจ่าย Bitcoin Lightning',
|
||||
},
|
||||
faq: {
|
||||
title: 'คำถามที่พบบ่อย',
|
||||
subtitle: 'ทุกอย่างที่คุณต้องรู้เกี่ยวกับ SkateSpots',
|
||||
whatIsNostr: 'Nostr คืออะไร?',
|
||||
whatIsNostrAnswer: 'Nostr คือโปรโตคอลเปิดสำหรับโซเชียลแบบกระจายศูนย์ ต่างจากแพลตฟอร์มทั่วไป คุณเป็นเจ้าของข้อมูลและตัวตนของคุณ',
|
||||
howToStart: 'เริ่มต้นอย่างไร?',
|
||||
howToStartAnswer: 'กด “สมัคร” เพื่อสร้างบัญชี Nostr ใหม่ หรือ “เข้าสู่ระบบ” ถ้ามีอยู่แล้ว ใช้ส่วนขยายเบราว์เซอร์ แอปมือถือ หรือคีย์ลับก็ได้',
|
||||
whatAreZaps: 'Zaps คืออะไร?',
|
||||
whatAreZapsAnswer: 'Zaps คือทิป Bitcoin Lightning ที่ส่งให้ผู้ใช้คนอื่น เป็นวิธีแสดงความชื่นชมเนื้อหาดี ๆ หรือสนับสนุนครีเอเตอร์',
|
||||
howToAddSpot: 'เพิ่มสปอตอย่างไร?',
|
||||
howToAddSpotAnswer: 'เข้าสู่ระบบ แล้วกด “เพิ่มสปอต” กรอกรายละเอียด ทั้งตำแหน่ง รูป และคำอธิบาย',
|
||||
isItFree: 'SkateSpots ฟรีไหม?',
|
||||
isItFreeAnswer: 'ฟรี! SkateSpots ใช้งานฟรีทั้งหมด จะส่ง Zaps (ทิป Bitcoin) ให้ผู้ใช้คนอื่นก็ได้',
|
||||
canIEditSpot: 'แก้ไขหรือลบสปอตของฉันได้ไหม?',
|
||||
canIEditSpotAnswer: 'ได้ คุณแก้ไขสปอตที่สร้างได้ เนื่องจากธรรมชาติกระจายศูนย์ของ Nostr คำขอลบจะถูกส่งไป แต่บาง relay อาจเก็บข้อมูลไว้',
|
||||
},
|
||||
onboarding: {
|
||||
welcome: 'ยินดีต้อนรับสู่ SkateSpots!',
|
||||
welcomeDesc: 'มาตั้งค่ากันในไม่กี่ขั้นตอน',
|
||||
step1Title: 'สร้างบัญชีของคุณ',
|
||||
step1Desc: 'กำลังสร้างคีย์ Nostr ของคุณ เหมือนรหัสผ่านที่คุณควบคุมเอง',
|
||||
step2Title: 'สำรองคีย์ของคุณ',
|
||||
step2Desc: 'เก็บวลีกู้คืนไว้ในที่ปลอดภัย นี่เป็นวิธีเดียวที่จะกู้บัญชีคืนได้!',
|
||||
step3Title: 'ตั้งค่าโปรไฟล์ของคุณ',
|
||||
step3Desc: 'บอกชุมชนเกี่ยวกับตัวคุณ',
|
||||
step4Title: 'พร้อมแล้ว!',
|
||||
step4Desc: 'เริ่มสำรวจสปอต อีเวนต์ และเชื่อมต่อกับนักสเก็ต',
|
||||
skipForNow: 'ข้ามไปก่อน',
|
||||
continueToApp: 'ไปที่แอป',
|
||||
},
|
||||
};
|
||||
|
||||
export default thBatch1;
|
||||
201
src/i18n/th/batch2.ts
Normal file
201
src/i18n/th/batch2.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* Thai (ไทย) — batch 2: the map (mapUi + mapPage).
|
||||
* Skate terms stay English (Spots, Shops, Gear, Crews, Stash, Tour, Crew).
|
||||
*/
|
||||
const thBatch2 = {
|
||||
mapUi: {
|
||||
mapFooter: {
|
||||
links: 'ลิงก์',
|
||||
hide: 'ซ่อนแถบลิงก์แผนที่',
|
||||
},
|
||||
findingSpots: 'กำลังหาสปอต',
|
||||
loadingSpots: 'กำลังโหลดสปอต',
|
||||
loadingMap: 'กำลังโหลดแผนที่...',
|
||||
mapViewAria: 'มุมมองแผนที่',
|
||||
listViewAria: 'มุมมองรายการ',
|
||||
layers: 'เลเยอร์',
|
||||
mapLayers: 'เลเยอร์แผนที่',
|
||||
showOnMap: 'แสดงบนแผนที่',
|
||||
showResults: 'แสดงผลลัพธ์',
|
||||
quickActions: 'ทางลัด',
|
||||
spots: 'Spots',
|
||||
shops: 'Shops',
|
||||
gear: 'Gear',
|
||||
tours: 'Tours',
|
||||
events: 'อีเวนต์',
|
||||
crews: 'Crews',
|
||||
spotType: 'ประเภทสปอต',
|
||||
sortBy: 'เรียงตาม',
|
||||
trustLevel: 'ระดับความน่าเชื่อถือ',
|
||||
weather: 'สภาพอากาศ',
|
||||
buySellGear: 'ซื้อ/ขาย Gear',
|
||||
stash: 'Stash',
|
||||
more: 'เพิ่มเติม',
|
||||
newHere: 'ใหม่ที่นี่?',
|
||||
welcomeDesc: 'ค้นพบสปอตสเก็ต อีเวนต์ และชุมชน',
|
||||
getOnZapstore: 'รับบน Zapstore',
|
||||
dismiss: 'ปิด',
|
||||
addedBy: 'เพิ่มโดย',
|
||||
directions: 'เส้นทาง',
|
||||
viewDetails: 'ดูรายละเอียด',
|
||||
viewDetailsArrow: 'ดูรายละเอียด →',
|
||||
spotDetails: 'รายละเอียดสปอต',
|
||||
skateShop: 'Skate Shop',
|
||||
skateSpot: 'Skate Spot',
|
||||
youAreHere: 'คุณอยู่ที่นี่',
|
||||
conditions: 'สภาพ',
|
||||
weatherScoreSuffix: '{{score}}/100 สภาพอากาศ',
|
||||
wind: 'ลม',
|
||||
humidity: 'ความชื้น',
|
||||
airQuality: 'คุณภาพอากาศ',
|
||||
threeDayForecast: 'พยากรณ์ 3 วัน',
|
||||
gearForSale: 'Gear ที่ขาย ({{count}})',
|
||||
viewItems: 'ดูสินค้า →',
|
||||
viewAllItems: 'ดูทั้งหมด {{count}} สินค้า →',
|
||||
viewGear: 'ดู Gear →',
|
||||
localTour: 'Local Tour',
|
||||
meetAt: 'นัดที่: {{place}}',
|
||||
hours: '{{count}} ชั่วโมง',
|
||||
level: 'ระดับ {{level}}',
|
||||
viewTour: 'ดูทัวร์ →',
|
||||
upcomingEvent: 'อีเวนต์ที่กำลังจะมา',
|
||||
viewEvent: 'ดูอีเวนต์ →',
|
||||
skateCrew: 'Skate Crew',
|
||||
members: 'สมาชิก {{count}} คน',
|
||||
since: 'ตั้งแต่ {{year}}',
|
||||
viewCrew: 'ดู Crew →',
|
||||
acceptsBitcoin: 'รับ Bitcoin',
|
||||
boosted: 'Boosted',
|
||||
website: 'เว็บไซต์',
|
||||
offlineSavedSpots: 'ออฟไลน์ · แสดงสปอตที่บันทึกไว้',
|
||||
cantReachRelays: 'เชื่อมต่อ relay ไม่ได้',
|
||||
relayOnlineNoAnswer: 'คุณออนไลน์อยู่ แต่ relay ของชุมชนยังไม่ตอบสนอง',
|
||||
youreOffline: 'คุณออฟไลน์อยู่',
|
||||
savedOnDevice: 'สปอตที่คุณเคยเห็นถูกบันทึกไว้ในเครื่องนี้และจะแสดงที่นี่',
|
||||
noSpotsMatchTitle: 'ไม่มีสปอตที่ตรง',
|
||||
noSpotsMatchDesc: 'ลองค้นหาอย่างอื่นหรือล้างตัวกรอง',
|
||||
noSpotsMappedYet: 'ยังไม่มีสปอตบนแผนที่',
|
||||
noSpotsMappedDesc: 'เป็นคนแรกที่แมปสปอตของคุณ ใช้เวลาไม่กี่นาที มันเป็นของคุณ และนักสเก็ตคนต่อไปจะขอบคุณ',
|
||||
addFirstSpot: 'เพิ่มสปอตแรก',
|
||||
findSpotsNearMe: 'หาสปอตใกล้ฉัน',
|
||||
locating: 'กำลังระบุตำแหน่ง…',
|
||||
refiningLocation: 'กำลังปรับตำแหน่ง…',
|
||||
refiningLocationDesc: 'อิงจากสปอตล่าสุดของคุณ — กำลังขอพิกัด GPS ใหม่',
|
||||
locatingYou: 'กำลังหาตำแหน่งคุณ…',
|
||||
locatingDesc: 'กำลังขอพิกัด GPS ครั้งแรกอาจใช้เวลาถึงหนึ่งนาที',
|
||||
foundYou: 'เจอตำแหน่งคุณแล้ว',
|
||||
foundYouDesc: 'จัดแผนที่ให้อยู่ที่ตำแหน่งของคุณ',
|
||||
locationPermissionNeeded: 'ต้องอนุญาตตำแหน่ง',
|
||||
permissionNeededDesc: 'อนุญาตตำแหน่งให้ SkateSpots ที่ ตั้งค่าเครื่อง → แอป → SkateSpots → สิทธิ์ แล้วกด "ใกล้ฉัน" อีกครั้ง',
|
||||
locationTimedOut: 'หาตำแหน่งหมดเวลา',
|
||||
timeoutNativeDesc: 'หาพิกัดไม่ทันเวลา ถ้าอยู่ในอาคาร GPS มองไม่เห็นดาวเทียม — เปิด Network Location (ตั้งค่า → ตำแหน่ง) หรือออกไปข้างนอกแล้วลองใหม่',
|
||||
timeoutWebDesc: 'หาตำแหน่งไม่ทันเวลา ตรวจสัญญาณแล้วลองใหม่',
|
||||
noLocationProvider: 'ไม่มีตัวให้ตำแหน่ง',
|
||||
noProviderNativeDesc: 'โทรศัพท์ไม่ให้ตำแหน่ง บน ROM ที่เน้นความเป็นส่วนตัว (เช่น GrapheneOS) เปิด Network Location ใน ตั้งค่า → ตำแหน่ง หรือตรวจว่า GPS เปิดและลองกลางแจ้ง',
|
||||
noProviderWebDesc: 'ไม่มีผู้ให้บริการตำแหน่ง เปิดบริการตำแหน่งแล้วลองใหม่',
|
||||
locationUnavailable: 'ตำแหน่งใช้ไม่ได้',
|
||||
unavailableNativeDesc: 'เปิด ตำแหน่ง/GPS ในตั้งค่าโทรศัพท์ อนุญาตตำแหน่งให้ SkateSpots แล้วลองใหม่',
|
||||
unavailableWebDesc: 'ระบุตำแหน่งไม่ได้ ตรวจว่าบริการตำแหน่งเปิดอยู่',
|
||||
locationBlocked: 'ตำแหน่งถูกบล็อก',
|
||||
locationBlockedDesc: 'เบราว์เซอร์บล็อกการเข้าถึงตำแหน่งสำหรับไซต์นี้ วิธีเปิดใหม่:',
|
||||
locationPlatformAndroid: 'Android Chrome',
|
||||
locationPlatformIos: 'iOS Safari',
|
||||
locationPlatformDesktop: 'Desktop Chrome',
|
||||
locationPlatformFirefox: 'Firefox',
|
||||
locationStepsAndroid: 'แตะไอคอนกุญแจบนแถบที่อยู่ → การตั้งค่าไซต์ → ตำแหน่ง → อนุญาต',
|
||||
locationStepsIos: 'แอปตั้งค่า → Safari → ตำแหน่ง → อนุญาต (หรือแตะปุ่ม "AA" บนแถบที่อยู่ของ Safari)',
|
||||
locationStepsDesktop: 'คลิกไอคอนกุญแจ → การตั้งค่าไซต์ → ตำแหน่ง → อนุญาต',
|
||||
locationStepsFirefox: 'คลิกไอคอนกุญแจ → การเชื่อมต่อปลอดภัย → ข้อมูลเพิ่มเติม → สิทธิ์ → การเข้าถึงตำแหน่ง → อนุญาต',
|
||||
gotIt: 'เข้าใจแล้ว',
|
||||
addSpot: 'เพิ่มสปอต',
|
||||
addShop: 'เพิ่มร้าน',
|
||||
addEvent: 'เพิ่มอีเวนต์',
|
||||
sellGear: 'ขาย Gear',
|
||||
offerTour: 'เสนอทัวร์',
|
||||
closeAddMenu: 'ปิดเมนูเพิ่ม',
|
||||
addMenuAria: 'เพิ่มสปอต ร้าน อีเวนต์ Gear หรือทัวร์',
|
||||
localChat: 'แชตท้องถิ่น',
|
||||
localChatDesc: 'แชตสดตามจุดกลางแผนที่ เลือกความกว้างของช่องได้',
|
||||
btcMapShowing: 'แสดง {{count}} สถานที่ที่รับ sats ของคุณใกล้ ๆ',
|
||||
btcMapShow: 'แสดงสถานที่ที่รับ sats ของคุณ (ผ่าน BTC Map)',
|
||||
yourPrivateStash: 'Stash ส่วนตัวของคุณ',
|
||||
stashIntro: 'ติดตาม Gear ตั้งราคา และหาเทรดกับนักสเก็ตคนอื่น',
|
||||
encryptionRequired: 'ต้องเข้ารหัส',
|
||||
encryptionRequiredDesc: 'ใช้ตัวเซ็นที่รองรับ NIP-44 (Alby, nos2x, Amber)',
|
||||
myStash: 'Stash ของฉัน',
|
||||
encrypted: 'เข้ารหัสแล้ว',
|
||||
itemsCount: '{{count}} รายการ',
|
||||
items: 'รายการ',
|
||||
wishlist: 'Wishlist',
|
||||
types: 'ประเภท',
|
||||
stashEmpty: 'Stash ของคุณว่างเปล่า',
|
||||
addFirstItem: 'เพิ่มรายการแรก',
|
||||
wishlistCount: 'Wishlist ({{count}})',
|
||||
myGearCount: 'Gear ของฉัน ({{count}})',
|
||||
openFullStash: 'เปิด Stash เต็ม',
|
||||
addItem: 'เพิ่มรายการ',
|
||||
searchSpotsPlaceholder: 'ค้นหาสปอต...',
|
||||
selected: 'เลือกแล้ว',
|
||||
noSpotsFound: 'ไม่พบสปอต ลองค้นหาอย่างอื่น',
|
||||
addingSpotsHint: 'การเพิ่มสปอตช่วยให้คนอื่นเจอคุณเมื่อดูตำแหน่งนั้น',
|
||||
report: 'รายงาน',
|
||||
spotSingular: '1 สปอต',
|
||||
spotPlural: '{{count}} สปอต',
|
||||
view: 'ดู',
|
||||
myCollections: 'คอลเลกชันของฉัน',
|
||||
collectionsSubtitle: 'จัดสปอตโปรดเป็นลิสต์ของคุณเอง',
|
||||
newCollection: 'คอลเลกชันใหม่',
|
||||
loginToCreate: 'เข้าสู่ระบบเพื่อสร้างคอลเลกชัน',
|
||||
loginToCreateDesc: 'บันทึกสปอตโปรดเป็นลิสต์ เช่น "ทริปบาร์เซโลนา 2024" หรือ "Best Rails in LA"',
|
||||
noCollections: 'ยังไม่มีคอลเลกชัน',
|
||||
noCollectionsDesc: 'สร้างคอลเลกชันแรกเพื่อเริ่มจัดสปอต',
|
||||
createFirstCollection: 'สร้างคอลเลกชันแรก',
|
||||
createNewCollection: 'สร้างคอลเลกชันใหม่',
|
||||
createCollectionDesc: 'ตั้งชื่อคอลเลกชันและคำอธิบาย (ไม่บังคับ)',
|
||||
collectionName: 'ชื่อคอลเลกชัน',
|
||||
collectionNamePlaceholder: 'ทริปบาร์เซโลนา 2024',
|
||||
descriptionOptional: 'คำอธิบาย (ไม่บังคับ)',
|
||||
descriptionPlaceholder: 'สปอตโปรดจากทริปบาร์เซโลนาของฉัน...',
|
||||
creating: 'กำลังสร้าง...',
|
||||
createCollection: 'สร้างคอลเลกชัน',
|
||||
editCollection: 'แก้ไขคอลเลกชัน',
|
||||
saving: 'กำลังบันทึก...',
|
||||
saveChanges: 'บันทึกการเปลี่ยนแปลง',
|
||||
deleteCollectionTitle: 'ลบคอลเลกชัน?',
|
||||
deleteCollectionDesc: 'การลบ "{{title}}" จะถาวร ตัวสปอตจะไม่ได้รับผลกระทบ',
|
||||
deleting: 'กำลังลบ...',
|
||||
collectionCreated: 'สร้างคอลเลกชันแล้ว!',
|
||||
collectionCreatedDesc: '"{{title}}" พร้อมใส่สปอตแล้ว',
|
||||
collectionCreateFailed: 'สร้างคอลเลกชันไม่สำเร็จ',
|
||||
collectionUpdated: 'อัปเดตคอลเลกชันแล้ว!',
|
||||
changesSaved: 'บันทึกการเปลี่ยนแปลงแล้ว',
|
||||
collectionUpdateFailed: 'อัปเดตคอลเลกชันไม่สำเร็จ',
|
||||
collectionDeleted: 'ลบคอลเลกชันแล้ว',
|
||||
collectionDeletedDesc: 'ลบ "{{title}}" แล้ว',
|
||||
collectionDeleteFailed: 'ลบคอลเลกชันไม่สำเร็จ',
|
||||
somethingWentWrong: 'มีบางอย่างผิดพลาด',
|
||||
},
|
||||
mapPage: {
|
||||
title: 'ร้านและสปอต',
|
||||
spots: 'Spots',
|
||||
shops: 'Shops',
|
||||
filters: 'ตัวกรอง',
|
||||
listView: 'รายการ',
|
||||
mapView: 'แผนที่',
|
||||
nearMe: 'ใกล้ฉัน',
|
||||
searchPlaceholder: 'ค้นหาสปอต...',
|
||||
noSpotsInArea: 'ไม่มีสปอตในพื้นที่นี้',
|
||||
zoomIn: 'ซูมเข้า',
|
||||
zoomOut: 'ซูมออก',
|
||||
myLocation: 'ตำแหน่งของฉัน',
|
||||
locationsFound: 'พบ {{count}} ตำแหน่ง',
|
||||
spotsOnly: 'เฉพาะ Spots',
|
||||
shopsOnly: 'เฉพาะ Shops',
|
||||
spotsAndShops: 'Spots & Shops',
|
||||
noSpotsMatch: 'ไม่มีสปอตที่ตรงกับตัวกรอง',
|
||||
noSpotsYet: 'ยังไม่มีการเพิ่มสปอต',
|
||||
addFirstSpot: 'เพิ่มสปอตแรก',
|
||||
},
|
||||
};
|
||||
|
||||
export default thBatch2;
|
||||
17
src/i18n/th/index.ts
Normal file
17
src/i18n/th/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import batch1 from './batch1';
|
||||
import batch2 from './batch2';
|
||||
|
||||
/**
|
||||
* Thai (ไทย) translations — composed from fragments so parallel translation
|
||||
* batches never touch one giant file (same pattern as `i18n/batch3|batch4`).
|
||||
*
|
||||
* Partial by design: any key not yet translated falls back to English, and
|
||||
* `scripts/check-i18n.mjs` reports the coverage. Add new fragments here as the
|
||||
* remaining namespaces land.
|
||||
*/
|
||||
const th = {
|
||||
...batch1,
|
||||
...batch2,
|
||||
};
|
||||
|
||||
export default th;
|
||||
|
|
@ -149,18 +149,18 @@
|
|||
/* ── Display font utilities ───────────────────────────────────────────────── */
|
||||
/* Bebas Neue — used for all major headlines */
|
||||
.font-display {
|
||||
font-family: 'Bebas Neue', 'Impact', 'Arial Black', sans-serif;
|
||||
font-family: 'Bebas Neue', 'Kanit', 'Impact', 'Arial Black', sans-serif;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* Barlow Condensed — used for subheadings, labels, nav */
|
||||
.font-condensed {
|
||||
font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
|
||||
font-family: 'Barlow Condensed', 'Kanit', 'Arial Narrow', sans-serif;
|
||||
}
|
||||
|
||||
.neon-header {
|
||||
animation: neon-glow 2s ease-in-out infinite;
|
||||
font-family: 'Bebas Neue', 'Impact', 'Arial Black', sans-serif;
|
||||
font-family: 'Bebas Neue', 'Kanit', 'Impact', 'Arial Black', sans-serif;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
|
|
@ -714,7 +714,7 @@ select {
|
|||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
font-family: 'Inter Variable', 'Inter', system-ui, sans-serif;
|
||||
font-family: 'Inter Variable', 'Inter', 'Kanit', system-ui, sans-serif;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.6), 0 2px 8px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
formatDistance as _formatDistance,
|
||||
format as _format,
|
||||
} from 'date-fns';
|
||||
import { enUS, es as esLocale } from 'date-fns/locale';
|
||||
import { enUS, es as esLocale, th as thLocale } from 'date-fns/locale';
|
||||
import type { Locale } from 'date-fns';
|
||||
import type { Language } from '@/contexts/language';
|
||||
|
||||
|
|
@ -28,8 +28,16 @@ let activeCode = 'en-US';
|
|||
|
||||
/** Called by LanguageProvider (during render) so children format in-language. */
|
||||
export function setDateLocale(language: Language): void {
|
||||
activeLocale = language === 'es' ? esLocale : enUS;
|
||||
activeCode = language === 'es' ? 'es-ES' : 'en-US';
|
||||
if (language === 'es') {
|
||||
activeLocale = esLocale;
|
||||
activeCode = 'es-ES';
|
||||
} else if (language === 'th') {
|
||||
activeLocale = thLocale;
|
||||
activeCode = 'th-TH';
|
||||
} else {
|
||||
activeLocale = enUS;
|
||||
activeCode = 'en-US';
|
||||
}
|
||||
}
|
||||
|
||||
/** The active date-fns locale. */
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ import '@fontsource/bebas-neue';
|
|||
import '@fontsource/barlow-condensed/400.css';
|
||||
import '@fontsource/barlow-condensed/600.css';
|
||||
import '@fontsource/barlow-condensed/700.css';
|
||||
// Kanit — Thai glyphs (Latin/Thai stack fallback; the other fonts have no Thai)
|
||||
import '@fontsource/kanit/400.css';
|
||||
import '@fontsource/kanit/500.css';
|
||||
import '@fontsource/kanit/600.css';
|
||||
import '@fontsource/kanit/700.css';
|
||||
|
||||
// Leaflet CSS is loaded via CDN in index.html
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default {
|
|||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Outfit Variable', 'Outfit', 'system-ui', 'sans-serif'],
|
||||
sans: ['Outfit Variable', 'Outfit', 'Kanit', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
|
|
|
|||
Loading…
Reference in a new issue