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 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
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
37 changes: 37 additions & 0 deletions babel.config.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module.exports = {
plugins: [
"babel-plugin-styled-components",
[
"formatjs",
{
// keep in sync with lang/extract-messages.ts
idInterpolationPattern: "[sha512:contenthash:base64:16]", // keep in sync with the hash used in extract script in package.json
},
],
],
presets: [
[
"@babel/preset-env",
{
targets: {
chrome: "100",
},
useBuiltIns: "usage",
corejs: "3.21.1",
},
],
[
"@babel/preset-react",
{
runtime: "automatic",
},
],
[
"@babel/preset-typescript",
{
isTSX: true,
allExtensions: true,
},
],
],
};
28 changes: 0 additions & 28 deletions babel.config.json

This file was deleted.

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