This is an automated email from the ASF dual-hosted git repository.

guoqqqi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 23103ba7a feat: add nav icons, show the active language, link the docs 
(#3452)
23103ba7a is described below

commit 23103ba7a70db12b449b4cad9f8b6244e4e4cbf7
Author: Yuhan <[email protected]>
AuthorDate: Wed Jul 29 22:05:41 2026 +0800

    feat: add nav icons, show the active language, link the docs (#3452)
---
 .../integration/i18n.lang-switch-no-crash.spec.ts  | 58 +++++++------
 .../regression/header.nav-icons-and-docs.spec.ts   | 97 ++++++++++++++++++++++
 package.json                                       |  2 +-
 pnpm-lock.yaml                                     | 12 +--
 src/components/Header/LanguageMenu.tsx             | 24 ++++--
 src/components/Header/SettingModalBtn.tsx          |  2 +-
 src/components/Header/index.tsx                    | 67 +++++++++++++--
 src/components/Navbar.tsx                          |  4 +-
 .../form-slice/FormPartSSL/FormItemCertKeyList.tsx |  2 +-
 src/components/form/TextareaWithUpload.tsx         |  2 +-
 src/components/page/ToAddPageBtn.tsx               |  2 +-
 src/config/navRoutes.ts                            | 32 +++++++
 src/locales/de/common.json                         |  1 +
 src/locales/en/common.json                         |  1 +
 src/locales/es/common.json                         |  1 +
 src/locales/tr/common.json                         |  1 +
 src/locales/zh/common.json                         |  1 +
 17 files changed, 258 insertions(+), 51 deletions(-)

diff --git a/e2e/tests/integration/i18n.lang-switch-no-crash.spec.ts 
b/e2e/tests/integration/i18n.lang-switch-no-crash.spec.ts
index cba90c7cc..c7323db51 100644
--- a/e2e/tests/integration/i18n.lang-switch-no-crash.spec.ts
+++ b/e2e/tests/integration/i18n.lang-switch-no-crash.spec.ts
@@ -14,7 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-/* eslint-disable playwright/no-wait-for-timeout, 
playwright/no-conditional-in-test -- regression test stabilization */
+/* eslint-disable playwright/no-wait-for-timeout -- the crash watcher needs a
+   settle window for asynchronous errors to surface after each switch */
 
 // Integration F-10: switching language must not crash. The dashboard ships
 // `en`, `zh`, `de`, `es`, `tr` locales; de/es/tr are mostly placeholders so
@@ -41,34 +42,36 @@ test('switching to every offered language never crashes the 
page', async ({
     crashes.expectNoCrash(`initial load of ${path}`);
   }
 
-  // The language switcher lives in the banner. Open it, pick every
-  // option, return to English. Fail if any toggle produces an
-  // unhandled error.
-  const banner = page.getByRole('banner');
-  const languageButton = banner
-    .getByRole('button')
-    .filter({ hasText: /(English|EN|中文|ZH|Deutsch|Español|Türkçe)/i })
-    .first();
+  // Locate the switcher structurally — it is the banner's only menu
+  // trigger. Matching on rendered text does not work: this test used to
+  // filter the banner's buttons by `/English|中文|.../` and silently
+  // `return` when nothing matched, which is exactly what happened for as
+  // long as the control was icon-only. It reported green without ever
+  // running the body below. Matching on the accessible name is no better,
+  // because `a11y.selectLanguage` is itself translated (zh: 选择语言), so
+  // the locator would break the moment the test switched away from English.
+  const languageButton = page
+    .getByRole('banner')
+    .locator('button[aria-haspopup="menu"]');
+  await expect(languageButton).toBeVisible();
 
-  if (!(await languageButton.isVisible().catch(() => false))) {
-    // Switcher not exposed via accessible text — abort gracefully rather
-    // than hard-fail. The crash-prevention assertion above still ran.
-    test.info().annotations.push({
-      type: 'skip-reason',
-      description: 'language switcher not discoverable by accessible name',
-    });
-    return;
-  }
-
-  const targetLanguages = ['中文', 'English'];
+  // Every other locale, ending back on English. de/es/tr carry the most
+  // placeholder copy and are the likeliest to blow up, so the point of
+  // this test is to actually visit them — the previous version only ever
+  // named 中文.
+  const targetLanguages = ['Deutsch', '中文', 'Español', 'Türkçe', 'English'];
   for (const label of targetLanguages) {
     await languageButton.click();
+    // Substring match: locales below 100% translated render a "(99%)"
+    // suffix inside the same menu item.
     const option = page.getByRole('menuitem', { name: label }).first();
-    if (await option.isVisible().catch(() => false)) {
-      await option.click();
-      await page.waitForTimeout(500);
-      crashes.expectNoCrash(`switched to ${label}`);
-    }
+    // The menu disables whichever language is already active. Asserting
+    // enabled keeps the loop honest — clicking a disabled item just times
+    // out with no indication of why.
+    await expect(option).toBeEnabled();
+    await option.click();
+    await page.waitForTimeout(500);
+    crashes.expectNoCrash(`switched to ${label}`);
   }
 
   // Visit a different page after the last switch and verify no late crash.
@@ -76,8 +79,9 @@ test('switching to every offered language never crashes the 
page', async ({
   await page.waitForTimeout(800);
   crashes.expectNoCrash('after language switches');
 
-  // Page must still render its main nav.
+  // Page must still render its main nav. Anchor the name: an unanchored
+  // /Routes/ also matches "Stream Routes" and trips strict mode.
   await expect(
-    page.getByRole('link', { name: /Routes|路由/ })
+    page.getByRole('link', { name: /^(Routes|路由)$/ })
   ).toBeVisible();
 });
diff --git a/e2e/tests/regression/header.nav-icons-and-docs.spec.ts 
b/e2e/tests/regression/header.nav-icons-and-docs.spec.ts
new file mode 100644
index 000000000..ee7e7061e
--- /dev/null
+++ b/e2e/tests/regression/header.nav-icons-and-docs.spec.ts
@@ -0,0 +1,97 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { routesPom } from '@e2e/pom/routes';
+import { test } from '@e2e/utils/test';
+import { uiGoto } from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+
+// NOTE: do not import from `@/config/navRoutes` here — it pulls in `~icons/*`,
+// a Vite-only virtual module that the Playwright node runtime cannot resolve.
+
+// The nav items gained decorative icons, and the header gained a Docs link
+// while the language control grew a visible label. Each of those can silently
+// break the accessible names that ~20 specs and every resource POM locate by.
+
+test('nav icons stay decorative: link names are the label alone', async ({
+  page,
+}) => {
+  await uiGoto(page, '/routes');
+
+  // Every nav entry renders an icon; none of them may leak into the
+  // accessible name (the POMs match with `exact: true`).
+  const names = [
+    'Services',
+    'Routes',
+    'Stream Routes',
+    'Upstreams',
+    'Consumers',
+    'Consumer Groups',
+    'SSLs',
+    'Global Rules',
+    'Plugin Metadata',
+    'Plugin Configs',
+    'Secrets',
+    'Protos',
+  ];
+  for (const name of names) {
+    await expect(
+      page.getByRole('link', { name, exact: true }),
+      `nav link "${name}" must keep an exact accessible name`
+    ).toBeVisible();
+  }
+
+  // Catches a nav entry added without extending this guard.
+  await expect(
+    page.getByRole('navigation').getByRole('link'),
+    'every nav entry must be covered by the names above'
+  ).toHaveCount(names.length);
+});
+
+test('the header exposes a Docs link to the upstream documentation', async ({
+  page,
+}) => {
+  await routesPom.toIndex(page);
+  await routesPom.isIndexPage(page);
+
+  const docs = page.getByRole('link', { name: 'Docs' });
+  await expect(docs).toBeVisible();
+  await expect(docs).toHaveAttribute('href', 
'https://apisix.apache.org/docs/');
+  await expect(docs).toHaveAttribute('target', '_blank');
+  // opening a new tab without this is a tabnabbing footgun
+  await expect(docs).toHaveAttribute('rel', /noopener/);
+});
+
+test('the language control shows the active language and keeps its a11y name', 
async ({
+  page,
+}) => {
+  await routesPom.toIndex(page);
+  await routesPom.isIndexPage(page);
+
+  // Visible label states the current language; the accessible name still
+  // announces the control's purpose and contains that label (WCAG 2.5.3).
+  const langBtn = page.getByRole('button', { name: 'Select language' });
+  await expect(langBtn).toBeVisible();
+  await expect(langBtn).toHaveAccessibleName('Select language: English');
+  await expect(langBtn).toContainText('English');
+
+  await langBtn.click();
+  await page.getByRole('menuitem', { name: 'Deutsch' }).click();
+
+  await expect(
+    page.getByRole('button', { name: 'Select language' })
+  ).toHaveAccessibleName('Select language: Deutsch');
+});
diff --git a/package.json b/package.json
index da68a3146..eb6e003cc 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
   "devDependencies": {
     "@eslint/js": "^9.32.0",
     "@estruyf/github-actions-reporter": "^1.11.0",
-    "@iconify-json/material-symbols": "^1.2.63",
+    "@iconify-json/tabler": "^1.2.37",
     "@m6web/eslint-plugin-i18n": "^2.0.4",
     "@playwright/test": "^1.57.0",
     "@svgr/core": "^8.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4063d16f7..0cafba596 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -127,9 +127,9 @@ importers:
       '@estruyf/github-actions-reporter':
         specifier: ^1.11.0
         version: 1.11.0(@playwright/[email protected])
-      '@iconify-json/material-symbols':
-        specifier: ^1.2.63
-        version: 1.2.63
+      '@iconify-json/tabler':
+        specifier: ^1.2.37
+        version: 1.2.37
       '@m6web/eslint-plugin-i18n':
         specifier: ^2.0.4
         version: 2.0.4([email protected]([email protected]))
@@ -925,8 +925,8 @@ packages:
     resolution: {integrity: 
sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
     engines: {node: '>=18.18'}
 
-  '@iconify-json/[email protected]':
-    resolution: {integrity: 
sha512-R4PS/l8K6j+dk2P2MoYFLJgfbZ4YDo6XjCOpX6b1tvX+BhiSpSOjc1b6cnb/mvWe+JWBKlt4pcvPNiAijFLPnA==}
+  '@iconify-json/[email protected]':
+    resolution: {integrity: 
sha512-tXc0jGhrxec6eTKxMI9We/r3cEItyU5dG8ngDOEKsL36a6+/xwk4sR7PIZROVajzxmvyUBsn659adDIfcswiUw==}
 
   '@iconify/[email protected]':
     resolution: {integrity: 
sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -4904,7 +4904,7 @@ snapshots:
 
   '@humanwhocodes/[email protected]': {}
 
-  '@iconify-json/[email protected]':
+  '@iconify-json/[email protected]':
     dependencies:
       '@iconify/types': 2.0.0
 
diff --git a/src/components/Header/LanguageMenu.tsx 
b/src/components/Header/LanguageMenu.tsx
index 73ff7caf9..461e6895e 100644
--- a/src/components/Header/LanguageMenu.tsx
+++ b/src/components/Header/LanguageMenu.tsx
@@ -14,12 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { ActionIcon, Anchor, Menu } from '@mantine/core';
+import { Anchor, Button, Menu } from '@mantine/core';
 import { useTranslation } from 'react-i18next';
 import i18nProgress from 'virtual:i18n-progress';
 
 import type { Resources } from '@/config/i18n';
-import IconLanguage from '~icons/material-symbols/language-chinese-array';
+import IconLanguage from '~icons/tabler/language';
 
 const LangMap: Record<keyof Resources, string> = {
   en: 'English',
@@ -47,16 +47,26 @@ const TranslationProgress = ({ lang }: { lang: string }) => 
{
 
 export const LanguageMenu = () => {
   const { i18n, t } = useTranslation();
+  const current = (i18n.resolvedLanguage ??
+    i18n.language) as keyof Resources;
+  // Fall back to the raw code so an unmapped locale still shows something.
+  const currentLabel = LangMap[current] ?? current;
   return (
     <Menu shadow="md" width={200}>
       <Menu.Target>
-        <ActionIcon
+        {/* Pairing the icon with the active language name makes the current
+            state readable at a glance instead of hidden behind a click. The
+            accessible name keeps the `a11y.selectLanguage` prefix — it states
+            the control's purpose and contains the visible text, so it
+            satisfies WCAG 2.5.3 (Label in Name). */}
+        <Button
           variant="light"
-          size="sm"
-          aria-label={t('a11y.selectLanguage')}
+          size="compact-sm"
+          leftSection={<IconLanguage aria-hidden focusable="false" />}
+          aria-label={`${t('a11y.selectLanguage')}: ${currentLabel}`}
         >
-          <IconLanguage />
-        </ActionIcon>
+          {currentLabel}
+        </Button>
       </Menu.Target>
       <Menu.Dropdown>
         {Object.keys(LangMap).map((lang) => (
diff --git a/src/components/Header/SettingModalBtn.tsx 
b/src/components/Header/SettingModalBtn.tsx
index 0ebb566f6..62027c041 100644
--- a/src/components/Header/SettingModalBtn.tsx
+++ b/src/components/Header/SettingModalBtn.tsx
@@ -19,7 +19,7 @@ import { useSetAtom } from 'jotai';
 import { useTranslation } from 'react-i18next';
 
 import { isSettingsOpenAtom } from '@/stores/global';
-import IconSettings from '~icons/material-symbols/settings';
+import IconSettings from '~icons/tabler/settings';
 
 export const SettingModalBtn = () => {
   const setIsSettingsOpen = useSetAtom(isSettingsOpenAtom);
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
index 964008931..494952e8b 100644
--- a/src/components/Header/index.tsx
+++ b/src/components/Header/index.tsx
@@ -14,15 +14,64 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { AppShell, Burger, Group, Image } from '@mantine/core';
+import {
+  ActionIcon,
+  AppShell,
+  Burger,
+  Button,
+  Group,
+  Image,
+  Text,
+} from '@mantine/core';
 import type { FC } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import apisixLogo from '@/assets/apisix-logo.svg';
+import IconDocs from '~icons/tabler/book';
 
 import { LanguageMenu } from './LanguageMenu';
 import { SettingModalBtn } from './SettingModalBtn';
 
+const DOCS_URL = 'https://apisix.apache.org/docs/';
+
+const docsAnchorProps = {
+  component: 'a',
+  href: DOCS_URL,
+  target: '_blank',
+  rel: 'noopener noreferrer',
+  variant: 'light',
+} as const;
+
+/**
+ * Labelled from `sm` up, icon-only below it. The header also carries the
+ * language control, whose label is the whole point of showing it, so Docs —
+ * the secondary affordance — is what sheds its label when space runs out.
+ * Long locales ("Documentación", "Dokumentation") overflow otherwise.
+ */
+const DocsBtn = () => {
+  const { t } = useTranslation();
+  return (
+    <>
+      <Button
+        {...docsAnchorProps}
+        visibleFrom="sm"
+        size="compact-sm"
+        leftSection={<IconDocs aria-hidden focusable="false" />}
+      >
+        {t('docs')}
+      </Button>
+      <ActionIcon
+        {...docsAnchorProps}
+        hiddenFrom="sm"
+        size="md"
+        aria-label={t('docs')}
+      >
+        <IconDocs aria-hidden focusable="false" />
+      </ActionIcon>
+    </>
+  );
+};
+
 const Logo = () => {
   const { t } = useTranslation();
   return (
@@ -39,8 +88,8 @@ export const Header: FC<HeaderProps> = (props) => {
   const { t } = useTranslation();
   return (
     <AppShell.Header>
-      <Group h="100%" px="md" justify="space-between">
-        <Group h="100%" gap="sm">
+      <Group h="100%" px="md" justify="space-between" wrap="nowrap">
+        <Group h="100%" gap="sm" wrap="nowrap">
           <Burger
             opened={opened}
             onClick={toggle}
@@ -49,9 +98,17 @@ export const Header: FC<HeaderProps> = (props) => {
             aria-label={t('a11y.toggleNavigation')}
           />
           <Logo />
-          <div>{t('apisix.dashboard')}</div>
+          {/* Below `sm` the burger appears and the header also carries the
+              Docs and language controls; with a long locale (e.g. German
+              "Dokumentation"/"Deutsch") that no longer fits on one row and
+              the controls overlapped the page content. The logo still
+              identifies the app, so drop the wordmark rather than the
+              control labels — showing the active language is the point of
+              the language button. */}
+          <Text visibleFrom="sm">{t('apisix.dashboard')}</Text>
         </Group>
-        <Group h="100%" gap="sm">
+        <Group h="100%" gap="sm" wrap="nowrap">
+          <DocsBtn />
           <SettingModalBtn />
           <LanguageMenu />
         </Group>
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 5253ca076..30aa7e928 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -43,11 +43,13 @@ export const Navbar = () => {
   const { t } = useTranslation();
   return (
     <AppShellNavbar>
-      {navRoutes.map((route) => (
+      {navRoutes.map(({ icon: Icon, ...route }) => (
         <NavbarLink
           {...route}
           key={route.to}
           label={t(`sources.${route.label}`)}
+          // decorative: the link's accessible name must stay the label alone
+          leftSection={<Icon aria-hidden focusable="false" />}
         />
       ))}
     </AppShellNavbar>
diff --git a/src/components/form-slice/FormPartSSL/FormItemCertKeyList.tsx 
b/src/components/form-slice/FormPartSSL/FormItemCertKeyList.tsx
index 9a323134b..96d77e574 100644
--- a/src/components/form-slice/FormPartSSL/FormItemCertKeyList.tsx
+++ b/src/components/form-slice/FormPartSSL/FormItemCertKeyList.tsx
@@ -20,7 +20,7 @@ import { useFieldArray, useFormContext, useFormState } from 
'react-hook-form';
 import { useTranslation } from 'react-i18next';
 
 import { FormItemTextareaWithUpload } from 
'@/components/form/TextareaWithUpload';
-import IconDelete from '~icons/material-symbols/delete-forever-outline';
+import IconDelete from '~icons/tabler/trash';
 
 import { FormSection } from '../FormSection';
 import type { SSLPostType } from './schema';
diff --git a/src/components/form/TextareaWithUpload.tsx 
b/src/components/form/TextareaWithUpload.tsx
index cff39311c..50aebb581 100644
--- a/src/components/form/TextareaWithUpload.tsx
+++ b/src/components/form/TextareaWithUpload.tsx
@@ -30,7 +30,7 @@ import {
 } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
 
-import IconUpload from '~icons/material-symbols/upload';
+import IconUpload from '~icons/tabler/upload';
 
 import { genControllerProps } from './util';
 
diff --git a/src/components/page/ToAddPageBtn.tsx 
b/src/components/page/ToAddPageBtn.tsx
index 4454581a6..430284706 100644
--- a/src/components/page/ToAddPageBtn.tsx
+++ b/src/components/page/ToAddPageBtn.tsx
@@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next';
 
 import { RouteLinkBtn } from '@/components/Btn';
 import type { FileRoutesByTo } from '@/routeTree.gen';
-import IconPlus from '~icons/material-symbols/add';
+import IconPlus from '~icons/tabler/plus';
 
 export type ToAddPageBtnProps = {
   to: keyof FilterKeys<FileRoutesByTo, 'add'>;
diff --git a/src/config/navRoutes.ts b/src/config/navRoutes.ts
index 848243541..d9f5e77ca 100644
--- a/src/config/navRoutes.ts
+++ b/src/config/navRoutes.ts
@@ -14,60 +14,92 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import type { ForwardRefExoticComponent, SVGProps } from 'react';
+
 import type { Resources } from '@/config/i18n';
 import type { FileRouteTypes } from '@/routeTree.gen';
+import IconStreamRoutes from '~icons/tabler/arrows-right-left';
+import IconSSLs from '~icons/tabler/certificate';
+import IconProtos from '~icons/tabler/file-code';
+import IconSecrets from '~icons/tabler/key';
+import IconPluginConfigs from '~icons/tabler/puzzle';
+import IconRoutes from '~icons/tabler/route';
+import IconUpstreams from '~icons/tabler/server-2';
+import IconServices from '~icons/tabler/stack-2';
+import IconPluginMetadata from '~icons/tabler/tags';
+import IconConsumers from '~icons/tabler/user';
+import IconConsumerGroups from '~icons/tabler/users';
+import IconGlobalRules from '~icons/tabler/world';
+
+/** An `~icons/*` component, as typed by `unplugin-icons/types/react`. */
+export type NavIcon = ForwardRefExoticComponent<
+  SVGProps<SVGSVGElement> & { title?: string }
+>;
 
 export type NavRoute = {
   to: FileRouteTypes['to'];
   label: keyof Resources['en']['common']['sources'];
+  icon: NavIcon;
 };
 export const navRoutes: NavRoute[] = [
   {
     to: '/services',
     label: 'services',
+    icon: IconServices,
   },
   {
     to: '/routes',
     label: 'routes',
+    icon: IconRoutes,
   },
   {
     to: '/stream_routes',
     label: 'streamRoutes',
+    icon: IconStreamRoutes,
   },
   {
     to: '/upstreams',
     label: 'upstreams',
+    icon: IconUpstreams,
   },
   {
     to: '/consumers',
     label: 'consumers',
+    icon: IconConsumers,
   },
   {
     to: '/consumer_groups',
     label: 'consumerGroups',
+    icon: IconConsumerGroups,
   },
   {
     to: '/ssls',
     label: 'ssls',
+    icon: IconSSLs,
   },
   {
     to: '/global_rules',
     label: 'globalRules',
+    icon: IconGlobalRules,
   },
   {
     to: '/plugin_metadata',
     label: 'pluginMetadata',
+    icon: IconPluginMetadata,
   },
   {
     to: '/plugin_configs',
     label: 'pluginConfigs',
+    icon: IconPluginConfigs,
   },
   {
     to: '/secrets',
     label: 'secrets',
+    icon: IconSecrets,
   },
   {
     to: '/protos',
     label: 'protos',
+    icon: IconProtos,
   },
 ];
diff --git a/src/locales/de/common.json b/src/locales/de/common.json
index a84c56780..99bcbf61b 100644
--- a/src/locales/de/common.json
+++ b/src/locales/de/common.json
@@ -12,6 +12,7 @@
   "credentials": {
     "singular": "Anmeldeinformation"
   },
+  "docs": "Dokumentation",
   "form": {
     "basic": {
       "desc": "Beschreibung",
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index e233fa1ba..953d58c4f 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -12,6 +12,7 @@
   "credentials": {
     "singular": "Credential"
   },
+  "docs": "Docs",
   "form": {
     "basic": {
       "desc": "Description",
diff --git a/src/locales/es/common.json b/src/locales/es/common.json
index 58615f839..9bf152463 100644
--- a/src/locales/es/common.json
+++ b/src/locales/es/common.json
@@ -12,6 +12,7 @@
   "credentials": {
     "singular": "Credencial"
   },
+  "docs": "Documentación",
   "form": {
     "basic": {
       "desc": "Descripción",
diff --git a/src/locales/tr/common.json b/src/locales/tr/common.json
index 168d81543..2f903497a 100644
--- a/src/locales/tr/common.json
+++ b/src/locales/tr/common.json
@@ -12,6 +12,7 @@
   "credentials": {
     "singular": "Credential"
   },
+  "docs": "Dokümanlar",
   "form": {
     "basic": {
       "desc": "Açıklama",
diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json
index 15830a184..d49f2d94f 100644
--- a/src/locales/zh/common.json
+++ b/src/locales/zh/common.json
@@ -12,6 +12,7 @@
   "credentials": {
     "singular": "凭证"
   },
+  "docs": "文档",
   "form": {
     "basic": {
       "desc": "描述",

Reply via email to