Prechádzať zdrojové kódy

Replace STRINGS constants with gettext

Karl Hobley 3 rokov pred
rodič
commit
1e884285a1
35 zmenil súbory, kde vykonal 186 pridanie a 412 odobranie
  1. 7 0
      .eslintrc.js
  2. 27 38
      client/src/components/CommentApp/components/Comment/index.tsx
  3. 4 7
      client/src/components/CommentApp/components/CommentHeader/index.tsx
  4. 26 59
      client/src/components/CommentApp/components/CommentReply/index.tsx
  5. 2 49
      client/src/components/CommentApp/main.tsx
  6. 2 3
      client/src/components/Draftail/CommentableEditor/CommentableEditor.tsx
  7. 7 7
      client/src/components/Draftail/EditorFallback/EditorFallback.js
  8. 2 4
      client/src/components/Draftail/blocks/EmbedBlock.js
  9. 4 6
      client/src/components/Draftail/blocks/ImageBlock.js
  10. 1 3
      client/src/components/Draftail/decorators/Document.js
  11. 1 3
      client/src/components/Draftail/decorators/Link.js
  12. 5 7
      client/src/components/Draftail/index.js
  13. 1 2
      client/src/components/Draftail/sources/ModalWorkflowSource.js
  14. 2 2
      client/src/components/Explorer/ExplorerHeader.tsx
  15. 6 3
      client/src/components/Explorer/ExplorerItem.tsx
  16. 5 5
      client/src/components/Explorer/ExplorerPanel.tsx
  17. 5 3
      client/src/components/Explorer/PageCount.tsx
  18. 1 2
      client/src/components/LoadingSpinner/LoadingSpinner.js
  19. 5 3
      client/src/components/PageExplorer/PageCount.tsx
  20. 2 2
      client/src/components/PageExplorer/PageExplorerHeader.tsx
  21. 6 3
      client/src/components/PageExplorer/PageExplorerItem.tsx
  22. 4 4
      client/src/components/PageExplorer/PageExplorerPanel.tsx
  23. 2 20
      client/src/components/Sidebar/Sidebar.stories.tsx
  24. 2 14
      client/src/components/Sidebar/Sidebar.tsx
  25. 0 1
      client/src/components/Sidebar/index.tsx
  26. 5 10
      client/src/components/Sidebar/modules/MainMenu.tsx
  27. 6 21
      client/src/components/Sidebar/modules/Search.tsx
  28. 3 6
      client/src/components/Sidebar/modules/WagtailBranding.tsx
  29. 0 1
      client/src/config/wagtailConfig.js
  30. 1 12
      client/src/config/wagtailConfig.test.js
  31. 35 1
      client/src/custom.d.ts
  32. 2 4
      client/src/entrypoints/admin/comments.js
  33. 1 3
      client/src/entrypoints/admin/modal-workflow.js
  34. 4 50
      client/tests/stubs.js
  35. 0 54
      wagtail/admin/localization.py

+ 7 - 0
.eslintrc.js

@@ -60,6 +60,13 @@ module.exports = {
     jest: true,
     browser: true,
   },
+  globals: {
+    gettext: 'readonly',
+    ngettext: 'readonly',
+    get_format: 'readonly',
+    gettext_noop: 'readonly',
+    pluralidx: 'readonly',
+  },
   rules: {
     'no-underscore-dangle': [
       'error',

+ 27 - 38
client/src/components/CommentApp/components/Comment/index.tsx

@@ -15,7 +15,6 @@ import {
 import { LayoutController } from '../../utils/layout';
 import { getNextReplyId } from '../../utils/sequences';
 import CommentReplyComponent from '../CommentReply';
-import type { TranslatableStrings } from '../../main';
 import { CommentHeader } from '../CommentHeader';
 import TextArea from '../TextArea';
 
@@ -79,12 +78,11 @@ export interface CommentProps {
   isVisible: boolean;
   layout: LayoutController;
   user: Author | null;
-  strings: TranslatableStrings;
 }
 
 export default class CommentComponent extends React.Component<CommentProps> {
   renderReplies({ hideNewReply = false } = {}): React.ReactFragment {
-    const { comment, isFocused, store, user, strings } = this.props;
+    const { comment, isFocused, store, user } = this.props;
 
     if (!comment.remoteId) {
       // Hide replies UI if the comment itself isn't saved yet
@@ -146,7 +144,6 @@ export default class CommentComponent extends React.Component<CommentProps> {
             user={user}
             comment={comment}
             reply={reply}
-            strings={strings}
             isFocused={isFocused}
           />,
         );
@@ -172,14 +169,14 @@ export default class CommentComponent extends React.Component<CommentProps> {
               type="submit"
               className="comment__button comment__button--primary"
             >
-              {strings.REPLY}
+              {gettext('Reply')}
             </button>
             <button
               type="button"
               onClick={onClickCancelReply}
               className="comment__button"
             >
-              {strings.CANCEL}
+              {gettext('Cancel')}
             </button>
           </div>
         </form>
@@ -200,7 +197,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
   }
 
   renderCreating(): React.ReactFragment {
-    const { comment, store, strings, isFocused } = this.props;
+    const { comment, store, isFocused } = this.props;
 
     const onChangeText = (value: string) => {
       store.dispatch(
@@ -229,7 +226,6 @@ export default class CommentComponent extends React.Component<CommentProps> {
           descriptionId={descriptionId}
           commentReply={comment}
           store={store}
-          strings={strings}
           focused={isFocused}
         />
         <form onSubmit={onSave}>
@@ -249,14 +245,14 @@ export default class CommentComponent extends React.Component<CommentProps> {
               type="submit"
               className="comment__button comment__button--primary"
             >
-              {strings.COMMENT}
+              {gettext('Comment')}
             </button>
             <button
               type="button"
               onClick={onCancel}
               className="comment__button"
             >
-              {strings.CANCEL}
+              {gettext('Cancel')}
             </button>
           </div>
         </form>
@@ -265,7 +261,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
   }
 
   renderEditing(): React.ReactFragment {
-    const { comment, store, strings, isFocused } = this.props;
+    const { comment, store, isFocused } = this.props;
 
     const onChangeText = (value: string) => {
       store.dispatch(
@@ -300,7 +296,6 @@ export default class CommentComponent extends React.Component<CommentProps> {
           descriptionId={descriptionId}
           commentReply={comment}
           store={store}
-          strings={strings}
           focused={isFocused}
         />
         <form onSubmit={onSave}>
@@ -319,14 +314,14 @@ export default class CommentComponent extends React.Component<CommentProps> {
               type="submit"
               className="comment__button comment__button--primary"
             >
-              {strings.SAVE}
+              {gettext('Save')}
             </button>
             <button
               type="button"
               onClick={onCancel}
               className="comment__button"
             >
-              {strings.CANCEL}
+              {gettext('Cancel')}
             </button>
           </div>
         </form>
@@ -336,25 +331,24 @@ export default class CommentComponent extends React.Component<CommentProps> {
   }
 
   renderSaving(): React.ReactFragment {
-    const { comment, store, strings, isFocused } = this.props;
+    const { comment, store, isFocused } = this.props;
 
     return (
       <>
         <CommentHeader
           commentReply={comment}
           store={store}
-          strings={strings}
           focused={isFocused}
         />
         <p className="comment__text">{comment.text}</p>
-        <div className="comment__progress">{strings.SAVING}</div>
+        <div className="comment__progress">{gettext('Saving...')}</div>
         {this.renderReplies({ hideNewReply: true })}
       </>
     );
   }
 
   renderSaveError(): React.ReactFragment {
-    const { comment, store, strings, isFocused } = this.props;
+    const { comment, store, isFocused } = this.props;
 
     const onClickRetry = async (e: React.MouseEvent) => {
       e.preventDefault();
@@ -367,19 +361,18 @@ export default class CommentComponent extends React.Component<CommentProps> {
         <CommentHeader
           commentReply={comment}
           store={store}
-          strings={strings}
           focused={isFocused}
         />
         <p className="comment__text">{comment.text}</p>
         {this.renderReplies({ hideNewReply: true })}
         <div className="comment__error">
-          {strings.SAVE_ERROR}
+          {gettext('Save error')}
           <button
             type="button"
             className="comment__button"
             onClick={onClickRetry}
           >
-            {strings.RETRY}
+            {gettext('Retry')}
           </button>
         </div>
       </>
@@ -387,7 +380,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
   }
 
   renderDeleteConfirm(): React.ReactFragment {
-    const { comment, store, strings, isFocused } = this.props;
+    const { comment, store, isFocused } = this.props;
 
     const onClickDelete = async (e: React.MouseEvent) => {
       e.preventDefault();
@@ -410,25 +403,24 @@ export default class CommentComponent extends React.Component<CommentProps> {
         <CommentHeader
           commentReply={comment}
           store={store}
-          strings={strings}
           focused={isFocused}
         />
         <p className="comment__text">{comment.text}</p>
         <div className="comment__confirm-delete">
-          {strings.CONFIRM_DELETE_COMMENT}
+          {gettext('Are you sure?')}
           <button
             type="button"
             className="comment__button"
             onClick={onClickCancel}
           >
-            {strings.CANCEL}
+            {gettext('Cancel')}
           </button>
           <button
             type="button"
             className="comment__button comment__button--primary"
             onClick={onClickDelete}
           >
-            {strings.DELETE}
+            {gettext('Delete')}
           </button>
         </div>
         {this.renderReplies({ hideNewReply: true })}
@@ -437,25 +429,24 @@ export default class CommentComponent extends React.Component<CommentProps> {
   }
 
   renderDeleting(): React.ReactFragment {
-    const { comment, store, strings, isFocused } = this.props;
+    const { comment, store, isFocused } = this.props;
 
     return (
       <>
         <CommentHeader
           commentReply={comment}
           store={store}
-          strings={strings}
           focused={isFocused}
         />
         <p className="comment__text">{comment.text}</p>
-        <div className="comment__progress">{strings.DELETING}</div>
+        <div className="comment__progress">{gettext('Deleting')}</div>
         {this.renderReplies({ hideNewReply: true })}
       </>
     );
   }
 
   renderDeleteError(): React.ReactFragment {
-    const { comment, store, strings, isFocused } = this.props;
+    const { comment, store, isFocused } = this.props;
 
     const onClickRetry = async (e: React.MouseEvent) => {
       e.preventDefault();
@@ -478,26 +469,25 @@ export default class CommentComponent extends React.Component<CommentProps> {
         <CommentHeader
           commentReply={comment}
           store={store}
-          strings={strings}
           focused={isFocused}
         />
         <p className="comment__text">{comment.text}</p>
         {this.renderReplies({ hideNewReply: true })}
         <div className="comment__error">
-          {strings.DELETE_ERROR}
+          {gettext('Delete error')}
           <button
             type="button"
             className="comment__button"
             onClick={onClickCancel}
           >
-            {strings.CANCEL}
+            {gettext('Cancel')}
           </button>
           <button
             type="button"
             className="comment__button"
             onClick={onClickRetry}
           >
-            {strings.RETRY}
+            {gettext('Retry')}
           </button>
         </div>
       </>
@@ -505,7 +495,7 @@ export default class CommentComponent extends React.Component<CommentProps> {
   }
 
   renderDefault(): React.ReactFragment {
-    const { comment, store, strings, isFocused } = this.props;
+    const { comment, store, isFocused } = this.props;
 
     // Show edit/delete buttons if this comment was authored by the current user
     let onEdit;
@@ -535,10 +525,10 @@ export default class CommentComponent extends React.Component<CommentProps> {
     let notice = '';
     if (!comment.remoteId) {
       // Save the page to add this comment
-      notice = strings.SAVE_PAGE_TO_ADD_COMMENT;
+      notice = gettext('Save the page to add this comment');
     } else if (comment.text !== comment.originalText) {
       // Save the page to save this comment
-      notice = strings.SAVE_PAGE_TO_SAVE_COMMENT_CHANGES;
+      notice = gettext('Save the page to save this comment');
     }
 
     return (
@@ -546,7 +536,6 @@ export default class CommentComponent extends React.Component<CommentProps> {
         <CommentHeader
           commentReply={comment}
           store={store}
-          strings={strings}
           onResolve={doResolveComment}
           onEdit={onEdit}
           onDelete={onDelete}

+ 4 - 7
client/src/components/CommentApp/components/CommentHeader/index.tsx

@@ -1,7 +1,6 @@
 import React, { FunctionComponent, useState, useEffect, useRef } from 'react';
 import Icon from '../../../Icon/Icon';
 import type { Store } from '../../state';
-import { TranslatableStrings } from '../../main';
 
 import { Author } from '../../state/comments';
 
@@ -19,7 +18,6 @@ interface CommentReply {
 interface CommentHeaderProps {
   commentReply: CommentReply;
   store: Store;
-  strings: TranslatableStrings;
   onResolve?(commentReply: CommentReply, store: Store): void;
   onEdit?(commentReply: CommentReply, store: Store): void;
   onDelete?(commentReply: CommentReply, store: Store): void;
@@ -30,7 +28,6 @@ interface CommentHeaderProps {
 export const CommentHeader: FunctionComponent<CommentHeaderProps> = ({
   commentReply,
   store,
-  strings,
   onResolve,
   onEdit,
   onDelete,
@@ -113,7 +110,7 @@ export const CommentHeader: FunctionComponent<CommentHeaderProps> = ({
           >
             <details open={menuOpen} onClick={toggleMenu}>
               <summary
-                aria-label={strings.MORE_ACTIONS}
+                aria-label={gettext('More actions')}
                 aria-haspopup="menu"
                 role="button"
                 onClick={toggleMenu}
@@ -129,12 +126,12 @@ export const CommentHeader: FunctionComponent<CommentHeaderProps> = ({
               >
                 {onEdit && (
                   <button type="button" role="menuitem" onClick={onClickEdit}>
-                    {strings.EDIT}
+                    {gettext('Edit')}
                   </button>
                 )}
                 {onDelete && (
                   <button type="button" role="menuitem" onClick={onClickDelete}>
-                    {strings.DELETE}
+                    {gettext('Delete')}
                   </button>
                 )}
                 {onResolve && (
@@ -143,7 +140,7 @@ export const CommentHeader: FunctionComponent<CommentHeaderProps> = ({
                     role="menuitem"
                     onClick={onClickResolve}
                   >
-                    {strings.RESOLVE}
+                    {gettext('Resolve')}
                   </button>
                 )}
               </div>

+ 26 - 59
client/src/components/CommentApp/components/CommentReply/index.tsx

@@ -3,7 +3,6 @@ import React from 'react';
 import type { Store } from '../../state';
 import type { Comment, CommentReply, Author } from '../../state/comments';
 import { updateReply, deleteReply } from '../../actions/comments';
-import type { TranslatableStrings } from '../../main';
 import { CommentHeader } from '../CommentHeader';
 import TextArea from '../TextArea';
 import Icon from '../../../Icon/Icon';
@@ -65,13 +64,12 @@ export interface CommentReplyProps {
   reply: CommentReply;
   store: Store;
   user: Author | null;
-  strings: TranslatableStrings;
   isFocused: boolean;
 }
 
 export default class CommentReplyComponent extends React.Component<CommentReplyProps> {
   renderEditing(): React.ReactFragment {
-    const { comment, reply, store, strings, isFocused } = this.props;
+    const { comment, reply, store, isFocused } = this.props;
 
     const onChangeText = (value: string) => {
       store.dispatch(
@@ -99,12 +97,7 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
 
     return (
       <>
-        <CommentHeader
-          commentReply={reply}
-          store={store}
-          strings={strings}
-          focused={isFocused}
-        />
+        <CommentHeader commentReply={reply} store={store} focused={isFocused} />
         <form onSubmit={onSave}>
           <TextArea
             className="comment-reply__input"
@@ -117,14 +110,14 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
               disabled={reply.newText.length === 0}
               className="comment-reply__button comment-reply__button--primary"
             >
-              {strings.SAVE}
+              {gettext('Save')}
             </button>
             <button
               type="button"
               className="comment-reply__button"
               onClick={onCancel}
             >
-              {strings.CANCEL}
+              {gettext('Cancel')}
             </button>
           </div>
         </form>
@@ -133,24 +126,19 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
   }
 
   renderSaving(): React.ReactFragment {
-    const { reply, store, strings, isFocused } = this.props;
+    const { reply, store, isFocused } = this.props;
 
     return (
       <>
-        <CommentHeader
-          commentReply={reply}
-          store={store}
-          strings={strings}
-          focused={isFocused}
-        />
+        <CommentHeader commentReply={reply} store={store} focused={isFocused} />
         <p className="comment-reply__text">{reply.text}</p>
-        <div className="comment-reply__progress">{strings.SAVING}</div>
+        <div className="comment-reply__progress">{gettext('Saving...')}</div>
       </>
     );
   }
 
   renderSaveError(): React.ReactFragment {
-    const { comment, reply, store, strings, isFocused } = this.props;
+    const { comment, reply, store, isFocused } = this.props;
 
     const onClickRetry = async (e: React.MouseEvent) => {
       e.preventDefault();
@@ -160,21 +148,16 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
 
     return (
       <>
-        <CommentHeader
-          commentReply={reply}
-          store={store}
-          strings={strings}
-          focused={isFocused}
-        />
+        <CommentHeader commentReply={reply} store={store} focused={isFocused} />
         <p className="comment-reply__text">{reply.text}</p>
         <div className="comment-reply__error">
-          {strings.SAVE_ERROR}
+          {gettext('Save error')}
           <button
             type="button"
             className="comment-reply__button"
             onClick={onClickRetry}
           >
-            {strings.RETRY}
+            {gettext('Retry')}
           </button>
         </div>
       </>
@@ -182,7 +165,7 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
   }
 
   renderDeleteConfirm(): React.ReactFragment {
-    const { comment, reply, store, strings, isFocused } = this.props;
+    const { comment, reply, store, isFocused } = this.props;
 
     const onClickDelete = async (e: React.MouseEvent) => {
       e.preventDefault();
@@ -202,28 +185,23 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
 
     return (
       <>
-        <CommentHeader
-          commentReply={reply}
-          store={store}
-          strings={strings}
-          focused={isFocused}
-        />
+        <CommentHeader commentReply={reply} store={store} focused={isFocused} />
         <p className="comment-reply__text">{reply.text}</p>
         <div className="comment-reply__confirm-delete">
-          {strings.CONFIRM_DELETE_COMMENT}
+          {gettext('Are you sure?')}
           <button
             type="button"
             className="comment-reply__button"
             onClick={onClickCancel}
           >
-            {strings.CANCEL}
+            {gettext('Cancel')}
           </button>
           <button
             type="button"
             className="comment-reply__button comment-reply__button--primary"
             onClick={onClickDelete}
           >
-            {strings.DELETE}
+            {gettext('Delete')}
           </button>
         </div>
       </>
@@ -231,24 +209,19 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
   }
 
   renderDeleting(): React.ReactFragment {
-    const { reply, store, strings, isFocused } = this.props;
+    const { reply, store, isFocused } = this.props;
 
     return (
       <>
-        <CommentHeader
-          commentReply={reply}
-          store={store}
-          strings={strings}
-          focused={isFocused}
-        />
+        <CommentHeader commentReply={reply} store={store} focused={isFocused} />
         <p className="comment-reply__text">{reply.text}</p>
-        <div className="comment-reply__progress">{strings.DELETING}</div>
+        <div className="comment-reply__progress">{gettext('Deleting')}</div>
       </>
     );
   }
 
   renderDeleteError(): React.ReactFragment {
-    const { comment, reply, store, strings, isFocused } = this.props;
+    const { comment, reply, store, isFocused } = this.props;
 
     const onClickRetry = async (e: React.MouseEvent) => {
       e.preventDefault();
@@ -268,28 +241,23 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
 
     return (
       <>
-        <CommentHeader
-          commentReply={reply}
-          store={store}
-          strings={strings}
-          focused={isFocused}
-        />
+        <CommentHeader commentReply={reply} store={store} focused={isFocused} />
         <p className="comment-reply__text">{reply.text}</p>
         <div className="comment-reply__error">
-          {strings.DELETE_ERROR}
+          {gettext('Delete error')}
           <button
             type="button"
             className="comment-reply__button"
             onClick={onClickCancel}
           >
-            {strings.CANCEL}
+            {gettext('Cancel')}
           </button>
           <button
             type="button"
             className="comment-reply__button"
             onClick={onClickRetry}
           >
-            {strings.RETRY}
+            {gettext('Retry')}
           </button>
         </div>
       </>
@@ -297,7 +265,7 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
   }
 
   renderDefault(): React.ReactFragment {
-    const { comment, reply, store, strings, isFocused } = this.props;
+    const { comment, reply, store, isFocused } = this.props;
 
     // Show edit/delete buttons if this reply was authored by the current user
     let onEdit;
@@ -327,7 +295,7 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
     let notice = '';
     if (!reply.remoteId || reply.text !== reply.originalText) {
       // Save the page to save this reply
-      notice = strings.SAVE_PAGE_TO_SAVE_REPLY;
+      notice = gettext('Save the page to save this reply');
     }
 
     return (
@@ -335,7 +303,6 @@ export default class CommentReplyComponent extends React.Component<CommentReplyP
         <CommentHeader
           commentReply={reply}
           store={store}
-          strings={strings}
           onEdit={onEdit}
           onDelete={onDelete}
           focused={isFocused}

+ 2 - 49
client/src/components/CommentApp/main.tsx

@@ -30,48 +30,6 @@ import CommentComponent from './components/Comment';
 import { CommentFormSetComponent } from './components/Form';
 import { INITIAL_STATE as INITIAL_SETTINGS_STATE } from './state/settings';
 
-export interface TranslatableStrings {
-  COMMENT: string;
-  SAVE: string;
-  SAVING: string;
-  CANCEL: string;
-  DELETE: string;
-  DELETING: string;
-  SHOW_COMMENTS: string;
-  EDIT: string;
-  REPLY: string;
-  RESOLVE: string;
-  RETRY: string;
-  DELETE_ERROR: string;
-  CONFIRM_DELETE_COMMENT: string;
-  SAVE_ERROR: string;
-  MORE_ACTIONS: string;
-  SAVE_PAGE_TO_ADD_COMMENT: string;
-  SAVE_PAGE_TO_SAVE_COMMENT_CHANGES: string;
-  SAVE_PAGE_TO_SAVE_REPLY: string;
-}
-
-export const defaultStrings = {
-  COMMENT: 'Comment',
-  SAVE: 'Save',
-  SAVING: 'Saving...',
-  CANCEL: 'Cancel',
-  DELETE: 'Delete',
-  DELETING: 'Deleting...',
-  SHOW_COMMENTS: 'Show comments',
-  EDIT: 'Edit',
-  REPLY: 'Reply',
-  RESOLVE: 'Resolve',
-  RETRY: 'Retry',
-  DELETE_ERROR: 'Delete error',
-  CONFIRM_DELETE_COMMENT: 'Are you sure?',
-  SAVE_ERROR: 'Save error',
-  MORE_ACTIONS: 'More actions',
-  SAVE_PAGE_TO_ADD_COMMENT: 'Save the page to add this comment',
-  SAVE_PAGE_TO_SAVE_COMMENT_CHANGES: 'Save the page to save this comment',
-  SAVE_PAGE_TO_SAVE_REPLY: 'Save the page to save this reply',
-};
-
 // This is done as this is serialized pretty directly from the Django model
 export interface InitialCommentReply {
   pk: number;
@@ -116,7 +74,6 @@ function renderCommentsUi(
   store: Store,
   layout: LayoutController,
   comments: Comment[],
-  strings: TranslatableStrings,
 ): React.ReactElement {
   const state = store.getState();
   const { commentsEnabled, user, currentTab } = state.settings;
@@ -140,7 +97,6 @@ function renderCommentsUi(
       isFocused={comment.localId === focusedComment}
       forceFocus={forceFocus}
       isVisible={layout.getCommentVisible(currentTab, comment.localId)}
-      strings={strings}
     />
   ));
   return <ol className="comments-list">{commentsRendered}</ol>;
@@ -242,13 +198,10 @@ export class CommentApp {
     initialComments: InitialComment[],
 
     authors: Map<string, { name: string; avatar_url: string }>,
-    translationStrings: TranslatableStrings | null,
   ) {
     let pinnedComment: number | null = null;
     this.setUser(userId, authors);
 
-    const strings = translationStrings || defaultStrings;
-
     // Check if there is "comment" query parameter.
     // If this is set, the user has clicked on a "View on frontend" link of an
     // individual comment. We should focus this comment and scroll to it
@@ -285,7 +238,7 @@ export class CommentApp {
       }
 
       ReactDOM.render(
-        renderCommentsUi(this.store, this.layout, commentList, strings),
+        renderCommentsUi(this.store, this.layout, commentList),
         element,
         () => {
           // Render again if layout has changed (eg, a comment was added, deleted or resized)
@@ -293,7 +246,7 @@ export class CommentApp {
           this.layout.refreshDesiredPositions(state.settings.currentTab);
           if (this.layout.refreshLayout()) {
             ReactDOM.render(
-              renderCommentsUi(this.store, this.layout, commentList, strings),
+              renderCommentsUi(this.store, this.layout, commentList),
               element,
             );
           }

+ 2 - 3
client/src/components/Draftail/CommentableEditor/CommentableEditor.tsx

@@ -32,7 +32,6 @@ import React, {
 } from 'react';
 import { useSelector, shallowEqual } from 'react-redux';
 
-import { STRINGS } from '../../../config/wagtailConfig';
 import Icon from '../../Icon/Icon';
 
 const { isOptionKeyCommand } = KeyBindingUtil;
@@ -219,7 +218,7 @@ function getCommentControl(
       <ToolbarButton
         name="comment"
         active={false}
-        title={`${STRINGS.ADD_A_COMMENT}\n${
+        title={`${gettext('Add a comment')}\n${
           IS_MAC_OS ? '⌘ + Alt + M' : 'Ctrl + Alt + M'
         }`}
         icon={
@@ -452,7 +451,7 @@ function getCommentDecorator(commentApp: CommentApp) {
         role="button"
         ref={annotationNode}
         onClick={onClick}
-        aria-label={STRINGS.FOCUS_COMMENT}
+        aria-label={gettext('Focus comment')}
         data-annotation
       >
         {children}

+ 7 - 7
client/src/components/Draftail/EditorFallback/EditorFallback.js

@@ -2,8 +2,6 @@ import PropTypes from 'prop-types';
 import React, { PureComponent } from 'react';
 import { convertFromRaw } from 'draft-js';
 
-import { STRINGS } from '../../../config/wagtailConfig';
-
 const MAX_EDITOR_RELOADS = 3;
 
 class EditorFallback extends PureComponent {
@@ -74,7 +72,7 @@ class EditorFallback extends PureComponent {
               className="Draftail-ToolbarButton"
               onClick={this.toggleContent}
             >
-              {STRINGS.SHOW_LATEST_CONTENT}
+              {gettext('Show latest content')}
             </button>
           )}
 
@@ -83,7 +81,7 @@ class EditorFallback extends PureComponent {
             className="Draftail-ToolbarButton"
             onClick={this.toggleError}
           >
-            {STRINGS.SHOW_ERROR}
+            {gettext('Show error')}
           </button>
 
           {/* At first we propose reloading the editor. If it still crashes, reload the whole page. */}
@@ -93,7 +91,7 @@ class EditorFallback extends PureComponent {
               className="Draftail-ToolbarButton"
               onClick={this.onReloadEditor}
             >
-              {STRINGS.RELOAD_EDITOR}
+              {gettext('Reload saved content')}
             </button>
           ) : (
             <button
@@ -101,7 +99,7 @@ class EditorFallback extends PureComponent {
               className="Draftail-ToolbarButton"
               onClick={() => window.location.reload(false)}
             >
-              {STRINGS.RELOAD_PAGE}
+              {gettext('Reload the page')}
             </button>
           )}
         </div>
@@ -109,7 +107,9 @@ class EditorFallback extends PureComponent {
         <div className="DraftEditor-root">
           <div className="public-DraftEditor-content">
             <div className="public-DraftEditorPlaceholder-inner">
-              {STRINGS.EDITOR_CRASH}
+              {gettext(
+                'The editor just crashed. Content has been reset to the last saved version.',
+              )}
 
               {showContent && (
                 <textarea

+ 2 - 4
client/src/components/Draftail/blocks/EmbedBlock.js

@@ -1,8 +1,6 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 
-import { STRINGS } from '../../../config/wagtailConfig';
-
 import MediaBlock from '../blocks/MediaBlock';
 
 /**
@@ -30,13 +28,13 @@ const EmbedBlock = (props) => {
         type="button"
         onClick={onEditEntity}
       >
-        {STRINGS.EDIT}
+        {gettext('Edit')}
       </button>
       <button
         className="button button-secondary no Tooltip__button"
         onClick={onRemoveEntity}
       >
-        {STRINGS.DELETE}
+        {gettext('Delete')}
       </button>
     </MediaBlock>
   );

+ 4 - 6
client/src/components/Draftail/blocks/ImageBlock.js

@@ -1,8 +1,6 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 
-import { STRINGS } from '../../../config/wagtailConfig';
-
 import MediaBlock from '../blocks/MediaBlock';
 
 /**
@@ -12,9 +10,9 @@ const ImageBlock = (props) => {
   const { blockProps } = props;
   const { entity, onEditEntity, onRemoveEntity } = blockProps;
   const { src, alt } = entity.getData();
-  let altLabel = STRINGS.DECORATIVE_IMAGE;
+  let altLabel = gettext('Decorative image');
   if (alt) {
-    altLabel = `${STRINGS.ALT_TEXT}: “${alt}”`;
+    altLabel = `${gettext('Alt text')}: “${alt}”`;
   }
 
   return (
@@ -26,13 +24,13 @@ const ImageBlock = (props) => {
         type="button"
         onClick={onEditEntity}
       >
-        {STRINGS.EDIT}
+        {gettext('Edit')}
       </button>
       <button
         className="button button-secondary no Tooltip__button"
         onClick={onRemoveEntity}
       >
-        {STRINGS.DELETE}
+        {gettext('Delete')}
       </button>
     </MediaBlock>
   );

+ 1 - 3
client/src/components/Draftail/decorators/Document.js

@@ -5,8 +5,6 @@ import Icon from '../../Icon/Icon';
 
 import TooltipEntity from '../decorators/TooltipEntity';
 
-import { STRINGS } from '../../../config/wagtailConfig';
-
 const documentIcon = <Icon name="doc-full" />;
 const missingDocumentIcon = <Icon name="warning" />;
 
@@ -19,7 +17,7 @@ const Document = (props) => {
 
   if (!url) {
     icon = missingDocumentIcon;
-    label = STRINGS.MISSING_DOCUMENT;
+    label = gettext('Missing document');
   } else {
     icon = documentIcon;
     label = data.filename || '';

+ 1 - 3
client/src/components/Draftail/decorators/Link.js

@@ -5,8 +5,6 @@ import Icon from '../../Icon/Icon';
 
 import TooltipEntity from '../decorators/TooltipEntity';
 
-import { STRINGS } from '../../../config/wagtailConfig';
-
 const LINK_ICON = <Icon name="link" />;
 const BROKEN_LINK_ICON = <Icon name="warning" />;
 const MAIL_ICON = <Icon name="mail" />;
@@ -23,7 +21,7 @@ export const getLinkAttributes = (data) => {
 
   if (!url) {
     icon = BROKEN_LINK_ICON;
-    label = STRINGS.BROKEN_LINK;
+    label = gettext('Broken link');
   } else if (data.id) {
     icon = LINK_ICON;
     label = url;

+ 5 - 7
client/src/components/Draftail/index.js

@@ -3,8 +3,6 @@ import ReactDOM from 'react-dom';
 import { DraftailEditor } from 'draftail';
 import { Provider } from 'react-redux';
 
-import { STRINGS } from '../../config/wagtailConfig';
-
 import Icon from '../Icon/Icon';
 
 export { default as Link } from './decorators/Link';
@@ -91,7 +89,7 @@ const initEditor = (selector, options, currentScript) => {
 
   const enableHorizontalRule = options.enableHorizontalRule
     ? {
-        description: STRINGS.HORIZONTAL_LINE,
+        description: gettext('Horizontal line'),
       }
     : false;
 
@@ -106,14 +104,14 @@ const initEditor = (selector, options, currentScript) => {
   const sharedProps = {
     rawContentState: rawContentState,
     onSave: serialiseInputValue,
-    placeholder: STRINGS.WRITE_HERE,
+    placeholder: gettext('Write here…'),
     spellCheck: true,
     enableLineBreak: {
-      description: STRINGS.LINE_BREAK,
+      description: gettext('Line break'),
       icon: BR_ICON,
     },
-    showUndoControl: { description: STRINGS.UNDO },
-    showRedoControl: { description: STRINGS.REDO },
+    showUndoControl: { description: gettext('Undo') },
+    showRedoControl: { description: gettext('Redo') },
     maxListNesting: 4,
     stripPastedStyles: false,
     ...options,

+ 1 - 2
client/src/components/Draftail/sources/ModalWorkflowSource.js

@@ -3,7 +3,6 @@ import { Component } from 'react';
 import { AtomicBlockUtils, Modifier, RichUtils, EditorState } from 'draft-js';
 import { ENTITY_TYPE, DraftUtils } from 'draftail';
 
-import { STRINGS } from '../../../config/wagtailConfig';
 import { getSelectionText } from '../DraftUtils';
 
 const $ = global.jQuery;
@@ -60,7 +59,7 @@ class ModalWorkflowSource extends Component {
       responses,
       onError: () => {
         // eslint-disable-next-line no-alert
-        window.alert(STRINGS.SERVER_ERROR);
+        window.alert(gettext('Server Error'));
         onClose();
       },
     });

+ 2 - 2
client/src/components/Explorer/ExplorerHeader.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { ADMIN_URLS, STRINGS } from '../../config/wagtailConfig';
+import { ADMIN_URLS } from '../../config/wagtailConfig';
 
 import Button from '../../components/Button/Button';
 import Icon from '../../components/Icon/Icon';
@@ -76,7 +76,7 @@ const ExplorerHeader: React.FunctionComponent<ExplorerHeaderProps> = ({
             name={isRoot ? 'home' : 'arrow-left'}
             className="icon--explorer-header"
           />
-          <span>{page.admin_display_title || STRINGS.PAGES}</span>
+          <span>{page.admin_display_title || gettext('Pages')}</span>
         </div>
       </Button>
       {!isSiteRoot &&

+ 6 - 3
client/src/components/Explorer/ExplorerItem.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { ADMIN_URLS, STRINGS, LOCALE_NAMES } from '../../config/wagtailConfig';
+import { ADMIN_URLS, LOCALE_NAMES } from '../../config/wagtailConfig';
 import Icon from '../../components/Icon/Icon';
 import Button from '../../components/Button/Button';
 import PublicationStatus from '../../components/PublicationStatus/PublicationStatus';
@@ -55,7 +55,7 @@ const ExplorerItem: React.FunctionComponent<ExplorerItemProps> = ({
       >
         <Icon
           name="edit"
-          title={STRINGS.EDIT_PAGE.replace('{title}', title)}
+          title={gettext("Edit '{title}'").replace('{title}', title || '')}
           className="icon--item-action"
         />
       </Button>
@@ -67,7 +67,10 @@ const ExplorerItem: React.FunctionComponent<ExplorerItemProps> = ({
         >
           <Icon
             name="arrow-right"
-            title={STRINGS.VIEW_CHILD_PAGES_OF_PAGE.replace('{title}', title)}
+            title={gettext("View child pages of '{title}'").replace(
+              '{title}',
+              title || '',
+            )}
             className="icon--item-action"
           />
         </Button>

+ 5 - 5
client/src/components/Explorer/ExplorerPanel.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import FocusTrap from 'focus-trap-react';
 
-import { STRINGS, MAX_EXPLORER_PAGES } from '../../config/wagtailConfig';
+import { MAX_EXPLORER_PAGES } from '../../config/wagtailConfig';
 
 import Button from '../Button/Button';
 import LoadingSpinner from '../LoadingSpinner/LoadingSpinner';
@@ -122,7 +122,7 @@ class ExplorerPanel extends React.Component<
     if (!page.isFetchingChildren && !page.children.items) {
       children = (
         <div key="empty" className="c-explorer__placeholder">
-          {STRINGS.NO_RESULTS}
+          {gettext('No results')}
         </div>
       );
     } else {
@@ -149,7 +149,7 @@ class ExplorerPanel extends React.Component<
         ) : null}
         {page.isError ? (
           <div key="error" className="c-explorer__placeholder">
-            {STRINGS.SERVER_ERROR}
+            {gettext('Server Error')}
           </div>
         ) : null}
       </div>
@@ -175,13 +175,13 @@ class ExplorerPanel extends React.Component<
       >
         <div role="dialog" className="explorer">
           <Button className="c-explorer__close">
-            {STRINGS.CLOSE_EXPLORER}
+            {gettext('Close explorer')}
           </Button>
           <Transition
             name={transition}
             className="c-explorer"
             component="nav"
-            label={STRINGS.PAGE_EXPLORER}
+            label={gettext('Page explorer')}
           >
             <div key={depth} className="c-transition-group">
               <ExplorerHeader

+ 5 - 3
client/src/components/Explorer/PageCount.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { ADMIN_URLS, STRINGS } from '../../config/wagtailConfig';
+import { ADMIN_URLS } from '../../config/wagtailConfig';
 import Icon from '../Icon/Icon';
 
 interface PageCountProps {
@@ -17,9 +17,11 @@ const PageCount: React.FunctionComponent<PageCountProps> = ({ page }) => {
 
   return (
     <a href={`${ADMIN_URLS.PAGES}${page.id}/`} className="c-explorer__see-more">
-      {STRINGS.SEE_ALL}
+      {gettext('See all')}
       <span>{` ${count} ${
-        count === 1 ? STRINGS.PAGE.toLowerCase() : STRINGS.PAGES.toLowerCase()
+        count === 1
+          ? gettext('Page').toLowerCase()
+          : gettext('Pages').toLowerCase()
       }`}</span>
       <Icon name="arrow-right" />
     </a>

+ 1 - 2
client/src/components/LoadingSpinner/LoadingSpinner.js

@@ -1,5 +1,4 @@
 import React from 'react';
-import { STRINGS } from '../../config/wagtailConfig';
 import Icon from '../../components/Icon/Icon';
 
 /**
@@ -8,7 +7,7 @@ import Icon from '../../components/Icon/Icon';
 const LoadingSpinner = () => (
   <span>
     <Icon name="spinner" className="c-spinner" />
-    {` ${STRINGS.LOADING}`}
+    {` ${gettext('Loading…')}`}
   </span>
 );
 

+ 5 - 3
client/src/components/PageExplorer/PageCount.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { ADMIN_URLS, STRINGS } from '../../config/wagtailConfig';
+import { ADMIN_URLS } from '../../config/wagtailConfig';
 import Icon from '../Icon/Icon';
 
 interface PageCountProps {
@@ -20,9 +20,11 @@ const PageCount: React.FunctionComponent<PageCountProps> = ({ page }) => {
       href={`${ADMIN_URLS.PAGES}${page.id}/`}
       className="c-page-explorer__see-more"
     >
-      {STRINGS.SEE_ALL}
+      {gettext('See all')}
       <span>{` ${count} ${
-        count === 1 ? STRINGS.PAGE.toLowerCase() : STRINGS.PAGES.toLowerCase()
+        count === 1
+          ? gettext('Page').toLowerCase()
+          : gettext('Pages').toLowerCase()
       }`}</span>
       <Icon name="arrow-right" />
     </a>

+ 2 - 2
client/src/components/PageExplorer/PageExplorerHeader.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { ADMIN_URLS, STRINGS } from '../../config/wagtailConfig';
+import { ADMIN_URLS } from '../../config/wagtailConfig';
 
 import Button from '../../components/Button/Button';
 import Icon from '../../components/Icon/Icon';
@@ -82,7 +82,7 @@ const PageExplorerHeader: React.FunctionComponent<PageExplorerHeaderProps> = ({
             name={isRoot ? 'home' : 'arrow-left'}
             className="icon--explorer-header"
           />
-          <span>{page.admin_display_title || STRINGS.PAGES}</span>
+          <span>{page.admin_display_title || gettext('Pages')}</span>
         </div>
       </Button>
       {!isSiteRoot &&

+ 6 - 3
client/src/components/PageExplorer/PageExplorerItem.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { ADMIN_URLS, STRINGS, LOCALE_NAMES } from '../../config/wagtailConfig';
+import { ADMIN_URLS, LOCALE_NAMES } from '../../config/wagtailConfig';
 import Icon from '../../components/Icon/Icon';
 import Button from '../../components/Button/Button';
 import PublicationStatus from '../../components/PublicationStatus/PublicationStatus';
@@ -58,7 +58,7 @@ const PageExplorerItem: React.FunctionComponent<PageExplorerItemProps> = ({
       >
         <Icon
           name="edit"
-          title={STRINGS.EDIT_PAGE.replace('{title}', title)}
+          title={gettext("Edit '{title}'").replace('{title}', title || '')}
           className="icon--item-action"
         />
       </Button>
@@ -71,7 +71,10 @@ const PageExplorerItem: React.FunctionComponent<PageExplorerItemProps> = ({
         >
           <Icon
             name="arrow-right"
-            title={STRINGS.VIEW_CHILD_PAGES_OF_PAGE.replace('{title}', title)}
+            title={gettext("View child pages of '{title}'").replace(
+              '{title}',
+              title || '',
+            )}
             className="icon--item-action"
           />
         </Button>

+ 4 - 4
client/src/components/PageExplorer/PageExplorerPanel.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { STRINGS, MAX_EXPLORER_PAGES } from '../../config/wagtailConfig';
+import { MAX_EXPLORER_PAGES } from '../../config/wagtailConfig';
 
 import LoadingSpinner from '../LoadingSpinner/LoadingSpinner';
 import Transition, { PUSH, POP } from '../Transition/Transition';
@@ -79,7 +79,7 @@ class PageExplorerPanel extends React.Component<
     if (!page.isFetchingChildren && !page.children.items) {
       children = (
         <div key="empty" className="c-page-explorer__placeholder">
-          {STRINGS.NO_RESULTS}
+          {gettext('No results')}
         </div>
       );
     } else {
@@ -107,7 +107,7 @@ class PageExplorerPanel extends React.Component<
         ) : null}
         {page.isError ? (
           <div key="error" className="c-page-explorer__placeholder">
-            {STRINGS.SERVER_ERROR}
+            {gettext('Server Error')}
           </div>
         ) : null}
       </div>
@@ -123,7 +123,7 @@ class PageExplorerPanel extends React.Component<
         name={transition}
         className="c-page-explorer"
         component="nav"
-        label={STRINGS.PAGE_EXPLORER}
+        label={gettext('Page explorer')}
       >
         <div key={depth} className="c-transition-group">
           <PageExplorerHeader

+ 2 - 20
client/src/components/Sidebar/Sidebar.stories.tsx

@@ -1,6 +1,6 @@
 import * as React from 'react';
 
-import { ModuleDefinition, Sidebar, Strings } from './Sidebar';
+import { ModuleDefinition, Sidebar } from './Sidebar';
 import { SearchModuleDefinition } from './modules/Search';
 import { MainMenuModuleDefinition } from './modules/MainMenu';
 import { PageExplorerMenuItemDefinition } from './menu/PageExplorerMenuItem';
@@ -12,12 +12,6 @@ export default {
   parameters: { layout: 'fullscreen' },
 };
 
-const STRINGS: Strings = {
-  DASHBOARD: 'Dashboard',
-  EDIT_YOUR_ACCOUNT: 'Edit your account',
-  SEARCH: 'Search',
-};
-
 function searchModule(): SearchModuleDefinition {
   return new SearchModuleDefinition('/admin/search/');
 }
@@ -188,12 +182,11 @@ function bogStandardMenuModule(): MainMenuModuleDefinition {
 
 interface RenderSidebarStoryOptions {
   rtl?: boolean;
-  strings?: Strings;
 }
 
 function renderSidebarStory(
   modules: ModuleDefinition[],
-  { rtl = false, strings = null }: RenderSidebarStoryOptions = {},
+  { rtl = false }: RenderSidebarStoryOptions = {},
 ) {
   // Simulate navigation
   const [currentPath, setCurrentPath] = React.useState('/admin/');
@@ -228,7 +221,6 @@ function renderSidebarStory(
         collapsedOnLoad={false}
         modules={modules}
         currentPath={currentPath}
-        strings={strings || STRINGS}
         navigate={navigate}
         onExpandCollapse={onExpandCollapse}
       />
@@ -358,15 +350,6 @@ export function withoutSearch() {
   return renderSidebarStory([wagtailBrandingModule(), bogStandardMenuModule()]);
 }
 
-// Translations taken from actual translation files at the time the code was written
-// There were a few strings missing in reports/workflows. I left these as English as
-// it's likely there will be a few untranslated strings on an Arabic site anyway.
-const STRINGS_AR: Strings = {
-  DASHBOARD: 'لوحة التحكم',
-  EDIT_YOUR_ACCOUNT: 'تعديل حسابك',
-  SEARCH: 'بحث',
-};
-
 function arabicMenuModule(): MainMenuModuleDefinition {
   return new MainMenuModuleDefinition(
     [
@@ -531,6 +514,5 @@ function arabicMenuModule(): MainMenuModuleDefinition {
 export function rightToLeft() {
   return renderSidebarStory([searchModule(), arabicMenuModule()], {
     rtl: true,
-    strings: STRINGS_AR,
   });
 }

+ 2 - 14
client/src/components/Sidebar/Sidebar.tsx

@@ -5,14 +5,6 @@ import Icon from '../Icon/Icon';
 // Please keep in sync with $menu-transition-duration variable in `client/scss/settings/_variables.scss`
 export const SIDEBAR_TRANSITION_DURATION = 150;
 
-export interface Strings {
-  DASHBOARD: string;
-  EDIT_YOUR_ACCOUNT: string;
-  SEARCH: string;
-  TOGGLE_SIDEBAR: string;
-  MAIN_MENU: string;
-}
-
 export interface ModuleRenderContext {
   key: number;
   slim: boolean;
@@ -20,7 +12,6 @@ export interface ModuleRenderContext {
   onAccountExpand: () => void;
   onSearchClick: () => void;
   currentPath: string;
-  strings: Strings;
   navigate(url: string): Promise<void>;
 }
 
@@ -31,7 +22,6 @@ export interface ModuleDefinition {
 export interface SidebarProps {
   modules: ModuleDefinition[];
   currentPath: string;
-  strings: Strings;
   collapsedOnLoad: boolean;
   navigate(url: string): Promise<void>;
   onExpandCollapse?(collapsed: boolean);
@@ -41,7 +31,6 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
   modules,
   currentPath,
   collapsedOnLoad = false,
-  strings,
   navigate,
   onExpandCollapse,
 }) => {
@@ -158,7 +147,6 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
       onAccountExpand,
       onSearchClick,
       currentPath,
-      strings,
       navigate,
     }),
   );
@@ -185,7 +173,7 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
           >
             <button
               onClick={onClickCollapseToggle}
-              aria-label={strings.TOGGLE_SIDEBAR}
+              aria-label={gettext('Toggle sidebar')}
               aria-expanded={slim ? 'false' : 'true'}
               type="button"
               className={`
@@ -214,7 +202,7 @@ export const Sidebar: React.FunctionComponent<SidebarProps> = ({
       </div>
       <button
         onClick={onClickOpenCloseToggle}
-        aria-label={strings.TOGGLE_SIDEBAR}
+        aria-label={gettext('Toggle sidebar')}
         aria-expanded={visibleOnMobile ? 'true' : 'false'}
         className={
           'button sidebar-nav-toggle' +

+ 0 - 1
client/src/components/Sidebar/index.tsx

@@ -44,7 +44,6 @@ export function initSidebar() {
     ReactDOM.render(
       <Sidebar
         modules={props.modules}
-        strings={wagtailConfig.STRINGS}
         collapsedOnLoad={collapsed}
         currentPath={window.location.pathname}
         navigate={navigate}

+ 5 - 10
client/src/components/Sidebar/modules/MainMenu.tsx

@@ -4,7 +4,7 @@ import Icon from '../../Icon/Icon';
 import { LinkMenuItemDefinition } from '../menu/LinkMenuItem';
 import { MenuItemDefinition } from '../menu/MenuItem';
 import { SubMenuItemDefinition } from '../menu/SubMenuItem';
-import { ModuleDefinition, Strings } from '../Sidebar';
+import { ModuleDefinition } from '../Sidebar';
 import Tippy from '@tippyjs/react';
 
 export function renderMenu(
@@ -67,8 +67,6 @@ interface MenuProps {
   expandingOrCollapsing: boolean;
   onAccountExpand: () => void;
   currentPath: string;
-  strings: Strings;
-
   navigate(url: string): Promise<void>;
 }
 
@@ -80,7 +78,6 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
   onAccountExpand,
   slim,
   currentPath,
-  strings,
   navigate,
 }) => {
   // navigationPath and activePath are two dot-delimited path's referencing a menu item
@@ -208,7 +205,7 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
 
   return (
     <>
-      <nav className={className} aria-label={strings.MAIN_MENU}>
+      <nav className={className} aria-label={gettext('Main menu')}>
         <ul className="sidebar-main-menu__list">
           {renderMenu('', menuItems, slim, state, dispatch, navigate)}
         </ul>
@@ -222,7 +219,7 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
       >
         <Tippy
           disabled={!slim}
-          content={strings.EDIT_YOUR_ACCOUNT}
+          content={gettext('Edit your account')}
           placement="right"
         >
           <button
@@ -242,9 +239,9 @@ export const Menu: React.FunctionComponent<MenuProps> = ({
             hover:w-bg-primary-200
             focus:w-bg-primary-200
             w-transition"
-            title={strings.EDIT_YOUR_ACCOUNT}
+            title={gettext('Edit your account')}
             onClick={onClickAccountSettings}
-            aria-label={strings.EDIT_YOUR_ACCOUNT}
+            aria-label={gettext('Edit your account')}
             aria-haspopup="menu"
             aria-expanded={accountSettingsOpen ? 'true' : 'false'}
             type="button"
@@ -296,7 +293,6 @@ export class MainMenuModuleDefinition implements ModuleDefinition {
     onAccountExpand,
     key,
     currentPath,
-    strings,
     navigate,
   }) {
     return (
@@ -309,7 +305,6 @@ export class MainMenuModuleDefinition implements ModuleDefinition {
         onAccountExpand={onAccountExpand}
         key={key}
         currentPath={currentPath}
-        strings={strings}
         navigate={navigate}
       />
     );

+ 6 - 21
client/src/components/Sidebar/modules/Search.tsx

@@ -1,11 +1,7 @@
 import * as React from 'react';
 
 import Icon from '../../Icon/Icon';
-import {
-  ModuleDefinition,
-  Strings,
-  SIDEBAR_TRANSITION_DURATION,
-} from '../Sidebar';
+import { ModuleDefinition, SIDEBAR_TRANSITION_DURATION } from '../Sidebar';
 
 import Tippy from '@tippyjs/react';
 
@@ -14,8 +10,6 @@ interface SearchInputProps {
   expandingOrCollapsing: boolean;
   onSearchClick: () => void;
   searchUrl: string;
-  strings: Strings;
-
   navigate(url: string): void;
 }
 
@@ -24,7 +18,6 @@ export const SearchInput: React.FunctionComponent<SearchInputProps> = ({
   expandingOrCollapsing,
   onSearchClick,
   searchUrl,
-  strings,
   navigate,
 }) => {
   const isVisible = !slim || expandingOrCollapsing;
@@ -56,7 +49,7 @@ export const SearchInput: React.FunctionComponent<SearchInputProps> = ({
       <div className="w-flex w-flex-row w-items-center w-h-full">
         <Tippy
           disabled={isVisible || !slim}
-          content={strings.SEARCH}
+          content={gettext('Search')}
           placement="right"
         >
           {/* Use padding left 23px to align icon in slim mode and padding right 18px to ensure focus is full width */}
@@ -76,7 +69,7 @@ export const SearchInput: React.FunctionComponent<SearchInputProps> = ({
           focus:w-text-white
           hover:w-bg-transparent`}
             type="submit"
-            aria-label={strings.SEARCH}
+            aria-label={gettext('Search')}
             onClick={(e) => {
               if (slim) {
                 e.preventDefault();
@@ -96,7 +89,7 @@ export const SearchInput: React.FunctionComponent<SearchInputProps> = ({
         </Tippy>
 
         <label className="w-sr-only" htmlFor="menu-search-q">
-          {strings.SEARCH}
+          {gettext('Search')}
         </label>
 
         {/* Classes marked important to trump the base input styling set in _forms.scss */}
@@ -119,7 +112,7 @@ export const SearchInput: React.FunctionComponent<SearchInputProps> = ({
           type="text"
           id="menu-search-q"
           name="q"
-          placeholder={strings.SEARCH}
+          placeholder={gettext('Search')}
           ref={searchInput}
         />
       </div>
@@ -134,14 +127,7 @@ export class SearchModuleDefinition implements ModuleDefinition {
     this.searchUrl = searchUrl;
   }
 
-  render({
-    slim,
-    key,
-    expandingOrCollapsing,
-    onSearchClick,
-    strings,
-    navigate,
-  }) {
+  render({ slim, key, expandingOrCollapsing, onSearchClick, navigate }) {
     return (
       <SearchInput
         searchUrl={this.searchUrl}
@@ -149,7 +135,6 @@ export class SearchModuleDefinition implements ModuleDefinition {
         key={key}
         expandingOrCollapsing={expandingOrCollapsing}
         onSearchClick={onSearchClick}
-        strings={strings}
         navigate={navigate}
       />
     );

+ 3 - 6
client/src/components/Sidebar/modules/WagtailBranding.tsx

@@ -1,10 +1,9 @@
 import * as React from 'react';
-import { ModuleDefinition, Strings } from '../Sidebar';
+import { ModuleDefinition } from '../Sidebar';
 import WagtailLogo from './WagtailLogo';
 
 interface WagtailBrandingProps {
   homeUrl: string;
-  strings: Strings;
   slim: boolean;
   currentPath: string;
   navigate(url: string): void;
@@ -12,7 +11,6 @@ interface WagtailBrandingProps {
 
 const WagtailBranding: React.FunctionComponent<WagtailBrandingProps> = ({
   homeUrl,
-  strings,
   slim,
   currentPath,
   navigate,
@@ -43,7 +41,7 @@ const WagtailBranding: React.FunctionComponent<WagtailBrandingProps> = ({
       <a
         className="sidebar-custom-branding"
         href={homeUrl}
-        aria-label={strings.DASHBOARD}
+        aria-label={gettext('Dashboard')}
         aria-current={currentPath === homeUrl ? 'page' : undefined}
         dangerouslySetInnerHTML={{
           __html: brandingLogo ? brandingLogo.innerHTML : '',
@@ -88,7 +86,7 @@ const WagtailBranding: React.FunctionComponent<WagtailBrandingProps> = ({
     <a
       className={desktopClassName}
       href={homeUrl}
-      aria-label={strings.DASHBOARD}
+      aria-label={gettext('Dashboard')}
       aria-current={currentPath === homeUrl ? 'page' : undefined}
       onClick={onClick}
       onMouseMove={onMouseMove}
@@ -114,7 +112,6 @@ export class WagtailBrandingModuleDefinition implements ModuleDefinition {
         key={key}
         homeUrl={this.homeUrl}
         slim={slim}
-        strings={strings}
         navigate={navigate}
         currentPath={currentPath}
       />

+ 0 - 1
client/src/config/wagtailConfig.js

@@ -1,5 +1,4 @@
 export const ADMIN_API = global.wagtailConfig.ADMIN_API;
-export const STRINGS = global.wagtailConfig.STRINGS;
 export const ADMIN_URLS = global.wagtailConfig.ADMIN_URLS;
 
 // Maximum number of pages to load inside the explorer menu.

+ 1 - 12
client/src/config/wagtailConfig.test.js

@@ -1,9 +1,4 @@
-import {
-  ADMIN_API,
-  STRINGS,
-  ADMIN_URLS,
-  MAX_EXPLORER_PAGES,
-} from './wagtailConfig';
+import { ADMIN_API, ADMIN_URLS, MAX_EXPLORER_PAGES } from './wagtailConfig';
 
 describe('wagtailConfig', () => {
   describe('ADMIN_API', () => {
@@ -12,12 +7,6 @@ describe('wagtailConfig', () => {
     });
   });
 
-  describe('STRINGS', () => {
-    it('exists', () => {
-      expect(STRINGS).toBeDefined();
-    });
-  });
-
   describe('ADMIN_URLS', () => {
     it('exists', () => {
       expect(ADMIN_URLS).toBeDefined();

+ 35 - 1
client/src/custom.d.ts

@@ -6,6 +6,8 @@ declare global {
     telepath: any;
   }
 
+  // Get text
+
   // Wagtail globals
 
   interface WagtailConfig {
@@ -26,7 +28,39 @@ declare global {
 
       display_name: string;
     }[];
-    STRINGS: any;
   }
   const wagtailConfig: WagtailConfig;
+
+  // Django i18n utilities
+
+  // https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#gettext
+  function gettext(text: string): string;
+
+  // https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#ngettext
+  function ngettext(singular: string, plural: string, count: number): string;
+
+  // https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#get-format
+  type FormatType =
+    | 'DATE_FORMAT'
+    | 'DATE_INPUT_FORMATS'
+    | 'DATETIME_FORMAT'
+    | 'DATETIME_INPUT_FORMATS'
+    | 'DECIMAL_SEPARATOR'
+    | 'FIRST_DAY_OF_WEEK'
+    | 'MONTH_DAY_FORMAT'
+    | 'NUMBER_GROUPING'
+    | 'SHORT_DATE_FORMAT'
+    | 'SHORT_DATETIME_FORMAT'
+    | 'THOUSAND_SEPARATOR'
+    | 'TIME_FORMAT'
+    | 'TIME_INPUT_FORMATS'
+    | 'YEAR_MONTH_FORMAT';
+
+  function get_format(formatType: FormatType): string;
+
+  // https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#gettext_noop
+  function gettext_noop(text: string): string;
+
+  // https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#pluralidx
+  function pluralidx(count: number): boolean;
 }

+ 2 - 4
client/src/entrypoints/admin/comments.js

@@ -1,5 +1,4 @@
 import { initCommentApp } from '../../components/CommentApp/main';
-import { STRINGS } from '../../config/wagtailConfig';
 
 const KEYCODE_M = 77;
 
@@ -101,11 +100,11 @@ window.comments = (() => {
     }
     onFocus() {
       this.node.classList.remove('button-secondary');
-      this.node.ariaLabel = STRINGS.UNFOCUS_COMMENT;
+      this.node.ariaLabel = gettext('Unfocus comment');
     }
     onUnfocus() {
       this.node.classList.add('button-secondary');
-      this.node.ariaLabel = STRINGS.FOCUS_COMMENT;
+      this.node.ariaLabel = gettext('Focus comment');
 
       // TODO: ensure comment is focused accessibly when this is clicked,
       // and that screenreader users can return to the annotation point when desired
@@ -267,7 +266,6 @@ window.comments = (() => {
       data.user,
       data.comments,
       new Map(Object.entries(data.authors)),
-      STRINGS,
     );
 
     formElement

+ 1 - 3
client/src/entrypoints/admin/modal-workflow.js

@@ -5,8 +5,6 @@ possibly after several navigation steps
 
 import $ from 'jquery';
 
-/* global wagtailConfig */
-
 /* eslint-disable */
 function ModalWorkflow(opts) {
   /* options passed in 'opts':
@@ -35,7 +33,7 @@ function ModalWorkflow(opts) {
   const container = $(
     '<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">\n  <div class="modal-dialog">\n    <div class="modal-content">\n      <button type="button" class="button close button--icon text-replace" data-dismiss="modal">' +
       iconClose +
-      wagtailConfig.STRINGS.CLOSE +
+      gettext('Close') +
       '</button>\n      <div class="modal-body"></div>\n    </div><!-- /.modal-content -->\n  </div><!-- /.modal-dialog -->\n</div>',
   );
 

+ 4 - 50
client/tests/stubs.js

@@ -18,56 +18,6 @@ global.wagtailConfig = {
     DATE_FORMAT: 'MMM. D, YYYY',
     SHORT_DATE_FORMAT: 'DD/MM/YYYY',
   },
-  STRINGS: {
-    DELETE: 'Delete',
-    EDIT: 'Edit',
-    PAGE: 'Page',
-    PAGES: 'Pages',
-    LOADING: 'Loading…',
-    NO_RESULTS: 'No results',
-    SERVER_ERROR: 'Server Error',
-    SEE_ALL: 'See all',
-    CLOSE_EXPLORER: 'Close explorer',
-    ALT_TEXT: 'Alt text',
-    DECORATIVE_IMAGE: 'Decorative image',
-    WRITE_HERE: 'Write here…',
-    HORIZONTAL_LINE: 'Horizontal line',
-    LINE_BREAK: 'Line break',
-    UNDO: 'Undo',
-    REDO: 'Redo',
-    RELOAD_PAGE: 'Reload the page',
-    RELOAD_EDITOR: 'Reload saved content',
-    SHOW_LATEST_CONTENT: 'Show latest content',
-    SHOW_ERROR: 'Show error',
-    EDITOR_CRASH:
-      'The editor just crashed. Content has been reset to the last saved version.',
-    BROKEN_LINK: 'Broken link',
-    MISSING_DOCUMENT: 'Missing document',
-    CLOSE: 'Close',
-    EDIT_PAGE: "Edit '{title}'",
-    VIEW_CHILD_PAGES_OF_PAGE: "View child pages of '{title}'",
-    PAGE_EXPLORER: 'Page explorer',
-    SAVE: 'Save',
-    SAVING: 'Saving...',
-    CANCEL: 'Cancel',
-    DELETING: 'Deleting...',
-    ADD_A_COMMENT: 'Add a comment',
-    SHOW_COMMENTS: 'Show comments',
-    REPLY: 'Reply',
-    RESOLVE: 'Resolve',
-    RETRY: 'Retry',
-    DELETE_ERROR: 'Delete error',
-    CONFIRM_DELETE_COMMENT: 'Are you sure?',
-    SAVE_ERROR: 'Save error',
-    SAVE_COMMENT_WARNING: 'This will be saved when the page is saved',
-    FOCUS_COMMENT: 'Focus comment',
-    UNFOCUS_COMMENT: 'Unfocus comment',
-    SAVE_PAGE_TO_ADD_COMMENT: 'Save the page to add this comment',
-    SAVE_PAGE_TO_SAVE_COMMENT_CHANGES: 'Save the page to save this comment',
-    SAVE_PAGE_TO_SAVE_REPLY: 'Save the page to save this reply',
-    TOGGLE_SIDEBAR: 'Toggle sidebar',
-    MAIN_MENU: 'Main menu',
-  },
   WAGTAIL_I18N_ENABLED: true,
   LOCALES: [
     {
@@ -101,3 +51,7 @@ global.IMAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = { type: 'image' };
 global.PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = { type: 'page' };
 global.EMBED_CHOOSER_MODAL_ONLOAD_HANDLERS = { type: 'embed' };
 global.DOCUMENT_CHOOSER_MODAL_ONLOAD_HANDLERS = { type: 'document' };
+
+global.gettext = (text) => text;
+global.ngettext = (text, ptext, count) => (count === 1 ? text : ptext);
+global.gettext_noop = (text) => text;

+ 0 - 54
wagtail/admin/localization.py

@@ -48,60 +48,6 @@ WAGTAILADMIN_PROVIDED_LANGUAGES = [
 # as the wagtailConfig.STRINGS object
 def get_js_translation_strings():
     return {
-        "DELETE": _("Delete"),
-        "EDIT": _("Edit"),
-        "PAGE": _("Page"),
-        "PAGES": _("Pages"),
-        "LOADING": _("Loading…"),
-        "NO_RESULTS": _("No results"),
-        "SERVER_ERROR": _("Server Error"),
-        "SEE_ALL": _("See all"),
-        "CLOSE_EXPLORER": _("Close explorer"),
-        "ALT_TEXT": _("Alt text"),
-        "DECORATIVE_IMAGE": _("Decorative image"),
-        "WRITE_HERE": _("Write here…"),
-        "HORIZONTAL_LINE": _("Horizontal line"),
-        "LINE_BREAK": _("Line break"),
-        "UNDO": _("Undo"),
-        "REDO": _("Redo"),
-        "RELOAD_PAGE": _("Reload the page"),
-        "RELOAD_EDITOR": _("Reload saved content"),
-        "SHOW_LATEST_CONTENT": _("Show latest content"),
-        "SHOW_ERROR": _("Show error"),
-        "EDITOR_CRASH": _(
-            "The editor just crashed. Content has been reset to the last saved version."
-        ),
-        "BROKEN_LINK": _("Broken link"),
-        "MISSING_DOCUMENT": _("Missing document"),
-        "CLOSE": _("Close"),
-        "EDIT_PAGE": _("Edit '{title}'"),
-        "VIEW_CHILD_PAGES_OF_PAGE": _("View child pages of '{title}'"),
-        "PAGE_EXPLORER": _("Page explorer"),
-        "SAVE": _("Save"),
-        "SAVING": _("Saving..."),
-        "CANCEL": _("Cancel"),
-        "DELETING": _("Deleting..."),
-        "ADD_A_COMMENT": _("Add a comment"),
-        "SHOW_COMMENTS": _("Show comments"),
-        "REPLY": _("Reply"),
-        "RESOLVE": _("Resolve"),
-        "RETRY": _("Retry"),
-        "DELETE_ERROR": _("Delete error"),
-        "CONFIRM_DELETE_COMMENT": _("Are you sure?"),
-        "SAVE_ERROR": _("Save error"),
-        "SAVE_COMMENT_WARNING": _("This will be saved when the page is saved"),
-        "FOCUS_COMMENT": _("Focus comment"),
-        "UNFOCUS_COMMENT": _("Unfocus comment"),
-        "COMMENT": _("Comment"),
-        "MORE_ACTIONS": _("More actions"),
-        "SAVE_PAGE_TO_ADD_COMMENT": _("Save the page to add this comment"),
-        "SAVE_PAGE_TO_SAVE_COMMENT_CHANGES": _("Save the page to save this comment"),
-        "SAVE_PAGE_TO_SAVE_REPLY": _("Save the page to save this reply"),
-        "TOGGLE_SIDEBAR": _("Toggle sidebar"),
-        "MAIN_MENU": _("Main menu"),
-        "DASHBOARD": _("Dashboard"),
-        "EDIT_YOUR_ACCOUNT": _("Edit your account"),
-        "SEARCH": _("Search"),
         "MONTHS": [str(m) for m in MONTHS.values()],
         # Django's WEEKDAYS list begins on Monday, but ours should start on Sunday, so start
         # counting from -1 and use modulo 7 to get an array index