Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .changeset/inline-default-value-drift-gate-3810.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
"@object-ui/app-shell": patch
"@object-ui/plugin-detail": patch
"@object-ui/plugin-list": patch
"@object-ui/console": patch
---

Align 43 inline `defaultValue` strings with the `en` pack, and make the call-site gate enforce it (objectui#3810)

`t(key, { defaultValue: 'English text' })` only renders that text when i18next
**misses** the key. Where the key exists in `packages/i18n/src/locales/en.ts` the
pack value always wins, so the inline string is dead code — and 43 of those dead
strings said something different from the sentence users actually read.

`scripts/check-i18n-call-site-keys.mjs` (objectui#3530) now compares the two
whenever a call site carries a literal `defaultValue` for a key `en` defines, and
fails on any byte of difference. It is a hard rule with **no baseline**: the
repo-wide census measured 43 sites in 19 files out of 851 literal inline defaults,
and all 43 are aligned here, so there is no debt for a ratchet to hold. A
`defaultValue` on a key that is *not* yet in `en` stays legal — that transition
runs for months (objectui#3546) and belongs to the existing `missing-key` rule,
which keeps reporting it alone.

Every fix moved the CALL SITE to the pack's wording. `en.ts` is untouched: its
values are what users read today, and changing one would oblige the same change in
the nine other packs (`scripts/check-i18n-en-drift.mjs`, objectui#3650). Six of the
43 differed only in an ellipsis (`...` against U+2026) — invisible in review, which
is how they survived three i18n gates that are each blind to this class by
construction.

The visible effect is confined to hosts that render these components with **no**
`I18nProvider` and no initialised i18next instance. There, react-i18next's
not-ready `t` returns the `defaultValue`, so the inline string was the rendered
one; it now matches what a provider-backed app has always shown. Inside the
console — provider mounted — nothing users see changes. The clearest converging
examples: the workspaces screen was written as "Organizations" at nine call sites
while every user has been reading "Workspaces"; the forgot-password success line
was written as "If an account exists, a reset link has been sent." while the pack
asserts "We've sent a password reset link to {{email}}."
8 changes: 4 additions & 4 deletions apps/console/src/pages/auth/ForgotPasswordPage.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,20 +31,20 @@ export function ForgotPasswordPage() {
linkComponent={RouterLink}
title={t('auth.forgotPassword.title', { defaultValue: 'Reset your password' })}
description={t('auth.forgotPassword.description', {
defaultValue: "Enter your email and we'll send you a reset link.",
defaultValue: "Enter your email address and we'll send you a link to reset your password",
})}
labels={{
emailLabel: t('auth.forgotPassword.emailLabel', { defaultValue: 'Email' }),
emailPlaceholder: t('auth.forgotPassword.emailPlaceholder', { defaultValue: 'name@example.com' }),
submitButton: t('auth.forgotPassword.submitButton', { defaultValue: 'Send reset link' }),
submitButton: t('auth.forgotPassword.submitButton', { defaultValue: 'Send Reset Link' }),
submittingButton: t('auth.forgotPassword.submittingButton', {
defaultValue: 'Sending',
defaultValue: 'Sending...',
}),
successTitle: t('auth.forgotPassword.successTitle', {
defaultValue: 'Check your email',
}),
successDescription: t('auth.forgotPassword.successDescription', {
defaultValue: 'If an account exists, a reset link has been sent.',
defaultValue: "We've sent a password reset link to {{email}}. Please check your inbox.",
}),
backToSignInText: t('auth.forgotPassword.backToSignInText', {
defaultValue: 'Back to sign in',
Expand Down
4 changes: 2 additions & 2 deletions apps/console/src/pages/auth/LoginPage.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -366,7 +366,7 @@ function LoginFormCard({
linkComponent={RouterLink}
errorMessages={{
INVALID_EMAIL_OR_PASSWORD: t('auth.login.errors.invalidCredentials', {
defaultValue: 'Invalid email or password',
defaultValue: 'Invalid email or password. Please try again.',
}),
EMAIL_NOT_VERIFIED: t('auth.login.errors.emailNotVerified', {
defaultValue: 'Please verify your email address before signing in.',
Expand All@@ -383,7 +383,7 @@ function LoginFormCard({
passwordPlaceholder: t('auth.login.passwordPlaceholder', { defaultValue: 'Enter your password' }),
forgotPasswordText: t('auth.login.forgotPasswordText', { defaultValue: 'Forgot password?' }),
submitButton: t('auth.login.submitButton', { defaultValue: 'Sign In' }),
submittingButton: t('auth.login.submittingButton', { defaultValue: 'Signing in' }),
submittingButton: t('auth.login.submittingButton', { defaultValue: 'Signing in...' }),
noAccountText: t('auth.login.noAccountText', { defaultValue: "Don't have an account?" }),
signUpText: t('auth.login.signUpText', { defaultValue: 'Sign up' }),
phoneLabel: t('auth.login.phoneLabel', { defaultValue: 'Phone number' }),
Expand Down
10 changes: 5 additions & 5 deletions apps/console/src/pages/auth/RegisterPage.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -151,9 +151,9 @@ export function RegisterPage() {
<AuthLayout formWidth="md">
<Card className="border-border/60 px-4 py-8 shadow-sm shadow-primary/5 backdrop-blur supports-[backdrop-filter]:bg-card/95">
<RegisterForm
title={t('auth.register.title', { defaultValue: 'Create your account' })}
title={t('auth.register.title', { defaultValue: 'Create an account' })}
description={t('auth.register.description', {
defaultValue: 'Sign up to get started',
defaultValue: 'Create your account to start building.',
})}
loginUrl={loginUrl}
linkComponent={RouterLink}
Expand DownExpand Up@@ -181,7 +181,7 @@ export function RegisterPage() {
defaultValue: 'Create a password (min. 8 characters)',
}),
confirmPasswordLabel: t('auth.register.confirmPasswordLabel', {
defaultValue: 'Confirm password',
defaultValue: 'Confirm Password',
}),
confirmPasswordPlaceholder: t('auth.register.confirmPasswordPlaceholder', {
defaultValue: 'Confirm your password',
Expand All@@ -192,8 +192,8 @@ export function RegisterPage() {
passwordTooShortError: t('auth.register.passwordTooShortError', {
defaultValue: 'Password must be at least 8 characters',
}),
submitButton: t('auth.register.submitButton', { defaultValue: 'Create account' }),
submittingButton: t('auth.register.submittingButton', { defaultValue: 'Creating account' }),
submitButton: t('auth.register.submitButton', { defaultValue: 'Create Account' }),
submittingButton: t('auth.register.submittingButton', { defaultValue: 'Creating account...' }),
hasAccountText: t('auth.register.hasAccountText', { defaultValue: 'Already have an account?' }),
signInText: t('auth.register.signInText', { defaultValue: 'Sign in' }),
}}
Expand Down
4 changes: 2 additions & 2 deletions apps/console/src/pages/auth/VerifyEmailPromptPage.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -79,7 +79,7 @@ export function VerifyEmailPromptPage() {
setResendError(description);
toast.error(
t('auth.verifyEmail.resendFailed', {
defaultValue: 'Failed to resend verification email',
defaultValue: 'Cannot resend verification email',
}),
{ description },
);
Expand DownExpand Up@@ -126,7 +126,7 @@ export function VerifyEmailPromptPage() {
>
<p className="font-medium">
{t('auth.verifyEmail.resendFailed', {
defaultValue: 'Failed to resend verification email',
defaultValue: 'Cannot resend verification email',
})}
</p>
<p className="mt-1 text-destructive/90">{resendError}</p>
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/console/home/HomeLayout.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export function HomeLayout({ children }: HomeLayoutProps) {
chat (the dock maximized; Home has no shell to dock a rail into). */}
{showChatbot && (
<ConsoleChatbotFab
appLabel={t('workspace.default', { defaultValue: 'Workspace' })}
appLabel={t('workspace.default', { defaultValue: 'My Workspace' })}
onOpenDock={openAssistant}
/>
)}
Expand Down
8 changes: 4 additions & 4 deletions packages/app-shell/src/console/home/HomePage.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -305,7 +305,7 @@ export function HomePage() {
if (loading) {
return (
<div className="flex flex-1 items-center justify-center py-20">
<div className="text-muted-foreground">{t('home.loading', { defaultValue: 'Loading workspace' })}</div>
<div className="text-muted-foreground">{t('home.loading', { defaultValue: 'Loading workspace...' })}</div>
</div>
);
}
Expand All@@ -327,12 +327,12 @@ export function HomePage() {
*/}
{isAdmin ? (
<Empty>
<EmptyTitle>{t('home.welcome', { product: getRuntimeConfig().branding.productName, defaultValue: 'Welcome to {{product}}' })}</EmptyTitle>
<EmptyTitle>{t('home.welcome', { product: getRuntimeConfig().branding.productName, defaultValue: 'Build your business system with AI' })}</EmptyTitle>
<EmptyDescription>
{buildAvailable
? t('home.welcomeAdminDescription', {
defaultValue:
'Describe your business in one sentence — AI generates the objects, screens, APIs and agent tools. Or set things up yourself from the Administration menu on the left.',
'Describe your business in one sentence — AI generates the objects, screens, APIs and agent tools. Or set things up yourself from the menu on the left.',
})
: askAvailable
? t('home.welcomeAdminDescriptionNoBuild', {
Expand All@@ -358,7 +358,7 @@ export function HomePage() {
<EmptyDescription>
{t('home.noAppsDescription', {
defaultValue:
'There are no applications available to you yet. Please contact your workspace administrator.',
'Your workspace is being set up — apps your admin shares with you will show up here.',
})}
</EmptyDescription>
</Empty>
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/console/home/HomeRail.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -188,7 +188,7 @@ export function HomeContinue({ items, onOpen, t }: { items: RecentItem[]; onOpen
export function HomeActivity({ items, onViewAll, t }: { items: ActivityItem[]; onViewAll: () => void; t: TFn }) {
const { language } = useObjectTranslation();
return (
<Card icon={Activity} title={t('sidebar.activityFeed', { defaultValue: 'Activity' })}>
<Card icon={Activity} title={t('sidebar.activityFeed', { defaultValue: 'Activity feed' })}>
{items.length === 0 ? (
<p className="text-sm text-muted-foreground">
{t('layout.activityFeed.empty', { defaultValue: 'No recent activity' })}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,11 +175,11 @@ export function OrganizationsPage() {
<div className="mx-auto w-full max-w-5xl px-4 sm:px-6 py-8 sm:py-12">
<div className="mb-8">
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">
{t('organizations.heading', { defaultValue: 'Your Organizations' })}
{t('organizations.heading', { defaultValue: 'Your Workspaces' })}
</h1>
<p className="text-sm text-muted-foreground mt-1">
{t('organizations.subtitle', {
defaultValue: 'Select an organization to continue, or create a new one.',
defaultValue: 'Select a workspace to continue, or create a new one.',
})}
</p>
</div>
Expand All@@ -191,7 +191,7 @@ export function OrganizationsPage() {
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t('organizations.searchPlaceholder', {
defaultValue: 'Search for an organization',
defaultValue: 'Search for a workspace',
})}
className="pl-9"
data-testid="organizations-search"
Expand All@@ -200,32 +200,32 @@ export function OrganizationsPage() {
{canCreateOrg && (
<Button onClick={() => setIsCreateOpen(true)} data-testid="organizations-new">
<Plus className="mr-2 h-4 w-4" />
{t('organizations.new', { defaultValue: 'New organization' })}
{t('organizations.new', { defaultValue: 'New workspace' })}
</Button>
)}
</div>

{orgList.length === 0 ? (
<Empty>
<EmptyTitle>
{t('organizations.emptyTitle', { defaultValue: 'No organizations yet' })}
{t('organizations.emptyTitle', { defaultValue: 'No workspaces yet' })}
</EmptyTitle>
<EmptyDescription>
{t('organizations.emptyDescription', {
defaultValue: 'Create your first organization to get started.',
defaultValue: 'Create your first workspace to get started.',
})}
</EmptyDescription>
{canCreateOrg && (
<Button className="mt-6" onClick={() => setIsCreateOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
{t('organizations.new', { defaultValue: 'New organization' })}
{t('organizations.new', { defaultValue: 'New workspace' })}
</Button>
)}
</Empty>
) : filtered.length === 0 ? (
<div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
{t('organizations.noMatches', {
defaultValue: 'No organizations match your search.',
defaultValue: 'No workspaces match your search.',
})}
</div>
) : (
Expand All@@ -251,7 +251,7 @@ export function OrganizationsPage() {
<div className="truncate font-medium">{org.name}</div>
<div className="truncate text-xs text-muted-foreground">
{isActive
? t('organizations.current', { defaultValue: 'Current organization' })
? t('organizations.current', { defaultValue: 'Current workspace' })
: org.slug}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/layout/AppHeader.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -749,7 +749,7 @@ export function AppHeader({
<>
<PathSep />
<span className="text-sm font-medium text-foreground/80 px-1.5">
{t('organizations.title', { defaultValue: 'Organizations' })}
{t('organizations.title', { defaultValue: 'Workspaces' })}
</span>
</>
)}
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/layout/AppSidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -644,7 +644,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
<SidebarGroup data-testid="system-fallback-nav">
<SidebarGroupLabel className="flex items-center gap-1.5">
<Settings className="h-3.5 w-3.5" />
{t('layout.appSwitcher.systemConsole', { defaultValue: 'System' })}
{t('layout.appSwitcher.systemConsole', { defaultValue: 'System Console' })}
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/layout/ChatDock.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -560,7 +560,7 @@ export function ChatDockLauncher({ onExpand, className }: ChatDockLauncherProps)
onClick={onExpand}
data-testid="chat-dock-launcher"
aria-label={t('console.ai.dock.open', { defaultValue: 'Open assistant' })}
title={t('console.ai.dock.open', { defaultValue: 'Open assistant (⌘⇧I)' })}
title={t('console.ai.dock.open', { defaultValue: 'Open assistant' })}
className={cn(
'fixed right-0 top-1/2 z-40 hidden h-16 w-7 -translate-y-1/2 rounded-l-md rounded-r-none border-r-0 bg-background shadow-md md:inline-flex',
className,
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/layout/InboxPopover.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,7 +312,7 @@ export function InboxPopover({
</TabsTrigger>
<TabsTrigger value="activity" className="text-xs gap-1.5 data-[state=active]:bg-transparent">
<ActivityIcon className="h-3.5 w-3.5" />
{t('sidebar.activityFeed', { defaultValue: 'Activity' })}
{t('sidebar.activityFeed', { defaultValue: 'Activity feed' })}
</TabsTrigger>
</TabsList>

Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/views/InterfaceListPage.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -433,7 +433,7 @@ export function InterfaceListPage({ page, className, onConfigChange, reserveEdit
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<Database className="h-6 w-6 text-muted-foreground" />
</div>
<EmptyTitle>{t('empty.objectNotFound', { defaultValue: 'Source object not found' })}</EmptyTitle>
<EmptyTitle>{t('empty.objectNotFound', { defaultValue: 'Object Not Found' })}</EmptyTitle>
<EmptyDescription>
{t('empty.interfacePageSourceMissing', {
defaultValue: 'This interface page references "{{name}}", which is not available.',
Expand Down
4 changes: 2 additions & 2 deletions packages/app-shell/src/views/RecordDetailView.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -1793,7 +1793,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
history: {
entries: historyEntries ?? [],
loading: historyLoading && historyEntries === null,
unknownUserText: t('detail.unknownUser', { defaultValue: 'Unknown user' }),
unknownUserText: t('detail.unknownUser', { defaultValue: 'Unknown' }),
},
}),
// Approvals tab (#3461) — only when the record actually has requests,
Expand DownExpand Up@@ -1958,7 +1958,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
// because the page is not focused or running over http://).
try {
await navigator.clipboard.writeText(window.location.href);
toast.success(t('detail.linkCopied', { defaultValue: 'Link copied' }));
toast.success(t('detail.linkCopied', { defaultValue: 'Link copied to clipboard' }));
} catch (err: any) {
toast.error(
t('detail.linkCopyFailed', { defaultValue: 'Failed to copy link' }) +
Expand Down
4 changes: 2 additions & 2 deletions packages/app-shell/src/views/ReportConfigPanel.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,11 +140,11 @@ export function ReportConfigPanel({
className="hidden sm:flex w-[440px] shrink-0 flex-col border-l bg-background h-full"
data-testid="report-config-panel"
role="complementary"
aria-label={t('report.editor.title', { defaultValue: 'Edit report' })}
aria-label={t('report.editor.title', { defaultValue: 'Title' })}
>
<div className="flex items-center justify-between gap-2 border-b px-4 py-2.5 shrink-0">
<div className="text-sm font-medium truncate">
{t('report.editor.title', { defaultValue: 'Edit report' })}
{t('report.editor.title', { defaultValue: 'Title' })}
</div>
<Button
variant="ghost"
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/src/views/ReportView.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -395,7 +395,7 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
<div className="flex-1 overflow-hidden flex flex-col sm:flex-row relative">
<div className="flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5">
<div className="w-full shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150">
<Suspense fallback={<div className="p-8 text-sm text-muted-foreground">{t('common.loading', { defaultValue: 'Loading' })}</div>}>
<Suspense fallback={<div className="p-8 text-sm text-muted-foreground">{t('common.loading', { defaultValue: 'Loading...' })}</div>}>
<div className="p-4 sm:p-6">
<ReportRenderer schema={previewReport} dataSource={dataSource as any} rows={reportRuntimeData} onDrill={handleDatasetDrill} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-detail/src/DetailView.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -954,7 +954,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
</TooltipTrigger>
<TooltipContent>
{idCopied
? t('detail.copied', { defaultValue: 'Copied' })
? t('detail.copied', { defaultValue: 'Copied!' })
: t('detail.copyRecordId', { defaultValue: 'Copy record ID' })}
</TooltipContent>
</Tooltip>
Expand Down
Loading
Loading