diff --git a/.changeset/12970-about-build.md b/.changeset/12970-about-build.md new file mode 100644 index 0000000000..4b83d2350c --- /dev/null +++ b/.changeset/12970-about-build.md @@ -0,0 +1,9 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/i18n': patch +'@object-ui/console': patch +--- + +The help menu gains an About entry on every console bar (the tenant console and the control plane draw the same header). It opens a small dialog naming the build the console was compiled from: `VITE_APP_VERSION` from the build's environment, or, when that is unset, the short sha of `HEAD` that the console's Vite config reads from git at build time. A console built with neither says "Unknown build" in words instead of showing an empty field. + +The dialog and Sentry's release fallback read the same value through one reader, so the two can no longer name different builds. The text comes from new `help.about*` keys in all ten language packs. diff --git a/apps/console/vite.config.ts b/apps/console/vite.config.ts index 885f088070..da8b4de797 100644 --- a/apps/console/vite.config.ts +++ b/apps/console/vite.config.ts @@ -8,6 +8,7 @@ import react from '@vitejs/plugin-react'; import path from 'path'; import fs from 'fs'; import zlib from 'node:zlib'; +import { execFileSync } from 'node:child_process'; // Relative specifiers carry their real file extension, and `__dirname` is // spelled `import.meta.dirname` throughout this file, so the config stays // loadable by Vite's `configLoader: 'native'` — which imports this file with @@ -587,6 +588,27 @@ function serveRuntimeAssets(): Plugin { // stable across HTML revisions. const basePath = process.env.VITE_BASE_PATH || './'; +// The console's build identity (objectui#12970): `VITE_APP_VERSION` from the +// build's environment when it is set (a CI or Docker build passes the objectui +// commit), else the short sha of `HEAD` read from git right here, so a local +// build names its commit too. Baked as `import.meta.env.VITE_APP_VERSION` — the +// one spelling `@object-ui/app-shell`'s `buildIdentity.ts` reads for both the +// help menu's About dialog and Sentry's release fallback. With neither (a build +// context without `.git`), nothing is baked and the dialog says the build is +// unknown. `rev-parse` answers in a shallow clone and on a detached HEAD alike. +function readGitShortSha(): string { + try { + return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { + cwd: import.meta.dirname, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return ''; + } +} +const consoleBuild = process.env.VITE_APP_VERSION?.trim() || readGitShortSha(); + // On Vercel/CI we skip the compression and visualizer plugins because the // Vercel CDN handles gzip/brotli automatically and bundle analysis is not // needed during CI builds. This reduces peak memory by ~1.5 GB. @@ -832,6 +854,7 @@ export default defineConfig({ 'process.env': {}, 'process.platform': '"browser"', 'process.version': '"0.0.0"', + ...(consoleBuild ? { 'import.meta.env.VITE_APP_VERSION': JSON.stringify(consoleBuild) } : {}), }, plugins: [ diff --git a/packages/app-shell/src/__tests__/buildIdentity-12970.test.ts b/packages/app-shell/src/__tests__/buildIdentity-12970.test.ts new file mode 100644 index 0000000000..c6607116ad --- /dev/null +++ b/packages/app-shell/src/__tests__/buildIdentity-12970.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The console's build identity is ONE read (objectui#12970): the help menu's + * About dialog and Sentry's release fallback both go through + * `resolveConsoleBuild`, so the two can never name different builds. + * + * Pinned through the pure readers because this repo's Vitest setup cannot put + * a `VITE_*` value on `import.meta.env` (the same reason `resolveSentryGate` + * is pure; see its header in `observability/sentry.ts`). + */ + +import { describe, it, expect } from 'vitest'; +import { resolveConsoleBuild, getConsoleBuild } from '../buildIdentity'; +import { resolveSentryRelease } from '../observability/sentry'; + +describe('resolveConsoleBuild: the console build identity (objectui#12970)', () => { + it('names the commit the build baked into VITE_APP_VERSION', () => { + expect(resolveConsoleBuild({ VITE_APP_VERSION: '0707b4ae' })).toBe('0707b4ae'); + }); + + it('trims surrounding whitespace from the baked value', () => { + expect(resolveConsoleBuild({ VITE_APP_VERSION: ' 0707b4ae\n' })).toBe('0707b4ae'); + }); + + it.each<[string, unknown]>([ + ['absent', undefined], + ['empty', ''], + ['whitespace only', ' '], + ['not a string', 42], + ])('answers null when the value is %s, so the dialog states the fallback', (_label, value) => { + expect(resolveConsoleBuild({ VITE_APP_VERSION: value })).toBeNull(); + }); + + it('answers null for a missing env object', () => { + expect(resolveConsoleBuild(undefined)).toBeNull(); + expect(resolveConsoleBuild(null)).toBeNull(); + }); + + it('reads null under this test setup, which bakes no VITE_APP_VERSION', () => { + expect(getConsoleBuild()).toBeNull(); + }); +}); + +describe('resolveSentryRelease: falls back to the same build identity (objectui#12970)', () => { + it('prefers an explicit VITE_SENTRY_RELEASE', () => { + expect(resolveSentryRelease({ VITE_SENTRY_RELEASE: 'rel-1', VITE_APP_VERSION: '0707b4ae' })).toBe('rel-1'); + }); + + it('falls back to the value resolveConsoleBuild reads, not a second spelling', () => { + for (const env of [ + { VITE_APP_VERSION: '0707b4ae' }, + { VITE_APP_VERSION: ' 0707b4ae ' }, + { VITE_SENTRY_RELEASE: '', VITE_APP_VERSION: 'be076dad' }, + ]) { + expect(resolveSentryRelease(env)).toBe(resolveConsoleBuild(env)); + } + }); + + it("answers 'unknown' when the build named neither", () => { + expect(resolveSentryRelease({})).toBe('unknown'); + expect(resolveSentryRelease({ VITE_SENTRY_RELEASE: ' ', VITE_APP_VERSION: '' })).toBe('unknown'); + }); +}); diff --git a/packages/app-shell/src/buildIdentity.ts b/packages/app-shell/src/buildIdentity.ts new file mode 100644 index 0000000000..2d359e0ece --- /dev/null +++ b/packages/app-shell/src/buildIdentity.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The console's own build identity — which objectui commit this bundle was + * compiled from (objectui#12970). + * + * ONE read, shared: the help menu's About dialog shows it, and Sentry tags its + * events with it when no `VITE_SENTRY_RELEASE` is set. There is no second + * spelling of the value anywhere in the shell. + * + * Where the value comes from — a property of the BUILD, never of the runtime + * (a server cannot know which console bundle it is serving): + * + * - `VITE_APP_VERSION` in the environment of the build that compiles the app. + * This package ships `tsc` output, so its `dist/` still reads + * `import.meta.env` and the APP's Vite build replaces it. + * - The console app's Vite config (`apps/console/vite.config.ts`) bakes the + * variable when it is set, and otherwise the short sha of `HEAD` read from + * git at build time, so a local build names its commit too. + * - Built with neither, the value is absent and this module answers `null`; + * the dialog then says so in words rather than showing an empty field. + * + * @module + */ + +/** The slice of `import.meta.env` this module reads. */ +export interface BuildIdentityEnv { + readonly VITE_APP_VERSION?: unknown; +} + +/** + * The build identity carried by `env`, or `null` when the build named none. + * + * A pure function of its input, split out from {@link getConsoleBuild} for the + * same reason `resolveSentryGate` is: this repo's Vitest setup cannot put a + * `VITE_*` value on `import.meta.env`, so the reading is pinned through this. + * Only a non-empty string counts; whitespace is trimmed. + */ +export function resolveConsoleBuild(env: BuildIdentityEnv | null | undefined): string | null { + const raw = env?.VITE_APP_VERSION; + if (typeof raw !== 'string') return null; + const value = raw.trim(); + return value === '' ? null : value; +} + +/** The build identity this bundle was compiled with, or `null`. */ +export function getConsoleBuild(): string | null { + return resolveConsoleBuild((import.meta as { env?: BuildIdentityEnv }).env); +} diff --git a/packages/app-shell/src/layout/AboutDialog.tsx b/packages/app-shell/src/layout/AboutDialog.tsx new file mode 100644 index 0000000000..0055502177 --- /dev/null +++ b/packages/app-shell/src/layout/AboutDialog.tsx @@ -0,0 +1,62 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * AboutDialog — the help menu's About entry (objectui#12970). + * + * Names the build this console was compiled from, so a reviewer can tell which + * objectui build a deployment serves. The value is the shared build identity + * (`buildIdentity.ts`), the same read Sentry's release falls back to. A console + * built without one says so in words; the field is never left empty. + * + * The runtime's own build identity is deliberately not shown here: the shell + * has no request that reads it today, and adding one is outside this dialog. + * @module + */ + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@object-ui/components'; +import { useObjectTranslation } from '@object-ui/i18n'; + +export interface AboutDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + /** The console's build identity, or `null` when the build named none. */ + build: string | null; +} + +export function AboutDialog({ open, onOpenChange, build }: AboutDialogProps) { + const { t } = useObjectTranslation(); + return ( + + + + {t('help.aboutTitle', { defaultValue: 'About this console' })} + + {t('help.aboutDescription', { defaultValue: 'The build this console was compiled from.' })} + + +
+
+ {t('help.aboutBuild', { defaultValue: 'Console build' })} +
+
+ {build ? ( + {build} + ) : ( + + {t('help.aboutBuildUnknown', { + defaultValue: 'Unknown build (no version was set when this console was built)', + })} + + )} +
+
+
+
+ ); +} diff --git a/packages/app-shell/src/layout/AppHeader.tsx b/packages/app-shell/src/layout/AppHeader.tsx index 4dbce5f187..b1af036b0a 100644 --- a/packages/app-shell/src/layout/AppHeader.tsx +++ b/packages/app-shell/src/layout/AppHeader.tsx @@ -69,6 +69,7 @@ import { Keyboard, Pencil, MoreHorizontal, + Info, } from 'lucide-react'; import { useState, useEffect, useLayoutEffect, useCallback, useRef, type RefObject } from 'react'; @@ -107,6 +108,8 @@ import { resolveViewId } from '../utils/resolveViewId.js'; import { getProductName, getLogoUrl } from '../runtime-config.js'; import { LocalizedSidebarTrigger } from './LocalizedSidebarTrigger.js'; import { PreviewBadge } from './PreviewBadge.js'; +import { AboutDialog } from './AboutDialog.js'; +import { getConsoleBuild } from '../buildIdentity.js'; import { GO_HOME_SHORTCUT, useAdvertiseShortcut, type ShortcutChord } from '../chrome/advertisedShortcuts.js'; function humanizeSlug(slug: string): string { @@ -443,6 +446,9 @@ export function AppHeader({ // Click-reachable entry for the keyboard-shortcuts dialog (was `?`-key only). // Shares the `?shortcuts=1` URL param with KeyboardShortcutsDialog (C2/C3). const { openOverlay: openShortcuts } = useUrlOverlay(KEYBOARD_SHORTCUTS_PARAM); + // The help menu's About dialog (objectui#12970): ephemeral, so component + // state, not a URL param — nobody shares a link to it. + const [aboutOpen, setAboutOpen] = useState(false); const { user, signOut, @@ -1137,8 +1143,19 @@ export function AppHeader({ {t('help.onlineDocs', { defaultValue: 'Online documentation' })} + {/* About — names the console's own build, on every variant + (objectui#12970). */} + setAboutOpen(true)} + > + + {t('help.about', { defaultValue: 'About' })} + + {/* Group 3: Account (theme + lang moved into avatar dropdown) */} diff --git a/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx b/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx new file mode 100644 index 0000000000..0544a15bac --- /dev/null +++ b/packages/app-shell/src/layout/__tests__/AppHeader.aboutBuild-12970.test.tsx @@ -0,0 +1,256 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The help menu's About entry (objectui#12970): on every `AppHeader` variant — + * the tenant console's `app` bar and the control plane's `home` / `orgs` / + * `studio` bars are the one component — the help menu carries 「关于」 / About, + * and the dialog it opens names the build this console was compiled from. + * + * The build identity is `buildIdentity.ts`'s one read, stubbed here through a + * mutable reading: this repo's Vitest setup cannot put a `VITE_*` value on + * `import.meta.env`, and the pure reader is pinned on its own in + * `buildIdentity-12970.test.ts`. + * + * `t` answers `«key»` and drops `defaultValue`, so a label written as a + * literal cannot pass for one read from the packs. + */ + +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, fireEvent, within } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions'; + +vi.mock('@object-ui/i18n', async (importOriginal) => ({ + ...(await importOriginal>()), + useObjectTranslation: () => ({ + language: 'en', + t: (key: string) => `«${key}»`, + }), + useObjectLabel: () => { + const label = (x: { name?: string; label?: string } | string) => + typeof x === 'string' ? x : String(x?.label ?? x?.name ?? ''); + return { + objectLabel: label, + objectPluralLabel: label, + dashboardLabel: label, + pageLabel: label, + reportLabel: label, + viewLabel: (_o: string, _v: string, fallback?: string) => fallback ?? '', + appLabel: label, + }; + }, +})); + +/** + * Passthrough menu primitives: Radix keeps a closed menu unmounted, so the + * help menu's CONTENT renders its children here. The dialog is the real one. + */ +vi.mock('@object-ui/components', async (importOriginal) => { + const actual = await importOriginal>(); + const strip = (p: any) => { + const { asChild, variant, size, align, sideOffset, ...rest } = p ?? {}; + return rest; + }; + const Pass = ({ children, ...p }: any) =>
{children}
; + return { + ...actual, + Button: ({ children, asChild, variant, size, ...p }: any) => + asChild ? {children} : , + DropdownMenu: ({ children }: any) =>
{children}
, + DropdownMenuTrigger: Pass, + DropdownMenuContent: Pass, + DropdownMenuItem: ({ children, onClick, asChild, ...p }: any) => ( +
{children}
+ ), + DropdownMenuLabel: Pass, + DropdownMenuSeparator: () => null, + DropdownMenuGroup: Pass, + Avatar: Pass, + AvatarImage: () => null, + AvatarFallback: Pass, + Popover: Pass, + PopoverTrigger: Pass, + PopoverContent: () => null, + cn: (...c: any[]) => c.filter(Boolean).join(' '), + }; +}); + +vi.mock('lucide-react', () => { + const Icon = () => ; + return new Proxy({ __esModule: true } as Record, { + get: (target, prop) => { + if (prop === 'then' || prop === '__esModule' || typeof prop === 'symbol') return target[prop]; + return Icon; + }, + has: (_target, prop) => prop !== 'then', + }); +}); + +vi.mock('@object-ui/react', async (importOriginal) => ({ + ...(await importOriginal>()), + useOffline: () => ({ isOnline: true }), +})); + +vi.mock('@object-ui/collaboration', async (importOriginal) => ({ + ...(await importOriginal>()), + PresenceAvatars: () => null, + useTenantPresence: () => [], +})); + +vi.mock('../ModeToggle', () => ({ ModeToggle: () => null })); +vi.mock('../LocaleSwitcher', () => ({ LocaleSwitcher: () => null })); +vi.mock('../NotificationPreferencesMenu', () => ({ NotificationPreferencesMenu: () => null })); +vi.mock('../WorkspaceSwitcher', () => ({ WorkspaceSwitcher: () => null })); +vi.mock('../CurrentOrganizationIndicator', () => ({ CurrentOrganizationIndicator: () => null })); +vi.mock('../ConnectionStatus', () => ({ ConnectionStatus: () => null })); +vi.mock('../AppSwitcher', () => ({ AppSwitcher: () => null })); +vi.mock('../LocalizedSidebarTrigger', () => ({ LocalizedSidebarTrigger: () => null })); +vi.mock('../PreviewBadge', () => ({ PreviewBadge: () => null })); +vi.mock('../InboxPopover', () => ({ InboxPopover: () => null })); +vi.mock('../../hooks/useAiSurface', () => ({ + useAiSurfaceEnabled: () => ({ enabled: false, isLoading: false }), +})); +vi.mock('../../context/NavigationContext.js', () => ({ + useNavigationContext: () => ({ currentAppName: 'crm', recordTitle: undefined }), +})); + +/** The build identity the header reads; each case sets it. */ +let consoleBuild: string | null = null; + +vi.mock('../../buildIdentity', async (importOriginal) => ({ + ...(await importOriginal>()), + getConsoleBuild: () => consoleBuild, +})); + +/** One stable identity for the whole file: AppHeader keys an effect on it. */ +const getAuthConfig = () => Promise.resolve({ features: { multiOrgEnabled: true } }); + +vi.mock('@object-ui/auth', async (importOriginal) => ({ + ...(await importOriginal>()), + useAuth: () => ({ + user: { id: 'u1', name: 'Lin Yue', email: 'linyue@example.com' }, + signOut: vi.fn(), + isAuthEnabled: true, + organizations: [], + activeOrganization: null, + isOrganizationsLoading: false, + getAuthConfig, + }), + getUserInitials: () => 'LY', + useWorkspaceAdminStatus: () => ({ isAdmin: false, isResolved: true }), +})); + +vi.mock('../../providers/MetadataProvider', () => ({ + useMetadata: () => ({ + apps: [{ name: 'crm', label: 'CRM', _packageId: 'com.example.crm' }], + dashboards: [], + pages: [], + reports: [], + }), +})); + +/** Stable identity: the header's readers key their effects on the adapter. */ +const fakeAdapter = { find: () => Promise.resolve({ data: [] }), getClient: () => undefined }; + +vi.mock('../../providers/AdapterProvider', () => ({ + useAdapter: () => fakeAdapter, +})); + +import { AppHeader, type AppHeaderVariant } from '../AppHeader'; + +const MEMBER: MePermissionsResponse = { + authenticated: true, + userId: 'u1', + tenantId: 'org_1', + roles: [], + permissionSets: [], + systemPermissions: [], + objects: {}, + fields: {}, +}; + +const PATHS: Record = { + app: '/apps/crm', + home: '/home', + orgs: '/organizations', + studio: '/studio', +}; + +function renderHeader(variant: AppHeaderVariant) { + return render( + + + + + ) : ( + + ) + } + /> + + + , + ); +} + +function openAbout() { + fireEvent.click(screen.getByTestId('action:about:open')); + return screen.getByTestId('overlay:about'); +} + +beforeEach(() => { + consoleBuild = null; + vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ data: [] }) }))); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('AppHeader help menu: About names the console build (objectui#12970)', () => { + it.each(['app', 'home', 'orgs', 'studio'])( + 'the `%s` bar\'s help menu carries About, read from the packs', + (variant) => { + renderHeader(variant); + + const item = screen.getByTestId('action:about:open'); + expect(item).toHaveAttribute('role', 'menuitem'); + expect(item).toHaveTextContent('«help.about»'); + // The dialog is closed until the item is chosen. + expect(screen.queryByTestId('overlay:about')).not.toBeInTheDocument(); + }, + ); + + it.each(['app', 'home'])( + 'on the `%s` bar the dialog names the commit the build baked in', + (variant) => { + consoleBuild = '0707b4ae'; + renderHeader(variant); + + const dialog = openAbout(); + expect(within(dialog).getByText('«help.aboutTitle»')).toBeInTheDocument(); + expect(within(dialog).getByText('«help.aboutBuild»')).toBeInTheDocument(); + const value = within(dialog).getByTestId('about:console-build'); + expect(value).toHaveTextContent('0707b4ae'); + expect(within(dialog).queryByText('«help.aboutBuildUnknown»')).not.toBeInTheDocument(); + }, + ); + + it.each(['app', 'home'])( + 'on the `%s` bar a build with no identity states the fallback, never an empty field', + (variant) => { + consoleBuild = null; + renderHeader(variant); + + const value = within(openAbout()).getByTestId('about:console-build'); + expect(value).toHaveTextContent('«help.aboutBuildUnknown»'); + expect(value.textContent?.trim()).not.toBe(''); + }, + ); +}); diff --git a/packages/app-shell/src/observability/sentry.ts b/packages/app-shell/src/observability/sentry.ts index 36a553bd38..858798af74 100644 --- a/packages/app-shell/src/observability/sentry.ts +++ b/packages/app-shell/src/observability/sentry.ts @@ -62,12 +62,14 @@ * - `VITE_SENTRY_RELEASE` — which bundle produced a stack trace. It must match * the source maps that bundle's pipeline uploaded, and a server cannot know * which Console build it is serving, so this one stays build-time. Falls - * back to `VITE_APP_VERSION`, then `'unknown'`. + * back to the console's build identity (`buildIdentity.ts`: `VITE_APP_VERSION`, + * or the git sha the console's Vite config baked), then `'unknown'`. * * @module */ import { getClientErrorReporting, type RuntimeClientErrorReporting } from '../runtime-config.js'; +import { resolveConsoleBuild, type BuildIdentityEnv } from '../buildIdentity.js'; type SentryModule = typeof import('@sentry/react'); @@ -172,6 +174,22 @@ function rate(value: unknown, fallback: number): number { : fallback; } +/** The slice of `import.meta.env` {@link resolveSentryRelease} reads. */ +export interface SentryReleaseEnv extends BuildIdentityEnv { + readonly VITE_SENTRY_RELEASE?: unknown; +} + +/** + * The release Sentry tags events with: `VITE_SENTRY_RELEASE` when the build set + * one, else the console's build identity — read through `resolveConsoleBuild`, + * the same one read the help menu's About dialog shows (objectui#12970) — else + * `'unknown'`. + */ +export function resolveSentryRelease(env: SentryReleaseEnv): string { + const explicit = typeof env.VITE_SENTRY_RELEASE === 'string' ? env.VITE_SENTRY_RELEASE.trim() : ''; + return explicit || resolveConsoleBuild(env) || 'unknown'; +} + /** * Returns the loaded Sentry module, or `null` if Sentry was never initialized * (e.g. the runtime served no DSN). Callers must handle the null case. @@ -216,7 +234,7 @@ export function initSentry(): Promise { // a release identifies which bundle produced a stack trace and has to // match the source maps that bundle's pipeline uploaded, which no // server can know. - release: env.VITE_SENTRY_RELEASE || env.VITE_APP_VERSION || 'unknown', + release: resolveSentryRelease(env), tracesSampleRate: gate.tracesSampleRate, // IP address + User-Agent — decided by `resolveSentryGate` above, and // OPT-IN there. Defaulting it on is how PII left an air-gapped network diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 519ded9f0a..ed855b0559 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -2831,6 +2831,11 @@ const ar = { allDocs: 'كل الوثائق', onlineDocs: 'الوثائق عبر الإنترنت', loadingDocumentation: 'جارٍ تحميل الوثائق', + about: 'حول', + aboutTitle: 'حول وحدة التحكم هذه', + aboutDescription: 'الإصدار الذي بُنيت منه وحدة التحكم هذه.', + aboutBuild: 'إصدار وحدة التحكم', + aboutBuildUnknown: 'إصدار غير معروف (لم يُحدَّد أي إصدار عند بناء وحدة التحكم هذه)', }, sidebar: { packageManagement: "إدارة الحزم", diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index d91ec25d49..976a480c09 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -2619,6 +2619,11 @@ const de = { allDocs: 'Gesamte Dokumentation', onlineDocs: 'Online-Dokumentation', loadingDocumentation: 'Dokumentation wird geladen', + about: 'Über', + aboutTitle: 'Über diese Konsole', + aboutDescription: 'Der Build, aus dem diese Konsole kompiliert wurde.', + aboutBuild: 'Konsolen-Build', + aboutBuildUnknown: 'Unbekannter Build (beim Erstellen dieser Konsole wurde keine Version gesetzt)', }, sidebar: { packageManagement: "Paketverwaltung", diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index aa219bb65c..ff101a5fa3 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -3150,6 +3150,11 @@ const en = { keyboardShortcuts: 'Keyboard shortcuts', onlineDocs: 'Online documentation', loadingDocumentation: 'Loading documentation', + about: 'About', + aboutTitle: 'About this console', + aboutDescription: 'The build this console was compiled from.', + aboutBuild: 'Console build', + aboutBuildUnknown: 'Unknown build (no version was set when this console was built)', }, sidebar: { settings: 'Settings', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index dabe9bd9cb..6f95b6261c 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -2671,6 +2671,11 @@ const es = { allDocs: 'Toda la documentación', onlineDocs: 'Documentación en línea', loadingDocumentation: 'Cargando documentación', + about: 'Acerca de', + aboutTitle: 'Acerca de esta consola', + aboutDescription: 'La compilación a partir de la cual se generó esta consola.', + aboutBuild: 'Compilación de la consola', + aboutBuildUnknown: 'Compilación desconocida (no se definió ninguna versión al compilar esta consola)', }, sidebar: { packageManagement: "Gestión de paquetes", diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index 094948b598..566a350758 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -2668,6 +2668,11 @@ const fr = { allDocs: 'Toute la documentation', onlineDocs: 'Documentation en ligne', loadingDocumentation: 'Chargement de la documentation', + about: 'À propos', + aboutTitle: 'À propos de cette console', + aboutDescription: 'Le build à partir duquel cette console a été compilée.', + aboutBuild: 'Build de la console', + aboutBuildUnknown: 'Build inconnu (aucune version n’a été définie lors de la compilation de cette console)', }, sidebar: { packageManagement: "Gestion des packages", diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 3c5a9ff052..5a2a35ce65 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -2621,6 +2621,11 @@ const ja = { allDocs: 'すべてのドキュメント', onlineDocs: 'オンラインドキュメント', loadingDocumentation: 'ドキュメントを読み込み中', + about: 'バージョン情報', + aboutTitle: 'このコンソールについて', + aboutDescription: 'このコンソールのコンパイル元となったビルドです。', + aboutBuild: 'コンソールのビルド', + aboutBuildUnknown: '不明なビルド(このコンソールのビルド時にバージョンが設定されていません)', }, sidebar: { packageManagement: "パッケージ管理", diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 31fd912b39..2a71ae941e 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -2619,6 +2619,11 @@ const ko = { allDocs: '전체 문서', onlineDocs: '온라인 문서', loadingDocumentation: '문서 로딩 중', + about: '정보', + aboutTitle: '이 콘솔 정보', + aboutDescription: '이 콘솔이 컴파일된 빌드입니다.', + aboutBuild: '콘솔 빌드', + aboutBuildUnknown: '알 수 없는 빌드(이 콘솔을 빌드할 때 버전이 설정되지 않음)', }, sidebar: { packageManagement: "패키지 관리", diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 2a43fbb41b..badff71dc6 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -2666,6 +2666,11 @@ const pt = { allDocs: 'Toda a documentação', onlineDocs: 'Documentação online', loadingDocumentation: 'Carregando documentação', + about: 'Sobre', + aboutTitle: 'Sobre este console', + aboutDescription: 'A build a partir da qual este console foi compilado.', + aboutBuild: 'Build do console', + aboutBuildUnknown: 'Build desconhecida (nenhuma versão foi definida ao compilar este console)', }, sidebar: { packageManagement: "Gerenciamento de pacotes", diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 61e8c66ae7..49d3392bc4 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -2735,6 +2735,11 @@ const ru = { allDocs: 'Вся документация', onlineDocs: 'Онлайн-документация', loadingDocumentation: 'Загрузка документации', + about: 'О программе', + aboutTitle: 'Об этой консоли', + aboutDescription: 'Сборка, из которой скомпилирована эта консоль.', + aboutBuild: 'Сборка консоли', + aboutBuildUnknown: 'Неизвестная сборка (при сборке этой консоли версия не была задана)', }, sidebar: { packageManagement: "Управление пакетами", diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 4fced541eb..e4a3a68e07 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -2698,6 +2698,11 @@ const zh = { allDocs: '全部文档', onlineDocs: '在线文档', loadingDocumentation: '正在加载文档', + about: '关于', + aboutTitle: '关于此控制台', + aboutDescription: '此控制台编译所用的构建。', + aboutBuild: '控制台构建', + aboutBuildUnknown: '未知构建(构建此控制台时未设置版本)', }, sidebar: { settings: '设置',