-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enable type-safe internationalization (and translation key autocomple…
…tability) (#199) ## Ticket Resolves #194 ## Changes - Add a new `i18n-types` NPM script for generating a TypeScript declaration of the JSON English locale file(s) - [Configure i18next to be type-safe](https://www.i18next.com/overview/typescript), to prevent references to i18n key paths that don't exist in the locale files ## Context for reviewers I'm interested to get feedback on this proposal. It comes with some tradeoffs... **Pros** - Type-safe i18n! This is useful for catching translation errors early. - In addition to type safety, it also enables autocomplete capability in IDE's (see demo below). - It's not always clear to teams how to enable type-safety of i18next, so providing this out-of-the-box could be helpful for teams that want it. **Cons** - This relies on a TypeScript file that references every English JSON file. I can definitely see an engineer adding a new English locale file and getting TypeScript errors when they attempt to reference keys in it, because they're already running the development server and didn't know to run `npm run i18n-types`. - I don't love that this relies on a new dependency to generate the TypeScript file. Instructing engineers in the README to manually update this file also didn't feel like a great option. - Like other TypeScript errors, the type errors that get shown for invalid keys don't make it obvious that the error is due to the key not existing in the locale definition. There is a nice feature where it can sometimes include a "Did you mean" suggestion though (see demo below). I _think_ the benefits of type-safe i18n is worth this added potential for confusion, but I'm curious to get people's thoughts on that. ## Testing https://github.com/navapbc/template-application-nextjs/assets/371943/78388995-f823-4168-996f-6db833a4b088 Example of the test coverage to help teams identify that the file needs to be updated: <img width="1142" alt="CleanShot 2023-08-19 at 10 10 01@2x" src="https://github.com/navapbc/template-application-nextjs/assets/371943/4d7873f2-a689-4a7a-a786-1ef5286000de">
- Loading branch information
Showing
9 changed files
with
134 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** | ||
* Run 'npm run i18n-types' to generate this file | ||
*/ | ||
import common from '../../public/locales/en/common.json'; | ||
|
||
const resources = { | ||
common | ||
} as const; | ||
|
||
export default resources; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/** | ||
* @file Type-safe internationalization. See the internationalization.md | ||
* doc file for more information. | ||
*/ | ||
import "i18next"; | ||
|
||
import i18nConfig from "next-i18next.config"; | ||
|
||
import resources from "./generated-i18n-bundle"; | ||
|
||
declare module "i18next" { | ||
interface CustomTypeOptions { | ||
resources: typeof resources; | ||
defaultNS: i18nConfig.defaultNS; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/** | ||
* Test to help ensure the generated i18n TypeScript file remains up to date | ||
* with the English locale files that are present. Since the generation script | ||
* is potentially a confusing aspect of the i18n approach, this test is intended | ||
* to help bring visibility to its existence and help clarify why an engineering | ||
* might be receiving type errors in their i18n code. | ||
* @jest-environment node | ||
*/ | ||
import fs from "fs"; | ||
import generatedEnglishResources from "src/types/generated-i18n-bundle"; | ||
|
||
/** | ||
* Add a custom matcher so we can provide a more helpful test failure message | ||
*/ | ||
function toHaveI18nNamespaces(received: string[], expected: string[]) { | ||
const missingNamespaces = expected.filter( | ||
(namespace) => !received.includes(namespace) | ||
); | ||
|
||
return { | ||
pass: missingNamespaces.length === 0, | ||
message: () => { | ||
const missingNamespacesString = missingNamespaces.join(", "); | ||
let message = `The src/types/generated-i18n-bundle.ts file is missing imports for these English namespaces: ${missingNamespacesString}`; | ||
message += `\n\nYou can fix this by re-running "npm run i18n-types" to regenerate the file.`; | ||
message += `\n\nYou likely added a new namespace to the English locale files but the i18n-types script hasn't ran yet.`; | ||
message += `\n\nIt's important for the generated-i18n-bundle.ts file to be up to date so that you don't get inaccurate TypeScript errors.`; | ||
|
||
return message; | ||
}, | ||
}; | ||
} | ||
|
||
expect.extend({ toHaveI18nNamespaces }); | ||
|
||
describe("types/generated-i18n-bundle.ts", () => { | ||
it("includes all English namespaces", () => { | ||
const i18nNamespaces = fs | ||
.readdirSync("public/locales/en") | ||
.map((filename) => filename.replace(".json", "")); | ||
|
||
// Not adding a type declaration for this matcher since it is only used in this test | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call | ||
expect(Object.keys(generatedEnglishResources)).toHaveI18nNamespaces( | ||
i18nNamespaces | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters