From 4a5691404c1074e7a578dce892733fc936b8cdaa Mon Sep 17 00:00:00 2001 From: Dessalines Date: Mon, 22 Mar 2021 09:22:23 -0400 Subject: [PATCH] Adding i18n support. Fixes #6 (#34) Co-authored-by: Nutomic --- .gitignore | 1 + .gitmodules | 4 + generate_translations.js | 68 +++++++++++ joinlemmy-translations | 1 + lemmy-docs | 2 +- package.json | 7 +- src/client/index.tsx | 4 + src/server/index.tsx | 7 ++ src/shared/components/app.tsx | 32 ++--- src/shared/components/apps.tsx | 11 +- src/shared/components/contact.tsx | 6 +- src/shared/components/donate-lines.tsx | 16 +-- src/shared/components/footer.tsx | 18 +-- src/shared/components/join.tsx | 7 +- src/shared/components/link-line.tsx | 18 +-- src/shared/components/main.tsx | 163 ++++++++++++------------- src/shared/components/sponsors.tsx | 11 +- src/shared/i18next.ts | 29 +++++ yarn.lock | 38 +++++- 19 files changed, 297 insertions(+), 146 deletions(-) create mode 100644 generate_translations.js create mode 160000 joinlemmy-translations create mode 100644 src/shared/i18next.ts diff --git a/.gitignore b/.gitignore index b7005b5..48fbe70 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ node_modules *.log *.swp *.orig +src/shared/translations diff --git a/.gitmodules b/.gitmodules index 353d4dd..079430b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,7 @@ path = lemmy-docs url = https://github.com/lemmynet/lemmy-docs branch = main +[submodule "joinlemmy-translations"] + path = joinlemmy-translations + url = https://github.com/lemmynet/joinlemmy-translations + branch = main diff --git a/generate_translations.js b/generate_translations.js new file mode 100644 index 0000000..60a37ba --- /dev/null +++ b/generate_translations.js @@ -0,0 +1,68 @@ +const fs = require("fs"); + +const translationDir = "joinlemmy-translations/translations/"; +const outDir = "src/shared/translations/"; +fs.mkdirSync(outDir, { recursive: true }); +fs.readdir(translationDir, (_err, files) => { + files.forEach(filename => { + const lang = filename.split(".")[0]; + try { + const json = JSON.parse( + fs.readFileSync(translationDir + filename, "utf8") + ); + let data = `export const ${lang} = {\n translation: {`; + for (const key in json) { + if (key in json) { + const value = json[key].replace(/"/g, '\\"'); + data += `\n ${key}: "${value}",`; + } + } + data += "\n },\n};"; + const target = outDir + lang + ".ts"; + fs.writeFileSync(target, data); + } catch (err) { + console.error(err); + } + }); +}); + +// generate types for i18n keys +const baseLanguage = "en"; + +fs.readFile(`${translationDir}${baseLanguage}.json`, "utf8", (_, fileStr) => { + const keys = Object.keys(JSON.parse(fileStr)); + + const data = `import { i18n } from "i18next"; + +declare module "i18next" { + export type I18nKeys = +${keys.map(key => ` | "${key}"`).join("\n")}; + + export interface TFunctionTyped { + // basic usage + < + TResult extends TFunctionResult = string, + TInterpolationMap extends Record = StringMap + >( + key: I18nKeys | I18nKeys[], + options?: TOptions | string + ): TResult; + // overloaded usage + < + TResult extends TFunctionResult = string, + TInterpolationMap extends Record = StringMap + >( + key: I18nKeys | I18nKeys[], + defaultValue?: string, + options?: TOptions | string + ): TResult; + } + + export interface i18nTyped extends i18n { + t: TFunctionTyped; + } +} +`; + + fs.writeFileSync(`${outDir}i18next.d.ts`, data); +}); diff --git a/joinlemmy-translations b/joinlemmy-translations new file mode 160000 index 0000000..c6811ee --- /dev/null +++ b/joinlemmy-translations @@ -0,0 +1 @@ +Subproject commit c6811ee56e38bf38eaa0fb4e9d5b4665f51d92c0 diff --git a/lemmy-docs b/lemmy-docs index 25e104c..322a2a7 160000 --- a/lemmy-docs +++ b/lemmy-docs @@ -1 +1 @@ -Subproject commit 25e104c1cd6f2a2fc5de095314747814d3d0c71d +Subproject commit 322a2a730ccd3835bbb1391d47daeab2a1492f32 diff --git a/package.json b/package.json index 0d156af..192fad4 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,10 @@ "build:dev": "webpack --mode=development", "build:prod": "webpack --mode=production", "clean": "yarn run rimraf dist", - "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src", + "lint": "node generate_translations.js && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src", "postinstall": "husky install", - "prebuild:dev": "yarn clean", - "prebuild:prod": "yarn clean", + "prebuild:dev": "yarn clean && node generate_translations.js", + "prebuild:prod": "yarn clean && node generate_translations.js", "start": "yarn build:dev --watch" }, "repository": "https://github.com/LemmyNet/joinlemmy-site", @@ -23,6 +23,7 @@ "inferno-create-element": "^7.4.8", "inferno-helmet": "^5.2.1", "inferno-hydrate": "^7.4.8", + "inferno-i18next": "github:nimbusec-oss/inferno-i18next#semver:^7.4.2", "inferno-router": "^7.4.8", "inferno-server": "^7.4.8" }, diff --git a/src/client/index.tsx b/src/client/index.tsx index ee7bef9..81808d2 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -1,6 +1,10 @@ import { hydrate } from "inferno-hydrate"; import { BrowserRouter } from "inferno-router"; import { App } from "../shared/components/app"; +import { i18n } from "../shared/i18next"; + +// Setting the language for js browsers +i18n.changeLanguage(navigator.language); const wrapper = ( diff --git a/src/server/index.tsx b/src/server/index.tsx index 8b75d3a..cfc3ae0 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -7,6 +7,7 @@ import { App } from "../shared/components/app"; // import { routes } from "../shared/routes"; import process from "process"; import { Helmet } from "inferno-helmet"; +import { i18n } from "../shared/i18next"; const server = express(); const port = 1234; @@ -21,6 +22,12 @@ server.get("/*", async (req, res) => { // const activeRoute = routes.find(route => matchPath(req.path, route)) || {}; const context = {} as any; + // Setting the language for non-js browsers + let lang = req.headers["accept-language"] + ? req.headers["accept-language"].split(",")[0] + : "en"; + i18n.changeLanguage(lang); + const wrapper = ( diff --git a/src/shared/components/app.tsx b/src/shared/components/app.tsx index ecc9da8..e204059 100644 --- a/src/shared/components/app.tsx +++ b/src/shared/components/app.tsx @@ -1,5 +1,7 @@ import { Component } from "inferno"; import { Route, Switch } from "inferno-router"; +import { Provider } from "inferno-i18next"; +import { i18n } from "../i18next"; import { routes } from "../routes"; import { NoMatch } from "./no-match"; import { Symbols } from "./symbols"; @@ -15,20 +17,22 @@ export class App extends Component { return ( <>
- - - {routes.map(({ path, exact, component: C, ...rest }) => ( - } - /> - ))} - } /> - -
- + + + + {routes.map(({ path, exact, component: C, ...rest }) => ( + } + /> + ))} + } /> + +
+ +
); diff --git a/src/shared/components/apps.tsx b/src/shared/components/apps.tsx index 444133f..a0779db 100644 --- a/src/shared/components/apps.tsx +++ b/src/shared/components/apps.tsx @@ -1,8 +1,9 @@ import { Component } from "inferno"; import { AppDetails } from "./app-details"; import { Helmet } from "inferno-helmet"; +import { i18n } from "../i18next"; -const title = "Lemmy - Apps and Libraries"; +const title = i18n.t("apps_title"); export class Apps extends Component { constructor(props: any, context: any) { @@ -15,8 +16,8 @@ export class Apps extends Component {
-

Lemmy Apps

-

Choose from any of the apps below.

+

{i18n.t("lemmy_apps")}

+

{i18n.t("choose_from_apps")}

@@ -66,7 +67,7 @@ export class Apps extends Component {
-

Web Apps

+

{i18n.t("web_apps")}

@@ -100,7 +101,7 @@ export class Apps extends Component {
-

Lemmy API Libraries

+

{i18n.t("api_libraries")}

  • diff --git a/src/shared/components/contact.tsx b/src/shared/components/contact.tsx index e16d78a..7bdb1df 100644 --- a/src/shared/components/contact.tsx +++ b/src/shared/components/contact.tsx @@ -1,7 +1,8 @@ import { Component } from "inferno"; import { Helmet } from "inferno-helmet"; +import { i18n } from "../i18next"; -const title = "Lemmy - Contact"; +const title = i18n.t("contact_title"); export class Contact extends Component { constructor(props: any, context: any) { @@ -14,8 +15,7 @@ export class Contact extends Component {
    -

    Contact

    - +

    {i18n.t("contact")}

    • Mastodon diff --git a/src/shared/components/donate-lines.tsx b/src/shared/components/donate-lines.tsx index 43bf30e..362136a 100644 --- a/src/shared/components/donate-lines.tsx +++ b/src/shared/components/donate-lines.tsx @@ -1,4 +1,7 @@ import { Component } from "inferno"; +import { Link } from "inferno-router"; +import { i18n } from "../i18next"; +import { T } from "inferno-i18next"; export class DonateLines extends Component { constructor(props: any, context: any) { @@ -8,20 +11,19 @@ export class DonateLines extends Component { return ( <>

      - Lemmy is free, open-source software, meaning no advertising, - monetizing, or venture capital, ever.{" "} - Your donations directly support full-time - development of the project. + + ### +

      @@ -29,7 +31,7 @@ export class DonateLines extends Component { class="col button primary" href="https://opencollective.com/lemmy" > - Support on OpenCollective + {i18n.t("support_on_opencollective")}
      diff --git a/src/shared/components/footer.tsx b/src/shared/components/footer.tsx index 0a670e9..fad00c4 100644 --- a/src/shared/components/footer.tsx +++ b/src/shared/components/footer.tsx @@ -1,5 +1,6 @@ import { Component } from "inferno"; import { LinkLine } from "./link-line"; +import { T } from "inferno-i18next"; export class Footer extends Component { constructor(props: any, context: any) { @@ -13,14 +14,15 @@ export class Footer extends Component {