Skip to content

Commit

Permalink
Fix bug 1556771 - Show a simple text editor for simple Fluent message…
Browse files Browse the repository at this point in the history
…s. (#1325)

This commit refactors the whole `editor` module into 3 smaller modules: 
- `modules/genericeditor` has code for the generic editor, the one we show for most formats;
- `modules/fluenteditor` has code specific for the Fluent format, including a source editor and all variants of the rich Fluent editor;
- `core/editor` has code that is shared by both modules, including a `connectedEditor` higher-order component that has all the data and methods Editor implementations need.

It also adds basic support for the Fluent Rich Editor. It allows having multiple translation forms for a single string, and switching between the "rich" mode and the "source" mode. It adds a "simple" rich editor for all strings that are, well, "simple".
  • Loading branch information
adngdb committed Jul 22, 2019
1 parent a336256 commit 8326077
Show file tree
Hide file tree
Showing 67 changed files with 1,903 additions and 993 deletions.
1 change: 1 addition & 0 deletions frontend/public/static/locale/en-US/translate.ftl
Expand Up @@ -237,6 +237,7 @@ notification--tt-checks-enabled = Translate Toolkit Checks enabled
notification--tt-checks-disabled = Translate Toolkit Checks disabled
notification--make-suggestions-enabled = Make Suggestions enabled
notification--make-suggestions-disabled = Make Suggestions disabled
notification--ftl-not-supported-rich-editor = Translation not supported in rich editor
notification--entity-not-found = Can’t load specified string
Expand Down
1 change: 1 addition & 0 deletions frontend/public/static/locale/fr/translate.ftl
Expand Up @@ -195,6 +195,7 @@ notification--same-translation = Une traduction identique existe déjà
notification--tt-checks-enabled = Vérifications Translate Toolkit activées
notification--tt-checks-disabled = Vérifications Translate Toolkit désactivées
notification--make-suggestions-enabled = Faire des suggestions activé
notification--ftl-not-supported-rich-editor = Traduction non supportée dans l'éditeur avancé
notification--make-suggestions-disabled = Faire des suggestions désactivé
Expand Down
Expand Up @@ -7,13 +7,15 @@ import * as notification from 'core/notification';
import { actions as pluralActions } from 'core/plural';
import { actions as resourceActions } from 'core/resource';
import { actions as statsActions } from 'core/stats';
import * as unsavedchanges from 'modules/unsavedchanges';

import type { Entity } from 'core/api';
import type { Locale } from 'core/locales';


export const RESET_FAILED_CHECKS: 'editor/RESET_FAILED_CHECKS' = 'editor/RESET_FAILED_CHECKS';
export const RESET_SELECTION: 'editor/RESET_SELECTION' = 'editor/RESET_SELECTION';
export const SET_INITIAL_TRANSLATION: 'editor/SET_INITIAL_TRANSLATION' = 'editor/SET_INITIAL_TRANSLATION';
export const UPDATE: 'editor/UPDATE' = 'editor/UPDATE';
export const UPDATE_FAILED_CHECKS: 'editor/UPDATE_FAILED_CHECKS' = 'editor/UPDATE_FAILED_CHECKS';
export const UPDATE_SELECTION: 'editor/UPDATE_SELECTION' = 'editor/UPDATE_SELECTION';
Expand Down Expand Up @@ -52,6 +54,22 @@ export function updateSelection(content: string): UpdateSelectionAction {
}


/**
* Update the content that should replace the currently selected text in the
* active editor.
*/
export type InitialTranslationAction = {|
+type: typeof SET_INITIAL_TRANSLATION,
+translation: string,
|};
export function setInitialTranslation(translation: string): InitialTranslationAction {
return {
type: SET_INITIAL_TRANSLATION,
translation,
};
}


/**
* Update failed checks in the active editor.
*/
Expand Down Expand Up @@ -166,6 +184,9 @@ export function sendTranslation(
const notif = _getOperationNotif(content.type);
dispatch(notification.actions.add(notif));

// Ignore existing unsavedchanges because they are saved now.
dispatch(unsavedchanges.actions.ignore());

dispatch(
entitiesActions.updateEntityTranslation(
entity.pk,
Expand Down Expand Up @@ -206,6 +227,7 @@ export default {
resetFailedChecks,
resetSelection,
sendTranslation,
setInitialTranslation,
update,
updateFailedChecks,
updateSelection,
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/core/editor/components/EditorMenu.css
@@ -0,0 +1,51 @@
.editor-menu {
color: #fff;
padding: 10px;
position: relative;
}

.editor-menu .actions {
float: right;
}

.editor-menu .actions button {
background: transparent;
border: none;
color: #EBEBEB;
height: 40px;
margin: 0 2px;
padding: 10px 3px;
text-transform: uppercase;
}

.editor-menu .actions button:hover {
color: #7BC876;
}

.editor-menu .actions .action-save,
.editor-menu .actions .action-suggest {
background: #7BC876;
border-radius: 3px;
color: #272A2F;
font-weight: 600;
margin-left: 10px;
padding: 10px;
}

.editor-menu .actions .action-suggest {
background: #4FC4F6;
}

.editor-menu .actions .action-save:hover,
.editor-menu .actions .action-suggest:hover {
color: #EBEBEB;
}

.editor-menu .banner {
font-style: italic;
line-height: 40px;
}

.editor-menu .banner a {
color: #7BC876;
}
111 changes: 111 additions & 0 deletions frontend/src/core/editor/components/EditorMenu.js
@@ -0,0 +1,111 @@
/* @flow */

import * as React from 'react';
import { Localized } from 'fluent-react';

import './EditorMenu.css';

import * as editor from 'core/editor';
import * as user from 'core/user';
import * as unsavedchanges from 'modules/unsavedchanges';

import type { EditorProps } from 'core/editor';


type Props = {
...EditorProps,
firstItemHook?: React.Node,
};

/**
* Render the options to control an Editor.
*/
export default class EditorMenu extends React.Component<Props> {
render() {
const props = this.props;

return <menu className='editor-menu'>
{ props.firstItemHook }
<editor.FailedChecks
source={ props.editor.source }
user={ props.user }
errors={ props.editor.errors }
warnings={ props.editor.warnings }
resetFailedChecks={ props.resetFailedChecks }
sendTranslation={ props.sendTranslation }
updateTranslationStatus={ props.updateTranslationStatus }
/>
<unsavedchanges.UnsavedChanges />
{ !props.user.isAuthenticated ?
<Localized
id="editor-editor-sign-in-to-translate"
a={
<user.SignInLink url={ props.user.signInURL }></user.SignInLink>
}
>
<p className='banner'>
{ '<a>Sign in</a> to translate.' }
</p>
</Localized>
: (props.entity && props.entity.readonly) ?
<Localized
id="editor-editor-read-only-localization"
>
<p className='banner'>This is a read-only localization.</p>
</Localized>
:
<React.Fragment>
<editor.EditorSettings
settings={ props.user.settings }
updateSetting={ props.updateSetting }
/>
<editor.KeyboardShortcuts />
<editor.TranslationLength
entity={ props.entity }
pluralForm={ props.pluralForm }
translation={ props.editor.translation }
/>
<div className="actions">
<Localized id="editor-editor-button-copy">
<button
className="action-copy"
onClick={ props.copyOriginalIntoEditor }
>
Copy
</button>
</Localized>
<Localized id="editor-editor-button-clear">
<button
className="action-clear"
onClick={ props.clearEditor }
>
Clear
</button>
</Localized>
{ props.user.settings.forceSuggestions ?
// Suggest button, will send an unreviewed translation.
<Localized id="editor-editor-button-suggest">
<button
className="action-suggest"
onClick={ props.sendTranslation }
>
Suggest
</button>
</Localized>
:
// Save button, will send an approved translation.
<Localized id="editor-editor-button-save">
<button
className="action-save"
onClick={ props.sendTranslation }
>
Save
</button>
</Localized>
}
</div>
</React.Fragment>
}
</menu>;
}
}
101 changes: 101 additions & 0 deletions frontend/src/core/editor/components/EditorMenu.test.js
@@ -0,0 +1,101 @@
import React from 'react';
import { shallow } from 'enzyme';

import EditorMenu from './EditorMenu';
import EditorSettings from './EditorSettings';
import KeyboardShortcuts from './KeyboardShortcuts';
import TranslationLength from './TranslationLength';


const LOCALE = {
code: 'kg',
}

const SELECTED_ENTITY = {
pk: 42,
original: 'le test',
original_plural: 'les tests',
translation: [
{ string: 'test' },
{ string: 'test plural' },
],
};


function createEditorMenu({
forceSuggestions = true,
isAuthenticated = true,
entity = SELECTED_ENTITY,
firstItemHook = null,
} = {}) {
return shallow(<EditorMenu
editor={
{ translation: 'initial' }
}
locale={ LOCALE }
parameters={
{ resource: 'resource' }
}
entity={ entity }
user={ {
isAuthenticated,
username: 'Sarevok',
settings: {
forceSuggestions,
},
} }
firstItemHook={ firstItemHook }
/>);
}


function expectHiddenSettingsAndActions(wrapper) {
expect(wrapper.find('button')).toHaveLength(0);
expect(wrapper.find(EditorSettings)).toHaveLength(0);
expect(wrapper.find(KeyboardShortcuts)).toHaveLength(0);
expect(wrapper.find(TranslationLength)).toHaveLength(0);
expect(wrapper.find('#editor-editor-button-copy')).toHaveLength(0);
}


describe('<EditorMenu>', () => {
it('renders correctly', () => {
const wrapper = createEditorMenu();

// 3 buttons to control the editor.
expect(wrapper.find('button')).toHaveLength(3);
});

it('shows the Save button when forceSuggestions is off', () => {
const wrapper = createEditorMenu({ forceSuggestions: false });

expect(wrapper.find('.action-save').exists()).toBeTruthy();
});

it('hides the settings and actions when the user is logged out', () => {
const wrapper = createEditorMenu({ isAuthenticated: false });

expectHiddenSettingsAndActions(wrapper);

expect(wrapper.find('#editor-editor-sign-in-to-translate')).toHaveLength(1);
});

it('hides the settings and actions when the entity is read-only', () => {
const entity = {
...SELECTED_ENTITY,
readonly: true,
}
const wrapper = createEditorMenu({ entity });

expectHiddenSettingsAndActions(wrapper);

expect(wrapper.find('#editor-editor-read-only-localization')).toHaveLength(1);
});

it('accepts a firstItemHook and shows it as its first child', () => {
const firstItemHook = <p>Hello</p>;
const wrapper = createEditorMenu({ firstItemHook });

expect(wrapper.find('menu').children().first().text()).toEqual('Hello');
});
});

0 comments on commit 8326077

Please sign in to comment.