Skip to content

feat(i18n): full internationalization — 14 locales, zero hardcoded English + Linux AppImage support#100

Merged
koala73 merged 28 commits intomainfrom
codex/pr-86
Feb 18, 2026
Merged

feat(i18n): full internationalization — 14 locales, zero hardcoded English + Linux AppImage support#100
koala73 merged 28 commits intomainfrom
codex/pr-86

Conversation

@koala73
Copy link
Owner

@koala73 koala73 commented Feb 18, 2026

Summary

  • Full i18n system with 14 locales: en, fr, de, es, it, pl, pt, nl, sv, ru, ar (RTL), zh, ja — all at 1132-key parity
  • Eliminated ~110 hardcoded English strings across 50+ source files, replaced with t() calls
  • RTL support for Arabic with proper regional code normalization (ar-SA → ar)
  • Dead English fallback literals (t() || 'English') removed from all components
  • Community discussion floating widget (localized)
  • Linux AppImage desktop build support
  • Proper noun heuristic fallback for trending keywords when ML unavailable

Key changes

  • New: src/services/i18n.ts — i18next setup with language detection, RTL, locale switching
  • New: 13 locale JSON files (1132 keys each) in src/locales/
  • New: src/styles/rtl-overrides.css + src/styles/lang-switcher.css
  • Modified: 50+ components/services to use t() instead of hardcoded strings
  • Modified: .github/workflows/build-desktop.yml — Linux CI matrix
  • Modified: scripts/desktop-package.mjs + download-node.sh — Linux target support

Test plan

  • Verify language switcher shows all 14 languages
  • Switch to Arabic — confirm dir="rtl" on <html>, layout mirrors
  • Switch to Japanese — confirm all panel labels, tooltips, popups render in Japanese
  • Switch to French — confirm no English leaks in panels, modals, map legend
  • Verify {{count}} interpolation works in timeAgo strings
  • Verify tsc --noEmit passes (confirmed locally)
  • Test community widget dismiss/localStorage persistence

Lib-LOCALE and others added 24 commits February 16, 2026 12:32
…omponents

- StrategicPosturePanel: 25 strings (military units, tooltips, timer)
- App: 4 strings (GitHub link, pin map, filter, source counter)
- StatusPanel: 5 strings (system status, timestamps, storage)
- PizzIntIndicator: 3 strings + t() import (DEFCON, title, updated)
- PlaybackControl: 2 strings + t() import (toggle mode, LIVE)
- CountryBriefPage: 4 strings (share, print, export, source ref)
- DeckGLMap: 4 strings + t() import (zoom controls, layer guide)
- MonitorPanel: 2 strings (placeholder, no matches)
- TechEventsPanel: 4 strings (loading, empty, show on map, more info)
- TechReadinessPanel: 3 strings (internet, mobile, R&D tooltips)
- Removed deb from tauri.conf.json build targets
- Removed duplicate monitor key from en.json
…ck, Monitor, DeckGLMap

- PizzIntIndicator: panel title, tensions title, source label, 6 status labels, 3 time-ago strings
- PlaybackControl: Historical Playback header, LIVE button
- MonitorPanel: Add Monitor button
- DeckGLMap: 25 layer labels, 8 view selector options, Layers header, All button
- Added ~50 new keys to en.json (English) and fr.json (French)
… 19 panels

- Panel.ts base class: showLoading/showError defaults now use t() keys
- Added 27 new common.* keys to en.json and fr.json
- Updated 19 panel files with t() calls for loading/error messages
- Added t() import to 12 panels: SatelliteFires, PopulationExposure,
  Displacement, ClimateAnomaly, StrategicRisk, Prediction, Cascade,
  GdeltIntel, NewsPanel, GeoHubs, Panel, PredictionPanel
- All showLoading custom messages now translated (UCDP events, thermal
  data, ETF data, stablecoins, climate data, etc.)
- All showError messages now translated (failed to load, no data, etc.)
…led sources strings

- App.ts: 2 showError calls (All sources/Intel sources disabled)
- PopulationExposurePanel: 'No exposure data available' panel-empty
- SatelliteFiresPanel: 'No fire data available' panel-empty
- UcdpEventsPanel: 'No events in this category' panel-empty
- ClimateAnomalyPanel: 'No significant anomalies detected' panel-empty
- export.ts: Export Data title + Export CSV/JSON button labels
- Added 8 new common.* keys to en.json and fr.json
- Added t() import to export.ts
- NewsPanel: Close button title attribute
- CountryIntelModal: 4 component tooltip titles (Unrest/Conflict/Security/Information)
- CIIPanel: Share story button + 4 component tooltip titles
- Added common.unrest/conflict/security/information/shareStory keys to en.json/fr.json
…riefPage

- CountryBriefPage: 2 aria-label='Close' + 3 export labels (Image/JSON/CSV)
- Added exportImage key to en.json and fr.json
- Expanded common section with ~48 native-translated keys in:
  de.json (German), es.json (Spanish), it.json (Italian), pl.json (Polish),
  nl.json (Dutch), pt.json (Portuguese), sv.json (Swedish)
- Also translated base nl/pt/sv common keys that were still in English
- Restructure popups keys in de/es/it/pl/nl/pt/sv to match en.json nested structure
- Move orphaned top-level keys (earthquake, base, protest, flight, nuclear, cable, pipeline, etc.) into popups.* namespace
- Add full native translations for ~200 popup keys per locale
- Ensures i18next resolves popups.* keys correctly for all languages
"here" and other basic English words (pronouns, prepositions, adverbs)
were not in SUPPRESSED_TRENDING_TERMS, causing false keyword spike
findings for common words.
…ages

Audit found missing keys in all non-EN locales (62-90 per file) and 25
stale keys in PT/NL/SV. Fixes gaps, removes stale keys, corrects FR
infra key misplacement, and adds complete Russian (ru) translation with
1013/1013 keys matching the English reference.
When the NER model isn't loaded, isSignificantTerm was returning true
for all terms, letting common words like "here" trigger keyword spike
findings. Now falls back to a capitalization heuristic: single-word
terms must appear capitalized (mid-sentence) in >50% of headlines to
be treated as significant. Also added ~45 missing English stopwords.
Shows a compact pill badge (bottom-right) inviting users to the GitHub
Discussions page. Includes pulsing dot indicator, CTA button, close
button, and "Don't show again" option that persists to localStorage.
- Add ar.json and zh.json translations (1013 keys each, full parity)
- RTL support: dir="rtl" on <html>, targeted CSS overrides in rtl-overrides.css
- Font fallbacks: system Arabic/CJK fonts via --font-body CSS variable
- i18n.ts: RTL_LANGUAGES set, applyDocumentDirection(), isRTL(), getLocale()
- story-renderer.ts: locale-aware date formatting via getLocale()
- Type declarations for both new locales
…TC clock

Replace hardcoded English text with t() calls across 12 source files:
signal context descriptions, alert titles/summaries, intel topic
names, map legend labels, asset/threat labels, UCDP panel headers,
stablecoin/status panel sections. Add ~114 new keys to en.json and
translate to all 11 locales (fr/de/es/it/pl/pt/nl/sv/ru/ar/zh).
Remove dead #timeDisplay span that was never updated by JS.

All 12 locales at 1127-key parity with 0 placeholder mismatches.
…AppImage, locale gaps, lang normalization

- Fix relative-time {{count}} placeholders in all 12 locales (5m ago, not m ago)
- Localize CommunityWidget: replace 3 hardcoded English strings with t() calls
- Add Linux AppImage to tech/finance Tauri configs, CI matrix (ubuntu-22.04), packaging scripts, and download-node.sh
- Fix language code normalization: add supportedLngs/nonExplicitSupportedLngs to i18next, normalize getCurrentLanguage()
- Translate ~240 untranslated English strings across 11 locale files (ru/ar/zh now 100% translated)
- Add components.community section to all 12 locales
- All 12 locales at 1130-key parity, 0 placeholder mismatches
applyDocumentDirection() now splits on '-' before checking RTL set,
so ar-SA correctly triggers dir="rtl" on first detect.
- CommunityWidget: aria-label="Close" → t('common.close')
- App.ts: ' (current)' suffix → t('common.currentVariant')
- fr.json: "Cloud & Infrastructure" → "Cloud et infrastructure"
- sv.json: "24h Vol" → "24t volym"
- Added common.close + common.currentVariant to all 12 locales
New ja.json with full translation parity to en.json.
Registered in i18n.ts: import, resources, supportedLngs, LANGUAGES, getLocale map.
t() always returns a string (key itself if missing), so || 'English'
fallbacks were unreachable dead code.
t() always returns a string, so || 'English' fallbacks were
unreachable. Removed all 15 instances.
@vercel
Copy link

vercel bot commented Feb 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Feb 18, 2026 4:20am
worldmonitor-finance Ready Ready Preview, Comment Feb 18, 2026 4:20am
worldmonitor-startup Ready Ready Preview, Comment Feb 18, 2026 4:20am

Request Review

@koala73
Copy link
Owner Author

koala73 commented Feb 18, 2026

Duplicate of #86 — created in error. All commits are on codex/pr-86 which feeds PR #86.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79f9f493b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

this.allNews = collectedNews;
this.initialLoadComplete = true;
maybeShowDownloadBanner();
mountCommunityWidget();

Choose a reason for hiding this comment

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

P2 Badge Mount community widget only once per session

mountCommunityWidget() is invoked inside loadNews(), and setupRefreshIntervals() schedules loadNews() repeatedly, so each refresh appends another .community-widget when the user has not dismissed it permanently. Because mountCommunityWidget() has no idempotency guard, long-running sessions can accumulate duplicate floating widgets (and duplicate click handlers), which degrades the UI and can confuse users.

Useful? React with 👍 / 👎.

Choose a reason for hiding this comment

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

P2 Badge Ignore missing i18n keys for runtime help text

The help text now comes from t(modals.runtimeConfig.help.${key}), but many secret keys do not have corresponding translations, and i18next returns the lookup key string in that case; since that value is truthy, the panel renders raw key paths (for example modals.runtimeConfig.help.GROQ_API_KEY) instead of omitting help text. This is a regression from the previous partial-help behavior and makes the settings UI noisy and misleading unless every secret key gets a translation entry.

Useful? React with 👍 / 👎.

@koala73 koala73 changed the title feat(i18n): full internationalization — 14 locales, zero hardcoded English feat(i18n): full internationalization — 14 locales, zero hardcoded English + Linux AppImage support Feb 18, 2026
Main variant: NHK World + Nikkei Asia in asia category.
Finance variant: Nikkei Asia in markets category.
Added asia.nikkei.com to RSS proxy allowlist.
…keys

- CommunityWidget: add DOM check to prevent duplicate widgets on repeated loadNews() calls
- RuntimeConfigPanel: compare t() result against key path to suppress missing help translations
@koala73
Copy link
Owner Author

koala73 commented Feb 18, 2026

Thanks again @Lib-LOCALE
It's out now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments