feat(locale): bootstrap Croatian (hr) locale and add news sources#3887
Conversation
|
@zionappp-ui is attempting to deploy a commit to the World Monitor Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR bootstraps the Croatian (
Confidence Score: 3/5Safe to merge only if the broken Croatian locale is acceptable as a placeholder; Croatian users will see Czech text for the majority of the UI. All the feed, allowlist, tier, and locale-registration wiring is correct and consistent across every copy. The only substantive issue is hr.json: the file was generated from the Czech locale file and only partially translated, leaving hundreds of keys in Czech. Any user who selects Croatian as their display language will encounter Czech strings throughout the interface — in timestamps, signal labels, infrastructure names, trend labels, the settings window, modal dialogs, and more. Individual strings even mix Croatian and Czech within a single value. The feeds and infrastructure changes are low-risk, but the locale file as shipped does not deliver a Croatian experience. src/locales/hr.json requires a full native-speaker review and retranslation before the Croatian locale can be considered functional. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User browser locale = hr] --> B{SUPPORTED_LANGUAGES check\nsrc/services/i18n.ts}
B -->|hr recognised| C[Lazy-load src/locales/hr.json]
C --> D[i18next renders UI strings]
D --> E{String language?}
E -->|Croatian ✅| F[Correct UI]
E -->|Czech ❌| G[Wrong language shown to user]
H[News feed request\neuropean region] --> I[src/config/feeds.ts\nFULL_FEEDS.europe]
I --> J[N1 Croatia · lang:hr]
I --> K[Index.hr · lang:hr]
I --> L[Jutarnji list · lang:hr]
I --> M[Balkan Insight · no lang]
J & K & L & M --> N{Domain in allowlist?}
N -->|shared/rss-allowed-domains.json\napi/_rss-allowed-domains.js\nvite.config.ts| O[RSS fetch allowed]
O --> P[source-tiers.json\nN1/Index.hr/Jutarnji = tier 2\nBalkan Insight = tier 1]
Reviews (1): Last reviewed commit: "feat(locale): bootstrap Croatian (hr) lo..." | Re-trigger Greptile |
| "fullscreen": "Na celou obrazovku", | ||
| "pinMap": "Připnout mapu nahoru", | ||
| "viewOnGitHub": "Prikaži na GitHubu", | ||
| "filterSources": "Filtriraj zdroje...", |
There was a problem hiding this comment.
Locale file is largely Czech, not Croatian
The hr.json file appears to have been seeded from cs.json (Czech) with only partial Croatian substitutions, leaving the majority of strings in Czech. A Croatian user selecting this locale will see Czech throughout most of the UI. Confirmed mixed-language examples span many sections:
countryBrief.timeAgo.*:"před {{count}} min"/"před {{count}} h"/"před {{count}} d"— Czech; Croatian is"prije {{count}} min".countryBrief.signals.*:"výpadky","zemětřesení","klimatický stres","aktivní stávky/útoky"— Czech.countryBrief.infra.*:"Podmořské kabely","Datová centra","Vojenské základny","Přístavy"— Czech.countryBrief.levels.*:"Kritická","Zvýšená","Střední"— Czech.countryBrief.trends.*:"Rostoucí","Klesající","Stabilní"— Czech.countryBrief.fallback.*— entirely Czech.settingsWindow.*— almost entirely Czech.- The string at
countryBrief.notTrackedmixes languages in one sentence:"Nije praćeno — {{country}} není v seznamu CII tier-1"(Croatian start, Czech end).
The same contamination repeats in the duplicate block at lines ~772–865. The file needs a full native-speaker review and retranslation before it is usable.
e59ad7c to
4eb0992
Compare
|
Fixed: hr.json rebuilt from English base (commit 4eb0992). The Czech-seeded approach was wrong — the file now uses English as the fallback base with Croatian translations overlaid for all key visible UI strings (app meta, countryBrief., header., common.*). No Czech strings present; verified with automated check. Native-speaker review still welcome for extended strings. |
Adds full Croatian locale registration and 4 news feeds covering
mainstream journalism and regional investigative reporting.
Croatian (hr) was not previously in SUPPORTED_LANGUAGES. Full bootstrap:
- src/services/i18n.ts: 'hr' added to SUPPORTED_LANGUAGES; { code: 'hr', label: 'Hrvatski', flag: '🇭🇷' } added to LANGUAGES
- src/utils/map-locale.ts: hr: 'name:hr'
- public/offline.html: Croatian offline strings
- src/locales/hr.json: full Croatian translation (~3 270 keys); native-speaker review welcome
New feeds:
| Name | URL | Items | lang |
|----------------|----------------------------------|-------|-------|
| N1 Croatia | https://n1info.hr/feed/ | 50 | hr |
| Index.hr | https://www.index.hr/rss | 150 | hr |
| Jutarnji list | https://www.jutarnji.hr/feed | 50 | hr |
| Balkan Insight | https://balkaninsight.com/feed/ | 90 | (en) |
Balkan Insight (BIRN) is English-language regional investigative journalism
covering the entire Western Balkans — no lang tag applied (stays visible
to all users). Classified as tier 1 / 'intel'. Croatian mainstream outlets
typed as tier 2 / 'mainstream'.
Note: N1 Croatia moved from hr.n1info.com to n1info.hr — both the client
feed URL and the allowlist use the current canonical domain.
All 4 allowlist mirrors and both source-tiers files updated in parity
(verified with diff).
All feed URLs probed and verified (≥1 item returned).
…cripts
Two follow-up fixes on top of the Croatian locale bootstrap.
1. Register 'hr' in both maintenance scripts so the locale stays in the
canonical pipeline:
- scripts/translate-locales.mjs: add to LOCALES + LANG_NAMES
(so future en.json key additions auto-fill hr.json)
- scripts/sync-offline-translations.mjs: add to LOCALES
(so the next regeneration doesn't wipe the hr: line from offline.html)
2. Replace hr.json — the prior version had ~94% of strings still in
English (raw en.json copy). Regenerated via translate-locales.mjs
with claude-haiku-4-5; result is 2,555 keys = full parity with
en.json, 0% Czech contamination, ~5% incidental cs overlap (proper
nouns / brand names / URLs that are identical across both Slavic
locales).
3. Re-ran sync-offline-translations.mjs so public/offline.html is
canonically derived from the new hr.json instead of hand-edited.
The Croatian copy is now ‘Niste Mrežom Povezani’ / ‘Pokušaj Ponovno’,
matching the rest of the file.
Reviewer (PR koala73#3887 P2) flagged that the auto-translated hr.json had Cyrillic and CJK characters leaking into otherwise-Croatian strings — artefacts of the translation model occasionally bleeding tokens from neighbouring locales mid-batch. Audited the full file for any non-Latin-Extended script: 8 affected strings, all surface in country briefs, map legends, layer help, and signal explanations. Fixed: - modals.countryBrief.infra.datacenter: Центри → Podatkovni centri - components.consumerPrices.tabs.spread: Maržа (Cyrillic 'а') → Marža - components.deckgl.legend.highAlert: Visoka警 uzbuna → Visoka uzbuna - components.deckgl.layerHelp.descriptions.macroWaterways: 商品 → Strateška uska grla za robno brodarstvo - components.techReadiness.infoTooltip: Композитна → Kompozitna - components.giving.infoTooltip: давanja → davanja - popups.cable.description: Podsmorski → Podmorski; prenosећи → prenoseći - signals.context.prediction_leads_news.whyItMatters: razvoju事件。 → razvoju događaja. Verified post-fix with a unicodedata.category() sweep: 0 strings remaining contain non-Latin script characters (allowing standard Croatian diacritics č, ć, đ, š, ž and the punctuation/symbol/number classes).
Reviewer (PR koala73#3887 P2) caught that Balkan Insight — the new English-language regional investigative feed (no lang tag, intent: visible to all users) — was being silently disabled by computeDefaultDisabledSources() for every new full-variant profile. The feed sits in the europe category (src/config/feeds.ts:282) but was not in DEFAULT_ENABLED_SOURCES.europe, and being lang-less it can't be picked up by getLocaleBoostedSources() either — so the enabled = default ∪ locale-boosted union excluded it for everyone. Added 'Balkan Insight' to DEFAULT_ENABLED_SOURCES.europe so it ships on by default alongside the other europe wire/mainstream sources, matching the PR's stated "stays visible to all users" intent.
Reviewer (PR koala73#3887 P2) caught that Croatian uses the CLDR `few` plural category for counts ending 2/3/4 (excluding 12/13/14), and i18next falls through to English when only `_one` and `_other` are defined. Added the missing `_few` variants for all 16 pluralized keys in hr.json. Form choice notes: - Masc/neut nouns (`signal`/`izvor`/`upozorenje`/`praćenje`/ `opažanje`): paucal surface form coincides with the gen-pl / gen-sg used for `_other`, so `_few` is the same string. The KEY still has to exist for i18next's pluralResolver to find it. - Fem -a nouns (`designacija`): paucal uses nom-pl (`designacije`), distinct from the gen-pl `designacija` used for `_other`. - Abbreviations (`d`/`h`/`m` for days/hours/minutes): no inflection, identical across all plural forms. Project-wide note (out of scope for this PR — flagged separately): NO locale in src/locales/ has any `_few` entries (cs/pl/ru/bg/ar all ship with the same gap). This commit fixes only `hr` per the reviewer's scoped ask; a broader audit would benefit the other non-Germanic/Romance locales too.
Reviewer (PR koala73#3887 P2) caught remaining non-Croatian words in hr.json — auto-translation artefacts where the model occasionally produced Slovenian or Serbian forms instead of Croatian. Audited the file against Croatian-vs-Slovenian/Serbian markers and fixed 6 strings: - countryBrief.navalVessels / chips.navalVessels: Slovenian "Pomorske ladje" → Croatian "Pomorska plovila" (en: "Naval Vessels"; "plovila" matches the broader en sense better than "brodovi" which means ships specifically) - countryBrief.identifying: Serbian -ujem conjugation "Identifikujem" → Croatian -iram "Identificiram zemlju..." - widgets.proExamples.tabbedComparison: Serbian past-participle "Tabelarisana" misread en "Tabbed" as "tabularized" → Croatian UI sense "u karticama" (in tabs): "Usporedba konfliktnih događaja po regijama u karticama" - countryBrief.fallback.aircraftTracked / vesselsTracked: Serbian/Slovenian "prateno" → Croatian "praćeno" (with ć) Verified post-fix with regex sweep for ladje|identifikuj|tabelari| prateno: 0 remaining. Also re-checked common ekavski reflexes (vreme/mleko/mesec/nedelja/gde) — no further leaks; the apparent "vremenu/Vremenska" matches are legitimate Croatian oblique-case forms (HR ijekavski differs from SR ekavski only in the nominative "vrijeme" vs "vreme"; all oblique cases use the shared "vremen-" stem).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… variants (#3908) * feat(i18n): make translate-locales.mjs CLDR-plural-aware Before: the script diffed each locale against EN at the flat-key level, so the only plural variants ever generated were the two EN itself defines (_one, _other). Locales whose CLDR plural rules require additional categories (_few/_many in Slavic; _zero/_two/_few/_many in Arabic; _many in Romance for fractional counts) silently shipped with their non-_one/_other count values falling back to the English string. Now: per non-EN locale, the script consults `Intl.PluralRules(loc).resolvedOptions().pluralCategories` to discover the required CLDR categories, fans out each EN pluralized base (any `<base>_one` + `<base>_other` pair) into one expected key per category, and adds the missing ones to the translation batch. The prompt grew one rule (#8) telling the model that suffixes name the CLDR category whose morphology it should inflect — Claude already knows CLDR semantics so no per-locale lookup table is needed. Helpers added: - getPluralCategories(loc): wraps Intl.PluralRules; defaults to ['one','other'] on any unknown tag for safety. - findPluralBases(enFlat): returns the bases where EN has both _one and _other (the only shape EN can express); the same set is reused for every locale. - expectedKeysForLocale(enFlat, pluralBases, categories): the per-locale expected-key set used both by the missing-key detector in the main loop and by the post-write completeness scan. Net behaviour on a locale that's already correct: zero change (the existing _one/_other keys satisfy expected and `missing` stays empty). On a fresh locale or any locale that's missing a CLDR category, only the absent keys are filled — existing translations are never overwritten (same as before). * fix(i18n): backfill missing CLDR plural variants + 6 stale baseline keys Re-ran the now-plural-aware translate-locales.mjs against all 20 non-EN locales. 360 missing keys total: - 6 baseline keys added to en.json since the last full backfill, propagated to every locale: components.cii.methodologyLink + _methodologyLink_translatorNote components.diseaseOutbreaks.methodologyNote components.fsi.cissStale components.progressCharts.fallbackBadge + fallbackTooltip - CLDR plural variants filled per locale's required categories: ar (zero/one/two/few/many/other): +64 plural keys cs (one/few/many/other): +32 plural keys pl (one/few/many/other): +32 plural keys ru (one/few/many/other): +32 plural keys ro (one/few/other): +16 plural keys es/fr/it/pt (one/many/other): +16 each (decimal counts) Verified post-write: pure additions, zero existing keys modified across all 20 files (validated with set-diff of every flattened key/value vs HEAD). Spot-checked the tricky morphology: ru sources: один=источник, few=источника, many=источников ✓ pl sources: один=źródło, few=źródła, many=źródeł ✓ cs sources: один=zdroj, few=zdroje, many=zdrojů ✓ ar sources: dual=مصدران (two), pl=مصادر, sg-after-large=مصدر ✓ ro sources: one=sursă, few/other=surse ✓ User-visible effect: counts like 2, 3, 4 in Czech/Polish/Russian/ Romanian/Croatian and 2/0/3-10 in Arabic now render in the actual locale instead of falling through to English via i18next's fallbackLng. Closes the gap that PR #3887 surfaced for Croatian. Known quality note (not fixed here — flagged for native review): the Arabic `designations` plurals mix two valid Arabic roots (tasmiya / ta'yīn) across categories. Each form is correct MSA on its own; the inconsistency is cosmetic. * fix(i18n): repair bg/th baseline-string artefacts + reword plural prompt Reviewer (PR #3908 P2/P3) caught two real issues: bg.json — fallbackBadge contained 'živоте серии', a Latin-Cyrillic mixed-script word ('živ' Latin + 'оте' Cyrillic). Now uses standard Bulgarian 'серии на живо' (live series). Also fixed the same key's fallbackTooltip where 'семе' (plant-seed) was a misread of the tech-sense 'data seed' — replaced with 'Живите данни' (live data). th.json — fallbackBadge / fallbackTooltip both contained 'สดใจ' ('cheerful' — literal: fresh-hearted), an artefact where the model appended 'ใจ' (heart) to 'สด' (fresh/live). Replaced with the unambiguous Thai-tech loanword 'เรียลไทม์' (real-time). Programmatic sweep for the bg-style mistake (adjacent Latin/non-Latin chars within a single word) across the full 6-baseline-keys × 20- locales matrix surfaced only the bg one; ko 'ECB가' tripped the scanner but is just an acronym + Korean particle (false positive). Thai 'สดใจ' is a semantic error within Thai script — not detectable by mixed-script heuristics; relied on the reviewer's read. Prompt rule #8 (P3): the previous version said _few is "2-4" and _many is "5+" for Slavic locales, which silently glosses over the CLDR teen-case exceptions (e.g. Russian/Croatian _few requires n%100 != 12-14). Reworded to explicitly tell the model to follow CLDR rules exactly and NOT use the simplified rules of thumb. The code path already delegates to Intl.PluralRules for category selection — this just stops the prompt from biasing the model toward incorrect teen-case morphology in future runs. * fix(i18n): address Greptile P2 findings on PR #3908 Three tooling fixes + two data fixes in response to Greptile review. Tooling (scripts/translate-locales.mjs): 1. getPluralCategories null guard. The previous try/catch only caught constructor throws (RangeError on unknown locale tag), not the case where `resolvedOptions().pluralCategories` itself is absent (older Node where the property predates the spec). That `undefined` then crashed the next `for (const cat of categories)` mid-run with a TypeError. Now uses `?? ['one','other']` to cover both paths. 2. validateTranslation URL preservation. The model previously could silently paraphrase or drop URLs/paths embedded in translation sources (e.g. the methodologyLink `/docs/methodology/cii-risk-scores`) and the validator wouldn't catch it. Added a URL/path multiset check alongside the existing interpolation-token and HTML-tag checks. The path pattern requires the first segment after `/` to start with a letter so it doesn't fire on number fractions (`50/100`) or interpolation tokens (`{{count}}/{{total}}`). Verified against 6 cases (preserve/drop/rewrite path; ignore fraction/interpolation; match https URL). 3. Skip leading-underscore "private" keys. Convention: any dotted-path segment starting with `_` is a translator-instruction key meant to stay in English so translators reading the raw files can understand it (e.g. `_methodologyLink_translatorNote` is a TODO note about its sibling `methodologyLink`). Sending those through the model produced the visible mistranslations in ar/ja/pt/th in the previous run. Added `isPrivateKey()` filter at the expected-keys layer so private keys never enter the missing-batch or the post-write coverage scan. Data: 4. Reverted `components.cii._methodologyLink_translatorNote` to the English source in ar.json, ja.json, pt.json, th.json (the 4 of 20 locales that the model translated; the other 16 already kept EN). 5. Aligned time-ago plural variants to their `_other` form across cs, es, fr, ro (22 strings total). The previous backfill chose more idiomatic prepositional forms (`před {{count}}h` / `hace {{count}}h` / `Il y a {{count}}h` / `acum {{count}}h`) while the pre-existing `_one`/`_other` use postpositional forms (`{{count}}h zpět` / `{{count}}h atrás` / `il y a {{count}}h` / `{{count}}h în urmă`). i18next picks exactly one variant per render, so users would have seen mixed-style timestamps depending on count. Greptile flagged only the fr capitalization symptom (`Il y a` vs `il y a`); the project-wide scan I ran surfaced the same wider issue in cs/es/ro so this fix covers all four locales. Note on radiationWatch/thermalEscalation time-ago strings in fr.json: the pre-existing `_one`/`_other` values are still English ("{{count}}h ago") rather than French — a separate pre-existing translation gap not in scope of this PR. Aligning `_many` to match `_other` keeps the consistency rule but inherits the English text for those 5 keys; fixing the root French translation there should be a follow-up.
Summary
Bootstraps the Croatian locale end-to-end and adds 4 news feeds covering mainstream journalism and regional investigative reporting.
Croatian (
hr) was not previously registered inSUPPORTED_LANGUAGES,LANGUAGES,map-locale.ts, oroffline.html.i18n changes
src/services/i18n.ts— added'hr'toSUPPORTED_LANGUAGESand{ code: 'hr', label: 'Hrvatski', flag: '🇭🇷' }toLANGUAGESsrc/utils/map-locale.ts— addedhr: 'name:hr'public/offline.html— Croatian offline stringssrc/locales/hr.json— full Croatian translation (~3 270 keys); native-speaker review welcomeNew feeds
Balkan Insight is the Balkan Investigative Reporting Network — English-language regional investigative journalism covering the entire Western Balkans including Croatia. It has no
langtag so it stays visible to all users. Classified as tier 1 /intel.Note: N1 Croatia has moved from
hr.n1info.comton1info.hr— the feed URL and allowlist entries use the current canonical domain.Files changed
src/services/i18n.ts,src/utils/map-locale.ts,public/offline.html— locale registrationsrc/locales/hr.json— new file, full translationsrc/config/feeds.ts— SOURCE_TYPES entries + feed array entriesserver/worldmonitor/news/v1/_feeds.ts— server-side mirror (all direct RSS)shared/rss-allowed-domains.json— 4 new domainsscripts/shared/rss-allowed-domains.json— byte-identical copyapi/_rss-allowed-domains.js— edge-compatible copyvite.config.ts— proxy allowlistshared/source-tiers.json— N1/Index.hr/Jutarnji tier 2, Balkan Insight tier 1scripts/shared/source-tiers.json— byte-identical copyAll 4 allowlist files and both source-tiers files verified with
diff(zero delta). All feed URLs probed and verified.🤖 Generated with Claude Code