Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simple translations model #4019

Merged
merged 21 commits into from
Nov 14, 2017
Merged

Add a simple translations model #4019

merged 21 commits into from
Nov 14, 2017

Conversation

eatyourgreens
Copy link
Contributor

@eatyourgreens eatyourgreens commented Sep 4, 2017

Project and workflows front-end for the translations API from zooniverse/panoptes#2435.

Gets localised strings from the translations API.
Uses Redux to store the resource translations.
Updates some project and task components to use localised strings instead of API resource properties (like project.display_name.)

Review Checklist

  • Does it work in all major browsers: Firefox, Chrome, Edge, Safari?
  • Does it work on mobile?
  • Can you rm -rf node_modules/ && npm install and app works as expected?
  • Did you deploy a staging branch?
  • If the component is in coffeescript, is it converted to ES6? Is it free of eslint errors? Is the conversion its own commit?
  • Are the tests passing locally and on Travis?

Optional

@eatyourgreens
Copy link
Contributor Author

eatyourgreens commented Sep 4, 2017

@eatyourgreens eatyourgreens force-pushed the translation-api branch 2 times, most recently from 01b002e to e4bd0f9 Compare September 6, 2017 13:56
@eatyourgreens
Copy link
Contributor Author

Moving the localisation of the interface text into #4027

@eatyourgreens eatyourgreens changed the base branch from master to localise-projects September 7, 2017 16:22
@eatyourgreens
Copy link
Contributor Author

Rebased off #4027 instead of master.

@eatyourgreens eatyourgreens changed the base branch from localise-projects to master September 29, 2017 11:14
@eatyourgreens
Copy link
Contributor Author

Rebased to latest master.

@eatyourgreens
Copy link
Contributor Author

Rebased to bring in updates to the survey task summary.

@eatyourgreens
Copy link
Contributor Author

TODO: mock up some translations for the tests. Survey task tests are failing because default translations don't exist.

@eatyourgreens eatyourgreens force-pushed the translation-api branch 2 times, most recently from 852601e to c519179 Compare October 10, 2017 11:45
@eatyourgreens
Copy link
Contributor Author

eatyourgreens commented Oct 10, 2017

Following discussions on Slack yesterday, components might be loaded in from zooniverse/Zooniverse-React-Components. So, components shouldn't have to decide where strings are coming from (a translation resource or the original Panoptes resource.)

In that case, translations might be better handled in a wrapper and passed down to individual components as a prop, to avoid awkward code like translations.strings.project.title || this.props.project.display_name when rendering strings.

@eatyourgreens eatyourgreens force-pushed the translation-api branch 5 times, most recently from 7b07d49 to 03cbbf8 Compare October 12, 2017 12:12
import React from 'react';
import translations from '../../../pages/project/translations';

function SurveyTranslations(props) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it really hard to follow what this function does 😞 (and I wrote it!)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is really hard. I lost track at the 6th "characteristics". I know I always say this but some destructuring may help?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any specific thoughts on how destructuring would work here? I had the same thought.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've started to rewrite the first block and I've got this

const taskStrings = translations.strings.workflow.tasks;
  const { characteristics, choices, questions } = task;
  Object.keys(characteristics).map((characteristicId) => {
    translation.characteristics[characteristicId] = Object.assign({}, characteristics[characteristicId]);
    if (taskStrings[`${props.taskKey}.characteristics.${characteristicId}.label`]) {
      translation.characteristics[characteristicId].label = taskStrings[`${props.taskKey}.characteristics.${characteristicId}.label`];
    }
    translation.characteristics[characteristicId].values = Object.assign({}, characteristics[characteristicId].values);
    Object.keys(task.characteristics[characteristicId].values).map((valueId) => {
      translation.characteristics[characteristicId].values[valueId] = Object.assign({}, characteristics[characteristicId].values[valueId]);
      if (taskStrings[`${props.taskKey}.characteristics.${characteristicId}.values.${valueId}.label`]) {
      translation.characteristics[characteristicId].values[valueId].label =
        taskStrings[`${props.taskKey}.characteristics.${characteristicId}.values.${valueId}.label`];
      }
    });
  });

which is slightly less convoluted, but I'd appreciate any advice you can offer to simplify it further.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing I can think of is moving the second .map block out to its own method. Just to give the eyes a little rest. I'll keep thinking.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roger wrote this for WGE at some point. I think it does what you want.

/**
 * Simple is object check.
 * @param item
 * @returns {boolean}
 */
export function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item) && item !== null);
}

/**
 * Deep merge two objects.
 * @param target
 * @param source
 */
export function mergeDeep(target, source) {
  if (isObject(target) && isObject(source)) {
    Object.keys(source).forEach(key => {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] });
      }
    });
  }
  return target;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep merge will get rid of all the Object.assign calls.

I'll have a think about how to reduce the number of maps. Maybe I'm looking at this the wrong way round and the code should be looping through taskStrings in order to assign translation strings to the translation object.


function SurveyTranslations(props) {
const { task } = props;
const translation = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe try const translation = Object.assign({}, task) here, so that translation labels automatically default to the workflow task labels, and the following code can be greatly simplified.

I can't remember if Object.assign() does a deep merge of nested objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically, this needs to be done in such a way that properties on translation aren't pointers to the original properties on task. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it does

The Object.assign() method only copies enumerable and own properties from a source object to a target object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the MDN article I linked to says that it creates references, not actual copies. We want to be able to change translation without changing task.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lodash has a deep merge method and we have it as a dependency. I think it might be used in the codebase somewhere already.

Copy link
Contributor Author

@eatyourgreens eatyourgreens Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have a look. I've rewritten the object cloning in cc21143 to make better use of temp variables and destructuring.

Here, we want to overwrite a key's value if a corresponding key exists in the Panoptes translation resource. The translation wrapper basically gives us something that looks like a task (but contains translated text when available) for backwards compatibility with projects that haven't been migrated over to use translations yet.

I'm also thinking that it might allow us to wrap ZRC components, and pass text into them via a translation prop. Code that doesn't use translations yet should still work by passing a task object as the translation (since the structures are identical) <Task translation={workflow.tasks[taskKey]} />.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also hoping that these adapters can be ripped out, or greatly simplified, once every Panoptes resource has been migrated across to use translations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, maybe translations is a good case for a generalized higher order component in ZRC?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly. I'm finding each resource needs its own, specific adapter component. There isn't much consistency between tasks, for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I can figure out a generalised map from taskStrings to translation then all of this can be turned into a generic adapter for workflow task translations.

Generic translations for all resources might require some reworking of Panoptes internals. I remember @camallen saying that workflow translations have kept the awkward structure that they have here, with the long string keys for tasks, because changing those keys would break anything that relies on them, like classification exports.

counterpart.setFallbackLocale('en');


const translations = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all works, but should probably be replaced with a Redux store, and load action, at some point.

@@ -69,7 +73,7 @@ const ProjectHomePage = (props) => {

<div>
<img role="presentation" src={avatarSrc} />
<span>&quot;{props.project.researcher_quote}&quot;</span>
<span>&quot;{translations.strings.project.researcher_quote || props.project.researcher_quote}&quot;</span>
Copy link
Contributor Author

@eatyourgreens eatyourgreens Oct 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, and subsequent changes, should be updated to use the translation wrapper pattern that's being used by workflow tasks.

@eatyourgreens eatyourgreens force-pushed the translation-api branch 2 times, most recently from b5dec24 to 2d84685 Compare October 13, 2017 11:04
Copy link
Contributor

@srallen srallen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying what's to be expected. I only have one minor comment about the new tests and The Shortcut translations prop also is throwing a prop type warning on the test survey project when using the default English translation:
Warning: Failed prop type: The prop `translation` is marked as required in `Shortcut`, but its value is `undefined`.

};

const project = {
id: 12345,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, but for accuracy with these mocks, the ids should be strings.

@eatyourgreens
Copy link
Contributor Author

@srallen Thanks for taking a look. I will have a look to see why the Shortcut task is failing like that.

@eatyourgreens
Copy link
Contributor Author

@srallen I can see the required prop warning, but the translation prop is defined in the Shortcut task, as far as I can tell, both in the component constructor and in componentWillMount. I'm at a loss as to how to fix that warning.

@srallen
Copy link
Contributor

srallen commented Nov 9, 2017

@eatyourgreens ok, it's not a blocker since it didn't seem to break anything, but it's nice to remove warning messages when we can. I'll do one last look over today.

@srallen
Copy link
Contributor

srallen commented Nov 10, 2017

I think this will ready to merge after a final rebase.

Adds a simple object to load and store translations.
Adds translations to some project components.
Adds a general translation wrapper for all workflow tasks.
Adds tests for the task translation wrapper.
Pass project translations as a separate object from project resources.
Break up some long variable names in the characteristics filter.
Translate the correct task for shortcuts.
Validate shortcut answers as an array.
Move the translations object to a redux store.
Add actions for loading and setting resource translations.
Remove the original store object.
Update ask translation tests to work with redux.
Renames project-translations for consistency.
Report resource type, ID, language and HTTP status code in API error warnings.
@eatyourgreens eatyourgreens merged commit 9ad40f9 into master Nov 14, 2017
@eatyourgreens eatyourgreens deleted the translation-api branch November 14, 2017 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants