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": "描述",