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)
|
## 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
|
- The app ships **English + Spanish (Castellano, Spain)**. The framework already
|
||||||
exists: `src/contexts/language.ts` (context/types/hooks) + `LanguageContext.tsx`
|
exists: `src/contexts/language.ts` (context/types/hooks) + `LanguageContext.tsx`
|
||||||
(provider), `src/i18n/en.ts` + `src/i18n/es.ts` (one default-export object
|
(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-variable/outfit": "^5.2.8",
|
||||||
"@fontsource/barlow-condensed": "^5.2.8",
|
"@fontsource/barlow-condensed": "^5.2.8",
|
||||||
"@fontsource/bebas-neue": "^5.2.7",
|
"@fontsource/bebas-neue": "^5.2.7",
|
||||||
|
"@fontsource/kanit": "^5.3.0",
|
||||||
"@getalby/sdk": "^5.1.1",
|
"@getalby/sdk": "^5.1.1",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
|
@ -1501,6 +1502,15 @@
|
||||||
"url": "https://github.com/sponsors/ayuhito"
|
"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": {
|
"node_modules/@getalby/lightning-tools": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz",
|
"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-variable/outfit": "^5.2.8",
|
||||||
"@fontsource/barlow-condensed": "^5.2.8",
|
"@fontsource/barlow-condensed": "^5.2.8",
|
||||||
"@fontsource/bebas-neue": "^5.2.7",
|
"@fontsource/bebas-neue": "^5.2.7",
|
||||||
|
"@fontsource/kanit": "^5.3.0",
|
||||||
"@getalby/sdk": "^5.1.1",
|
"@getalby/sdk": "^5.1.1",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@netlify/blobs": "^11.1.1",
|
"@netlify/blobs": "^11.1.1",
|
||||||
"@noble/curves": "^2.0.1",
|
"@noble/curves": "^2.0.1",
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
"@scure/base": "^1.1.1",
|
|
||||||
"@nostrify/nostrify": "^0.47.1",
|
"@nostrify/nostrify": "^0.47.1",
|
||||||
"@nostrify/react": "^0.2.17",
|
"@nostrify/react": "^0.2.17",
|
||||||
"@nostrify/types": "^0.36.7",
|
"@nostrify/types": "^0.36.7",
|
||||||
|
|
@ -77,6 +77,7 @@
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
|
"@scure/base": "^1.1.1",
|
||||||
"@scure/bip32": "^2.0.1",
|
"@scure/bip32": "^2.0.1",
|
||||||
"@scure/bip39": "1.6.0",
|
"@scure/bip39": "1.6.0",
|
||||||
"@std/encoding": "npm:@jsr/std__encoding@^0.224.3",
|
"@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
|
* 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).
|
* `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.
|
* 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
|
* Translations are loaded with esbuild (already a dependency) so the TypeScript
|
||||||
* source files can be imported directly without a build step.
|
* source files can be imported directly without a build step.
|
||||||
|
|
@ -98,6 +101,7 @@ function collectUsedKeys(files) {
|
||||||
async function main() {
|
async function main() {
|
||||||
const en = flatten(await loadTranslations(join(SRC, 'i18n/en.ts')));
|
const en = flatten(await loadTranslations(join(SRC, 'i18n/en.ts')));
|
||||||
const es = flatten(await loadTranslations(join(SRC, 'i18n/es.ts')));
|
const es = flatten(await loadTranslations(join(SRC, 'i18n/es.ts')));
|
||||||
|
const th = flatten(await loadTranslations(join(SRC, 'i18n/th/index.ts')));
|
||||||
|
|
||||||
const problems = [];
|
const problems = [];
|
||||||
|
|
||||||
|
|
@ -112,6 +116,13 @@ async function main() {
|
||||||
if (!es.has(key)) problems.push(`used but missing in es: ${key} (${files[0]})`);
|
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) {
|
if (problems.length) {
|
||||||
console.error(`\n✖ i18n check failed (${problems.length}):`);
|
console.error(`\n✖ i18n check failed (${problems.length}):`);
|
||||||
for (const p of problems.slice(0, 200)) console.error(` • ${p}`);
|
for (const p of problems.slice(0, 200)) console.error(` • ${p}`);
|
||||||
|
|
@ -119,7 +130,7 @@ async function main() {
|
||||||
process.exit(1);
|
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) => {
|
main().catch((err) => {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ interface LanguageOption {
|
||||||
const languages: LanguageOption[] = [
|
const languages: LanguageOption[] = [
|
||||||
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧' },
|
{ code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧' },
|
||||||
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
{ code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸' },
|
||||||
|
{ code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface LanguageToggleProps {
|
interface LanguageToggleProps {
|
||||||
|
|
|
||||||
|
|
@ -1113,7 +1113,9 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
|
||||||
// the app's current language, keeping skate trick names in English.
|
// the app's current language, keeping skate trick names in English.
|
||||||
const languageDirective = language === 'es'
|
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.'
|
? '\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
|
const systemPrompt = (personal.available && isPro
|
||||||
? `${SYSTEM_PROMPT}${PERSONAL_TOOLS_PROMPT}`
|
? `${SYSTEM_PROMPT}${PERSONAL_TOOLS_PROMPT}`
|
||||||
: SYSTEM_PROMPT) + languageDirective;
|
: SYSTEM_PROMPT) + languageDirective;
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,16 @@ interface LanguageProviderProps {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pick the starting language from the visitor's browser on their FIRST visit.
|
* 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
|
* A device set to Thai (`th`, `th-TH`) lands in Thai; Spanish (`es-*`) in
|
||||||
* else falls back to English. Once the user taps the language toggle their
|
* Castellano; anything else falls back to English. Once the user taps the
|
||||||
* choice is stored and always wins.
|
* language toggle their choice is stored and always wins.
|
||||||
*/
|
*/
|
||||||
function detectLanguage(): Language {
|
function detectLanguage(): Language {
|
||||||
if (typeof navigator === 'undefined') return 'en';
|
if (typeof navigator === 'undefined') return 'en';
|
||||||
const tags = [navigator.language, ...(navigator.languages ?? [])];
|
const tags = [navigator.language, ...(navigator.languages ?? [])].map((t) => t?.toLowerCase() ?? '');
|
||||||
return tags.some((tag) => tag?.toLowerCase().startsWith('es')) ? 'es' : 'en';
|
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 { createContext, useContext } from 'react';
|
||||||
import en from '@/i18n/en';
|
import en from '@/i18n/en';
|
||||||
import es from '@/i18n/es';
|
import es from '@/i18n/es';
|
||||||
|
import th from '@/i18n/th';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Language context + types + hooks. Split from the `LanguageProvider` component
|
* Language context + types + hooks. Split from the `LanguageProvider` component
|
||||||
|
|
@ -8,7 +9,7 @@ import es from '@/i18n/es';
|
||||||
* `react-refresh/only-export-components` rule.
|
* `react-refresh/only-export-components` rule.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type Language = 'en' | 'es';
|
export type Language = 'en' | 'es' | 'th';
|
||||||
|
|
||||||
type TranslationKey = string;
|
type TranslationKey = string;
|
||||||
type TranslationValue = string | { [key: string]: TranslationValue };
|
type TranslationValue = string | { [key: string]: TranslationValue };
|
||||||
|
|
@ -30,7 +31,7 @@ export interface LanguageContextType {
|
||||||
|
|
||||||
export const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
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`). */
|
/** Get a nested value from an object using dot notation (`nav.home`). */
|
||||||
export function getNestedValue(obj: Translations, path: string): string | undefined {
|
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 ───────────────────────────────────────────────── */
|
/* ── Display font utilities ───────────────────────────────────────────────── */
|
||||||
/* Bebas Neue — used for all major headlines */
|
/* Bebas Neue — used for all major headlines */
|
||||||
.font-display {
|
.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;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Barlow Condensed — used for subheadings, labels, nav */
|
/* Barlow Condensed — used for subheadings, labels, nav */
|
||||||
.font-condensed {
|
.font-condensed {
|
||||||
font-family: 'Barlow Condensed', 'Arial Narrow', sans-serif;
|
font-family: 'Barlow Condensed', 'Kanit', 'Arial Narrow', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.neon-header {
|
.neon-header {
|
||||||
animation: neon-glow 2s ease-in-out infinite;
|
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;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -714,7 +714,7 @@ select {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
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);
|
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,
|
formatDistance as _formatDistance,
|
||||||
format as _format,
|
format as _format,
|
||||||
} from 'date-fns';
|
} 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 { Locale } from 'date-fns';
|
||||||
import type { Language } from '@/contexts/language';
|
import type { Language } from '@/contexts/language';
|
||||||
|
|
||||||
|
|
@ -28,8 +28,16 @@ let activeCode = 'en-US';
|
||||||
|
|
||||||
/** Called by LanguageProvider (during render) so children format in-language. */
|
/** Called by LanguageProvider (during render) so children format in-language. */
|
||||||
export function setDateLocale(language: Language): void {
|
export function setDateLocale(language: Language): void {
|
||||||
activeLocale = language === 'es' ? esLocale : enUS;
|
if (language === 'es') {
|
||||||
activeCode = language === 'es' ? 'es-ES' : 'en-US';
|
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. */
|
/** The active date-fns locale. */
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ import '@fontsource/bebas-neue';
|
||||||
import '@fontsource/barlow-condensed/400.css';
|
import '@fontsource/barlow-condensed/400.css';
|
||||||
import '@fontsource/barlow-condensed/600.css';
|
import '@fontsource/barlow-condensed/600.css';
|
||||||
import '@fontsource/barlow-condensed/700.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
|
// Leaflet CSS is loaded via CDN in index.html
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export default {
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Outfit Variable', 'Outfit', 'system-ui', 'sans-serif'],
|
sans: ['Outfit Variable', 'Outfit', 'Kanit', 'system-ui', 'sans-serif'],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: 'hsl(var(--border))',
|
border: 'hsl(var(--border))',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue