Adding a New Locale to MysticX
This guide covers every file and step required to add a new language to the app. Follow the stages in order.
Current locales (12): en, zh_CN, zh_TW, ja, ko, pt, es, fr, de, ar, id, nl
Stage 1: Infrastructure
These 7 files wire the new locale into the framework. Do all of them before touching any translation content.
1.1 i18n/consts.ts
Add the new locale to LOCALE_AND_LABEL_MAP (the native-language label) and LOCALE_ENGLISH_LABELS (the English label for admin UI):
export const LOCALE_AND_LABEL_MAP = {
// ...existing entries...
xx: 'Native Name', // ADD
} as const;
export const LOCALE_ENGLISH_LABELS: Record<SupportedLocale, string> = {
// ...existing entries...
xx: 'English Name', // ADD
};After this change, TypeScript will error in every
trans()call that is missing the new key. This is your work checklist for Stage 4.
1.2 i18n/routing.ts
Add the locale to the locales array. Use the URL-segment format (hyphens, not underscores — so zh-TW not zh_TW):
locales: ['en', 'zh-CN', 'zh-TW', 'ja', 'ko', 'pt', 'es', 'fr', 'de', 'ar', 'id', 'nl', 'xx'],1.3 i18n/useTrans.ts
Add a case to the normalizedLocale block. If the locale starts with zh, add it BEFORE the generic zh check to avoid it being swallowed by zh_CN:
const normalizedLocale: SupportedLocale = (() => {
const lower = locale.toLowerCase();
if (lower.startsWith('zh-tw') || lower.startsWith('zh_tw')) return 'zh_TW';
if (lower.startsWith('zh')) return 'zh_CN';
// ...existing cases...
if (lower === 'xx') return 'xx'; // ADD
return DEFAULT_LOCALE as SupportedLocale;
})();1.4 i18n/getTrans.ts
Same logic as useTrans.ts — same ordering rule applies:
if (detected?.toLowerCase() === 'xx') return 'xx'; // ADD1.5 i18n/useAppLocale.tsx
Same logic again:
if (locale.toLowerCase() === 'xx') return 'xx'; // ADD1.6 i18n/request.ts
Update the messageFile mapping to resolve the new locale to its JSON file. If the new locale has a hyphenated URL segment (like zh-TW), add it before the generic prefix check:
const messageFile =
locale.startsWith('zh-TW') || locale.startsWith('zh_TW') ? 'zh_TW'
: locale.startsWith('zh') ? 'zh_CN'
: locale;For a simple locale (xx) with no prefix collision, no change is needed here — the default pass-through handles it.
1.7 Create the message file
echo '{}' > messages/xx.jsonVerify infrastructure
pnpm tsc --noEmit 2>&1 | head -60You should see TypeScript errors for every trans() call missing xx:. That count is your Stage 4 scope.
Stage 2: Auth & Localization Files
Add the new locale key to every error message object in lib/localization/. Each file follows this pattern:
SOME_ERROR_CODE: {
en: 'Some error message',
zh_CN: '...',
// ...existing locales...
xx: '...', // ADD
},Files to update (in order of size):
| File | Notes |
|---|---|
lib/localization/anonymous-error-codes.ts | |
lib/localization/api-key-error-codes.ts | |
lib/localization/captcha-error-codes.ts | |
lib/localization/haveibeenpwned-error-codes.ts | |
lib/localization/passkey-error-codes.ts | |
lib/localization/phone-number-error-codes.ts | |
lib/localization/username-error-codes.ts | |
lib/localization/multi-session-error-codes.ts | |
lib/localization/team-error-codes.ts | |
lib/localization/two-factor-error-codes.ts | |
lib/localization/admin-error-codes.ts | |
lib/localization/generic-oauth-error-codes.ts | |
lib/localization/email-otp-error-codes.ts | |
lib/localization/organization-error-codes.ts | |
lib/localization/base-error-codes.ts | |
lib/localization/stripe-localization.ts | |
lib/localization/auth-localization.ts | Largest file — process in 500-line chunks |
lib/localization/buildAuthLocalization.ts | NO CHANGES NEEDED — derives types from TAppLocale automatically |
Stage 3: SEO & Metadata
lib/buildPageMetadata.ts
3.1 Add to localesInUrl (use the URL-segment format):
const localesInUrl = ['zh-CN', 'zh-TW', 'ja', 'ko', 'pt', 'es', 'fr', 'de', 'ar', 'id', 'nl', 'xx'];3.2 Add to alternates.languages:
languages: {
'en-US': buildUrl('en'),
// ...existing entries...
'xx-XX': buildUrl('xx'), // ADD — use correct BCP 47 tag
'x-default': buildUrl('en'),
},Stage 4: trans() Call Sites
Add the new locale key to every trans({...}) call in the app. Run this to get the current file list:
grep -rl "trans({" --include="*.tsx" --include="*.ts" app/ components/ lib/ providers/ | sortRules
- Never use English as a fallback. Every value must be in the target language.
- For JSX values like
trans({ en: <><strong>Hello</strong></> }), keep the JSX structure intact — only translate the text inside. - Spot-check each file after editing:
grep -c "xx:" <file>should roughly equalgrep -c "trans({" <file>.
Current file list (~75 files as of April 2026)
Root layout, error, and 404 pages — app/[locale]/
layout.tsxerror.tsxnot-found.tsx
Site header — app/[locale]/_components/SiteHeader/
CardOfDayButton.tsxCreditBadge.tsxLanguageModal.tsxMobileDrawerMenu.tsxNotificationBell.tsxSiteHeader.tsxUserDropdown.tsx
Site footer — app/[locale]/_components/SiteFooter/
SiteFooter.tsx
Other shared layout components — app/[locale]/_components/
OnLayoutLoaded.tsx
Home page and auth pages — app/[locale]/(main)/
page.tsxauth/[path]/page.tsxauth/telegram/page.tsx
Card picker modal — app/[locale]/(main)/_components/CardPickerModal/
CardPickerModalCenterInfo.tsxCardPickerModalControls.tsxCardRevealSpread.tsx
Home page hero — app/[locale]/(main)/_components/
HeroSection.tsx
Dashboard — app/[locale]/(main)/dashboard/
BtnSignOut.tsxpage.tsx
Profile and insights (my account) — app/[locale]/(main)/me/
_components/PersonalInfoModal.tsx_components/ProfilePageContent.tsx(insights)/_components/InsightsNav.tsx(insights)/card-of-day/_components/CardOfDayEntryModal.tsx(insights)/card-of-day/_components/CardOfDayHistoryContent.tsx(insights)/card-of-day/_components/CardOfDayListView.tsx(insights)/journal/_components/JournalContent.tsx(insights)/soul-journey/_components/SoulJourneyContent.tsx(insights)/weekly-guidance/_components/WeeklyGuidanceContent.tsxcard-skins/_components/MyCardSkinsContent.tsxinvitations/_components/InvitationsPageContent.tsxmembership/_components/MembershipContent.tsxtarot-readers/_components/MyTarotReadersContent.tsx
Membership / pricing page — app/[locale]/(main)/membership/
_components/ComparePlansSection.tsx_components/FAQSection.tsx_components/HeroSection.tsx_components/MembershipPageContent.tsx_components/PricingSection.tsxpage.tsx
Card skins market — app/[locale]/(main)/market/card-skins/
_components/CardSkinsMarketContent.tsxpage.tsx
Tarot readers market — app/[locale]/(main)/market/tarot-readers/
_components/TarotReadersMarketContent.tsx[slug]/_components/ReaderAudioPlayer.tsx[slug]/_components/ReaderCta.tsx[slug]/_components/ReaderDetailContent.tsx[slug]/_components/ReaderPersonalityWidget.tsx[slug]/page.tsxpage.tsx
Reading chat interface — app/[locale]/(main)/tarot-readings/[readingId]/_components/
ChatMessageBubble.tsxChatUserInput.tsxLuckEnhancement.tsxMessageVoiceButton.tsxReadingDetailView.tsxReadingInfoPanel.tsxSuggestedQuestions.tsx
Shared spread page components — app/[locale]/(main)/(tarot-spreads)/_components/
CardPositionsExplainedSection.tsxSpreadFAQSection.tsxSpreadHeroSection/SpreadHeroSection.tsx
Individual spread pages (13 spreads) — app/[locale]/(main)/(tarot-spreads)/
celtic-cross/_components/CelticCrossContent.tsx+page.tsxdaily-tarot/_components/DailyTarotContent.tsx+page.tsxinner-child-healing/_components/InnerChildHealingContent.tsx+page.tsxlove-tarot/_components/LoveReadingChoiceModal.tsxlove-tarot/_components/LoveTarotContent.tsx+page.tsxobstacle-key/_components/ObstacleKeyContent.tsx+page.tsxone-card/_components/OneCardContent.tsx+page.tsxrelationship-compass/_components/RelationshipCompassContent.tsx+page.tsxshadow-work/_components/ShadowWorkContent.tsx+page.tsxthree-card/_components/ThreeCardContent.tsx+page.tsxtwin-flame-mirror/_components/TwinFlameMirrorContent.tsx+page.tsxtwo-path-choice/_components/TwoPathChoiceContent.tsx+page.tsxyes-or-no/_components/YesOrNoContent.tsx+page.tsx
Account settings back-link — app/[locale]/(main)/account/
[path]/_components/BackToProfileLink.tsx
Affiliate program page — app/[locale]/(main)/affiliate-program/
_components/AffiliateProgramAllowedPromotionMethodsSection.tsx_components/AffiliateProgramCommissionLevelsSection.tsx_components/AffiliateProgramFAQ.tsx_components/AffiliateProgramFinalCtaSection.tsx_components/AffiliateProgramHeroSection.tsx_components/AffiliateProgramHowToJoinSection.tsx_components/AffiliateProgramLevelRequirementsSection.tsx_components/AffiliateProgramWhyPromoteSection.tsxpage.tsx
Blog — app/[locale]/(main)/blog/
_components/BlogIndexContent.tsx[slug]/_components/BlogPostPageContent.tsxcategory/[categorySlug]/_components/BlogCategoryPageContent.tsxcategory/[categorySlug]/page.tsxpage.tsxtag/[tagSlug]/_components/BlogTagPageContent.tsxtag/[tagSlug]/page.tsx
Card of the day (public listing) — app/[locale]/(main)/card-of-day/
CardOfDayHistory.tsxCardOfDayListView.tsx
Feedback / support page — app/[locale]/(main)/feedback/
_components/FeedbackFAQ.tsx_components/FeedbackForm.tsx_components/FeedbackPageContent.tsxpage.tsx
Share page and OG image — app/[locale]/(main)/share/card-of-the-day/[slug]/
opengraph-image.tsxpage.tsx
Spread the Insight campaign page — app/[locale]/(main)/spread-the-insight/
_components/ActivityPeriodBanner.tsx_components/ActivityRulesSection.tsx_components/ContentRequirementsSection.tsx_components/FinalCtaSection.tsx_components/HeroSection.tsx_components/HowToParticipateSection.tsx_components/PlatformsSection.tsx_components/RewardsTierSection.tsxpage.tsx
API route — app/api/
me/avatar/route.ts
Global shared components — components/
AiMemoryModal.tsxAuthModal.tsxAvatarCropModal/AvatarCropModal.tsxblog/BlogCard.tsxblog/BlogCategoryChips.tsxblog/BlogHero.tsxblog/BlogRelatedPosts.tsxCardOfDay/CardOfDayCalendar.tsxCardOfDay/CardOfDayModal.tsxCardOfDay/CardOfDayPicker.tsxCardOfDay/CardOfDayResult.tsxCardOfDay/ShareModal.tsxCreditPurchaseModal.tsxDeleteAccountCard.tsxFeedbackWidget/FeedbackWidget.tsxlist/AllLoaded.tsxlist/NoData.tsxSpreadSuggestOverlay/SpreadSuggestOverlay.tsxTarotReaderSelector.tsx
Auth library and app providers — lib/ and providers/
lib/auth.tsxproviders/AppAuthUIProvider.tsx
Stage 5: Check for New Files
After the bulk work, re-run the discovery grep to catch any files added since this guide was last updated:
grep -rl "trans({" --include="*.tsx" --include="*.ts" app/ components/ lib/ providers/ | sortDiff against the list in Stage 4. Any new file must have the new locale key added.
Stage 6: Verification
6.1 TypeScript — must be zero errors
pnpm tsc --noEmitErrors will point to the exact file and line of any missing locale key.
6.2 Grep check
# Files where trans() count doesn't match xx: count
for f in $(grep -rl "trans({" --include="*.tsx" --include="*.ts" app/ components/ lib/ providers/); do
trans_count=$(grep -c "trans({" "$f" 2>/dev/null || echo 0)
xx_count=$(grep -c "xx:" "$f" 2>/dev/null || echo 0)
if [ "$trans_count" -gt "$xx_count" ]; then
echo "NEEDS CHECK: $f (trans=$trans_count, xx=$xx_count)"
fi
done6.3 Build check
pnpm build6.4 Visual QA
Start the dev server and visit:
| URL | Check |
|---|---|
/xx | Home page in new locale |
/xx/one-card | One-card spread page |
/xx/three-card | Three-card spread page |
/xx/daily-tarot | Daily tarot page |
Look for: English text that should be translated, broken layouts, missing locale in the language switcher dropdown.
Common Pitfalls
zh_TW swallowed by the generic zh check — If adding any zh-* variant, place it BEFORE the startsWith('zh') check in useTrans.ts, getTrans.ts, useAppLocale.tsx, and request.ts. Missing this maps all zh-XX URLs to Simplified Chinese.
English text in locale slots — Verify every id:, nl:, or new locale value is actually in the target language. Models sometimes default to English when uncertain.
Traditional vs Simplified Chinese — zh_TW uses traditional characters (繁體) and different vocabulary from zh_CN (简体). Never copy-paste zh_CN values into zh_TW.
buildAuthLocalization.ts needs no changes — It derives types from TAppLocale automatically once consts.ts is updated.
localesInUrl in buildPageMetadata.ts — Forgetting to add the locale here means hreflang tags will be missing for the new locale, harming SEO.
New tarot spread pages — Each new spread page adds 2 files (page.tsx + SpreadNameContent.tsx). Re-run the Stage 5 discovery grep before calling the work done.