feat(ui): add i18n scaffolding

Adds a minimal i18n provider with locale preference support and migrates folder selection copy to message keys.
This commit is contained in:
Shantur Rathore
2026-01-26 10:22:03 +00:00
parent 96f5a0ab44
commit 33939f4096
6 changed files with 193 additions and 34 deletions

View File

@@ -0,0 +1,104 @@
import { createContext, createMemo, createSignal, onMount, useContext } from "solid-js"
import type { ParentComponent } from "solid-js"
import { useConfig } from "../../stores/preferences"
import { enMessages } from "./messages/en"
type Messages = Record<string, string>
export type Locale = "en"
const SUPPORTED_LOCALES: readonly Locale[] = ["en"] as const
const messagesByLocale: Record<Locale, Messages> = {
en: enMessages,
}
function normalizeLocaleTag(value: string): string {
return value.trim().replace(/_/g, "-")
}
function matchSupportedLocale(value: string | undefined): Locale | null {
if (!value) return null
const normalized = normalizeLocaleTag(value)
const lower = normalized.toLowerCase()
const supportedLower = new Map(SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale]))
const exact = supportedLower.get(lower)
if (exact) return exact
const base = lower.split("-")[0]
if (!base) return null
const baseMatch = supportedLower.get(base)
return baseMatch ?? null
}
function detectNavigatorLocale(): Locale | null {
if (typeof navigator === "undefined") return null
const candidates = Array.isArray(navigator.languages) && navigator.languages.length > 0
? navigator.languages
: navigator.language
? [navigator.language]
: []
for (const candidate of candidates) {
const match = matchSupportedLocale(candidate)
if (match) return match
}
return null
}
function interpolate(template: string, params?: Record<string, unknown>): string {
if (!params) return template
return template.replace(/\{(\w+)\}/g, (_match, key: string) => {
const value = params[key]
return value === undefined || value === null ? "" : String(value)
})
}
export interface I18nContextValue {
locale: () => Locale
t: (key: string, params?: Record<string, unknown>) => string
}
const I18nContext = createContext<I18nContextValue>()
export const I18nProvider: ParentComponent = (props) => {
const { preferences } = useConfig()
const [detectedLocale, setDetectedLocale] = createSignal<Locale>("en")
onMount(() => {
const detected = detectNavigatorLocale()
if (detected) setDetectedLocale(detected)
})
const locale = createMemo<Locale>(() => {
const configured = matchSupportedLocale(preferences().locale)
return configured ?? detectedLocale() ?? "en"
})
const messages = createMemo<Messages>(() => messagesByLocale[locale()])
function t(key: string, params?: Record<string, unknown>): string {
const current = messages()[key]
const fallback = enMessages[key as keyof typeof enMessages]
const template = current ?? fallback ?? key
return interpolate(template, params)
}
const value: I18nContextValue = {
locale,
t,
}
return <I18nContext.Provider value={value}>{props.children}</I18nContext.Provider>
}
export function useI18n(): I18nContextValue {
const context = useContext(I18nContext)
if (!context) {
throw new Error("useI18n must be used within I18nProvider")
}
return context
}

View File

@@ -0,0 +1,39 @@
export const enMessages = {
"folderSelection.logoAlt": "CodeNomad logo",
"folderSelection.tagline": "Select a folder to start coding with AI",
"folderSelection.links.github": "CodeNomad GitHub",
"folderSelection.links.githubStars": "CodeNomad GitHub Stars",
"folderSelection.links.discord": "CodeNomad Discord",
"folderSelection.empty.title": "No Recent Folders",
"folderSelection.empty.description": "Browse for a folder to get started",
"folderSelection.recent.title": "Recent Folders",
"folderSelection.recent.subtitle.one": "{count} folder available",
"folderSelection.recent.subtitle.other": "{count} folders available",
"folderSelection.recent.remove": "Remove from recent",
"folderSelection.browse.title": "Browse for Folder",
"folderSelection.browse.subtitle": "Select any folder on your computer",
"folderSelection.browse.button": "Browse Folders",
"folderSelection.browse.buttonOpening": "Opening...",
"folderSelection.advancedSettings": "Advanced Settings",
"folderSelection.hints.navigate": "Navigate",
"folderSelection.hints.select": "Select",
"folderSelection.hints.remove": "Remove",
"folderSelection.hints.browse": "Browse",
"folderSelection.loading.title": "Starting instance...",
"folderSelection.loading.subtitle": "Hang tight while we prepare your workspace.",
"folderSelection.dialog.title": "Select Workspace",
"folderSelection.dialog.description": "Select workspace to start coding.",
"time.relative.justNow": "just now",
"time.relative.daysAgoShort": "{count}d ago",
"time.relative.hoursAgoShort": "{count}h ago",
"time.relative.minutesAgoShort": "{count}m ago",
} as const