From 1cc112802c2b94cc61886e197d12eb4a4c434631 Mon Sep 17 00:00:00 2001 From: Kamil Jarosz Date: Sun, 17 May 2026 00:22:33 +0200 Subject: [PATCH 1/3] Add static i18n This adds i18n with minimal changes. The package produces a set of translated websites available under different paths (/, /pl-PL/, etc.). This uses simple JSON-based translations files and a custom translate function that translates the website completely server-side. --- .gitignore | 1 + eslint.config.mjs | 10 + messages/en-US.json | 4 + messages/pl-PL.json | 4 + next.config.js | 18 +- package-lock.json | 519 ++++++++++++++++++++++++++++++++++++++ package.json | 2 + scripts/build-i18n.ts | 32 +++ src/app/layout.tsx | 3 +- src/app/page.tsx | 5 +- src/components/footer.tsx | 3 +- src/i18n/messages.d.ts | 4 + src/i18n/t.ts | 12 + 13 files changed, 611 insertions(+), 6 deletions(-) create mode 100644 messages/en-US.json create mode 100644 messages/pl-PL.json create mode 100644 scripts/build-i18n.ts create mode 100644 src/i18n/messages.d.ts create mode 100644 src/i18n/t.ts diff --git a/.gitignore b/.gitignore index e595a637..64bfecf1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # next.js /.next/ /out/ +/dist # production /build diff --git a/eslint.config.mjs b/eslint.config.mjs index 4d3362ec..d9e7eb0a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,6 +41,16 @@ export default [ files: ["postcss.config.js", "next.config.js"], languageOptions: { sourceType: "commonjs", + globals: { + require: "readonly", + module: "writable", + __dirname: "readonly", + __filename: "readonly", + process: "readonly", + }, + }, + rules: { + "@typescript-eslint/no-require-imports": "off", }, }, ]; diff --git a/messages/en-US.json b/messages/en-US.json new file mode 100644 index 00000000..c6889463 --- /dev/null +++ b/messages/en-US.json @@ -0,0 +1,4 @@ +{ + "home.title": "An open source Flash Player emulator", + "footer.tagline": "Putting Flash back on the web" +} diff --git a/messages/pl-PL.json b/messages/pl-PL.json new file mode 100644 index 00000000..2ef761bd --- /dev/null +++ b/messages/pl-PL.json @@ -0,0 +1,4 @@ +{ + "home.title": "Emulator Flash Playera, który jest open source", + "footer.tagline": "Przywracamy Flasha do sieci" +} diff --git a/next.config.js b/next.config.js index 42d1ba58..8b410429 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,10 @@ // @ts-check +const path = require("path"); + +const locale = process.env.NEXT_PUBLIC_BUILD_LOCALE || "en-US"; +const defaultLocale = "en-US"; + /** * @type {import('next').NextConfig} **/ @@ -8,7 +13,18 @@ const nextConfig = { images: { unoptimized: true, }, - basePath: "", + basePath: locale === defaultLocale ? "" : `/${locale}`, + turbopack: { + resolveAlias: { + "i18n:messages": `./messages/${locale}.json`, + }, + }, + webpack: (config) => { + config.resolve.alias["i18n:messages"] = path.resolve( + `./messages/${locale}.json`, + ); + return config; + }, }; module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 8f19891f..ef087563 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "semver": "^7.8.0", + "tsx": "^4.19.4", "typescript": "^6.0.3" } }, @@ -568,6 +569,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -4720,6 +5163,48 @@ "benchmarks" ] }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5499,6 +5984,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -9656,6 +10156,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz", + "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index b1848fdc..e9ed7037 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev", "build": "next build", + "build:i18n": "tsx scripts/build-i18n.ts", "start": "next start", "lint": "eslint .", "format": "npx prettier . --write" @@ -50,6 +51,7 @@ "rehype-raw": "^7.0.0", "rehype-slug": "^6.0.0", "semver": "^7.8.0", + "tsx": "^4.19.4", "typescript": "^6.0.3" } } diff --git a/scripts/build-i18n.ts b/scripts/build-i18n.ts new file mode 100644 index 00000000..8292a43a --- /dev/null +++ b/scripts/build-i18n.ts @@ -0,0 +1,32 @@ +import { execSync } from "child_process"; +import { readdirSync, rmSync, cpSync, mkdirSync } from "fs"; +import { basename, resolve } from "path"; + +const DIST_DIR = "dist"; +const DEFAULT_LOCALE = "en-US"; + +rmSync(DIST_DIR, { recursive: true, force: true }); +mkdirSync(DIST_DIR); + +const locales = readdirSync("messages") + .filter((f) => f.endsWith(".json")) + .map((f) => basename(f, ".json")); + +for (const locale of locales) { + console.log(`==> Building locale: ${locale}`); + + execSync("npm run build", { + stdio: "inherit", + env: { ...process.env, NEXT_PUBLIC_BUILD_LOCALE: locale }, + }); + + if (locale === DEFAULT_LOCALE) { + cpSync("out", DIST_DIR, { recursive: true }); + } else { + const dest = resolve(DIST_DIR, locale); + mkdirSync(dest, { recursive: true }); + cpSync("out", dest, { recursive: true }); + } +} + +console.log(`==> Done. Output in ${DIST_DIR}/`); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 87428efa..ca28904b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import { MantineProvider, ColorSchemeScript } from "@mantine/core"; import { cssResolver, theme } from "@/theme"; import { Header } from "@/components/header"; import { FooterSocial } from "@/components/footer"; +import { locale } from "@/i18n/t"; export const metadata: Metadata = { title: "Ruffle - Flash Emulator", @@ -30,7 +31,7 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + {/**/} diff --git a/src/app/page.tsx b/src/app/page.tsx index 2be81104..f66854b7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -16,6 +16,7 @@ import { IconCheck } from "@tabler/icons-react"; import React from "react"; import { getLatestRelease } from "@/app/downloads/github"; import { GithubRelease } from "./downloads/config"; +import { t } from "@/i18n/t"; const InteractiveLogo = dynamic(() => import("../components/logo"), { ssr: false, @@ -44,9 +45,7 @@ export default function Home() { - - An open source Flash Player emulator - + {t("home.title")}
- Putting Flash back on the web + {t("footer.tagline")} ; + export default messages; +} diff --git a/src/i18n/t.ts b/src/i18n/t.ts new file mode 100644 index 00000000..23a30507 --- /dev/null +++ b/src/i18n/t.ts @@ -0,0 +1,12 @@ +import messages from "i18n:messages"; + +export const locale = process.env.NEXT_PUBLIC_BUILD_LOCALE || "en-US"; + +export function t( + key: string, + params?: Record, +): string { + const message = (messages as Record)[key] ?? key; + if (!params) return message; + return message.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`)); +} From a22bf88d069ab19b84f776d2b29762aa98d591e0 Mon Sep 17 00:00:00 2001 From: Kamil Jarosz Date: Sun, 17 May 2026 01:39:13 +0200 Subject: [PATCH 2/3] Add language selector This adds a language selector at the bottom right that allows switching between different languages. --- messages/en-US.json | 1 + messages/pl-PL.json | 1 + src/components/footer.module.css | 9 ++++++ src/components/footer.tsx | 31 +++++++++++++----- src/components/locale-selector.module.css | 12 +++++++ src/components/locale-selector.tsx | 38 +++++++++++++++++++++++ src/i18n/locales.ts | 17 ++++++++++ 7 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 src/components/locale-selector.module.css create mode 100644 src/components/locale-selector.tsx create mode 100644 src/i18n/locales.ts diff --git a/messages/en-US.json b/messages/en-US.json index c6889463..8e9d12ee 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1,4 +1,5 @@ { + "locale.name": "English – United States", "home.title": "An open source Flash Player emulator", "footer.tagline": "Putting Flash back on the web" } diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 2ef761bd..db30a111 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -1,4 +1,5 @@ { + "locale.name": "Polski – Polska", "home.title": "Emulator Flash Playera, który jest open source", "footer.tagline": "Przywracamy Flasha do sieci" } diff --git a/src/components/footer.module.css b/src/components/footer.module.css index 5bb0232e..96d3d40f 100644 --- a/src/components/footer.module.css +++ b/src/components/footer.module.css @@ -29,8 +29,17 @@ } } +.linkbox { + display: flex; + flex-direction: column; + align-items: baseline; + flex-grow: 1; +} + .links { @media (max-width: $mantine-breakpoint-xs) { margin-top: var(--mantine-spacing-md); } + + margin-bottom: rem(10); } diff --git a/src/components/footer.tsx b/src/components/footer.tsx index a1f44637..4f097946 100644 --- a/src/components/footer.tsx +++ b/src/components/footer.tsx @@ -1,6 +1,8 @@ import { Container, Group, ActionIcon, rem, Text } from "@mantine/core"; import Link from "@/components/link"; -import { t } from "@/i18n/t"; +import { t, locale } from "@/i18n/t"; +import { getLocales, defaultLocale } from "@/i18n/locales"; +import { LocaleSelector } from "@/components/locale-selector"; import { IconBrandX, @@ -47,6 +49,8 @@ const allSocials = [ ]; export function FooterSocial() { + const locales = getLocales(); + const socials = allSocials.map((social, i) => ( - - {socials} + + + + {socials} + + + + +
diff --git a/src/components/locale-selector.module.css b/src/components/locale-selector.module.css new file mode 100644 index 00000000..363e4321 --- /dev/null +++ b/src/components/locale-selector.module.css @@ -0,0 +1,12 @@ +.select { + color: var(--ruffle-orange); + background: var(--ruffle-blue-7); + border: 0px; + padding: 2px 6px; + border-radius: var(--mantine-radius-sm); + font-size: var(--mantine-font-size-sm); +} + +.select:hover { + background: var(--ruffle-blue-6); +} diff --git a/src/components/locale-selector.tsx b/src/components/locale-selector.tsx new file mode 100644 index 00000000..4ace0f99 --- /dev/null +++ b/src/components/locale-selector.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import classes from "./locale-selector.module.css"; + +interface Props { + locales: { code: string; name: string }[]; + defaultLocale: string; + currentLocale: string; +} + +export function LocaleSelector({ + locales, + defaultLocale, + currentLocale, +}: Props) { + const pathname = usePathname(); + + function onChange(e: React.ChangeEvent) { + const code = e.target.value; + window.location.href = + code === defaultLocale ? pathname : `/${code}${pathname}`; + } + + return ( + + ); +} diff --git a/src/i18n/locales.ts b/src/i18n/locales.ts new file mode 100644 index 00000000..f148a965 --- /dev/null +++ b/src/i18n/locales.ts @@ -0,0 +1,17 @@ +import { readdirSync, readFileSync } from "fs"; +import { basename, resolve } from "path"; + +export const defaultLocale = "en-US"; + +export function getLocales(): { code: string; name: string }[] { + const dir = resolve(process.cwd(), "messages"); + return readdirSync(dir) + .filter((f) => f.endsWith(".json")) + .map((f) => { + const code = basename(f, ".json"); + const messages = JSON.parse( + readFileSync(resolve(dir, f), "utf-8"), + ) as Record; + return { code, name: messages["locale.name"] ?? code }; + }); +} From f50a0234b0f84e7ea12c3805a48fc773edae21bd Mon Sep 17 00:00:00 2001 From: Kamil Jarosz Date: Sun, 17 May 2026 01:39:17 +0200 Subject: [PATCH 3/3] Update deployment to use the i18n build --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 063954a6..e0755bca 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -72,14 +72,14 @@ jobs: - name: Install dependencies run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} - name: Build with Next.js - run: ${{ steps.detect-package-manager.outputs.runner }} next build + run: ${{ steps.detect-package-manager.outputs.runner }} npm run build:i18n env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BASE_URL: https://ruffle.rs - name: Upload artifact uses: actions/upload-pages-artifact@v5 with: - path: ./out + path: ./dist # Deployment job deploy: