Skip to content

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):

typescript
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):

typescript
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:

typescript
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:

typescript
if (detected?.toLowerCase() === 'xx') return 'xx';  // ADD

1.5 i18n/useAppLocale.tsx

Same logic again:

typescript
if (locale.toLowerCase() === 'xx') return 'xx';  // ADD

1.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:

typescript
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

bash
echo '{}' > messages/xx.json

Verify infrastructure

bash
pnpm tsc --noEmit 2>&1 | head -60

You 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:

typescript
SOME_ERROR_CODE: {
  en: 'Some error message',
  zh_CN: '...',
  // ...existing locales...
  xx: '...',  // ADD
},

Files to update (in order of size):

FileNotes
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.tsLargest file — process in 500-line chunks
lib/localization/buildAuthLocalization.tsNO CHANGES NEEDED — derives types from TAppLocale automatically

Stage 3: SEO & Metadata

lib/buildPageMetadata.ts

3.1 Add to localesInUrl (use the URL-segment format):

typescript
const localesInUrl = ['zh-CN', 'zh-TW', 'ja', 'ko', 'pt', 'es', 'fr', 'de', 'ar', 'id', 'nl', 'xx'];

3.2 Add to alternates.languages:

typescript
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:

bash
grep -rl "trans({" --include="*.tsx" --include="*.ts" app/ components/ lib/ providers/ | sort

Rules

  1. Never use English as a fallback. Every value must be in the target language.
  2. For JSX values like trans({ en: <><strong>Hello</strong></> }), keep the JSX structure intact — only translate the text inside.
  3. Spot-check each file after editing: grep -c "xx:" <file> should roughly equal grep -c "trans({" <file>.

Current file list (~75 files as of April 2026)

Root layout, error, and 404 pages — app/[locale]/

  • layout.tsx
  • error.tsx
  • not-found.tsx

Site header — app/[locale]/_components/SiteHeader/

  • CardOfDayButton.tsx
  • CreditBadge.tsx
  • LanguageModal.tsx
  • MobileDrawerMenu.tsx
  • NotificationBell.tsx
  • SiteHeader.tsx
  • UserDropdown.tsx
  • SiteFooter.tsx

Other shared layout components — app/[locale]/_components/

  • OnLayoutLoaded.tsx

Home page and auth pages — app/[locale]/(main)/

  • page.tsx
  • auth/[path]/page.tsx
  • auth/telegram/page.tsx

Card picker modal — app/[locale]/(main)/_components/CardPickerModal/

  • CardPickerModalCenterInfo.tsx
  • CardPickerModalControls.tsx
  • CardRevealSpread.tsx

Home page hero — app/[locale]/(main)/_components/

  • HeroSection.tsx

Dashboard — app/[locale]/(main)/dashboard/

  • BtnSignOut.tsx
  • page.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.tsx
  • card-skins/_components/MyCardSkinsContent.tsx
  • invitations/_components/InvitationsPageContent.tsx
  • membership/_components/MembershipContent.tsx
  • tarot-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.tsx
  • page.tsx

Card skins market — app/[locale]/(main)/market/card-skins/

  • _components/CardSkinsMarketContent.tsx
  • page.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.tsx
  • page.tsx

Reading chat interface — app/[locale]/(main)/tarot-readings/[readingId]/_components/

  • ChatMessageBubble.tsx
  • ChatUserInput.tsx
  • LuckEnhancement.tsx
  • MessageVoiceButton.tsx
  • ReadingDetailView.tsx
  • ReadingInfoPanel.tsx
  • SuggestedQuestions.tsx

Shared spread page components — app/[locale]/(main)/(tarot-spreads)/_components/

  • CardPositionsExplainedSection.tsx
  • SpreadFAQSection.tsx
  • SpreadHeroSection/SpreadHeroSection.tsx

Individual spread pages (13 spreads) — app/[locale]/(main)/(tarot-spreads)/

  • celtic-cross/_components/CelticCrossContent.tsx + page.tsx
  • daily-tarot/_components/DailyTarotContent.tsx + page.tsx
  • inner-child-healing/_components/InnerChildHealingContent.tsx + page.tsx
  • love-tarot/_components/LoveReadingChoiceModal.tsx
  • love-tarot/_components/LoveTarotContent.tsx + page.tsx
  • obstacle-key/_components/ObstacleKeyContent.tsx + page.tsx
  • one-card/_components/OneCardContent.tsx + page.tsx
  • relationship-compass/_components/RelationshipCompassContent.tsx + page.tsx
  • shadow-work/_components/ShadowWorkContent.tsx + page.tsx
  • three-card/_components/ThreeCardContent.tsx + page.tsx
  • twin-flame-mirror/_components/TwinFlameMirrorContent.tsx + page.tsx
  • two-path-choice/_components/TwoPathChoiceContent.tsx + page.tsx
  • yes-or-no/_components/YesOrNoContent.tsx + page.tsx
  • [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.tsx
  • page.tsx

Blog — app/[locale]/(main)/blog/

  • _components/BlogIndexContent.tsx
  • [slug]/_components/BlogPostPageContent.tsx
  • category/[categorySlug]/_components/BlogCategoryPageContent.tsx
  • category/[categorySlug]/page.tsx
  • page.tsx
  • tag/[tagSlug]/_components/BlogTagPageContent.tsx
  • tag/[tagSlug]/page.tsx

Card of the day (public listing) — app/[locale]/(main)/card-of-day/

  • CardOfDayHistory.tsx
  • CardOfDayListView.tsx

Feedback / support page — app/[locale]/(main)/feedback/

  • _components/FeedbackFAQ.tsx
  • _components/FeedbackForm.tsx
  • _components/FeedbackPageContent.tsx
  • page.tsx

Share page and OG image — app/[locale]/(main)/share/card-of-the-day/[slug]/

  • opengraph-image.tsx
  • page.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.tsx
  • page.tsx

API route — app/api/

  • me/avatar/route.ts

Global shared components — components/

  • AiMemoryModal.tsx
  • AuthModal.tsx
  • AvatarCropModal/AvatarCropModal.tsx
  • blog/BlogCard.tsx
  • blog/BlogCategoryChips.tsx
  • blog/BlogHero.tsx
  • blog/BlogRelatedPosts.tsx
  • CardOfDay/CardOfDayCalendar.tsx
  • CardOfDay/CardOfDayModal.tsx
  • CardOfDay/CardOfDayPicker.tsx
  • CardOfDay/CardOfDayResult.tsx
  • CardOfDay/ShareModal.tsx
  • CreditPurchaseModal.tsx
  • DeleteAccountCard.tsx
  • FeedbackWidget/FeedbackWidget.tsx
  • list/AllLoaded.tsx
  • list/NoData.tsx
  • SpreadSuggestOverlay/SpreadSuggestOverlay.tsx
  • TarotReaderSelector.tsx

Auth library and app providers — lib/ and providers/

  • lib/auth.tsx
  • providers/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:

bash
grep -rl "trans({" --include="*.tsx" --include="*.ts" app/ components/ lib/ providers/ | sort

Diff 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

bash
pnpm tsc --noEmit

Errors will point to the exact file and line of any missing locale key.

6.2 Grep check

bash
# 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
done

6.3 Build check

bash
pnpm build

6.4 Visual QA

Start the dev server and visit:

URLCheck
/xxHome page in new locale
/xx/one-cardOne-card spread page
/xx/three-cardThree-card spread page
/xx/daily-tarotDaily 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 Chinesezh_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.

Internal documentation for MysticX team