Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/config/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//= link application.css

//= link i18n-strings.js
//= link asset-strings.js
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did it turn out that this is necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, I believe it is

//= link email.css
//= link es5-shim.min.js
//= link html5shiv.js
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
//= require i18n-strings
//= require local-time
//= require asset-strings
12 changes: 12 additions & 0 deletions app/assets/javascripts/asset-strings.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
window.LoginGov = window.LoginGov || {}
<% image_keys = [
'idv/phone.png'
] %>

window.LoginGov.AssetStrings = {
images: {}
};

<% image_keys.each do |key| %>
window.LoginGov.AssetStrings.images['<%=key %>'] = '<%= ActionController::Base.helpers.image_path(key) %>';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As used in i18n-strings.js.erb, the j helper is an alias for escape_javascript, which may be important to help escape potentially unexpected / conflicting characters (e.g. early terminating single quote ' or </script>, etc).

<% end %>
Comment on lines +1 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Based on our previous conversation, I think we should be able to make this less opinionated about the particular kinds of assets, instead creating a single object which can contain any of the paths we might want in the client:

Suggested change
window.LoginGov = window.LoginGov || {}
<% image_keys = [
'idv/phone.png'
] %>
window.LoginGov.AssetStrings = {
images: {}
};
<% image_keys.each do |key| %>
window.LoginGov.AssetStrings.images['<%=key %>'] = '<%= ActionController::Base.helpers.image_path(key) %>';
<% end %>
window.LoginGov = window.LoginGov || {};
window.LoginGov.AssetStrings = {};
<% keys = [
'idv/phone.png'
] %>
<% keys.each do |key| %>
window.LoginGov.AssetStrings['<%= ActionController::Base.helpers.j key %>'] = '<%= ActionController::Base.helpers.j ActionController::Base.helpers.asset_path key %>';
<% end %>

In my testing, this creates an object mapping the original path to the one including the digest:

image

3 changes: 2 additions & 1 deletion app/assets/javascripts/i18n-strings.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ window.LoginGov = window.LoginGov || {};
'zxcvbn.feedback.this_is_a_very_common_password',
'zxcvbn.feedback.this_is_similar_to_a_commonly_used_password',
'zxcvbn.feedback.for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases',
'zxcvbn.feedback.use_a_longer_keyboard_pattern_with_more_turns'
'zxcvbn.feedback.use_a_longer_keyboard_pattern_with_more_turns',
'doc_auth.headings.welcome'
] %>

window.LoginGov.I18n = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React from 'react';
import useI18n from '../hooks/use-i18n';
import { useImage } from '../hooks/use-assets';

function DocumentCapture() {
return 'Document Capture';
const t = useI18n();
const imageTag = useImage();
return <img src={imageTag('idv/phone.png')} alt={t('doc_auth.headings.welcome')} />;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the idea is to have this correspond to an equivalent Rails helper method, and since image_tag in Rails would generate the full markup for the image, should we consider to refer to this as imagePath, since it's intended to correspond to image_path (return the src value)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I also wonder if, rather than making separate helpers for each type of asset, it would be possible to create one generic helper which can return the path for any asset, regardless of type.

Considering asset_path as an alternative to image_path, where the usage could be something like:

const getAssetPath = useAssets();
const src = getAssetPath('images/idv/phone.png');

}

export default DocumentCapture;
5 changes: 5 additions & 0 deletions app/javascript/app/document-capture/context/assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createContext } from 'react';

const AssetContext = createContext({});

export default AssetContext;
5 changes: 5 additions & 0 deletions app/javascript/app/document-capture/context/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createContext } from 'react';

const I18nContext = createContext({});

export default I18nContext;
14 changes: 14 additions & 0 deletions app/javascript/app/document-capture/hooks/use-assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useContext } from 'react';
import AssetContext from '../context/assets';

function useImage() {
const strings = useContext(AssetContext);
const imageStrings = strings.images || {};
const imageTag = (key) => {
const resolvedImage = imageStrings[key];
return resolvedImage !== undefined ? resolvedImage : key;
};
return imageTag;
}

export { useImage };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In separate work I came across the need to recreate an existing shared view element in React (the accordion), and it occurred to me that it would be a nice abstraction to be able to use an image as a regular <img> HTML tag, in a way that the application would substitute the digest-modified path automatically. I was considering this a new Image component.

Example usage:

<Image
  src="plus.svg"
  alt={ t('image_description.accordian_plus_buttom') }
  width={ 16 }
  className="plus-icon display-none"
/>

Recreating this:

= image_tag asset_url('plus.svg'), alt: t('image_description.accordian_plus_buttom'),
width: 16, class: 'plus-icon display-none'

Repurposing src as a reference to an asset path might be a bit too magical, so naming it something like assetPath could be more appropriate.

As far as it relates here, I still think we'll need the asset context, but I'm not yet sure about these hooks. It seems like if we had an Image component abstraction, it wouldn't be too much a regular bother to reference the context using useContext, without any intermediary useAsset or useImage hooks.

10 changes: 10 additions & 0 deletions app/javascript/app/document-capture/hooks/use-i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useContext } from 'react';
import I18nContext from '../context/i18n';

function useI18n() {
const strings = useContext(I18nContext);
const t = (key) => (Object.prototype.hasOwnProperty.call(strings, key) ? strings[key] : key); // eslint-disable-line
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What was the reason for disabling ESLint for this line?

return t;
}

export default useI18n;
10 changes: 9 additions & 1 deletion app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import React from 'react';
import { render } from 'react-dom';
import DocumentCapture from '../app/document-capture/components/document-capture';
import AssetContext from '../app/document-capture/context/assets';
import I18nContext from '../app/document-capture/context/i18n';

const { I18n: i18n, AssetStrings } = window.LoginGov;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This relates to a review comment at #3909 (review) which I'll need to respond to, but the idea with renaming LoginGov.I18n to i18n was to adhere to camel-case conventions with lowercase first letter. The same could apply to AssetStrings.

But it depends on how we'd want to approach the naming convention, since there is some precedent in the broader ecosystem for naming fixed enumerated sets of values in this way as PascalCase (for example, see Google style guide or TypeScript enum documentation).


const appRoot = document.getElementById('document-capture-form');
appRoot.innerHTML = '';
render(
<DocumentCapture />,
<AssetContext.Provider value={AssetStrings}>
<I18nContext.Provider value={i18n.strings[i18n.currentLocale()]}>
<DocumentCapture />
</I18nContext.Provider>
</AssetContext.Provider>,
appRoot,
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ describe('document-capture/components/document-capture', () => {
useDOM();

it('renders', () => {
const { getByText } = render(<DocumentCapture />);
const { getByAltText } = render(<DocumentCapture />);

const button = getByText('Document Capture');

expect(button).to.be.ok();
const img = getByAltText('doc_auth.headings.welcome');
expect(img).to.be.ok();
});
});
16 changes: 16 additions & 0 deletions spec/javascripts/app/document-capture/context/i18n-spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { useContext } from 'react';
import { render } from '@testing-library/react';
import I18nContext from '../../../../../app/javascript/app/document-capture/context/i18n';
import { useDOM } from '../../../support/dom';

describe('document-capture/context/i18n', () => {
useDOM();

const ContextValue = () => JSON.stringify(useContext(I18nContext));

it('defaults to empty object', () => {
const { container } = render(<ContextValue />);

expect(container.textContent).to.equal('{}');
});
});
27 changes: 27 additions & 0 deletions spec/javascripts/app/document-capture/hooks/use-i18n-spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { render } from '@testing-library/react';
import I18nContext from '../../../../../app/javascript/app/document-capture/context/i18n';
import useI18n from '../../../../../app/javascript/app/document-capture/hooks/use-i18n';
import { useDOM } from '../../../support/dom';

describe('document-capture/hooks/use-i18n', () => {
useDOM();

const LocalizedString = ({ stringKey }) => useI18n()(stringKey);

it('returns localized key value', () => {
const { container } = render(
<I18nContext.Provider value={{ sample: 'translation' }}>
<LocalizedString stringKey="sample" />
</I18nContext.Provider>,
);

expect(container.textContent).to.equal('translation');
});

it('falls back to key value', () => {
const { container } = render(<LocalizedString stringKey="sample" />);

expect(container.textContent).to.equal('sample');
});
});