Skip to content

Commit

Permalink
[Issue #2498] update some docs (#3202)
Browse files Browse the repository at this point in the history
* Bringing frontend and README docs up to date (plus a few updates for typos, formatting, etc.)
* Note that internationalization file was largely copy pasted from the Nava platform
  • Loading branch information
doug-s-nava authored Dec 13, 2024
1 parent cff9c79 commit 41be850
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 119 deletions.
108 changes: 68 additions & 40 deletions documentation/frontend/featureFlags.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
# Feature flags
# Background

This feature flags implementation stores feature flags settings on user cookies. The intent is for these feature flags
to be user configurable, so that 1. we can gate incomplete features to make it easier to coordinate development work,
and 2. links can be sent to individuals with customized query parameters to customize the user's experience.
Simpler Grants maintains a feature flag system within its NextJS app that currently allows for custom behavior for top level pages. Each feature flag is a simple boolean, and when turned on (or turned to `true`), the page that is set up to respond to this flag can opt out of the standard render. For example, when a "searchOff" feature flag is set to "true", the search page is configured to redirect to a maintenance page.

For codebase/deployment level feature flagging that isn't meant for users to be able to configure, those continue to be
implemented using `process.env`.
Our feature flags implementation can read feature flag values from environment variables, but can also read them from the frontend, and stores user settings on client side cookies. The intent is for these feature flags to be user configurable, so that

## Feature flags in use
1. we can gate incomplete features to make it easier to coordinate development work, and
2. links can be sent to individuals with customized query parameters to customize the user's experience.

You can find and update the feature flags in the [feature flags typescript file](/frontend/src/constants/featureFlags.ts).
This means that any codebase/deployment level behavior that isn't meant for users to be able to configure themselves via the methods mentioned above should not use feature flags.

## Non-developer usage
# Current Feature Flags

There are two ways to manage feature flags.
You can find and update the feature flags currently in use by the application in the [feature flags typescript file](/frontend/src/constants/featureFlags.ts).

# Usage

## Conventions and limitations

Feature flags will follow these conventions!

- feature flags should be named and conceived such that their default value is `false` or `off`. This allows us to easily implement custom behavior across the board when flags are turned on. For example, a feature flag to toggle the opportunity page should be something like `opportunityOff` or `disableOpportunity` and with a default value of `false`.
- feature flag names will use simple camel case naming, for example `searchOff`, `disableApplicationForm`
- names of environment variables for controlling feature flag values should match the name of the flags within the code, except
- using snake case
- all caps
- with `FEATURE_` at the front
- ex: flag `doSomethingSpecial` in code would be controlled by env var called `FEATURE_DO_SOMETHING_SPECIAL`

Feature flags have the following limitations!

- feature flags cannot control behavior of non page level components
- feature flags cannot pass props to components
- feature flags cannot affect build time behavior

Generally this means that feature flags can only currently be used to gate particular pages by redirecting them to other places, though slightly more creative usages are possible.

## Setting Flags

There are three ways to manage feature flags!

### Via environment variables

Each feature flag can be set by a corresponding environment variable based on the following the convention discussed above.

### Via the user's web browser

Have the user visit `/dev/feature-flags` in their browser; they can configure feature flags directly via the GUI. **This page is only accessible in non-production environments**.
Have the user visit `/dev/feature-flags` in their browser; they can configure feature flags directly via the GUI.

### Via query parameters set on urls

Expand All @@ -34,27 +62,33 @@ For example:
To set multiple feature flags on a single url, separate their key/value pairs with a `;`. For example:
`{url}?_ff=v2:true;another_flag:true`. If the same flag is included multiple times, the last one takes precedence.

Note, setting feature flags via the url will persist these values in the user's cookies. This is so that you can send
links to stakeholders with feature flags personalized to the experience you'd like them to experience.
Note, setting feature flags via the url will persist these values in the user's cookies. This way, users can send links to pages with feature flagged behavior. If a feature is released behind a feature flag a link could be sent to interested parties for feedback using the feature flagged url that would load the feature before it goes public. For example, to allow a particular stakeholder to try out a `v2` feature, but not necessarily enable it
for everyone else, you could simply send them the url `{url}?_ff=v2:true`.

## Resetting feature flags

To reset all feature flags to the default values in your browser cookie, add a query of `?_ff=reset` to the url.

## Precedence

For example, if you want to allow a particular stakeholder to trial out the `v2` feature, but not necessarily enable it
for everyone else, simply send them the url `{url}?_ff=v2:true`.
Each feature flag will be set with a default value within the code. This default can be overridden in the following order:

### Resetting feature flags
- environment variables
- cookies
- query params

To reset all feature flags to the default values in your browser cookie, add a query of `?_ff=reset` to the url.
This means that if an flag is set to "false" by default, an environment variable set to "true" would override this, but a cookie value set to "false" would override the environment variable, and a query param set to "true" would take precedence over everything else.

## Development usage
# Development usage

Feature flags are implemented via two interfaces:

- The `useFeatureFlags` hook
- The `FeatureFlagsManager` class

### Frontend
## Frontend

When writing frontend code, we recommend using the `useFeatureFlag` hook since it is simpler to reference and also
updates React state when updating feature flag values. You get the same interface as if you used the class directly.
When writing frontend code, we recommend using the `useFeatureFlag` hook since it is simpler to reference and also updates React state when updating feature flag values. You get the same interface as if you used the class directly.

```tsx
function MyComponent() {
Expand All @@ -79,7 +113,7 @@ function MyComponent() {
}
```

### Backend
## Backend

When writing backend code, you should use the manager class directly.

Expand All @@ -93,15 +127,15 @@ export default async function handler(request, response) {
}
```

### Adding feature flags
## Adding feature flags

To add a new feature flag, you must:

1. Add it and a default value to `FeatureFlagsManager._defaultFeatureFlags`
1. Add it to the list of feature flags in this file.
1. That's it! Everything else is handled for you!
1. Add it and a default value to the object exported from [the FeatureFlags constants file](https://github.com/HHS/simpler-grants-gov/blob/main/frontend/src/constants/featureFlags.ts)
2. If you want to control the feature flag with an environment variable add it to the list of environment variables [in the terraform](https://github.com/HHS/simpler-grants-gov/blob/main/infra/frontend/app-config/env-config/environment-variables.tf), and add variables for each environment in SSM
3. That's it! Everything else is handled for you!

### Testing
## Testing

There are a few utility methods for helping with tests that involve feature flags.

Expand Down Expand Up @@ -137,22 +171,16 @@ the feature flags integration also depends on some `node` environment features (
`jsdomNodeEnvironment` jest environment polyfills node fetch globals to `jsdom` so that your tests can both have access
to `window` and use feature flag and feature flag mocking functionality.

### How it works
## How it works

1. The next.js middleware hooks into `FeatureFlagsManager.middleware` which will take query params from the url and save
the parsed values into the feature flags cookie (before a backend view function runs).
1. The backend view function can determine if a feature flag is enabled by using `FeatureFlagsManager`. See above for
usage details.
1. The frontend components can determine if a feature flag is enabled by using `useFeatureFlags` (or
`FeatureFlagsManager` directly). See above for usage details. Note, the feature flags query params in the url are
meant for the middleware only and are ignored with normal frontend usage.
1. The next.js middleware hooks into `FeatureFlagsManager.middleware` which will take query params from the url and save the parsed values into the feature flags cookie (before a backend view function runs).
1. The backend view function can determine if a feature flag is enabled by using `FeatureFlagsManager`. See above for usage details.
1. The frontend components can determine if a feature flag is enabled by using `useFeatureFlags` (or `FeatureFlagsManager` directly). See above for usage details. Note, the feature flags query params in the url are meant for the middleware only and are ignored with normal frontend usage.

## Graceful user cookie handling
# Graceful user cookie handling

The `FeatureFlagsManager` and middleware integration intelligently and gracefully handles cookie value errors such that
if the cookie were ever to get corrupted, the site will not break.
The `FeatureFlagsManager` and middleware integration intelligently and gracefully handles cookie value errors such that if the cookie were ever to get corrupted, the site will not break.

- Invalid query param formats, invalid feature flag names, and invalid feature flag values are ignored
- If the cookie value is corrupted, a new cookie value set to the default feature flag values will be set
- The cookie is read every time we check if a feature flag is enabled, so it is also handles changes to the cookie made
by another page or request
- The cookie is read every time we check if a feature flag is enabled, so it also handles changes to the cookie made by another page or request
96 changes: 57 additions & 39 deletions documentation/frontend/internationalization.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,74 @@
# Internationalization (i18n)

- [I18next](https://www.i18next.com/) is used for internationalization.
- Next.js's [internationalized routing](https://nextjs.org/docs/advanced-features/i18n-routing) feature is enabled. Toggling between languages is done by changing the URL's path prefix (e.g. `/about` ➡️ `/es/about`).
- Configuration for the i18n routing and i18next libraries are located in [`next-i18next.config.js`](../../frontend/next-i18next.config.js).
- [storybook-react-i18next](https://storybook.js.org/addons/storybook-react-i18next) adds a globe icon to Storybook's toolbar for toggling languages.
- [next-intl](https://next-intl-docs.vercel.app) is used for internationalization. Toggling between languages is done by changing the URL's path prefix (e.g. `/about` ➡️ `/es-US/about`).
- Configuration is located in [`i18n/config.ts`](../frontend/src/i18n/config.ts). For the most part, you shouldn't need to edit this file unless adding a new formatter or new language.

## Managing translations

- Translations are managed in the `public/locales` directory, where each language has its own directory (e.g. `en` and `es`).
- [Namespaces](https://www.i18next.com/principles/namespaces) can be used to organize translations into smaller files. For large sites, it's common to create a namespace for each controller, page, or feature (whatever level makes most sense).
- There are a number of built-in formatters based on [JS's `Intl` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) that can be used in locale strings, and custom formatters can be added as well. [See the i18next formatting docs for details](https://www.i18next.com/translation-function/formatting#built-in-formats).
- Translations are managed as files in the [`i18n/messages`](../frontend/src/i18n/messages/) directory, where each language has its own directory (e.g. `en-US` and `es-US`).
- How you organize translations is up to you, but here are some suggestions:
- Group your messages. It's recommended to use component/page names as namespaces and embrace them as the primary unit of organization in your app.
- By default, all messages are in a single file, but you can split them into multiple files if you prefer. Continue to export all messages from `i18n/messages/{locale}/index.ts` so that they can be imported from a single location, and so files that depend on the messages don't need to be updated.
- There are a number of built-in formatters based on [JS's `Intl` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) that can be used in locale strings, and custom formatters can be added as well. [See the formatting docs for details](https://next-intl-docs.vercel.app/docs/usage/numbers).
- If a string's translation is missing in another language, the default language (English) will be used as a fallback.

### Type-safe translations

The app is configured to report errors if you attempt to reference an i18n key path that doesn't exist in a locale file.

[Learn more about using TypeScript with next-intl](https://next-intl-docs.vercel.app/docs/workflows/typescript).

## Load translations

1. `serverSideTranslations` must be called in [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching/get-static-props) or [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) to load translations for a page.
Locale messages should only ever be loaded on the server-side, to avoid bloating the client-side bundle. If a client component needs to access translations, only the messages required by that component should be passed into it.

```tsx
import type { GetServerSideProps } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
[See the Internationalization of Server & Client Components docs](https://next-intl-docs.vercel.app/docs/environments/server-client-components) for more details.

export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
// serverSideTranslations takes an optional second argument to limit
// which namespaces are sent to the client
const translations = await serverSideTranslations(locale ?? "en");
return { props: { ...translations } };
};
```
## Add a new language

Note that `serverSideTranslations` needs imported in the same file as the `getServerSideProps` / `getStaticProps` function, so that Next.js properly excludes it from the client-side bundle, where Node.js APIs (e.g. `fs`) aren't available.
1. Add a language folder, using the same BCP47 language tag: `mkdir -p src/i18n/messages/<lang>`
1. Add a language file: `touch src/i18n/messages/<lang>/index.ts` and add the translated content. The JSON structure should be the same across languages. However, non-default languages can omit keys, in which case the default language will be used as a fallback.
1. Update [`i18n/config.ts`](../../app/src/i18n/config.ts) to include the new language in the `locales` array.

1. Then use the `useTranslation` hook's `t()` method, or the `Trans` component to render localized strings.
## Structuring your messages

```tsx
import { Trans, useTranslation } from "next-i18next";
In terms of best practices, [it is recommended](https://next-intl-docs.vercel.app/docs/usage/messages#structuring-messages) to structure your messages such that they correspond to the component that will be using them. You can nest these definitions arbitrarily deep if you have a particularly complex need.

const Page = () => {
// Optionally pass in the namespace of the translation file (e.g. common) to use
const { t } = useTranslation("common");
return (
<>
<h1>{t("About.title")}</h1>
<Trans i18nKey="About.summary" />
</>
);
};
```
It is always preferable to structure messages per their usage rather than due to some side effect of a technological implementation. The idea is to group them semantically but also preserve maximum flexibility for a translator. For instance, splitting up a paragraph in order to separate out a link might lead to awkward translation, so it is best to keep it as a single message. The info below shows techniques for common needs that prevent unnecessary splits of content.

Refer to the [i18next](https://www.i18next.com/) and [react-i18next](https://react.i18next.com/) documentation for more usage docs.
### Variables

## Add a new language
Messages do not need to be split in order to incorporate dynamic data. Instead, these can be inserted via the [interpolation functionality](https://next-intl-docs.vercel.app/docs/usage/messages#interpolation-of-dynamic-values):

```json
"message": "Hello {name}!"
```

```tsx
t("message", { name: "Jane" }); // "Hello Jane!"
```

### Rich text messages

If your app needs a particular chunk of content to contain something other than plain text (such as links, formatting, or a custom component), you can utilize the "rich text" functionality ([see docs](https://next-intl-docs.vercel.app/docs/usage/messages#rich-text)). This allows one to embed arbitrary custom tags into the translation content strings and specify how each of those should be handled.

Example from their docs:

```json
{
"message": "Please refer to <guidelines>the guidelines</guidelines>."
}
```

```tsx
// Returns `<>Please refer to <a href="/guidelines">the guidelines</a>.</>`
t.rich("message", {
guidelines: (chunks) => <a href="/guidelines">{chunks}</a>,
});
```

If you have something that you are going to use repeatedly throughout your app, you can specify it in the [`defaultTranslationValues` config](https://next-intl-docs.vercel.app/docs/usage/configuration#default-translation-values).

### Other needs

1. Edit `next-i18next.config.js` and add the language to `locales`, using the BCP47 language tag (e.g. `en` or `es`).
1. Add a language folder, using the same BCP47 language tag: `mkdir -p public/locales/<lang>`
1. Add a language file: `touch public/locales/<lang>/common.json` and add the translated content. The JSON structure should be the same across languages. However, non-default languages can omit keys, in which case the default language will be used as a fallback.
1. Optionally, add a label for the language to the `locales` object in [`.storybook/preview.js`](../../frontend/.storybook/preview.js)
For examples of other functionality such as pluralization, arrays of content, etc, please [see the docs](https://next-intl-docs.vercel.app/docs/usage/messages).
Loading

0 comments on commit 41be850

Please sign in to comment.