diff --git a/src/renderer/src/components/github-project/ProjectRoadmap.plugin-locale.test.tsx b/src/renderer/src/components/github-project/ProjectRoadmap.plugin-locale.test.tsx
new file mode 100644
index 000000000000..3e3b092a1391
--- /dev/null
+++ b/src/renderer/src/components/github-project/ProjectRoadmap.plugin-locale.test.tsx
@@ -0,0 +1,106 @@
+// @vitest-environment happy-dom
+
+import type { ReactNode } from 'react'
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import ProjectRoadmap from './ProjectRoadmap'
+import { i18n, setRendererPluginLanguagePacks } from '@/i18n/i18n'
+import { DEFAULT_LOCALE } from '@/i18n/supported-languages'
+import { pluginLanguageResourceId } from '../../../../shared/plugins/plugin-language-pack-artifact'
+import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github/project-types'
+
+vi.mock('@/components/ui/tooltip', () => ({
+ Tooltip: ({ children }: { children: ReactNode }) => <>{children}>,
+ TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}>,
+ TooltipContent: ({ children }: { children: ReactNode }) =>
{children}
+}))
+
+// The exact pack from the field crash report (v1.4.219, Windows).
+const PACK_ID = 'plugin:stablyai.orca-portuguese/pt-BR' as const
+const PACK_RESOURCE = pluginLanguageResourceId(PACK_ID)
+
+function roadmapTable(): GitHubProjectTable {
+ const row: GitHubProjectRow = {
+ id: 'one',
+ itemType: 'ISSUE',
+ content: {
+ number: 7,
+ title: 'Scheduled',
+ body: null,
+ url: 'https://github.com/o/r/issues/7',
+ state: 'OPEN',
+ stateReason: null,
+ isDraft: null,
+ repository: 'o/r',
+ assignees: [],
+ labels: [],
+ parentIssue: null,
+ issueType: null
+ },
+ fieldValuesByFieldId: {
+ f_start: { kind: 'date', fieldId: 'f_start', date: '2026-09-01' },
+ f_end: { kind: 'date', fieldId: 'f_end', date: '2026-10-15' }
+ },
+ updatedAt: '2026-08-31T00:00:00Z',
+ position: 0
+ }
+ return {
+ project: {
+ id: 'PVT_1',
+ owner: 'stablyai',
+ ownerType: 'organization',
+ number: 3,
+ title: 'Orca',
+ url: 'https://github.com/orgs/stablyai/projects/3'
+ },
+ selectedView: {
+ id: 'PVTV_1',
+ number: 2,
+ name: 'Roadmap',
+ layout: 'ROADMAP_LAYOUT',
+ filter: '',
+ fields: [
+ { kind: 'field', id: 'f_title', name: 'Title', dataType: 'TITLE' },
+ { kind: 'field', id: 'f_start', name: 'Start date', dataType: 'DATE' },
+ { kind: 'field', id: 'f_end', name: 'Target date', dataType: 'DATE' }
+ ],
+ groupByFields: [],
+ sortByFields: []
+ },
+ rows: [row],
+ totalCount: 1,
+ parentFieldDropped: false
+ }
+}
+
+afterEach(async () => {
+ cleanup()
+ window.localStorage.clear()
+ setRendererPluginLanguagePacks([])
+ await i18n.changeLanguage(DEFAULT_LOCALE)
+})
+
+describe('ProjectRoadmap with a plugin language pack active', () => {
+ it('formats ticks with the pack locale instead of the synthetic resource tag', async () => {
+ setRendererPluginLanguagePacks([
+ {
+ id: PACK_ID,
+ resourceLanguage: PACK_RESOURCE,
+ pluginKey: 'stablyai.orca-portuguese',
+ locale: 'pt-BR',
+ // Non-empty so i18next resolves to the pack, as a shipped pack does.
+ catalog: { common: { cancel: 'Cancelar' } }
+ }
+ ])
+ await i18n.changeLanguage(PACK_RESOURCE)
+ expect(i18n.resolvedLanguage).toBe(PACK_RESOURCE)
+
+ render(list} />)
+
+ const expectedMonth = new Intl.DateTimeFormat('pt-BR', {
+ month: 'short',
+ timeZone: 'UTC'
+ }).format(new Date(Date.UTC(2026, 8, 1)))
+ expect(screen.getAllByText(expectedMonth, { exact: false }).length).toBeGreaterThan(0)
+ })
+})
diff --git a/src/renderer/src/components/github-project/ProjectRoadmap.tsx b/src/renderer/src/components/github-project/ProjectRoadmap.tsx
index 5e06e41d8700..26c0c40824ee 100644
--- a/src/renderer/src/components/github-project/ProjectRoadmap.tsx
+++ b/src/renderer/src/components/github-project/ProjectRoadmap.tsx
@@ -3,7 +3,7 @@ import { CalendarClock } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion'
import { cn } from '@/lib/utils'
-import { i18n, translate } from '@/i18n/i18n'
+import { getIntlLocale, translate } from '@/i18n/i18n'
import ProjectGroupHeader from './ProjectGroupHeader'
import ProjectRoadmapBar from './ProjectRoadmapBar'
import { ProjectTitleCell } from './ProjectCellIdentity'
@@ -48,7 +48,7 @@ export default function ProjectRoadmap({
}: Props): React.JSX.Element {
const view = table.selectedView
const prefersReducedMotion = usePrefersReducedMotion()
- const locale = i18n.resolvedLanguage ?? i18n.language
+ const locale = getIntlLocale()
// Why: the grid lives on UTC calendar days (parseRoadmapDate), so "today"
// must be the viewer's LOCAL calendar date mapped to UTC midnight — the raw
// instant would shift the marker into the wrong day off UTC.
diff --git a/src/renderer/src/i18n/no-raw-language-intl-locale.test.ts b/src/renderer/src/i18n/no-raw-language-intl-locale.test.ts
new file mode 100644
index 000000000000..563d9a05b2ae
--- /dev/null
+++ b/src/renderer/src/i18n/no-raw-language-intl-locale.test.ts
@@ -0,0 +1,91 @@
+/**
+ * Plugin language packs make `i18n.language` a synthetic `plugin` tag that
+ * every `Intl` constructor rejects with a RangeError, so Intl locales must come
+ * from `getIntlLocale()`. This catches the two shapes that shipped that crash:
+ * the raw language passed straight to Intl, or held in a `locale` variable.
+ */
+import { readdirSync, readFileSync, statSync } from 'node:fs'
+import { relative, resolve } from 'node:path'
+// TypeScript 7 is a native CLI; AST tests still need the legacy JavaScript API.
+import ts from 'typescript-api'
+import { describe, expect, it } from 'vitest'
+
+const RENDERER_ROOT = resolve('src/renderer/src')
+const LOCALE_METHODS = new Set([
+ 'toLocaleString',
+ 'toLocaleDateString',
+ 'toLocaleTimeString',
+ 'localeCompare'
+])
+const RAW_LANGUAGE = /\b(?:i18n|i18next)\??\.(?:language|resolvedLanguage)\b/
+
+function collectSourceFiles(dir: string, files: string[] = []): string[] {
+ for (const name of readdirSync(dir)) {
+ const filePath = resolve(dir, name)
+ if (statSync(filePath).isDirectory()) {
+ collectSourceFiles(filePath, files)
+ } else if (
+ /\.(ts|tsx)$/.test(name) &&
+ !/\.test\.(ts|tsx)$/.test(name) &&
+ !filePath.includes('/i18n/locales/')
+ ) {
+ files.push(filePath)
+ }
+ }
+ return files
+}
+
+function isLocaleSink(node: ts.CallExpression | ts.NewExpression, file: ts.SourceFile): boolean {
+ const callee = node.expression
+ if (callee.getText(file).startsWith('Intl.')) {
+ return true
+ }
+ return ts.isPropertyAccessExpression(callee) && LOCALE_METHODS.has(callee.name.text)
+}
+
+describe('Intl locale source', () => {
+ it('never feeds the raw i18n language to Intl', () => {
+ const violations: string[] = []
+
+ for (const filePath of collectSourceFiles(RENDERER_ROOT)) {
+ const source = readFileSync(filePath, 'utf8')
+ if (!RAW_LANGUAGE.test(source)) {
+ continue
+ }
+ const file = ts.createSourceFile(
+ filePath,
+ source,
+ ts.ScriptTarget.Latest,
+ true,
+ filePath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
+ )
+ const report = (node: ts.Node): void => {
+ const { line } = file.getLineAndCharacterOfPosition(node.getStart(file))
+ violations.push(`${relative(process.cwd(), filePath)}:${line + 1}`)
+ }
+
+ function visit(node: ts.Node): void {
+ if (
+ (ts.isCallExpression(node) || ts.isNewExpression(node)) &&
+ isLocaleSink(node, file) &&
+ node.arguments?.some((arg) => RAW_LANGUAGE.test(arg.getText(file)))
+ ) {
+ report(node)
+ } else if (
+ ts.isVariableDeclaration(node) &&
+ ts.isIdentifier(node.name) &&
+ /^locale$/i.test(node.name.text) &&
+ node.initializer &&
+ RAW_LANGUAGE.test(node.initializer.getText(file))
+ ) {
+ report(node)
+ }
+ ts.forEachChild(node, visit)
+ }
+
+ visit(file)
+ }
+
+ expect(violations).toEqual([])
+ }, 15_000)
+})