diff --git a/pontoon/base/static/css/dark-theme.css b/pontoon/base/static/css/dark-theme.css
index e752e2e164..df047b9695 100644
--- a/pontoon/base/static/css/dark-theme.css
+++ b/pontoon/base/static/css/dark-theme.css
@@ -23,17 +23,17 @@
--popup-background-2: #272a2f;
--icon-background-2: #4d5967;
- /* Homepage */
- --homepage-background-image: url(../img/background.svg);
- --homepage-tour-button-background: #ffffff;
- --homepage-tour-button-color: #000000;
-
/* Tooltip */
--tooltip-background: #000000dd;
--tooltip-color: #ffffff;
--tooltip-color-2: #888888;
--tooltip-border: #4d5967;
+ /* Homepage */
+ --homepage-background-image: url(../img/background.svg);
+ --homepage-tour-button-background: #ffffff;
+ --homepage-tour-button-color: #000000;
+
/* Translation status */
--status-translated: #7bc876;
--status-translated-alt: #7bc876;
diff --git a/translate/public/locale/en-US/translate.ftl b/translate/public/locale/en-US/translate.ftl
index e3f1642c7f..3776682cf6 100644
--- a/translate/public/locale/en-US/translate.ftl
+++ b/translate/public/locale/en-US/translate.ftl
@@ -297,6 +297,16 @@ entitydetails-PluralString--singular = SINGULAR
entitieslist-Entity--sibling-strings-title =
.title = Click to reveal sibling strings
+entitieslist-EntitiesList--clear-selected = CLEAR
+ .title = Uncheck selected strings
+
+entitieslist-EntitiesList--edit-selected =
+ EDIT { $count } { $count ->
+ [one] STRING
+ *[other] STRINGS
+ }
+ .title = Edit Selected Strings
+
## Translation Form
diff --git a/translate/src/App.css b/translate/src/App.css
index 8d54517d58..e865a3d6a0 100644
--- a/translate/src/App.css
+++ b/translate/src/App.css
@@ -120,4 +120,31 @@
#app .entity-navigation button.previous {
margin-right: 15px;
}
+
+ /* History */
+ #app .entity-details .history {
+ background: var(--editor-menu-background);
+ }
+
+ /* Helpers */
+ #app > .main-content .third-column {
+ border-left: none;
+ }
+
+ #app > .main-content .third-column .react-tabs span.count {
+ background: var(--light-grey-6);
+ box-shadow: 0 0 5px;
+ color: var(--light-grey-6); /* used as box-shadow color */
+ font-size: 0;
+ height: 4px;
+ width: 4px;
+ padding: 0;
+ position: absolute;
+ }
+
+ #app > .main-content .third-column .react-tabs span.count:has(.preferred),
+ #app > .main-content .third-column .react-tabs span.count:has(.pinned) {
+ background: var(--status-translated-alt);
+ color: var(--status-translated-alt); /* used as box-shadow color */
+ }
}
diff --git a/translate/src/hooks/useNarrowScreen.ts b/translate/src/hooks/useNarrowScreen.ts
new file mode 100644
index 0000000000..ca1444fb84
--- /dev/null
+++ b/translate/src/hooks/useNarrowScreen.ts
@@ -0,0 +1,21 @@
+import { useEffect, useState } from 'react';
+
+const NARROW_SCREEN_MAX_WIDTH = 600;
+
+/**
+ * Return true if the screen is narrower than 600px. Useful in Responsive Web Design.
+ */
+export function useNarrowScreen(): boolean {
+ const [isNarrow, setIsNarrow] = useState(
+ window.innerWidth <= NARROW_SCREEN_MAX_WIDTH,
+ );
+
+ useEffect(() => {
+ const handleWindowResize = () =>
+ setIsNarrow(window.innerWidth <= NARROW_SCREEN_MAX_WIDTH);
+ window.addEventListener('resize', handleWindowResize);
+ return () => window.removeEventListener('resize', handleWindowResize);
+ }, []);
+
+ return isNarrow;
+}
diff --git a/translate/src/modules/entitieslist/components/EntitiesList.css b/translate/src/modules/entitieslist/components/EntitiesList.css
index c09e869ee6..a6432936d9 100644
--- a/translate/src/modules/entitieslist/components/EntitiesList.css
+++ b/translate/src/modules/entitieslist/components/EntitiesList.css
@@ -41,3 +41,45 @@
font-size: 128px;
margin-bottom: 20px;
}
+
+.entities .toolbar {
+ position: sticky;
+ bottom: 0;
+ background: var(--tooltip-background);
+ border-top: 1px solid var(--light-grey-1);
+ box-sizing: border-box;
+ line-height: 23px;
+ padding: 10px 12px;
+ width: 100%;
+ height: auto;
+}
+
+.entities .toolbar button {
+ background: none;
+ border: none;
+ color: var(--tooltip-color-2);
+ font-weight: 300;
+}
+
+.entities .toolbar .clear-selected {
+ float: left;
+}
+
+.entities .toolbar .clear-selected .fa {
+ padding-right: 6px;
+}
+
+.entities .toolbar .edit-selected {
+ float: right;
+ text-align: right;
+}
+
+.entities .toolbar .edit-selected .fa {
+ padding-left: 6px;
+}
+
+.entities .toolbar .clear-selected:hover,
+.entities .toolbar .edit-selected:hover,
+.entities .toolbar .edit-selected .selected-count {
+ color: var(--status-translated);
+}
diff --git a/translate/src/modules/entitieslist/components/EntitiesList.tsx b/translate/src/modules/entitieslist/components/EntitiesList.tsx
index a0ce5110c6..2e3b5dc6e5 100644
--- a/translate/src/modules/entitieslist/components/EntitiesList.tsx
+++ b/translate/src/modules/entitieslist/components/EntitiesList.tsx
@@ -1,7 +1,9 @@
+import { Localized } from '@fluent/react';
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import useInfiniteScroll from 'react-infinite-scroll-hook';
import type { Entity as EntityType } from '~/api/entity';
+import { EntitiesList as EntitiesListContext } from '~/context/EntitiesList';
import { Locale } from '~/context/Locale';
import { Location } from '~/context/Location';
import {
@@ -13,6 +15,7 @@ import { useEntities } from '~/modules/entities/hooks';
import { SkeletonLoader } from '~/modules/loaders';
import { ENTITY_NOT_FOUND } from '~/modules/notification/messages';
import { useAppDispatch, useAppSelector, useAppStore } from '~/hooks';
+import { useNarrowScreen } from '~/hooks/useNarrowScreen';
import { usePrevious } from '~/hooks/usePrevious';
import {
checkSelection,
@@ -29,6 +32,51 @@ import { Entity } from './Entity';
import { USER } from '~/modules/user';
import { ShowNotification } from '~/context/Notification';
+const EntitiesToolbar = ({
+ count,
+ onEdit,
+ onClear,
+}: {
+ count: number;
+ onEdit: () => void;
+ onClear: () => void;
+}) => (
+
+ ,
+ }}
+ >
+
+
+ ,
+ stress: ,
+ }}
+ vars={{ count }}
+ >
+
+
+
+);
+
/**
* Displays a list of entities and their current translation.
*
@@ -224,6 +272,12 @@ export function EntitiesList(): React.ReactElement<'div'> {
);
}
+ const selectedEntitiesCount = batchactions.entities.length;
+ const isNarrowScreen = useNarrowScreen();
+ const entitiesList = useContext(EntitiesListContext);
+ const quitBatchActions = useCallback(() => dispatch(resetSelection()), []);
+ const showBatchActions = useCallback(() => entitiesList.show(false), []);
+
return (
@@ -234,9 +288,7 @@ export function EntitiesList(): React.ReactElement<'div'> {
toggleForBatchEditing={toggleForBatchEditing}
entity={entity}
isReadOnlyEditor={entity.readonly || !isAuthUser}
- selected={
- !batchactions.entities.length && entity.pk === location.entity
- }
+ selected={!selectedEntitiesCount && entity.pk === location.entity}
selectEntity={selectEntity}
getSiblingEntities={getSiblingEntities_}
parameters={location}
@@ -244,6 +296,13 @@ export function EntitiesList(): React.ReactElement<'div'> {
))}
{hasNextPage &&
}
+ {selectedEntitiesCount === 0 || !isNarrowScreen ? null : (
+
+ )}
);
}
diff --git a/translate/src/modules/entitydetails/components/ContextIssueButton.css b/translate/src/modules/entitydetails/components/ContextIssueButton.css
index 36dea8e26e..dc1fcc0562 100644
--- a/translate/src/modules/entitydetails/components/ContextIssueButton.css
+++ b/translate/src/modules/entitydetails/components/ContextIssueButton.css
@@ -1,4 +1,4 @@
-.metadata .source-string-comment .context-issue-button {
+.context-issue-button {
background: var(--dark-grey-1);
border: 1px solid var(--dark-grey-2);
border-radius: 4px;
@@ -11,6 +11,6 @@
padding: 2px 4px;
}
-.metadata .source-string-comment .context-issue-button:hover {
+.context-issue-button:hover {
border-color: var(--translation-border);
}
diff --git a/translate/src/modules/entitydetails/components/ContextIssueButton.tsx b/translate/src/modules/entitydetails/components/ContextIssueButton.tsx
index b687b6f19c..948387a616 100644
--- a/translate/src/modules/entitydetails/components/ContextIssueButton.tsx
+++ b/translate/src/modules/entitydetails/components/ContextIssueButton.tsx
@@ -9,15 +9,10 @@ type Props = {
export function ContextIssueButton(props: Props): React.ReactElement<'div'> {
return (
-
-
-
-
-
+
+
+
);
}
diff --git a/translate/src/modules/entitydetails/components/EntityDetails.css b/translate/src/modules/entitydetails/components/EntityDetails.css
index 5f46318512..fe333362c8 100644
--- a/translate/src/modules/entitydetails/components/EntityDetails.css
+++ b/translate/src/modules/entitydetails/components/EntityDetails.css
@@ -13,6 +13,11 @@
width: 66.67%;
}
+.entity-details .main-column .original-string-panel {
+ border-bottom: 1px solid var(--main-border-1);
+ padding: 10px;
+}
+
.entity-details .third-column {
width: 33.33%;
border-left: 1px solid var(--main-border-1);
diff --git a/translate/src/modules/entitydetails/components/EntityDetails.tsx b/translate/src/modules/entitydetails/components/EntityDetails.tsx
index cc16114c0b..c0ff95da8c 100644
--- a/translate/src/modules/entitydetails/components/EntityDetails.tsx
+++ b/translate/src/modules/entitydetails/components/EntityDetails.tsx
@@ -7,9 +7,11 @@ import React, {
} from 'react';
import { EntityView, useActiveTranslation } from '~/context/EntityView';
+import { Locale } from '~/context/Locale';
import { Location } from '~/context/Location';
import { UnsavedActions } from '~/context/UnsavedChanges';
import { Editor } from '~/modules/editor/components/Editor';
+import { OriginalString } from '~/modules/originalstring';
import { TERM } from '~/modules/terms';
import { get as getTerms } from '~/modules/terms/actions';
import { USER } from '~/modules/user';
@@ -25,10 +27,13 @@ import {
} from '~/modules/teamcomments/actions';
import { getPlainMessage } from '~/utils/message';
-import './EntityDetails.css';
+import { ContextIssueButton } from './ContextIssueButton';
import { EntityNavigation } from './EntityNavigation';
import { Helpers } from './Helpers';
import { Metadata } from './Metadata';
+import { Screenshots } from './Screenshots';
+
+import './EntityDetails.css';
/**
* Component showing details about an entity.
@@ -88,21 +93,39 @@ export function EntityDetails(): React.ReactElement<'section'> | null {
[dispatch],
);
+ const { code } = useContext(Locale);
+
+ const openTeamComments = useCallback(() => {
+ const teamCommentsTab = commentTabRef.current;
+
+ // FIXME: This is an ugly hack.
+ // https://github.com/mozilla/pontoon/issues/2300
+ const index = teamCommentsTab?._reactInternalFiber.index ?? 0;
+
+ setCommentTabIndex(index);
+ setContactPerson(selectedEntity.project.contact.name);
+ }, [selectedEntity, setCommentTabIndex, setContactPerson]);
+
+ const showContextIssueButton =
+ user.isAuthenticated && selectedEntity.project.contact;
+
// No content while loading entity data
return selectedEntity.pk === 0 ? null : (
-
+
+ {showContextIssueButton && (
+
+ )}
+
+
+
+
diff --git a/translate/src/modules/entitydetails/components/Helpers.tsx b/translate/src/modules/entitydetails/components/Helpers.tsx
index cba0547231..30b16824bb 100644
--- a/translate/src/modules/entitydetails/components/Helpers.tsx
+++ b/translate/src/modules/entitydetails/components/Helpers.tsx
@@ -5,6 +5,7 @@ import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import type { Entity } from '~/api/entity';
import { HelperSelection } from '~/context/HelperSelection';
import type { Location } from '~/context/Location';
+import { useNarrowScreen } from '~/hooks/useNarrowScreen';
import type { TermState } from '~/modules/terms';
import type { UserState } from '~/modules/user';
import { Machinery, MachineryCount } from '~/modules/machinery';
@@ -56,6 +57,138 @@ export function Helpers({
const isTerminologyProject = parameters.project === 'terminology';
+ function MachineryTab() {
+ return (
+ <>
+
+ {'MACHINERY'}
+
+
+ >
+ );
+ }
+
+ function OtherLocalesTab() {
+ return (
+ <>
+ {'LOCALES'}
+
+ >
+ );
+ }
+
+ function TermsTab() {
+ return (
+ <>
+ {'TERMS'}
+
+ >
+ );
+ }
+
+ function CommentsTab() {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ function MachineryPanel() {
+ return (
+ <>
+
+ >
+ );
+ }
+
+ function OtherLocalesPanel() {
+ return (
+ <>
+
+ >
+ );
+ }
+
+ function TermsPanel() {
+ return (
+ <>
+
+ >
+ );
+ }
+
+ function CommentsPanel() {
+ return (
+ <>
+
+ >
+ );
+ }
+
+ if (useNarrowScreen()) {
+ return (
+ <>
+
+ {
+ if (index === lastIndex) {
+ return false;
+ } else {
+ setTab(index);
+ }
+ setCommentTabIndex(index);
+ }}
+ >
+
+
+
+
+
+
+
+ {isTerminologyProject ? null : (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {isTerminologyProject ? null : (
+
+
+
+ )}
+
+
+
+
+
+ >
+ );
+ }
+
return (
<>
@@ -66,33 +199,20 @@ export function Helpers({
{isTerminologyProject ? null : (
-
- {'TERMS'}
-
-
+
)}
-
-
+
{isTerminologyProject ? null : (
-
+
)}
-
+
@@ -108,27 +228,17 @@ export function Helpers({
>
-
- {'MACHINERY'}
-
-
+
-
- {'LOCALES'}
-
-
+
-
+
-
+
diff --git a/translate/src/modules/entitydetails/components/Metadata.css b/translate/src/modules/entitydetails/components/Metadata.css
index 63cc58f13e..06481240a9 100644
--- a/translate/src/modules/entitydetails/components/Metadata.css
+++ b/translate/src/modules/entitydetails/components/Metadata.css
@@ -1,11 +1,9 @@
.metadata {
- border-bottom: 1px solid var(--main-border-1);
color: var(--translation-secondary-color);
font-size: 12px;
font-style: italic;
min-height: 114px;
line-height: 22px;
- padding: 10px;
}
.metadata h2 {
@@ -36,30 +34,6 @@
margin: 0 3px;
}
-.metadata .original {
- color: var(--translation-color);
- font-size: 14px;
- font-style: normal;
- line-height: 22px;
- margin-top: -2px; /* Align with the source string comment button */
- padding-bottom: 6px;
- text-align: start;
- white-space: pre-wrap;
-}
-
-.metadata .original .placeable {
- cursor: pointer;
-}
-
-.metadata .original .term {
- background: inherit;
- border-bottom: 1px solid var(--status-translated);
- color: inherit;
- cursor: pointer;
- font-weight: normal;
- font-style: inherit;
-}
-
.metadata ul {
list-style: none;
margin: 0;
diff --git a/translate/src/modules/entitydetails/components/Metadata.tsx b/translate/src/modules/entitydetails/components/Metadata.tsx
index 02e6b6eecb..c6ba47208e 100644
--- a/translate/src/modules/entitydetails/components/Metadata.tsx
+++ b/translate/src/modules/entitydetails/components/Metadata.tsx
@@ -1,32 +1,22 @@
import { Localized } from '@fluent/react';
import parse from 'html-react-parser';
-import React, { useCallback, useContext, useLayoutEffect } from 'react';
+import React, { useContext, useLayoutEffect } from 'react';
// @ts-expect-error Working types are unavailable for react-linkify 0.2.2
import Linkify from 'react-linkify';
import type { Entity } from '~/api/entity';
-import { Locale } from '~/context/Locale';
-import type { TermState } from '~/modules/terms';
-import type { UserState } from '~/modules/user';
-import { OriginalString } from '~/modules/originalstring';
import type { TeamCommentState } from '~/modules/teamcomments';
-import { ContextIssueButton } from './ContextIssueButton';
+import { Locale } from '~/context/Locale';
import { FluentAttribute } from './FluentAttribute';
import { Property } from './Property';
-import { Screenshots } from './Screenshots';
import './Metadata.css';
type Props = {
entity: Entity;
- terms: TermState;
teamComments: TeamCommentState;
- user: UserState;
- commentTabRef: React.RefObject<{ _reactInternalFiber: { index: number } }>;
navigateToPath: (path: string) => void;
- setCommentTabIndex: (id: number) => void;
- setContactPerson: (contact: string) => void;
};
const Datum = ({
@@ -230,34 +220,14 @@ const EntityContext = ({
* - a link to the project
*/
export function Metadata({
- commentTabRef,
entity,
navigateToPath,
- setCommentTabIndex,
- setContactPerson,
- terms,
teamComments,
- user,
}: Props): React.ReactElement {
const { code } = useContext(Locale);
- const openTeamComments = useCallback(() => {
- const teamCommentsTab = commentTabRef.current;
- const index = teamCommentsTab?._reactInternalFiber.index ?? 0;
- setCommentTabIndex(index);
- setContactPerson(entity.project.contact.name);
- }, [entity, setCommentTabIndex, setContactPerson]);
-
- const contactPerson = entity.project.contact;
- const showContextIssueButton = user.isAuthenticated && contactPerson;
-
return (
- {showContextIssueButton && (
-
- )}
-
-
diff --git a/translate/src/modules/originalstring/components/OriginalString.css b/translate/src/modules/originalstring/components/OriginalString.css
new file mode 100644
index 0000000000..950b5c3d11
--- /dev/null
+++ b/translate/src/modules/originalstring/components/OriginalString.css
@@ -0,0 +1,22 @@
+.original-string-panel .original {
+ color: var(--translation-color);
+ font-size: 14px;
+ font-style: normal;
+ line-height: 22px;
+ margin: -2px 0 6px; /* Align with the source string comment button */
+ text-align: start;
+ white-space: pre-wrap;
+}
+
+.original-string-panel .original .placeable {
+ cursor: pointer;
+}
+
+.original-string-panel .original .term {
+ background: inherit;
+ border-bottom: 1px solid var(--status-translated);
+ color: inherit;
+ cursor: pointer;
+ font-weight: normal;
+ font-style: inherit;
+}
diff --git a/translate/src/modules/originalstring/components/OriginalString.tsx b/translate/src/modules/originalstring/components/OriginalString.tsx
index f2fc73c5a2..89157df8ed 100644
--- a/translate/src/modules/originalstring/components/OriginalString.tsx
+++ b/translate/src/modules/originalstring/components/OriginalString.tsx
@@ -19,6 +19,8 @@ import { PluralString } from './PluralString';
import { RichString } from './RichString';
import { TermsPopup } from './TermsPopup';
+import './OriginalString.css';
+
type Props = {
navigateToPath: (path: string) => void;
terms: TermState;
diff --git a/translate/src/modules/search/components/FiltersPanel.css b/translate/src/modules/search/components/FiltersPanel.css
index 69c0dea574..cbbf52d4ae 100644
--- a/translate/src/modules/search/components/FiltersPanel.css
+++ b/translate/src/modules/search/components/FiltersPanel.css
@@ -229,7 +229,7 @@
.filters-panel .toolbar {
position: sticky;
bottom: 0;
- background: #111111;
+ background: var(--tooltip-background);
border-top: 1px solid var(--light-grey-1);
box-sizing: border-box;
line-height: 23px;
@@ -240,7 +240,7 @@
.filters-panel .toolbar button {
background: none;
border: none;
- color: var(--transla-grey-7);
+ color: var(--tooltip-color-2);
font-weight: 300;
}
diff --git a/translate/src/modules/terms/components/TermCount.tsx b/translate/src/modules/terms/components/TermCount.tsx
index a7ffefb656..291264f02b 100644
--- a/translate/src/modules/terms/components/TermCount.tsx
+++ b/translate/src/modules/terms/components/TermCount.tsx
@@ -15,5 +15,9 @@ export function TermCount(props: Props): null | React.ReactElement<'span'> {
const termCount = terms.terms.length;
+ if (!termCount) {
+ return null;
+ }
+
return
{termCount};
}