Skip to content

feat: i18n (en/pt-br) #434#677

Merged
escapedcat merged 27 commits intomainfrom
feat/434_i18n
Feb 11, 2026
Merged

feat: i18n (en/pt-br) #434#677
escapedcat merged 27 commits intomainfrom
feat/434_i18n

Conversation

@escapedcat
Copy link
Contributor

@escapedcat escapedcat commented Feb 10, 2026

User description

Fixes: #434

Add i18n for EN and PT_BR only for now

Notes:

  • design somewhat cramped with longer words (let's wait what German will do :P)
  • not yet translated
    • area views
    • add merchant
    • ...
image image

PR Type

Enhancement


Description

  • Add comprehensive i18n support for English and Portuguese (Brazil)

  • Implement language switching with localStorage persistence

  • Translate all UI text across merchant pages, navigation, and search

  • Add smart locale detection based on browser language and saved preference

  • Create translation files with 377 English and 221 Portuguese entries


Diagram Walkthrough

flowchart LR
  A["svelte-i18n Package"] --> B["i18n Configuration"]
  B --> C["Translation Files"]
  C --> D["EN.json & PT-BR.json"]
  B --> E["Locale Detection"]
  E --> F["Browser Language + localStorage"]
  G["Components"] --> H["Import _ function"]
  H --> I["Replace hardcoded strings"]
  I --> J["Dynamic translations"]
  K["Footer"] --> L["Language Selector"]
  L --> M["EN/PT Toggle"]
  M --> N["localStorage Persistence"]
Loading

File Walkthrough

Relevant files
Enhancement
14 files
index.ts
Initialize svelte-i18n with smart locale detection             
+34/-0   
+layout.svelte
Add i18n initialization and loading state wrapper               
+26/-9   
+page.svelte
Translate home page hero, buttons, and description             
+12/-13 
+page.svelte
Translate map loading status messages                                       
+6/-5     
+page.svelte
Translate merchant detail page with 40+ translation keys 
+44/-42 
MerchantListPanel.svelte
Translate search, filter, and merchant list UI                     
+51/-31 
MapSearchBar.svelte
Translate search bar placeholders and aria labels               
+11/-7   
Header.svelte
Make navigation links reactive with i18n translations       
+86/-46 
Footer.svelte
Add language selector with EN/PT toggle and persistence   
+60/-10 
MerchantDetailsContent.svelte
Translate merchant details drawer content                               
+30/-22 
MerchantListItem.svelte
Translate merchant list item labels and status                     
+9/-6     
PaymentMethodIcon.svelte
Translate payment method tooltips with key mapping             
+22/-7   
MapLoadingMain.svelte
Translate map loading status fallback text                             
+2/-1     
analytics.ts
Add language_switch event type for tracking                           
+2/-1     
Documentation
2 files
en.json
Complete English translation file with 377 keys                   
+377/-0 
pt-BR.json
Complete Portuguese (Brazil) translation file with 221 keys
+221/-0 
Dependencies
1 files
package.json
Add svelte-i18n dependency version 4.0.1                                 
+1/-0     

Summary by CodeRabbit

  • New Features

    • App now supports English and Brazilian Portuguese with translated UI across the site.
    • Language selector added to the footer; choice is remembered and tracked.
    • Loading screen shown while translations initialize.
  • Chore

    • Added runtime internationalization dependency and locale data files.
  • Bug Fixes

    • UI strings and accessibility labels updated for better clarity.

@netlify
Copy link

netlify bot commented Feb 10, 2026

Deploy Preview for btcmap ready!

Name Link
🔨 Latest commit 2c1913b
🔍 Latest deploy log https://app.netlify.com/projects/btcmap/deploys/698c4cbb04332c00086cefc7
😎 Deploy Preview https://deploy-preview-677--btcmap.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 58 (🔴 down 40 from production)
Accessibility: 97 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 99 (🔴 down 1 from production)
PWA: 90 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@socket-security
Copy link

socket-security bot commented Feb 10, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedsvelte-i18n@​4.0.1991008780100

View full report

@socket-security
Copy link

socket-security bot commented Feb 10, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Protestware or unwanted behavior: npm es5-ext

Note: The script attempts to run a local post-install script, which could potentially contain malicious code. The error handling suggests that it is designed to fail silently, which is a common tactic in malicious scripts.

From: ?npm/svelte-i18n@4.0.1npm/es5-ext@0.10.64

ℹ Read more on: This package | This alert | What is protestware?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Consider that consuming this package may come along with functionality unrelated to its primary purpose.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/es5-ext@0.10.64. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

Adds full internationalization: new svelte-i18n dependency, i18n core module with locale detection and lazy loading (en, pt-BR), locale files, translated strings across many components/pages, language switcher UI, and a new analytics event for language switches.

Changes

Cohort / File(s) Summary
Package & i18n core
package.json, src/lib/i18n/index.ts
Adds svelte-i18n dependency and new i18n initializer with getInitialLocale, lazy locale registration (en, pt-BR), exports locale and _.
Locale data
src/lib/i18n/locales/en.json, src/lib/i18n/locales/pt-BR.json
Adds comprehensive English and Brazilian Portuguese translation JSON files.
Layout & routing
src/routes/+layout.svelte, src/routes/+page.svelte, src/routes/map/+page.svelte
Waits for i18n init in layout (loading UI), updates document lang reactively, replaces static texts with translation lookups on home and map pages.
Header & Footer (navigation & language UI)
src/components/layout/Header.svelte, src/components/layout/Footer.svelte
Nav and dropdown labels driven by i18n keys/ids; Footer adds language selector, switchLanguage persists locale and tracks analytics.
Map & search UIs
src/routes/map/components/MapSearchBar.svelte, src/routes/map/components/MerchantListPanel.svelte
Placeholders, ARIA labels, status messages localized; MerchantListPanel adds helper getCategoryLabel and several exported props for external control.
Merchant & map components
src/components/MapLoadingMain.svelte, src/components/MerchantDetailsContent.svelte, src/components/PaymentMethodIcon.svelte, src/components/MerchantListItem.svelte, src/routes/merchant/[id]/+page.svelte
Replaces numerous hardcoded strings with $_(...) lookups; PaymentMethodIcon tooltip/alt text now translation-backed.
Analytics
src/lib/analytics.ts
Adds 'language_switch' to the EventName union type.
Other map pages/components
src/routes/map/+page.svelte, src/routes/map/components/*
Status and progress messages switched to i18n lookups; no control-flow changes.

Sequence Diagram

sequenceDiagram
    participant App as App Init
    participant Locale as getInitialLocale()
    participant Storage as localStorage
    participant Browser as Browser Language
    participant I18n as svelte-i18n
    participant Component as UI Component

    App->>Locale: call getInitialLocale()
    Locale->>Storage: read 'language'
    alt stored language valid
        Storage-->>Locale: return stored locale (en or pt-BR)
    else no valid stored language
        Locale->>Browser: read navigator.language
        alt startsWith 'pt'
            Browser-->>Locale: choose pt-BR
        else
            Browser-->>Locale: default to en
        end
    end
    Locale-->>I18n: init(initialLocale, fallbackLocale: en)
    I18n->>I18n: register locales via lazy imports (en, pt-BR)
    I18n-->>App: ready / isLoading false
    App->>Component: render using $locale and $_()
    Component-->>App: display localized UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

Review effort 3/5

Poem

🐇 I hopped through keys and JSON leaves,
swapped words in headers, buttons, and eaves.
en and pt-BR now share the trail,
a tiny hop that tells the tale.
Cheers — the app speaks both without fail!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: i18n (en/pt-br) #434' clearly summarizes the main change: adding internationalization support for English and Portuguese (Brazil) locales.
Description check ✅ Passed The PR description provides comprehensive details including the issue reference, clear description of changes, screenshots, and a detailed file walkthrough covering all modifications.
Linked Issues check ✅ Passed The PR successfully addresses issue #434 by implementing comprehensive i18n support for EN and PT-BR with locale detection, language switching, and translation files as required.
Out of Scope Changes check ✅ Passed All changes are in-scope: i18n initialization, translation files, component updates for i18n integration, language selector UI, and analytics event for language switching align directly with the i18n objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/434_i18n

Comment @coderabbitai help to get the list of available commands and usage tips.

This comment was marked as resolved.

@escapedcat escapedcat marked this pull request as ready for review February 10, 2026 16:05
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 10, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
XSS via HTML

Description: New {@html ...} rendering for translated content (e.g., {@html $_('info.noCommunity', {
values: { createLink: '...' } })} in src/routes/merchant/[id]/+page.svelte
lines 922-928, and similarly in src/routes/+page.svelte) can enable XSS if translation
strings or interpolated values ever become attacker-controlled (e.g., via compromised
translation files or future dynamic/remote i18n content). +page.svelte [922-928]

Referred Code
	<!-- eslint-disable-next-line svelte/no-at-html-tags -->
	{@html $_('info.noCommunity', {
		values: {
			createLink: `<a href="${resolve('/communities')}" class="text-link transition-colors hover:text-hover">${$_('info.created')}</a>`
		}
	})}
</p>
Ticket Compliance
🟡
🎫 #434
🟢 Add/consider internationalization (i18n) support across the UI (replace hardcoded strings
with translation keys).
Provide at least initial locale coverage (notably English plus another language, per PR
scope).
Implement locale detection (e.g., from browser language and/or saved preference).
Ensure translated strings cover major user-facing areas (navigation, map search/list,
merchant pages, etc.).
Implement a way to switch languages and persist the preference for future visits.
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing fallback case: getCategoryLabel() returns labelMap[key] without a fallback which can yield undefined at
runtime if an unexpected CategoryKey value appears.

Referred Code
// Get translated category label
function getCategoryLabel(key: CategoryKey): string {
	const labelMap: Record<CategoryKey, string> = {
		all: $_('categories.all'),
		restaurants: $_('categories.restaurants'),
		shopping: $_('categories.shopping'),
		groceries: $_('categories.groceries'),
		coffee: $_('categories.coffee'),
		atms: $_('categories.atms'),
		hotels: $_('categories.hotels'),
		beauty: $_('categories.beauty')
	};
	return labelMap[key];
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Raw HTML rendering: The PR introduces {@html ...} rendering for translated content, which can become an XSS
risk if translation strings/values are ever influenced by untrusted input or editable CMS
content.

Referred Code
	<p class="p-5 text-body dark:text-white">{$_('info.loadingCommunities')}</p>
{:else}
	<p class="p-5 text-body dark:text-white">
		<!-- eslint-disable-next-line svelte/no-at-html-tags -->
		{@html $_('info.noCommunity', {
			values: {
				createLink: `<a href="${resolve('/communities')}" class="text-link transition-colors hover:text-hover">${$_('info.created')}</a>`
			}
		})}
	</p>

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@escapedcat escapedcat changed the title feat: i18n #434 feat: i18n (en/pt-br) #434 Feb 10, 2026
@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 10, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Ensure dynamic language updates for labels

Make the labelMap inside getCategoryLabel reactive by using a $: block, so that
category labels update when the application's language changes.

src/routes/map/components/MerchantListPanel.svelte [28-40]

-function getCategoryLabel(key: CategoryKey): string {
+let getCategoryLabel: (key: CategoryKey) => string;
+
+$: {
 	const labelMap: Record<CategoryKey, string> = {
 		all: $_('categories.all'),
 		restaurants: $_('categories.restaurants'),
 		shopping: $_('categories.shopping'),
 		groceries: $_('categories.groceries'),
 		coffee: $_('categories.coffee'),
 		atms: $_('categories.atms'),
 		hotels: $_('categories.hotels'),
 		beauty: $_('categories.beauty')
 	};
-	return labelMap[key];
+	getCategoryLabel = (key) => labelMap[key];
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a bug where category labels would not update on language change and provides a valid fix to make the translations reactive.

Medium
High-level
Consider making navigation links reactive

Refactor the reactive navigation link definitions in Header.svelte to be static
arrays containing translation keys. This avoids inefficiently re-creating the
arrays on every state change, with translations being applied directly and
reactively in the template instead.

Examples:

src/components/layout/Header.svelte [11-85]
	$: navLinks = [
		{ id: 'maps', title: $_('nav.maps'), url: '', icon: 'map' as MobileNavIconName },
		{ id: 'apps', title: $_('nav.apps'), url: '/apps', icon: 'apps' as MobileNavIconName },
		{ id: 'stats', title: $_('nav.stats'), url: '', icon: 'stats' as MobileNavIconName },
		{ id: 'areas', title: $_('nav.areas'), url: '', icon: 'areas' as MobileNavIconName },
		{ id: 'maintain', title: $_('nav.maintain'), url: '', icon: 'contribute' as MobileNavIconName },
		{
			id: 'wiki',
			title: $_('nav.wiki'),
			url: 'https://gitea.btcmap.org/teambtcmap/btcmap-general/wiki',

 ... (clipped 65 lines)

Solution Walkthrough:

Before:

// src/components/layout/Header.svelte
<script>
  import { _ } from 'svelte-i18n';
  
  // All link arrays are reactive to update translations
  $: navLinks = [
    { id: 'maps', title: $_('nav.maps'), ... },
    { id: 'apps', title: $_('nav.apps'), ... },
    ...
  ];
  $: mapsDropdownLinks = [
    { title: $_('nav.merchantMap'), ... },
    ...
  ];
</script>

{#each navLinks as link (link.id)}
  <!-- Titles are pre-translated in the script block -->
  <...>{link.title}</...>
{/each}

After:

// src/components/layout/Header.svelte
<script>
  import { _ } from 'svelte-i18n';
  
  // Link arrays are static, holding translation keys
  const navLinks = [
    { id: 'maps', titleKey: 'nav.maps', ... },
    { id: 'apps', titleKey: 'nav.apps', ... },
    ...
  ];
  const mapsDropdownLinks = [
    { titleKey: 'nav.merchantMap', ... },
    ...
  ];
</script>

{#each navLinks as link (link.id)}
  <!-- Translations are applied reactively in the template -->
  <...>{$_ (link.titleKey)}</...>
{/each}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies an inefficient reactive pattern in Header.svelte where large arrays are unnecessarily recreated, and proposes a more performant and idiomatic Svelte solution that is already used elsewhere in the PR.

Medium
General
Improve accessibility with translated aria-labels

Replace hardcoded aria-label attributes on language switch buttons with
translation keys to ensure screen readers announce them in the user's current
language.

src/components/layout/Footer.svelte [56-80]

 <button
 	on:click={() => switchLanguage('en')}
 	disabled={$locale === 'en'}
-	aria-label="Switch to English"
+	aria-label={$_('aria.switchToEnglish')}
 	class="
 		{$locale === 'en'
 		? 'cursor-default font-bold text-body underline dark:text-white/50'
 		: 'text-link transition-colors hover:text-hover dark:text-white/50 dark:hover:text-link'}
 	"
 >
 	EN
 </button>
 <span class="text-body dark:text-white/50"> / </span>
 <button
 	on:click={() => switchLanguage('pt-BR')}
 	disabled={$locale === 'pt-BR'}
-	aria-label="Mudar para Português"
+	aria-label={$_('aria.switchToPortuguese')}
 	class="
 		{$locale === 'pt-BR'
 		? 'cursor-default font-bold text-body underline dark:text-white/50'
 		: 'text-link transition-colors hover:text-hover dark:text-white/50 dark:hover:text-link'}
 	"
 >
 	PT
 </button>
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies hardcoded aria-label attributes and proposes using translation keys to improve accessibility, which is a valid and good practice.

Medium
Localize footer links

Replace hardcoded name fields in the links array with nameKey fields and
corresponding translation keys to enable localization for all footer links.

src/components/layout/Footer.svelte [18-34]

 const links = [
   { link: '/about-us', nameKey: 'footer.aboutUs' },
   { link: '/media', nameKey: 'footer.media' },
   { link: '/license', nameKey: 'footer.license' },
   { link: '/privacy-policy', nameKey: 'footer.privacy' },
-  { link: 'https://stats.uptimerobot.com/7kgEVtzlV1', name: 'Status' },
+  { link: 'https://stats.uptimerobot.com/7kgEVtzlV1', nameKey: 'footer.status' },
   ...(env.PUBLIC_UMAMI_URL
-    ? [{ link: env.PUBLIC_UMAMI_URL, name: 'Analytics', external: true }]
+    ? [{ link: env.PUBLIC_UMAMI_URL, nameKey: 'footer.analytics', external: true }]
     : []),
   {
     link: 'https://bitcoin.rocks/business/',
     nameKey: 'footer.bitcoinForBusiness',
     external: true
   },
-  { link: '/bitcoin.pdf', name: 'White Paper' },
-  { link: '/cypherpunks-manifesto.pdf', name: 'Cypherpunks' }
+  { link: '/bitcoin.pdf', nameKey: 'footer.whitePaper' },
+  { link: '/cypherpunks-manifesto.pdf', nameKey: 'footer.cypherpunks' }
 ];

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that some footer links are not internationalized and provides a good solution to make them translatable, improving consistency.

Medium
Add tooltip fallback

Add a fallback to the titleText variable, so if a translation key is not found,
it defaults to the label prop, preventing an empty tooltip.

src/components/PaymentMethodIcon.svelte [72-76]

-$: titleText = $_(tooltipKeys[method][statusKey]);
+$: titleText = $_(tooltipKeys[method][statusKey]) || label;
 ...
 <img bind:this={imgElement} src={iconSrc} alt={label} class={sizeClass} title={titleText} />

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 5

__

Why: The suggestion improves robustness by adding a fallback for tooltips, ensuring a meaningful label is always displayed even if a translation is missing.

Low
  • Update

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/map/components/MerchantListPanel.svelte (1)

384-423: ⚠️ Potential issue | 🟡 Minor

Localize the result-count strings and zoom button aria-label to ensure complete i18n coverage.

These strings render English-only text: the "X of Y results" count (lines 391-395), the "count results" count (lines 396-397), and the zoom button's aria-label (lines 417-419).

The translation file already includes "resultsCount": "{count} result(s)", but the component uses hardcoded English for these cases instead of $_() function calls.

Add the following keys to src/lib/i18n/locales/en.json and all translation files:

  • "search.resultsFiltered": "{count} of {total} result(s)" for the filtered results case
  • "aria.zoomToResults": "Zoom map to show {count} result(s)" for the button aria-label

Then update the component to use these keys with appropriate values parameters.

🤖 Fix all issues with AI agents
In `@src/components/layout/Footer.svelte`:
- Around line 59-72: Replace the hard-coded aria-label strings on the language
buttons with localized keys: create aria.switchToEnglish and
aria.switchToPortuguese in your locale JSONs (under an "aria" object) and use
the app's localization function to set the attributes instead of literal text;
update the two button attributes that reference aria-label (the elements that
check $locale and call switchLanguage('pt-BR')) to call the translator (e.g.,
t('aria.switchToEnglish') / t('aria.switchToPortuguese') or your project's
equivalent) so the labels follow the active locale.

In `@src/lib/i18n/locales/en.json`:
- Line 129: The string value for the "improveVisibility" key contains a grammar
typo: change "it's" (contraction) to the possessive "its" so the entry
"improveVisibility" reads "Boost this location to improve its visibility on the
map." — update the value for the "improveVisibility" JSON key accordingly.

In `@src/lib/i18n/locales/pt-BR.json`:
- Line 8: The pt-BR locale sets "home.hashtag" to "#SPEDN ✊" which differs from
other locales — confirm whether the emoji change is intentional; if you want
consistent hashtags across locales, update the value of the "home.hashtag" key
in src/lib/i18n/locales/pt-BR.json to match the canonical emoji used in the EN
locale (and, if applicable, align other locale files' "home.hashtag" values to
the same emoji for consistency).
🧹 Nitpick comments (5)
src/routes/+page.svelte (1)

9-9: Inconsistent import path for i18n.

This file imports _ directly from 'svelte-i18n', but other files in this PR (e.g., PaymentMethodIcon.svelte, MapSearchBar.svelte) import from '$lib/i18n'. Consider using the centralized $lib/i18n import for consistency across the codebase.

Suggested change
-	import { _ } from 'svelte-i18n';
+	import { _ } from '$lib/i18n';
src/routes/+layout.svelte (1)

80-86: Consider adding error handling for i18n initialization failure.

The loading state blocks the entire app until i18n initializes. If i18n fails to load (e.g., network error fetching locale files), users would be stuck on the loading screen indefinitely.

Consider adding a timeout or error boundary that falls back to rendering the app without translations.

src/components/layout/Header.svelte (1)

9-9: Consider consistent i18n import path.

Same as noted in +page.svelte - this imports directly from 'svelte-i18n' while other components use '$lib/i18n'.

src/routes/map/components/MapSearchBar.svelte (1)

136-139: Consider reformatting for readability.

The inline conditional with split tags across lines is harder to read. Consider restructuring:

Optional formatting improvement
-			{$_('search.nearby')}{`#if` isLoadingCount}<span class="opacity-60">
-					...</span
-				>{:else if formattedCount}
-				{formattedCount}{/if}
+			{$_('search.nearby')}{`#if` isLoadingCount}<span class="opacity-60">...</span>{:else if formattedCount}{formattedCount}{/if}
src/components/layout/Footer.svelte (1)

9-15: Prefer SvelteKit’s browser helper for the SSR guard.

Keeps SSR checks consistent and avoids repeating typeof window patterns.

♻️ Proposed update
+ import { browser } from '$app/environment';
  import SocialLink from '$components/SocialLink.svelte';
  import { socials } from '$lib/store';
  import { env } from '$env/dynamic/public';
  import { locale, _ } from '$lib/i18n';
  import Icon from '$components/Icon.svelte';
  import { trackEvent } from '$lib/analytics';

  function switchLanguage(newLocale: string) {
    trackEvent('language_switch', { language: newLocale });
    locale.set(newLocale);
-   if (typeof window !== 'undefined') {
+   if (browser) {
      localStorage.setItem('language', newLocale);
    }
  }

…#434

- Install svelte-i18n package
- Create i18n configuration and directory structure
- Add English and Brazilian Portuguese translation files
- Translate home page hero section (headline, buttons, description)
- Translate all navigation items (top-level menu and dropdowns)
- Use placeholder support for OpenStreetMap link in description
- Make navigation items reactive for future locale switching

🤖 Generated with opencode
- Import isLoading from svelte-i18n in +layout.svelte
- Add loading state while i18n initializes
- Wrap main content in {#if !} block
- Prevents 'Cannot format a message without first setting the initial locale' error
- Shows 'Loading...' text briefly while locale data loads

🤖 Generated with opencode
- Add translate icon + language toggle in footer (inline with links)
- Export locale store from i18n configuration
- Add localStorage persistence for language preference
- Translate 'Language'/'Idioma' label based on active language
- Style active language with bold + underline
- Style inactive language as clickable link with hover
- Manual switching only (no auto-detection for now)

🤖 Generated with opencode
- Remove 'Language'/'Idioma' label for cleaner look
- Change separator from · to / (more standard)
- Fix vertical alignment with other footer links (remove inline-flex)
- Make translate icon match text color
- Reduce spacing between footer links (mx-2.5 → mx-1.5)

🤖 Generated with opencode
- Change icon color from text-body to text-link for consistency
- Icon now matches the visual style of footer links in light mode

🤖 Generated with opencode
- Add unique 'id' field to navLinks for language-independent identification
- Change dropdown conditionals from comparing title text to comparing id
- Fixes dropdown menus disappearing when switching to Portuguese
- Desktop and mobile navigation now work correctly in both languages

🤖 Generated with opencode
- Add getInitialLocale() function with smart detection logic
- Check localStorage preference first (user choice wins)
- Detect browser language via navigator.language as fallback
- Map Portuguese variants (pt, pt-BR, pt-PT) to pt-BR
- Mirrors theme detection pattern (localStorage > system > default)
- Benefits 30% of traffic (Brazilian users auto-detected)

🤖 Generated with opencode
- Add search, categories, merchant, verification sections
- Add payment, boost, status, info sections
- Add comments, errors, ARIA labels, footer sections
- Include Portuguese translations for all new strings
- Covers map components, merchant details, and UI elements

🤖 Generated with opencode
- Import locale from svelte-i18n in +layout.svelte
- Add reactive statement to update document.documentElement.lang
- Lang attribute now changes automatically when user switches language
- Improves SEO and accessibility for screen readers

🤖 Generated with opencode
- Export _ translation function from i18n/index.ts
- Make footer links use translation keys
- Add translations for all footer links
- Links now update when language changes
- Keep Status, Analytics, White Paper, Cypherpunks as-is (standard names)

🤖 Generated with opencode
- Add translation function import
- Translate deleted merchant banner
- Translate action buttons: Navigate, Edit, Share, Comments
- Translate payment methods section
- Translate verification section (Last Surveyed, Verify Location)
- Translate boost section (Boost Expires, Boost/Extend buttons)
- Translate View Full Details button
- All titles and tooltips now translatable

🤖 Generated with opencode
- Add translation function import
- Create tooltip key map for btc/ln/nfc methods
- Use translation keys for accepted/not accepted/unknown states
- Simplify logic with lookup table
- All payment method tooltips now translatable

🤖 Generated with opencode
- Add translation function import
- Translate search placeholders (worldwide/nearby)
- Translate mode toggle buttons (Worldwide/Nearby)
- Translate category filter buttons with helper function
- Translate all status messages (searching, no results, zoom in)
- Translate Show all button
- Translate all aria-labels for accessibility
- Add getCategoryLabel() helper for dynamic translation
- Fix unused variable ESLint warning

🤖 Generated with opencode
- Add translation function import
- Translate Unknown placeholder for merchant name
- Translate payment method labels passed to PaymentMethodIcon
- Translate status badges (Boosted, Verified, Outdated)
- All merchant list item text now translatable

🤖 Generated with opencode
- Add translation function import
- Translate search placeholder based on mode
- Translate mode toggle buttons (Worldwide/Nearby)
- Translate all aria-labels (searchInput, clearSearch, searching)
- Floating search bar now fully translatable

🤖 Generated with opencode
- Import trackEvent from analytics
- Track language_switch event with selected language
- Add language_switch to EventName type in analytics.ts
- Analytics will now show language preference usage patterns

🤖 Generated with opencode
- Add placeholder support to noCommunity translation
- Add 'created' translation key (en: created, pt: criada)
- Use {@html} with createLink placeholder in merchant page
- Link text now translatable based on locale
- Restores navigation to /communities page
- Addresses PR review functional regression feedback

🤖 Generated with opencode
- Change img alt from method code (btc/ln/nfc) to label prop
- Screen readers now announce 'On-chain' instead of 'btc'
- Improves accessibility for payment method icons
- Label prop is now used and meaningful
- Addresses PR review accessibility feedback

🤖 Generated with opencode


- Replace JavaScript condition (link.link !== '/cypherpunks-manifesto.pdf')
- Use Tailwind's last:mb-0 pseudo-class selector
- All links get mb-2.5 by default, last link overrides with mb-0
- Pure CSS solution - no JavaScript logic needed
- More maintainable and automatically adapts to link changes
- Cleaner code, language-independent

🤖 Generated with opencode
…434

- Change 'Aplicativos' → 'Apps' (common Brazilian tech term)
- Change 'Estatísticas' → 'Stats' (widely understood)
- Change 'Apoie-nos' → 'Apoiar' (shorter form)
- Change 'Dashboard' → 'Dashboard' (keep English, standard term)
- Change 'Classificação' → 'Ranking' (shorter, commonly used)
- Change 'Tornar-se um Etiquetador' → 'Tornar-se Etiquetador' (remove article)
- Prevents nav text wrapping in desktop header
- Maintains readability while fitting fixed-width layout

🤖 Generated with opencode
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/routes/merchant/[id]/+page.svelte (3)

247-294: ⚠️ Potential issue | 🟡 Minor

Hardcoded English strings in tippy tooltips.

These tippy tooltip contents remain in English while the rest of the component uses i18n. For consistency, these should use the translation keys that already exist in the locale files (e.g., payment.onchainAccepted, verification.verifiedTooltip).

🌐 Suggested fix for tippy tooltips
 $: thirdPartyTooltip &&
 	data &&
 	tippy([thirdPartyTooltip], {
-		content: 'Third party app required'
+		content: $_('payment.thirdPartyRequired')
 	});

 $: onchainTooltip &&
 	data &&
 	tippy([onchainTooltip], {
 		content:
 			data.osmTags?.['payment:onchain'] === 'yes'
-				? 'On-chain accepted'
+				? $_('payment.onchainAccepted')
 				: data.osmTags?.['payment:onchain'] === 'no'
-					? 'On-chain not accepted'
-					: 'On-chain unknown'
+					? $_('payment.onchainNotAccepted')
+					: $_('payment.onchainUnknown')
 	});

 $: lnTooltip &&
 	data &&
 	tippy([lnTooltip], {
 		content:
 			data.osmTags?.['payment:lightning'] === 'yes'
-				? 'Lightning accepted'
+				? $_('payment.lightningAccepted')
 				: data.osmTags?.['payment:lightning'] === 'no'
-					? 'Lightning not accepted'
-					: 'Lightning unknown'
+					? $_('payment.lightningNotAccepted')
+					: $_('payment.lightningUnknown')
 	});

 $: nfcTooltip &&
 	data &&
 	tippy([nfcTooltip], {
 		content:
 			data.osmTags?.['payment:lightning_contactless'] === 'yes'
-				? 'Lightning Contactless accepted'
+				? $_('payment.contactlessAccepted')
 				: data.osmTags?.['payment:lightning_contactless'] === 'no'
-					? 'Lightning contactless not accepted'
-					: 'Lightning contactless unknown'
+					? $_('payment.contactlessNotAccepted')
+					: $_('payment.contactlessUnknown')
 	});

 $: verifiedTooltip &&
 	tippy([verifiedTooltip], {
-		content: 'Verified within the last year'
+		content: $_('verification.verifiedTooltip')
 	});

 $: outdatedTooltip &&
 	tippy([outdatedTooltip], {
-		content: 'Outdated please re-verify'
+		content: $_('verification.outdatedTooltip')
 	});

827-827: ⚠️ Potential issue | 🟡 Minor

Hardcoded "No comments yet." string.

This string should use the existing translation key $_('comments.none') for consistency with the rest of the i18n implementation.

🌐 Suggested fix
 {:else}
-	<p class="p-5 text-body dark:text-white">No comments yet.</p>
+	<p class="p-5 text-body dark:text-white">{$_('comments.none')}</p>
 {/if}

580-583: ⚠️ Potential issue | 🟡 Minor

Hardcoded toast message.

The success toast message "Link copied to clipboard!" should be translated for consistency.

🌐 Suggested fix (requires adding translation key)
 on:click={() => {
 	navigator.clipboard.writeText(`https://btcmap.org/merchant/${data.id}`);
-	successToast('Link copied to clipboard!');
+	successToast($_('merchant.linkCopied'));
 }}

Add to locale files:

"merchant": {
  ...
  "linkCopied": "Link copied to clipboard!"
}
src/routes/map/components/MerchantListPanel.svelte (2)

391-397: ⚠️ Potential issue | 🟡 Minor

Result count text not fully internationalized.

The filtered results count ("X of Y results") uses English text with template literals instead of i18n. This should use a translation key with interpolation for consistency.

🌐 Suggested approach

Add a translation key like search.filteredResultsCount with value "{filtered} of {total} result(s)" and use:

 {:else if selectedCategory !== 'all' && filteredSearchResults.length !== searchResults.length}
-	{filteredSearchResults.length} of {searchResults.length} result{searchResults.length !==
-	1
-		? 's'
-		: ''}
+	{$_('search.filteredResultsCount', { values: { filtered: filteredSearchResults.length, total: searchResults.length } })}
 {:else}
-	{searchResults.length} result{searchResults.length !== 1 ? 's' : ''}
+	{$_('search.resultsCount', { values: { count: searchResults.length } })}
 {/if}

417-419: ⚠️ Potential issue | 🟡 Minor

aria-label not internationalized.

The "Show All" button's aria-label uses English template string. This should use a translation key for accessibility in other locales.

🌐 Suggested fix
-aria-label="Zoom map to show {filteredSearchResults.length === 1
-	? '1 result'
-	: `all ${filteredSearchResults.length} results`}"
+aria-label={$_('search.showAllAriaLabel', { values: { count: filteredSearchResults.length } })}

Add translation key:

"search": {
  "showAllAriaLabel": "Zoom map to show all {count} result(s)"
}
🧹 Nitpick comments (2)
src/components/layout/Header.svelte (1)

9-9: Inconsistent import path for i18n.

This file imports _ directly from 'svelte-i18n', while other components in this PR (e.g., PaymentMethodIcon.svelte, MapSearchBar.svelte, MerchantListPanel.svelte) import from '$lib/i18n'. Using the centralized $lib/i18n path ensures consistent configuration and makes future refactoring easier.

♻️ Suggested fix
-import { _ } from 'svelte-i18n';
+import { _ } from '$lib/i18n';
src/routes/map/components/MerchantListPanel.svelte (1)

361-361: Variable naming shadows i18n function.

The destructured variable _category could cause confusion since _ is the imported i18n function. Consider using a clearer name like category or categoryConfig.

♻️ Suggested fix
-{`#each` CATEGORY_ENTRIES as [key, _category] (key)}
+{`#each` CATEGORY_ENTRIES as [key, category] (key)}

Copy link
Member

@dadofsambonzuki dadofsambonzuki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@escapedcat escapedcat merged commit 215ceea into main Feb 11, 2026
12 checks passed
@escapedcat escapedcat deleted the feat/434_i18n branch February 11, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Think about Internationalization (i18n)

3 participants