Browse Source

feat: localize search UI and frame titles

pull/10428/head
Karan Thakur 2 weeks ago
parent
commit
037886ae57
  1. 636
      packages/excalidraw/components/App.tsx
  2. 77
      packages/excalidraw/components/CommandPalette/CommandPalette.tsx
  3. 6
      packages/excalidraw/components/DefaultSidebar.tsx
  4. 8
      packages/excalidraw/components/LibraryMenuItems.tsx
  5. 61
      packages/excalidraw/components/SearchMenu.tsx
  6. 34
      packages/excalidraw/components/UserList.tsx
  7. 22
      packages/excalidraw/locales/en.json
  8. 32
      packages/excalidraw/locales/zh-CN.json
  9. 20
      packages/excalidraw/scene/export.ts

636
packages/excalidraw/components/App.tsx

File diff suppressed because it is too large Load Diff

77
packages/excalidraw/components/CommandPalette/CommandPalette.tsx

@ -84,13 +84,13 @@ import type { Action } from "../../actions/types"; @@ -84,13 +84,13 @@ import type { Action } from "../../actions/types";
const lastUsedPaletteItem = atom<CommandPaletteItem | null>(null);
export const DEFAULT_CATEGORIES = {
app: "App",
export: "Export",
tools: "Tools",
editor: "Editor",
elements: "Elements",
links: "Links",
library: "Library",
app: "app",
export: "export",
tools: "tools",
editor: "editor",
elements: "elements",
links: "links",
library: "library",
};
const getCategoryOrder = (category: string) => {
@ -234,8 +234,8 @@ function CommandPaletteInner({ @@ -234,8 +234,8 @@ function CommandPaletteInner({
elements={libraryItem.elements}
/>
),
category: "Library",
order: getCategoryOrder("Library"),
category: DEFAULT_CATEGORIES.library,
order: getCategoryOrder(DEFAULT_CATEGORIES.library),
haystack: deburr(libraryItem.name),
perform: () => {
app.onInsertElements(
@ -347,12 +347,12 @@ function CommandPaletteInner({ @@ -347,12 +347,12 @@ function CommandPaletteInner({
predicate: action.predicate
? action.predicate
: (elements, appState, appProps, app) => {
const selectedElements = getSelectedElements(
elements,
appState,
);
return selectedElements.length > 0;
},
const selectedElements = getSelectedElements(
elements,
appState,
);
return selectedElements.length > 0;
},
}),
),
);
@ -518,10 +518,10 @@ function CommandPaletteInner({ @@ -518,10 +518,10 @@ function CommandPaletteInner({
if (
appProps.UIOptions.tools?.[
value as Extract<
typeof value,
keyof AppProps["UIOptions"]["tools"]
>
value as Extract<
typeof value,
keyof AppProps["UIOptions"]["tools"]
>
] === false
) {
return acc;
@ -617,9 +617,8 @@ function CommandPaletteInner({ @@ -617,9 +617,8 @@ function CommandPaletteInner({
...command,
icon: command.icon || boltIcon,
order: command.order ?? getCategoryOrder(command.category),
haystack: `${deburr(command.label.toLocaleLowerCase())} ${
command.keywords?.join(" ") || ""
}`,
haystack: `${deburr(command.label.toLocaleLowerCase())} ${command.keywords?.join(" ") || ""
}`,
};
});
@ -685,11 +684,11 @@ function CommandPaletteInner({ @@ -685,11 +684,11 @@ function CommandPaletteInner({
return typeof command.predicate === "function"
? command.predicate(
app.scene.getNonDeletedElements(),
uiAppState as AppState,
appProps,
app,
)
app.scene.getNonDeletedElements(),
uiAppState as AppState,
appProps,
app,
)
: command.predicate === undefined || command.predicate;
},
);
@ -838,14 +837,14 @@ function CommandPaletteInner({ @@ -838,14 +837,14 @@ function CommandPaletteInner({
let matchingCommands =
commandSearch?.length > 1
? [
...allCommands
.filter(isCommandAvailable)
.sort((a, b) => a.order - b.order),
...libraryCommands,
]
: allCommands
...allCommands
.filter(isCommandAvailable)
.sort((a, b) => a.order - b.order);
.sort((a, b) => a.order - b.order),
...libraryCommands,
]
: allCommands
.filter(isCommandAvailable)
.sort((a, b) => a.order - b.order);
const showLastUsed =
!commandSearch && lastUsed && isCommandAvailable(lastUsed);
@ -855,8 +854,8 @@ function CommandPaletteInner({ @@ -855,8 +854,8 @@ function CommandPaletteInner({
getNextCommandsByCategory(
showLastUsed
? matchingCommands.filter(
(command) => command.label !== lastUsed?.label,
)
(command) => command.label !== lastUsed?.label,
)
: matchingCommands,
),
);
@ -947,7 +946,7 @@ function CommandPaletteInner({ @@ -947,7 +946,7 @@ function CommandPaletteInner({
Object.keys(commandsByCategory).map((category, idx) => {
return (
<div className="command-category" key={category}>
<div className="command-category-title">{category}</div>
<div className="command-category-title">{t(`commandPalette.categories.${category}` as TranslationKeys)}</div>
{commandsByCategory[category].map((command) => (
<CommandItem
key={command.label}
@ -957,7 +956,7 @@ function CommandPaletteInner({ @@ -957,7 +956,7 @@ function CommandPaletteInner({
onMouseMove={() => setCurrentCommand(command)}
showShortcut={app.editorInterface.formFactor !== "phone"}
appState={uiAppState}
size={category === "Library" ? "large" : "small"}
size={category === DEFAULT_CATEGORIES.library ? "large" : "small"}
/>
))}
</div>
@ -1007,7 +1006,7 @@ const CommandItem = ({ @@ -1007,7 +1006,7 @@ const CommandItem = ({
appState: UIAppState;
size?: "small" | "large";
}) => {
const noop = () => {};
const noop = () => { };
return (
<div

6
packages/excalidraw/components/DefaultSidebar.tsx

@ -91,9 +91,9 @@ export const DefaultSidebar = Object.assign( @@ -91,9 +91,9 @@ export const DefaultSidebar = Object.assign(
isForceDocked || onDock === false || (!onDock && docked != null)
? undefined
: // compose to allow the host app to listen on default behavior
composeEventHandlers(onDock, (docked) => {
setAppState({ defaultSidebarDockedPreference: docked });
})
composeEventHandlers(onDock, (docked) => {
setAppState({ defaultSidebarDockedPreference: docked });
})
}
>
<Sidebar.Tabs>

8
packages/excalidraw/components/LibraryMenuItems.tsx

@ -246,7 +246,7 @@ export default function LibraryMenuItems({ @@ -246,7 +246,7 @@ export default function LibraryMenuItems({
const itemsRenderedPerBatch =
svgCache.size >=
(filteredItems.length ? filteredItems : libraryItems).length
(filteredItems.length ? filteredItems : libraryItems).length
? CACHED_ITEMS_RENDERED_PER_BATCH
: ITEMS_RENDERED_PER_BATCH;
@ -340,7 +340,7 @@ export default function LibraryMenuItems({ @@ -340,7 +340,7 @@ export default function LibraryMenuItems({
setSearchInputValue("");
}}
>
<kbd>esc</kbd> to clear
<kbd>{t("buttons.escape")}</kbd> {t("labels.toClear")}
</div>
)}
</div>
@ -380,8 +380,8 @@ export default function LibraryMenuItems({ @@ -380,8 +380,8 @@ export default function LibraryMenuItems({
className="library-menu-items-container"
style={
pendingElements.length ||
unpublishedItems.length ||
publishedItems.length
unpublishedItems.length ||
publishedItems.length
? { justifyContent: "flex-start" }
: { borderBottom: 0 }
}

61
packages/excalidraw/components/SearchMenu.tsx

@ -23,7 +23,7 @@ import { @@ -23,7 +23,7 @@ import {
} from "@excalidraw/common";
import { newTextElement } from "@excalidraw/element";
import { isTextElement, isFrameLikeElement } from "@excalidraw/element";
import { isTextElement, isFrameLikeElement, isFrameElement } from "@excalidraw/element";
import { getDefaultFrameName } from "@excalidraw/element/frame";
@ -50,7 +50,7 @@ import { @@ -50,7 +50,7 @@ import {
import "./SearchMenu.scss";
import type { AppClassProperties, SearchMatch } from "../types";
import type { AppClassProperties, SearchMatch, UIAppState } from "../types";
const searchQueryAtom = atom<string>("");
export const searchItemInFocusAtom = atom<number | null>(null);
@ -107,7 +107,7 @@ export const SearchMenu = () => { @@ -107,7 +107,7 @@ export const SearchMenu = () => {
app.scene.getSceneNonce() !== lastSceneNonceRef.current
) {
searchedQueryRef.current = null;
handleSearch(searchQuery, app, (matchItems, index) => {
handleSearch(searchQuery, app, (matchItems: SearchMatchItem[], index: number | null) => {
setSearchMatches({
nonce: randomInteger(),
items: matchItems,
@ -117,13 +117,13 @@ export const SearchMenu = () => { @@ -117,13 +117,13 @@ export const SearchMenu = () => {
setAppState({
searchMatches: matchItems.length
? {
focusedId: null,
matches: matchItems.map((searchMatch) => ({
id: searchMatch.element.id,
focus: false,
matchedLines: searchMatch.matchedLines,
})),
}
focusedId: null,
matches: matchItems.map((searchMatch: SearchMatchItem) => ({
id: searchMatch.element.id,
focus: false,
matchedLines: searchMatch.matchedLines,
})),
}
: null,
});
});
@ -140,7 +140,7 @@ export const SearchMenu = () => { @@ -140,7 +140,7 @@ export const SearchMenu = () => {
const goToNextItem = () => {
if (searchMatches.items.length > 0) {
setFocusIndex((focusIndex) => {
setFocusIndex((focusIndex: number | null) => {
if (focusIndex === null) {
return 0;
}
@ -152,7 +152,7 @@ export const SearchMenu = () => { @@ -152,7 +152,7 @@ export const SearchMenu = () => {
const goToPreviousItem = () => {
if (searchMatches.items.length > 0) {
setFocusIndex((focusIndex) => {
setFocusIndex((focusIndex: number | null) => {
if (focusIndex === null) {
return 0;
}
@ -165,7 +165,7 @@ export const SearchMenu = () => { @@ -165,7 +165,7 @@ export const SearchMenu = () => {
};
useEffect(() => {
setAppState((state) => {
setAppState((state: UIAppState) => {
if (!state.searchMatches) {
return null;
}
@ -178,7 +178,7 @@ export const SearchMenu = () => { @@ -178,7 +178,7 @@ export const SearchMenu = () => {
return {
searchMatches: {
focusedId,
matches: state.searchMatches.matches.map((match, index) => {
matches: state.searchMatches.matches.map((match: any, index: number) => {
if (index === focusIndex) {
return { ...match, focus: true };
}
@ -341,11 +341,10 @@ export const SearchMenu = () => { @@ -341,11 +341,10 @@ export const SearchMenu = () => {
});
}, [setAppState, stableState, app]);
const matchCount = `${searchMatches.items.length} ${
searchMatches.items.length === 1
? t("search.singleResult")
: t("search.multipleResults")
}`;
const matchCount = `${searchMatches.items.length} ${searchMatches.items.length === 1
? t("search.singleResult")
: t("search.multipleResults")
}`;
return (
<div className="layer-ui__search">
@ -356,11 +355,11 @@ export const SearchMenu = () => { @@ -356,11 +355,11 @@ export const SearchMenu = () => {
ref={searchInputRef}
placeholder={t("search.placeholder")}
icon={searchIcon}
onChange={(value) => {
onChange={(value: string) => {
setInputValue(value);
setIsSearching(true);
const searchQuery = value.trim() as SearchQuery;
handleSearch(searchQuery, app, (matchItems, index) => {
handleSearch(searchQuery, app, (matchItems: SearchMatchItem[], index: number | null) => {
setSearchMatches({
nonce: randomInteger(),
items: matchItems,
@ -371,13 +370,13 @@ export const SearchMenu = () => { @@ -371,13 +370,13 @@ export const SearchMenu = () => {
setAppState({
searchMatches: matchItems.length
? {
focusedId: null,
matches: matchItems.map((searchMatch) => ({
id: searchMatch.element.id,
focus: false,
matchedLines: searchMatch.matchedLines,
})),
}
focusedId: null,
matches: matchItems.map((searchMatch: SearchMatchItem) => ({
id: searchMatch.element.id,
focus: false,
matchedLines: searchMatch.matchedLines,
})),
}
: null,
});
@ -462,7 +461,7 @@ const ListItem = (props: { @@ -462,7 +461,7 @@ const ListItem = (props: {
active: props.highlighted,
})}
onClick={props.onClick}
ref={(ref) => {
ref={(ref: HTMLDivElement | null) => {
if (props.highlighted) {
ref?.scrollIntoView({ behavior: "auto", block: "nearest" });
}
@ -504,7 +503,7 @@ const MatchListBase = (props: MatchListProps) => { @@ -504,7 +503,7 @@ const MatchListBase = (props: MatchListProps) => {
<div className="title-icon">{frameToolIcon}</div>
<div>{t("search.frames")}</div>
</div>
{frameNameMatches.map((searchMatch, index) => (
{frameNameMatches.map((searchMatch: SearchMatchItem, index: number) => (
<ListItem
key={searchMatch.element.id + searchMatch.index}
searchQuery={props.searchQuery}
@ -524,7 +523,7 @@ const MatchListBase = (props: MatchListProps) => { @@ -524,7 +523,7 @@ const MatchListBase = (props: MatchListProps) => {
<div className="title-icon">{TextIcon}</div>
<div>{t("search.texts")}</div>
</div>
{textMatches.map((searchMatch, index) => (
{textMatches.map((searchMatch: SearchMatchItem, index: number) => (
<ListItem
key={searchMatch.element.id + searchMatch.index}
searchQuery={props.searchQuery}

34
packages/excalidraw/components/UserList.tsx

@ -221,11 +221,11 @@ export const UserList = React.memo( @@ -221,11 +221,11 @@ export const UserList = React.memo(
<Island padding={2}>
{uniqueCollaboratorsArray.length >=
SHOW_COLLABORATORS_FILTER_AT && (
<QuickSearch
placeholder={t("quickSearch.placeholder")}
onChange={setSearchTerm}
/>
)}
<QuickSearch
placeholder={t("quickSearch.placeholder")}
onChange={setSearchTerm}
/>
)}
<ScrollableList
className={"dropdown-menu UserList__collaborators"}
placeholder={t("userList.empty")}
@ -233,18 +233,18 @@ export const UserList = React.memo( @@ -233,18 +233,18 @@ export const UserList = React.memo(
{/* The list checks for `Children.count()`, hence defensively returning empty list */}
{filteredCollaborators.length > 0
? [
<div className="hint">{t("userList.hint.text")}</div>,
filteredCollaborators.map((collaborator) =>
renderCollaborator({
actionManager,
collaborator,
socketId: collaborator.socketId,
withName: true,
isBeingFollowed:
collaborator.socketId === userToFollow,
}),
),
]
<div className="hint">{t("userList.hint.text")}</div>,
filteredCollaborators.map((collaborator) =>
renderCollaborator({
actionManager,
collaborator,
socketId: collaborator.socketId,
withName: true,
isBeingFollowed:
collaborator.socketId === userToFollow,
}),
),
]
: []}
</ScrollableList>
<Popover.Arrow

22
packages/excalidraw/locales/en.json

@ -1,10 +1,13 @@ @@ -1,10 +1,13 @@
{
"labels": {
"defaultFrameName": "Frame",
"defaultAiFrameName": "AI Frame",
"paste": "Paste",
"pasteAsPlaintext": "Paste as plaintext",
"pasteCharts": "Paste charts",
"selectAll": "Select all",
"multiSelect": "Add element to selection",
"toClear": "to clear",
"moveCanvas": "Move canvas",
"cut": "Cut",
"copy": "Copy",
@ -196,9 +199,17 @@ @@ -196,9 +199,17 @@
"multipleResults": "results",
"placeholder": "Find text on canvas...",
"frames": "Frames",
"texts": "Texts"
"texts": "Text",
"search": "Search",
"noResults": "No results",
"tabs": {
"shapes": "Shapes",
"actions": "Actions",
"properties": "Properties"
}
},
"buttons": {
"escape": "Esc",
"clearReset": "Reset the canvas",
"exportJSON": "Export to file",
"exportImage": "Export image...",
@ -647,6 +658,15 @@ @@ -647,6 +658,15 @@
"placeholder": "Search menus, commands, and discover hidden gems",
"noMatch": "No matching commands..."
},
"categories": {
"app": "App",
"export": "Export",
"tools": "Tools",
"editor": "Editor",
"elements": "Elements",
"links": "Links",
"library": "Library"
},
"itemNotAvailable": "Command is not available...",
"shortcutHint": "For Command palette, use {{shortcut}}"
},

32
packages/excalidraw/locales/zh-CN.json

@ -1,10 +1,13 @@ @@ -1,10 +1,13 @@
{
"labels": {
"defaultFrameName": "画框",
"defaultAiFrameName": "AI 画框",
"paste": "粘贴",
"pasteAsPlaintext": "粘贴为纯文本",
"pasteCharts": "粘贴图表",
"selectAll": "全部选中",
"multiSelect": "添加元素到选区",
"selectAll": "全选",
"multiSelect": "将元素添加到选中区域",
"toClear": "以清除",
"moveCanvas": "移动画布",
"cut": "剪切",
"copy": "拷贝",
@ -196,9 +199,17 @@ @@ -196,9 +199,17 @@
"multipleResults": "结果",
"placeholder": "在画布上查找文本…",
"frames": "画框",
"texts": "文本"
"texts": "文本",
"search": "搜索",
"noResults": "未找到结果",
"tabs": {
"shapes": "形状",
"actions": "操作",
"properties": "属性"
}
},
"buttons": {
"escape": "Esc",
"clearReset": "重置画布",
"exportJSON": "导出为文件",
"exportImage": "导出图片...",
@ -644,9 +655,18 @@ @@ -644,9 +655,18 @@
"recents": "最近使用",
"search": {
"placeholder": "搜索菜单、命令、探索隐藏功能",
"noMatch": "没有匹配的命令……"
"noMatch": "无匹配项…"
},
"categories": {
"app": "应用",
"export": "导出",
"tools": "工具",
"editor": "编辑器",
"elements": "元素",
"links": "链接",
"library": "库"
},
"itemNotAvailable": "命令不可用……",
"itemNotAvailable": "命令不可用...",
"shortcutHint": "用 {{shortcut}} 打开命令面板"
},
"keys": {
@ -659,6 +679,6 @@ @@ -659,6 +679,6 @@
"shift": "Shift",
"spacebar": "空格",
"delete": "Delete",
"mmb": "鼠标滚轮"
"mmb": "鼠标中键"
}
}

20
packages/excalidraw/scene/export.ts

@ -24,7 +24,7 @@ import { @@ -24,7 +24,7 @@ import {
import { newElementWith } from "@excalidraw/element";
import { isFrameLikeElement } from "@excalidraw/element";
import { isFrameLikeElement, isFrameElement } from "@excalidraw/element";
import {
getElementsOverlappingFrame,
@ -52,6 +52,7 @@ import type { @@ -52,6 +52,7 @@ import type {
import { getDefaultAppState } from "../appState";
import { base64ToString, decode, encode, stringToBase64 } from "../data/encode";
import { serializeAsJSON } from "../data/json";
import { t } from "../i18n";
import { Fonts } from "../fonts";
@ -116,7 +117,11 @@ const addFrameLabelsAsTextElements = ( @@ -116,7 +117,11 @@ const addFrameLabelsAsTextElements = (
strokeColor: opts.exportWithDarkMode
? FRAME_STYLE.nameColorDarkTheme
: FRAME_STYLE.nameColorLightTheme,
text: getFrameLikeTitle(element),
text:
element.name ??
(isFrameElement(element)
? t("labels.defaultFrameName")
: t("labels.defaultAiFrameName")),
});
textElement.y -= textElement.height;
@ -408,8 +413,7 @@ export const exportToSvg = async ( @@ -408,8 +413,7 @@ export const exportToSvg = async (
const rect = svgRoot.ownerDocument.createElementNS(SVG_NS, "rect");
rect.setAttribute(
"transform",
`translate(${frame.x + offsetX} ${frame.y + offsetY}) rotate(${
frame.angle
`translate(${frame.x + offsetX} ${frame.y + offsetY}) rotate(${frame.angle
} ${cx} ${cy})`,
);
rect.setAttribute("width", `${frame.width}`);
@ -483,10 +487,10 @@ export const exportToSvg = async ( @@ -483,10 +487,10 @@ export const exportToSvg = async (
canvasBackgroundColor: viewBackgroundColor,
embedsValidationStatus: renderEmbeddables
? new Map(
elementsForRender
.filter((element) => isFrameLikeElement(element))
.map((element) => [element.id, true]),
)
elementsForRender
.filter((element) => isFrameLikeElement(element))
.map((element) => [element.id, true]),
)
: new Map(),
reuseImages: opts?.reuseImages ?? true,
},

Loading…
Cancel
Save