This commit is contained in:
Your Name committed 2026-07-21 21:22:40 -04:00
1 parent cfd642dee0
commit ae3799d8ce
21 files changed
+172 -30

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ export default function CategoriesPage() {
const [isUploading, setIsUploading] = useState(false) const [isUploading, setIsUploading] = useState(false)
const [editingCategory, setEditingCategory] = useState<Partial<Category> | null>(null) const [editingCategory, setEditingCategory] = useState<Partial<Category> | null>(null)
const [activeLang, setActiveLang] = useState('th') const [activeLang, setActiveLang] = useState('th')
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it']
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false) const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
const [onSelectMedia, setOnSelectMedia] = useState<((url: string) => void) | null>(null) const [onSelectMedia, setOnSelectMedia] = useState<((url: string) => void) | null>(null)
+1 -1
View File
@@ -1036,7 +1036,7 @@ export default function PageBuilderPage() {
{/* Premium Sub-header for Language Tabs */} {/* Premium Sub-header for Language Tabs */}
<div className="px-8 py-4 border-b border-white/5 bg-night-950/50 flex items-center overflow-x-auto hide-scrollbar"> <div className="px-8 py-4 border-b border-white/5 bg-night-950/50 flex items-center overflow-x-auto hide-scrollbar">
<div className="bg-black/40 p-1 rounded-xl flex gap-1 border border-white/5"> <div className="bg-black/40 p-1 rounded-xl flex gap-1 border border-white/5">
{['th', 'en', 'ja', 'zh', 'vi', 'ko'].map((lang) => ( {['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'].map((lang) => (
<button <button
key={lang} key={lang}
type="button" type="button"
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import { API_BASE, type AttributeDefinition, type Category } from '@homaroma/types' import { API_BASE, type AttributeDefinition, type Category } from '@homaroma/types'
import { Edit3, Plus, Save, Settings2, Trash2, X } from 'lucide-react' import { Edit3, Plus, Save, Settings2, Trash2, X } from 'lucide-react'
const LANGUAGES = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] as const const LANGUAGES = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'] as const
const ATTRIBUTE_TYPES = ['TEXT', 'NUMBER', 'BOOLEAN', 'SELECT', 'JSON'] as const const ATTRIBUTE_TYPES = ['TEXT', 'NUMBER', 'BOOLEAN', 'SELECT', 'JSON'] as const
type EditableDefinition = { type EditableDefinition = {
+1 -1
View File
@@ -185,7 +185,7 @@ export default function ProductCmsPage() {
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false) const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
const [mediaTargetField, setMediaTargetField] = useState<'imageUrl' | null>(null) const [mediaTargetField, setMediaTargetField] = useState<'imageUrl' | null>(null)
const [isProductPickerOpen, setIsProductPickerOpen] = useState(false) const [isProductPickerOpen, setIsProductPickerOpen] = useState(false)
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it']
const selectedSection = selectedSectionId === 'new' const selectedSection = selectedSectionId === 'new'
? null ? null
: detailSections.find((section) => section.id === selectedSectionId) || null : detailSections.find((section) => section.id === selectedSectionId) || null
+1 -1
View File
@@ -44,7 +44,7 @@ export default function ProductEditPage() {
// Form State // Form State
const [activeLang, setActiveLang] = useState('th') const [activeLang, setActiveLang] = useState('th')
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it']
const [categoryId, setCategoryId] = useState<number>(0) const [categoryId, setCategoryId] = useState<number>(0)
const [name, setName] = useState<Record<string, string>>({}) const [name, setName] = useState<Record<string, string>>({})
@@ -21,7 +21,7 @@ const IconSelect = ({ value, onChange }: { value?: string, onChange: (val: strin
) )
} }
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko']; const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
type MenuEditorPageProps = { type MenuEditorPageProps = {
menuName: 'main' | 'footer-explore' | 'footer-support'; menuName: 'main' | 'footer-explore' | 'footer-support';
+1 -1
View File
@@ -1,4 +1,4 @@
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] as const; const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'] as const;
export const SUPPORTED_LANGS = LANGS; export const SUPPORTED_LANGS = LANGS;
+1 -1
View File
@@ -118,7 +118,7 @@ export const articlesRoute = new Elysia({ prefix: "/articles" })
const payload = body as any; const payload = body as any;
const results: any[] = []; const results: any[] = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko']; const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
for (const lang of LANGS) { for (const lang of LANGS) {
const title = typeof payload.title === 'string' ? (lang === 'th' ? payload.title : null) : payload.title?.[lang]; const title = typeof payload.title === 'string' ? (lang === 'th' ? payload.title : null) : payload.title?.[lang];
+1 -1
View File
@@ -1,7 +1,7 @@
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import prisma from "../lib/prisma"; import prisma from "../lib/prisma";
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko']; const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
/** Resolve category name based on lang */ /** Resolve category name based on lang */
function resolveCatName(cat: any, lang: string): any { function resolveCatName(cat: any, lang: string): any {
+1 -1
View File
@@ -198,7 +198,7 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
const payload = body as any; const payload = body as any;
const results: any[] = []; const results: any[] = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko']; const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
for (const lang of LANGS) { for (const lang of LANGS) {
const label = typeof payload.label === 'string' ? (lang === 'th' ? payload.label : null) : payload.label?.[lang]; const label = typeof payload.label === 'string' ? (lang === 'th' ? payload.label : null) : payload.label?.[lang];
+1 -1
View File
@@ -340,7 +340,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" })
const payload = body as any; const payload = body as any;
const results: any[] = []; const results: any[] = [];
const LANGS = ["th", "en", "ja", "zh", "vi", "ko"]; const LANGS = ["th", "en", "ja", "zh", "vi", "ko", "lo", "it"];
for (const lang of LANGS) { for (const lang of LANGS) {
const title = const title =
+2 -2
View File
@@ -68,7 +68,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
const payload = body as any; const payload = body as any;
const results: any[] = []; const results: any[] = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko']; const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
for (const lang of LANGS) { for (const lang of LANGS) {
const name = typeof payload.name === 'string' ? (lang === 'th' ? payload.name : null) : payload.name?.[lang]; const name = typeof payload.name === 'string' ? (lang === 'th' ? payload.name : null) : payload.name?.[lang];
@@ -925,7 +925,7 @@ export const productsRoute = new Elysia({ prefix: "/products" })
// Update translations // Update translations
// If name, description, details are strings, treat as 'th' // If name, description, details are strings, treat as 'th'
// If they are objects, iterate over them // If they are objects, iterate over them
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko']; const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
for (const lang of languages) { for (const lang of languages) {
const updateData: any = {}; const updateData: any = {};
let shouldUpdate = false; let shouldUpdate = false;
+1 -1
View File
@@ -72,7 +72,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" })
const payload = body as any; const payload = body as any;
const results: any[] = []; const results: any[] = [];
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko']; const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'];
for (const lang of LANGS) { for (const lang of LANGS) {
const footerDescription = typeof payload.footerDescription === 'string' ? (lang === 'th' ? payload.footerDescription : null) : payload.footerDescription?.[lang]; const footerDescription = typeof payload.footerDescription === 'string' ? (lang === 'th' ? payload.footerDescription : null) : payload.footerDescription?.[lang];
@@ -13,6 +13,7 @@ import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import ProductCard from '@/components/ProductCard' import ProductCard from '@/components/ProductCard'
import { getCategoriesResult, getProduct, getProductDetailSectionsResult, getProductResult, getProductReviewsResult, getProductsResult, getStoreSettingsResult, type ProductReview } from '@/lib/cms' import { getCategoriesResult, getProduct, getProductDetailSectionsResult, getProductResult, getProductReviewsResult, getProductsResult, getStoreSettingsResult, type ProductReview } from '@/lib/cms'
import { resolveCategoryName, resolveLocalized } from '@/lib/localization' import { resolveCategoryName, resolveLocalized } from '@/lib/localization'
import { SUPPORTED_LOCALES } from '@/lib/locales'
import { extractIdFromSlug, generateProductSlug } from '@/lib/slugify' import { extractIdFromSlug, generateProductSlug } from '@/lib/slugify'
import type { ProductAttribute } from '@homaroma/types' import type { ProductAttribute } from '@homaroma/types'
@@ -361,8 +362,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://www.homaroma.com' const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://www.homaroma.com'
const customLanguages: Record<string, string> = {} const customLanguages: Record<string, string> = {}
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] for (const l of SUPPORTED_LOCALES) {
for (const l of languages) {
const locName = resolveLocalized(product.name, l) || product.brand.name const locName = resolveLocalized(product.name, l) || product.brand.name
customLanguages[l] = `${baseUrl}/${l}/products/${generateProductSlug(locName, product.id)}` customLanguages[l] = `${baseUrl}/${l}/products/${generateProductSlug(locName, product.id)}`
} }
+9 -10
View File
@@ -2,14 +2,13 @@ import { MetadataRoute } from 'next';
import { getCategories, getProducts, getArticlesResult } from '../lib/cms'; import { getCategories, getProducts, getArticlesResult } from '../lib/cms';
import { generateProductSlug } from '../lib/slugify'; import { generateProductSlug } from '../lib/slugify';
import { resolveLocalized } from '../lib/localization'; import { resolveLocalized } from '../lib/localization';
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '../lib/locales';
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
function createAlternates(path: string) { function createAlternates(path: string) {
const alternates: Record<string, string> = {}; const alternates: Record<string, string> = {};
for (const lang of languages) { for (const lang of SUPPORTED_LOCALES) {
alternates[lang] = `${baseUrl}/${lang}${path}`; alternates[lang] = `${baseUrl}/${lang}${path}`;
} }
return alternates; return alternates;
@@ -32,7 +31,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
]; ];
const routes: MetadataRoute.Sitemap = staticPages.map((route) => ({ const routes: MetadataRoute.Sitemap = staticPages.map((route) => ({
url: `${baseUrl}/th${route}`, url: `${baseUrl}/${DEFAULT_LOCALE}${route}`,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: 'daily', changeFrequency: 'daily',
priority: route === '' ? 1 : 0.8, priority: route === '' ? 1 : 0.8,
@@ -42,7 +41,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
})); }));
const categoryRoutes: MetadataRoute.Sitemap = categories.map((category) => ({ const categoryRoutes: MetadataRoute.Sitemap = categories.map((category) => ({
url: `${baseUrl}/th/category/${category.id}`, url: `${baseUrl}/${DEFAULT_LOCALE}/category/${category.id}`,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: 'weekly', changeFrequency: 'weekly',
priority: 0.7, priority: 0.7,
@@ -53,17 +52,17 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const productRoutes: MetadataRoute.Sitemap = products.map((product) => { const productRoutes: MetadataRoute.Sitemap = products.map((product) => {
const alternates: Record<string, string> = {}; const alternates: Record<string, string> = {};
let thSlug = ''; let defaultSlug = '';
for (const lang of languages) { for (const lang of SUPPORTED_LOCALES) {
const localizedName = resolveLocalized(product.name, lang) || (product.brand ? product.brand.name : 'product'); const localizedName = resolveLocalized(product.name, lang) || (product.brand ? product.brand.name : 'product');
const slug = generateProductSlug(localizedName, product.id); const slug = generateProductSlug(localizedName, product.id);
alternates[lang] = `${baseUrl}/${lang}/products/${slug}`; alternates[lang] = `${baseUrl}/${lang}/products/${slug}`;
if (lang === 'th') thSlug = slug; if (lang === DEFAULT_LOCALE) defaultSlug = slug;
} }
return { return {
url: `${baseUrl}/th/products/${thSlug}`, url: `${baseUrl}/${DEFAULT_LOCALE}/products/${defaultSlug}`,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: 'daily', changeFrequency: 'daily',
priority: 0.9, priority: 0.9,
@@ -77,7 +76,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
if (articlesResult.ok && articlesResult.data) { if (articlesResult.ok && articlesResult.data) {
articlesResult.data.forEach((article) => { articlesResult.data.forEach((article) => {
articleRoutes.push({ articleRoutes.push({
url: `${baseUrl}/th/article/${article.slug}`, url: `${baseUrl}/${DEFAULT_LOCALE}/article/${article.slug}`,
lastModified: new Date(article.updatedAt || article.createdAt), lastModified: new Date(article.updatedAt || article.createdAt),
changeFrequency: 'monthly', changeFrequency: 'monthly',
priority: 0.6, priority: 0.6,
@@ -7,13 +7,14 @@ import i18n from '../lib/i18n';
import { useCartStore } from '../store/cartStore'; import { useCartStore } from '../store/cartStore';
import { useWishlistStore } from '../store/wishlistStore'; import { useWishlistStore } from '../store/wishlistStore';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '../lib/locales';
export default function ClientProviders({ children }: { children: ReactNode }) { export default function ClientProviders({ children }: { children: ReactNode }) {
useEffect(() => { useEffect(() => {
// With Locale Routing, the URL is the source of truth for language. // With Locale Routing, the URL is the source of truth for language.
// Sync i18n to match the current URL if needed. // Sync i18n to match the current URL if needed.
const pathLang = window.location.pathname.split('/')[1]; const pathLang = window.location.pathname.split('/')[1];
const targetLang = pathLang && ['th', 'en', 'ja', 'zh', 'vi', 'ko'].includes(pathLang) ? pathLang : 'th'; const targetLang = pathLang && (SUPPORTED_LOCALES as readonly string[]).includes(pathLang) ? pathLang : DEFAULT_LOCALE;
if (targetLang !== i18n.language) { if (targetLang !== i18n.language) {
void i18n.changeLanguage(targetLang); void i18n.changeLanguage(targetLang);
+4 -2
View File
@@ -2,7 +2,7 @@
import Image from 'next/image' import Image from 'next/image'
import Link from './LocalizedLink' import Link from './LocalizedLink'
import { getPathWithoutLocale, getPathWithLocale } from '../lib/locales' import { getPathWithoutLocale, getPathWithLocale, type Locale } from '../lib/locales'
import { useRouter, usePathname } from 'next/navigation' import { useRouter, usePathname } from 'next/navigation'
import { useState, useEffect, useMemo, useRef } from 'react' import { useState, useEffect, useMemo, useRef } from 'react'
import type { Category } from '@homaroma/types' import type { Category } from '@homaroma/types'
@@ -31,7 +31,9 @@ const LANGUAGES = [
{ code: 'zh', label: '简体中文', short: 'ZH' }, { code: 'zh', label: '简体中文', short: 'ZH' },
{ code: 'vi', label: 'Tiếng Việt', short: 'VN' }, { code: 'vi', label: 'Tiếng Việt', short: 'VN' },
{ code: 'ko', label: '한국어', short: 'KO' }, { code: 'ko', label: '한국어', short: 'KO' },
] { code: 'lo', label: 'ລາວ', short: 'LO' },
{ code: 'it', label: 'Italiano', short: 'IT' },
] satisfies Array<{ code: Locale; label: string; short: string }>
function setLanguageCookie(code: string) { function setLanguageCookie(code: string) {
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
+4
View File
@@ -7,6 +7,8 @@ import jaTranslation from '../locales/ja/translation.json'
import zhTranslation from '../locales/zh/translation.json' import zhTranslation from '../locales/zh/translation.json'
import viTranslation from '../locales/vi/translation.json' import viTranslation from '../locales/vi/translation.json'
import koTranslation from '../locales/ko/translation.json' import koTranslation from '../locales/ko/translation.json'
import loTranslation from '../locales/lo/translation.json'
import itTranslation from '../locales/it/translation.json'
// Check if it's already initialized to prevent re-initialization on HMR // Check if it's already initialized to prevent re-initialization on HMR
if (!i18n.isInitialized) { if (!i18n.isInitialized) {
@@ -20,6 +22,8 @@ if (!i18n.isInitialized) {
zh: { translation: zhTranslation }, zh: { translation: zhTranslation },
vi: { translation: viTranslation }, vi: { translation: viTranslation },
ko: { translation: koTranslation }, ko: { translation: koTranslation },
lo: { translation: loTranslation },
it: { translation: itTranslation },
}, },
lng: 'th', lng: 'th',
fallbackLng: 'en', fallbackLng: 'en',
+1 -1
View File
@@ -1,4 +1,4 @@
export const SUPPORTED_LOCALES = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] as const; export const SUPPORTED_LOCALES = ['th', 'en', 'ja', 'zh', 'vi', 'ko', 'lo', 'it'] as const;
export type Locale = typeof SUPPORTED_LOCALES[number]; export type Locale = typeof SUPPORTED_LOCALES[number];
export const DEFAULT_LOCALE: Locale = 'th'; export const DEFAULT_LOCALE: Locale = 'th';
@@ -0,0 +1,68 @@
{
"nav": {
"home": "Home",
"shop": "Negozio",
"promotions": "Promozioni",
"collections": "Collezioni",
"scentGuide": "Guida alle fragranze",
"about": "Chi siamo",
"menu": "Menu",
"faq": "FAQ",
"contact": "Contatti",
"shopNow": "Acquista ora",
"findMyScent": "Trova la tua fragranza ✨",
"search": {
"title": "Cerca nella collezione",
"placeholder": "Cerca fragranze, categorie...",
"quickLinks": "Link rapidi",
"allProducts": "Tutti i prodotti"
}
},
"cart": {
"title": "Il tuo carrello",
"items": "{{count}} articoli",
"empty": "Il tuo carrello è vuoto.",
"continueShopping": "Continua lo shopping",
"subtotal": "Subtotale",
"shippingNote": "Spedizione e tasse saranno calcolate al checkout.",
"proceed": "Vai al checkout"
},
"footer": {
"catchphrase": "Eleva il tuo spazio con fragranze premium.",
"explore": "Esplora",
"support": "Supporto",
"followUs": "Seguici",
"contactUs": "Contattaci",
"rights": "Tutti i diritti riservati."
},
"dynamic": {
"heroLearnMore": "Scopri di più",
"products": "Prodotti",
"viewAll": "Vedi tutta la collezione →",
"emptyProducts": "Nessun prodotto trovato in questa sezione.",
"authorBy": "Di",
"notes": {
"top": "Note di testa",
"heart": "Note di cuore",
"base": "Note di fondo"
},
"contactInfo": {
"address": "Indirizzo",
"email": "Email",
"line": "Line",
"hours": "Orari"
}
},
"contactForm": {
"title": "Contattaci",
"subtitle": "Invia un messaggio al nostro team e ti risponderemo presto.",
"successTitle": "Messaggio inviato con successo",
"successBody": "Grazie per averci contattato. Il nostro team risponderà il prima possibile.",
"name": "Nome completo",
"namePlaceholder": "Inserisci il tuo nome",
"email": "Email",
"message": "Messaggio",
"messagePlaceholder": "Scrivi qui il tuo messaggio...",
"submit": "Invia messaggio"
}
}
@@ -0,0 +1,68 @@
{
"nav": {
"home": "ໜ້າຫຼັກ",
"shop": "ຮ້ານຄ້າ",
"promotions": "ໂປຣໂມຊັນ",
"collections": "ຄໍເລັກຊັນ",
"scentGuide": "ຄູ່ມືກິ່ນຫອມ",
"about": "ກ່ຽວກັບພວກເຮົາ",
"menu": "ເມນູ",
"faq": "ຄຳຖາມທີ່ພົບເລື້ອຍ",
"contact": "ຕິດຕໍ່",
"shopNow": "ຊື້ຕອນນີ້",
"findMyScent": "ຄົ້ນຫາກິ່ນທີ່ເໝາະກັບທ່ານ ✨",
"search": {
"title": "ຄົ້ນຫາຄໍເລັກຊັນ",
"placeholder": "ຄົ້ນຫາກິ່ນ, ໝວດສິນຄ້າ...",
"quickLinks": "ລິ້ງດ່ວນ",
"allProducts": "ສິນຄ້າທັງໝົດ"
}
},
"cart": {
"title": "ຕະກ້າສິນຄ້າຂອງທ່ານ",
"items": "{{count}} ລາຍການ",
"empty": "ຕະກ້າສິນຄ້າຂອງທ່ານວ່າງຢູ່.",
"continueShopping": "ຊື້ສິນຄ້າຕໍ່",
"subtotal": "ຍອດລວມ",
"shippingNote": "ຄ່າຈັດສົ່ງ ແລະ ພາສີຈະຄຳນວນໃນຂັ້ນຕອນຊຳລະເງິນ.",
"proceed": "ໄປຊຳລະເງິນ"
},
"footer": {
"catchphrase": "ຍົກລະດັບພື້ນທີ່ຂອງທ່ານດ້ວຍກິ່ນຫອມພຣີມຽມ.",
"explore": "ສຳຫຼວດ",
"support": "ຊ່ວຍເຫຼືອ",
"followUs": "ຕິດຕາມພວກເຮົາ",
"contactUs": "ຕິດຕໍ່ພວກເຮົາ",
"rights": "ສະຫງວນລິຂະສິດ."
},
"dynamic": {
"heroLearnMore": "ຮຽນຮູ້ເພີ່ມເຕີມ",
"products": "ສິນຄ້າ",
"viewAll": "ເບິ່ງຄໍເລັກຊັນທັງໝົດ →",
"emptyProducts": "ບໍ່ພົບສິນຄ້າໃນສ່ວນນີ້.",
"authorBy": "ໂດຍ",
"notes": {
"top": "ກິ່ນຊັ້ນຕົ້ນ",
"heart": "ກິ່ນຊັ້ນກາງ",
"base": "ກິ່ນຊັ້ນຖານ"
},
"contactInfo": {
"address": "ທີ່ຢູ່",
"email": "ອີເມວ",
"line": "Line",
"hours": "ເວລາເຮັດການ"
}
},
"contactForm": {
"title": "ຕິດຕໍ່ພວກເຮົາ",
"subtitle": "ສົ່ງຂໍ້ຄວາມຫາທີມງານຂອງພວກເຮົາ ແລະ ພວກເຮົາຈະຕອບກັບໂດຍໄວ.",
"successTitle": "ສົ່ງຂໍ້ຄວາມສຳເລັດ",
"successBody": "ຂອບໃຈທີ່ຕິດຕໍ່ມາ. ທີມງານຈະຕອບກັບໂດຍໄວທີ່ສຸດ.",
"name": "ຊື່ເຕັມ",
"namePlaceholder": "ກອກຊື່ຂອງທ່ານ",
"email": "ອີເມວ",
"message": "ຂໍ້ຄວາມ",
"messagePlaceholder": "ພິມຂໍ້ຄວາມຂອງທ່ານທີ່ນີ້...",
"submit": "ສົ່ງຂໍ້ຄວາມ"
}
}