adds translation linting and addresses issues the linter found

This commit is contained in:
Alex Corey 2021-01-14 15:25:58 -05:00
parent 0f0857747d
commit 1f81592b5c
46 changed files with 146 additions and 70 deletions

View File

@ -8,8 +8,8 @@
"modules": true
}
},
"plugins": ["react-hooks", "jsx-a11y"],
"extends": ["airbnb", "prettier", "prettier/react", "plugin:jsx-a11y/strict"],
"plugins": ["react-hooks", "jsx-a11y", "i18next"],
"extends": ["airbnb", "prettier", "prettier/react", "plugin:jsx-a11y/strict", "plugin:i18next/recommended"],
"settings": {
"react": {
"version": "16.5.2"
@ -24,6 +24,7 @@
"window": true
},
"rules": {
"i18next/no-literal-string": [2, {"markupOnly": true, "ignoreAttribute": ["to", "streamType", "path", "component", "variant", "key", "position", "promptName", "color","promptId", "headingLevel", "size", "target", "autoComplete","trigger", "from", "name", "fieldId", "css", "gutter", "dataCy", "tooltipMaxWidth", "mode", "aria-labelledby","aria-hidden","sortKey", "ouiaId", "credentialTypeNamespace", "link", "value", "credentialTypeKind", "linkTo", "scrollToAlignment", "displayKey", "sortedColumnKey", "maxHeight", "role", "aria-haspopup", "dropDirection", "resizeOrientation", "src", "theme"], "ignore":["Ansible", "Tower", "JSON", "YAML", "lg", "START"],"ignoreComponent":["code", "Omit","PotentialLink", "TypeRedirect", "Radio", "RunOnRadio", "NodeTypeLetter", "SelectableItem", "Dash"], "ignoreCallee": ["describe"] }],
"camelcase": "off",
"arrow-parens": "off",
"comma-dangle": "off",

View File

@ -7172,6 +7172,15 @@
"lodash": "^4.17.15"
}
},
"eslint-plugin-i18next": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-i18next/-/eslint-plugin-i18next-5.0.0.tgz",
"integrity": "sha512-ixbgSMrSb0dZsO6WPElg4JvPiQKLDA3ZpBuayxToADan1TKcbzKXT2A42Vyc0lEDhJRPL6uZnmm8vPjODDJypg==",
"dev": true,
"requires": {
"requireindex": "~1.1.0"
}
},
"eslint-plugin-import": {
"version": "2.22.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz",
@ -15163,6 +15172,12 @@
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
},
"requireindex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz",
"integrity": "sha1-5UBLgVV+91225JxacgBIk/4D4WI=",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",

View File

@ -43,6 +43,7 @@
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^5.0.0",
"eslint-import-resolver-webpack": "0.11.1",
"eslint-plugin-i18next": "^5.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.11.1",

View File

@ -1,6 +1,7 @@
import 'styled-components/macro';
import React, { useState, useEffect } from 'react';
import { node, number, oneOfType, shape, string, arrayOf } from 'prop-types';
import { Trans, withI18n } from '@lingui/react';
import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core';
import { DetailName, DetailValue } from '../DetailList';
import MultiButtonToggle from '../MultiButtonToggle';
@ -111,7 +112,7 @@ function VariablesDetail({ dataCy, helpText, value, label, rows, fullHeight }) {
css="color: var(--pf-global--danger-color--100);
font-size: var(--pf-global--FontSize--sm"
>
Error: {error.message}
<Trans>Error:</Trans> {error.message}
</div>
)}
</DetailValue>
@ -131,4 +132,4 @@ VariablesDetail.defaultProps = {
helpText: '',
};
export default VariablesDetail;
export default withI18n()(VariablesDetail);

View File

@ -1,13 +1,13 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { shallow, mount } from 'enzyme';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import VariablesDetail from './VariablesDetail';
jest.mock('../../api');
describe('<VariablesDetail>', () => {
test('should render readonly CodeMirrorInput', () => {
const wrapper = shallow(
const wrapper = mountWithContexts(
<VariablesDetail value="---foo: bar" label="Variables" />
);
const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
@ -18,7 +18,7 @@ describe('<VariablesDetail>', () => {
});
test('should detect JSON', () => {
const wrapper = shallow(
const wrapper = mountWithContexts(
<VariablesDetail value='{"foo": "bar"}' label="Variables" />
);
const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
@ -28,7 +28,7 @@ describe('<VariablesDetail>', () => {
});
test('should convert between modes', () => {
const wrapper = shallow(
const wrapper = mountWithContexts(
<VariablesDetail value="---foo: bar" label="Variables" />
);
wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
@ -43,7 +43,9 @@ describe('<VariablesDetail>', () => {
});
test('should render label and value= --- when there are no values', () => {
const wrapper = shallow(<VariablesDetail value="" label="Variables" />);
const wrapper = mountWithContexts(
<VariablesDetail value="" label="Variables" />
);
expect(wrapper.find('VariablesDetail___StyledCodeMirrorInput').length).toBe(
1
);
@ -51,7 +53,7 @@ describe('<VariablesDetail>', () => {
});
test('should update value if prop changes', () => {
const wrapper = mount(
const wrapper = mountWithContexts(
<VariablesDetail value="---foo: bar" label="Variables" />
);
act(() => {
@ -67,13 +69,17 @@ describe('<VariablesDetail>', () => {
});
test('should default yaml value to "---"', () => {
const wrapper = shallow(<VariablesDetail value="" label="Variables" />);
const wrapper = mountWithContexts(
<VariablesDetail value="" label="Variables" />
);
const input = wrapper.find('VariablesDetail___StyledCodeMirrorInput');
expect(input.prop('value')).toEqual('---');
});
test('should default empty json to "{}"', () => {
const wrapper = mount(<VariablesDetail value="" label="Variables" />);
const wrapper = mountWithContexts(
<VariablesDetail value="" label="Variables" />
);
act(() => {
wrapper.find('MultiButtonToggle').invoke('onChange')('javascript');
});

View File

@ -258,7 +258,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
key="delete"
onDelete={handleJobDelete}
itemsToDelete={selected}
pluralizedItemName="Jobs"
pluralizedItemName={i18n._(t`Jobs`)}
/>,
<JobListCancelButton
key="cancel"

View File

@ -5,6 +5,7 @@ import { t } from '@lingui/macro';
import { Button, Chip } from '@patternfly/react-core';
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
import { RocketIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import { ActionsTd, ActionItem } from '../PaginatedTable';
import LaunchButton from '../LaunchButton';
import StatusLabel from '../StatusLabel';
@ -14,6 +15,7 @@ import CredentialChip from '../CredentialChip';
import { formatDateString } from '../../util/dates';
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
const Dash = styled.span``;
function JobListItem({
i18n,
job,
@ -58,7 +60,7 @@ function JobListItem({
<span>
<Link to={`/jobs/${JOB_TYPE_URL_SEGMENTS[job.type]}/${job.id}`}>
<b>
{job.id} &mdash; {job.name}
{job.id} <Dash>&mdash;</Dash> {job.name}
</b>
</Link>
</span>

View File

@ -103,7 +103,7 @@ function Lookup(props) {
<Fragment>
<InputGroup onBlur={onBlur}>
<Button
aria-label="Search"
aria-label={i18n._(t`Search`)}
id={id}
onClick={() => dispatch({ type: 'TOGGLE_MODAL' })}
variant={ButtonVariant.control}

View File

@ -63,7 +63,7 @@ function NotificationListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={`items-list-item-${notification.id}`}
id={`items-list-item-${notification.id}`}
columns={showApprovalsToggle ? 4 : 3}

View File

@ -138,7 +138,7 @@ function PromptDetail({ i18n, resource, launchConfig = {}, overrides = {} }) {
<>
<Divider css="margin-top: var(--pf-global--spacer--lg)" />
<PromptHeader>{i18n._(t`Prompted Values`)}</PromptHeader>
<DetailList aria-label="Prompt Overrides">
<DetailList aria-label={i18n._(t`Prompt Overrides`)}>
{launchConfig.ask_job_type_on_launch && (
<Detail
label={i18n._(t`Job Type`)}

View File

@ -101,7 +101,7 @@ function ScheduleListItem({ i18n, isSelected, onSelect, schedule }) {
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
key="actions"

View File

@ -78,7 +78,7 @@ function ApplicationListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -83,7 +83,7 @@ function CredentialListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -60,7 +60,7 @@ function CredentialTypeListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -250,7 +250,7 @@ function DashboardTemplateList({ i18n }) {
key="delete"
onDelete={handleTemplateDelete}
itemsToDelete={selected}
pluralizedItemName="Templates"
pluralizedItemName={i18n._(t`Templates`)}
/>,
]}
/>

View File

@ -111,7 +111,10 @@ function DashboardTemplateListItem({
</DataListCell>,
]}
/>
<DataListAction aria-label="actions" aria-labelledby={labelId}>
<DataListAction
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
>
{template.type === 'workflow_job_template' && (
<Tooltip content={i18n._(t`Visualizer`)} position="top">
<Button

View File

@ -42,7 +42,7 @@ function HostGroupItem({ i18n, group, inventoryId, isSelected, onSelect }) {
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -64,7 +64,7 @@ function HostListItem({ i18n, host, isSelected, onSelect, detailUrl }) {
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -173,7 +173,7 @@ function InstanceGroupListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -113,7 +113,7 @@ function InstanceListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -66,7 +66,7 @@ function InventoryGroupHostListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -48,7 +48,7 @@ function InventoryGroupItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -48,7 +48,7 @@ function InventoryHostGroupItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -59,7 +59,7 @@ function InventoryHostItem(props) {
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -56,7 +56,7 @@ function InventoryRelatedGroupListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -88,7 +88,7 @@ function InventorySourceListItem({
<DataListAction
id="actions"
aria-labelledby="actions"
aria-label="actions"
aria-label={i18n._(t`actions`)}
>
{source.summary_fields.user_capabilities.start && (
<InventorySourceSyncButton source={source} />

View File

@ -152,7 +152,7 @@ function SmartInventoryDetail({ inventory, i18n }) {
{user_capabilities?.edit && (
<Button
component={Link}
aria-label="edit"
aria-label={i18n._(t`edit`)}
to={`/inventories/smart_inventory/${id}/edit`}
>
{i18n._(t`Edit`)}

View File

@ -127,7 +127,10 @@ function NotificationTemplateListItem({
</DataListCell>,
]}
/>
<DataListAction aria-label="actions" aria-labelledby={labelId}>
<DataListAction
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
>
<Tooltip content={i18n._(t`Test Notification`)} position="top">
<Button
aria-label={i18n._(t`Test Notification`)}

View File

@ -1,6 +1,6 @@
import 'styled-components/macro';
import React, { useEffect, useRef } from 'react';
import { withI18n } from '@lingui/react';
import { Trans, withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { useField, useFormikContext } from 'formik';
import { Switch, Text } from '@patternfly/react-core';
@ -69,9 +69,11 @@ function CustomMessagesSubForm({ defaultMessages, type, i18n }) {
css="margin-bottom: var(--pf-c-content--MarginBottom)"
>
<small>
Use custom messages to change the content of notifications sent
when a job starts, succeeds, or fails. Use curly braces to access
information about the job:{' '}
<Trans>
Use custom messages to change the content of notifications sent
when a job starts, succeeds, or fails. Use curly braces to
access information about the job:{' '}
</Trans>
<code>
{'{{'} job_friendly_name {'}}'}
</code>
@ -79,12 +81,15 @@ function CustomMessagesSubForm({ defaultMessages, type, i18n }) {
<code>
{'{{'} url {'}}'}
</code>
, or attributes of the job such as{' '}
, <Trans>or attributes of the job such as</Trans>{' '}
<code>
{'{{'} job.status {'}}'}
</code>
. You may apply a number of possible variables in the message.
Refer to the{' '}
.{' '}
<Trans>
You may apply a number of possible variables in the message.
Refer to the{' '}
</Trans>{' '}
<a
href="https://docs.ansible.com/ansible-tower/latest/html/userguide/notifications.html#create-custom-notifications"
target="_blank"
@ -92,7 +97,7 @@ function CustomMessagesSubForm({ defaultMessages, type, i18n }) {
>
Ansible Tower documentation
</a>{' '}
for more details.
<Trans>for more details.</Trans>
</small>
</Text>
<FormFullWidthLayout>

View File

@ -172,7 +172,7 @@ function OrganizationsList({ i18n }) {
key="delete"
onDelete={handleOrgDelete}
itemsToDelete={selected}
pluralizedItemName="Organizations"
pluralizedItemName={i18n._(t`Organizations`)}
/>,
]}
/>

View File

@ -32,7 +32,7 @@ function OrganizationTeamListItem({ i18n, team, detailUrl }) {
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -86,7 +86,7 @@ function ProjectJobTemplateListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -149,7 +149,7 @@ function ProjectListItem({
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -21,8 +21,16 @@ const ArchiveSubForm = ({
<span>
{i18n._(t`Example URLs for Remote Archive Source Control include:`)}
<ul css={{ margin: '10px 0 10px 20px' }}>
<li>https://github.com/username/project/archive/v0.0.1.tar.gz</li>
<li>https://github.com/username/project/archive/v0.0.2.zip</li>
<li>
<code>
https://github.com/username/project/archive/v0.0.1.tar.gz
</code>
</li>
<li>
<code>
https://github.com/username/project/archive/v0.0.2.zip
</code>
</li>
</ul>
</span>
}

View File

@ -23,9 +23,15 @@ const GitSubForm = ({
<span>
{i18n._(t`Example URLs for GIT Source Control include:`)}
<ul css="margin: 10px 0 10px 20px">
<li>https://github.com/ansible/ansible.git</li>
<li>git@github.com:ansible/ansible.git</li>
<li>git://servername.example.com/ansible.git</li>
<li>
<code>https://github.com/ansible/ansible.git</code>
</li>
<li>
<code>git@github.com:ansible/ansible.git</code>
</li>
<li>
<code>git://servername.example.com/ansible.git</code>
</li>
</ul>
{i18n._(t`Note: When using SSH protocol for GitHub or
Bitbucket, enter an SSH key only, do not enter a username
@ -58,8 +64,12 @@ const GitSubForm = ({
<br />
{i18n._(t`Examples include:`)}
<ul css={{ margin: '10px 0 10px 20px' }}>
<li>refs/*:refs/remotes/origin/*</li>
<li>refs/pull/62/head:refs/remotes/origin/pull/62/head</li>
<li>
<code>refs/*:refs/remotes/origin/*</code>
</li>
<li>
<code>refs/pull/62/head:refs/remotes/origin/pull/62/head</code>
</li>
</ul>
{i18n._(t`The first fetches all references. The second
fetches the Github pull request number 62, in this example

View File

@ -22,9 +22,15 @@ const SvnSubForm = ({
<span>
{i18n._(t`Example URLs for Subversion Source Control include:`)}
<ul css={{ margin: '10px 0 10px 20px' }}>
<li>https://github.com/ansible/ansible</li>
<li>svn://servername.example.com/path</li>
<li>svn+ssh://servername.example.com/path</li>
<li>
<code>https://github.com/ansible/ansible</code>
</li>
<li>
<code>svn://servername.example.com/path</code>
</li>
<li>
<code>svn+ssh://servername.example.com/path</code>
</li>
</ul>
</span>
}

View File

@ -68,7 +68,7 @@ function TeamListItem({ team, isSelected, onSelect, detailUrl, i18n }) {
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`Actions`)}
aria-labelledby={labelId}
id={labelId}
>

View File

@ -247,7 +247,7 @@ function TemplateList({ i18n }) {
key="delete"
onDelete={handleTemplateDelete}
itemsToDelete={selected}
pluralizedItemName="Templates"
pluralizedItemName={i18n._(t`Templates`)}
/>,
]}
/>

View File

@ -112,7 +112,10 @@ function TemplateListItem({
</DataListCell>,
]}
/>
<DataListAction aria-label="actions" aria-labelledby={labelId}>
<DataListAction
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
>
{template.type === 'workflow_job_template' && (
<Tooltip content={i18n._(t`Visualizer`)} position="top">
<Button

View File

@ -14,7 +14,7 @@ function LinkDeleteModal({ i18n }) {
return (
<AlertModal
variant="danger"
title="Remove Link"
title={i18n._(t`Remove Link`)}
isOpen={linkToDelete}
onClose={() => dispatch({ type: 'SET_LINK_TO_DELETE', value: null })}
actions={[

View File

@ -293,10 +293,14 @@ describe('<JobTemplateForm />', () => {
).toBe(true);
expect(
wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('readOnly')
wrapper
.find('input[aria-label="workflow job template webhook key"]')
.prop('readOnly')
).toBe(true);
expect(
wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('value')
wrapper
.find('input[aria-label="workflow job template webhook key"]')
.prop('value')
).toBe('webhook key');
await act(() =>
wrapper.find('Button[aria-label="Update webhook key"]').prop('onClick')()

View File

@ -196,7 +196,7 @@ function WebhookSubForm({ i18n, templateType }) {
<TextInput
id="template-webhook_key"
isReadOnly
aria-label="wfjt-webhook-key"
aria-label={i18n._(t`workflow job template webhook key`)}
value={webhookKeyField.value}
/>
<Button

View File

@ -65,7 +65,9 @@ describe('<WebhookSubForm />', () => {
wrapper.find('TextInputBase[aria-label="Webhook URL"]').prop('value')
).toContain('/api/v2/job_templates/51/github/');
expect(
wrapper.find('TextInputBase[aria-label="wfjt-webhook-key"]').prop('value')
wrapper
.find('TextInputBase[aria-label="workflow job template webhook key"]')
.prop('value')
).toBe('webhook key');
expect(
wrapper
@ -89,7 +91,9 @@ describe('<WebhookSubForm />', () => {
wrapper.find('TextInputBase[aria-label="Webhook URL"]').prop('value')
).toContain('/api/v2/job_templates/51/gitlab/');
expect(
wrapper.find('TextInputBase[aria-label="wfjt-webhook-key"]').prop('value')
wrapper
.find('TextInputBase[aria-label="workflow job template webhook key"]')
.prop('value')
).toBe('A NEW WEBHOOK KEY WILL BE GENERATED ON SAVE.');
});
test('should have disabled button to update webhook key', async () => {

View File

@ -221,10 +221,14 @@ describe('<WorkflowJobTemplateForm/>', () => {
wrapper.find('Checkbox[aria-label="Enable Webhook"]').prop('isChecked')
).toBe(true);
expect(
wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('readOnly')
wrapper
.find('input[aria-label="workflow job template webhook key"]')
.prop('readOnly')
).toBe(true);
expect(
wrapper.find('input[aria-label="wfjt-webhook-key"]').prop('value')
wrapper
.find('input[aria-label="workflow job template webhook key"]')
.prop('value')
).toBe('sdfghjklmnbvcdsew435678iokjhgfd');
await act(() =>
wrapper.find('Button[aria-label="Update webhook key"]').prop('onClick')()

View File

@ -162,7 +162,7 @@ function UserList({ i18n }) {
key="delete"
onDelete={handleUserDelete}
itemsToDelete={selected}
pluralizedItemName="Users"
pluralizedItemName={i18n._(t`Users`)}
/>,
]}
/>

View File

@ -95,7 +95,7 @@ function UserListItem({ user, isSelected, onSelect, detailUrl, i18n }) {
]}
/>
<DataListAction
aria-label="actions"
aria-label={i18n._(t`actions`)}
aria-labelledby={labelId}
id={labelId}
>