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

feat: integrate i18n infrastructure #11

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion .eslintrc-typescript.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const unusedIgnorePattern = "^_";

module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended",
// prettier needs to come last
"prettier",
],
Expand Down Expand Up @@ -148,7 +151,9 @@ module.exports = {
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
argsIgnorePattern: unusedIgnorePattern,
varsIgnorePattern: unusedIgnorePattern,
destructuredArrayIgnorePattern: unusedIgnorePattern,
},
],
"@typescript-eslint/no-useless-constructor": "warn",
Expand Down
28 changes: 27 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
/* eslint-env node */
const unusedIgnorePattern = "^_";

module.exports = {
extends: ["eslint:recommended", "plugin:storybook/recommended", "prettier"],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: [
"react",
"react-hooks",
"@typescript-eslint",
"@tanstack/query",
"formatjs",
],
rules: {
curly: ["error", "all"],
"no-console": [
Expand All @@ -14,9 +22,27 @@ module.exports = {
allow: ["warn", "error"],
},
],
"no-unused-vars": "error",
"no-unused-vars": [
"error",
{
argsIgnorePattern: unusedIgnorePattern,
varsIgnorePattern: unusedIgnorePattern,
},
],
"no-extra-boolean-cast": "off",
eqeqeq: "error",
"formatjs/no-extra-boolean-cast": "off",
"formatjs/enforce-default-message": "error",
"formatjs/enforce-placeholders": "error",
"formatjs/no-multiple-whitespaces": "error",
"formatjs/no-multiple-plurals": "error",
"formatjs/no-invalid-icu": "error",
"formatjs/no-id": "error",
"formatjs/no-offset": "error",
"formatjs/no-complex-selectors": "error",
"formatjs/no-useless-message": "error",
"formatjs/prefer-formatted-message": "error",
"formatjs/prefer-pound-in-plural": "error",
},
overrides: [
{
Expand Down
11 changes: 10 additions & 1 deletion babel.config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"plugins": ["babel-plugin-styled-components"],
"plugins": [
"babel-plugin-styled-components",
[
"formatjs",
{
// keep in sync with lang/extract-messages.ts
Copy link
Owner

@momesana momesana Mar 23, 2024

Choose a reason for hiding this comment

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

This is a comment and doesn't belong in a json file (same applies to yumlet where we have introduced it).

"idInterpolationPattern": "[sha512:contenthash:base64:16]" // keep in sync with the hash used in extract script in package.json
}
]
],
"presets": [
[
"@babel/preset-env",
Expand Down
70 changes: 70 additions & 0 deletions lang/extract-messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { readdir, writeFile } from "node:fs/promises";
import { extract, compile } from "@formatjs/cli-lib";
import { resolve } from "path";
import process from "process";
import url from "url";
import type { MessageDescriptor } from "@formatjs/cli-lib";
import { supportedLocales } from "./supportedLocales.ts";

const dirname = url.fileURLToPath(new URL(".", import.meta.url));
const localesFolder = resolve(dirname, "locales");
const messagesPath = resolve(dirname, "messages.json");

async function main() {
try {
await extractMessages();
await compileLocales();
} catch (err: unknown) {
console.error(err); // eslint-disable-line no-console
}
}

async function extractMessages() {
const patternsToExclude = ["/src/api/generated/", "/src/stories/", ".d.ts"];

const files = (
await readdir(resolve(process.cwd(), "src/"), {
recursive: true,
withFileTypes: true,
})
)
.filter(
entry =>
(entry.isFile() && entry.name.endsWith("ts")) ||
entry.name.endsWith("tsx"),
)
.map(entry => resolve(entry.path, entry.name))
.filter(filePath => !patternsToExclude.some(dir => filePath.includes(dir)));

const resultAsString = await extract(files, {
idInterpolationPattern: "[sha512:contenthash:base64:16]",
flatten: true,
extractSourceLocation: true,
});

// we want to omit the attributes start and end as they only describe the
// tokens in the token stream which is most likely irrelevant to the translator
const result = Object.fromEntries(
Object.entries(
JSON.parse(resultAsString) as Record<string, MessageDescriptor>,
).map<[string, MessageDescriptor]>(
([k, { start: _ignoreStart, end: _ignoreEnd, ...rest }]) => [k, rest],
),
);

await writeFile(messagesPath, JSON.stringify(result, undefined, 4), {
encoding: "utf8",
});
}

async function compileLocales() {
const localeAsString = await compile([messagesPath]);

for (const locale of supportedLocales) {
await writeFile(resolve(localesFolder, `${locale}.json`), localeAsString, {
encoding: "utf8",
});
}
}

void main();
3 changes: 3 additions & 0 deletions lang/locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"NNQFHO0+LVD2AC/L": "Title"
}
3 changes: 3 additions & 0 deletions lang/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"NNQFHO0+LVD2AC/L": "Title"
}
3 changes: 3 additions & 0 deletions lang/locales/fa.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"NNQFHO0+LVD2AC/L": "Title"
}
9 changes: 9 additions & 0 deletions lang/messages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"NNQFHO0+LVD2AC/L": {
"col": 8,
"defaultMessage": "Title",
"description": "A title for the test page",
"file": "/home/ehsan/repos/sparse.tech/react-ts-webpack-template/src/pages/test.tsx",
"line": 29
}
}
1 change: 1 addition & 0 deletions lang/supportedLocales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const supportedLocales = ["en", "de", "fa"] as const;
3 changes: 3 additions & 0 deletions lang/translations/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"NNQFHO0+LVD2AC/L": "Diese Seite wurde zu Testzwecken erstellt"
}
3 changes: 3 additions & 0 deletions lang/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"NNQFHO0+LVD2AC/L": "This page has been created for testing purposes"
}
3 changes: 3 additions & 0 deletions lang/translations/fa.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"NNQFHO0+LVD2AC/L": "این صفحه برای اهداف آزمایشی ساخته شده است"
}
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"prepare": "husky install",
"test:unit": "jest test/unit",
"storybook": "storybook dev -p 6006",
"i18n:extract": "node --loader ts-node/esm ./lang/extract-messages.ts",
"build-storybook": "storybook build"

},
"keywords": [],
"author": "",
Expand All @@ -31,6 +31,8 @@
"@babel/preset-typescript": "^7.23.3",
"@commitlint/cli": "^19.0.3",
"@commitlint/config-conventional": "^19.0.3",
"@formatjs/cli": "^6.2.7",
"@formatjs/cli-lib": "^6.3.6",
"@jest/globals": "^29.7.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@storybook/addon-essentials": "^7.6.17",
Expand All @@ -41,16 +43,20 @@
"@storybook/react-webpack5": "^7.6.17",
"@storybook/testing-library": "^0.2.2",
"@svgr/webpack": "^8.1.0",
"@tanstack/eslint-plugin-query": "^5.27.7",
"@types/node": "^20.11.28",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"@types/styled-components": "^5.1.34",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"babel-loader": "^9.1.3",
"babel-plugin-formatjs": "^10.5.13",
"babel-plugin-styled-components": "^2.1.4",
"css-loader": "^6.10.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-formatjs": "^4.12.2",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "^0.8.0",
Expand All @@ -61,16 +67,20 @@
"react-refresh": "^0.14.0",
"storybook": "^7.6.17",
"style-loader": "^3.3.4",
"ts-node": "^10.9.2",
"type-fest": "^4.11.1",
"typescript": "^5.4.2",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.2"
},
"dependencies": {
"@tanstack/react-query": "^5.28.0",
"@tanstack/react-query-devtools": "^5.28.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.13",
"react-intl": "^6.6.2",
"styled-components": "^6.1.8"
}
}
Loading
Loading