Merge branch 'type-i18n' into 'develop'
dev: Add type annotations to i18n Co-authored-by: Lhcfl <Lhcfl@outlook.com> See merge request firefish/firefish!10773
This commit is contained in:
commit
0d27f038da
|
@ -8,4 +8,5 @@ This directory contains all of the packages Firefish uses.
|
|||
- `client`: Web interface written in Vue3 and TypeScript
|
||||
- `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript
|
||||
- `firefish-js`: TypeScript SDK for both backend and client
|
||||
- `i18n`: Typed i18n for client
|
||||
- `megalodon`: TypeScript library used for partial Mastodon API compatibility
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"gsap": "^3.12.5",
|
||||
"idb-keyval": "6.2.1",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"i18n": "workspace:*",
|
||||
"json5": "2.2.3",
|
||||
"katex": "0.16.10",
|
||||
"long": "^5.2.3",
|
||||
|
|
|
@ -1,36 +1,6 @@
|
|||
import { markRaw } from "vue";
|
||||
import { locale } from "@/config";
|
||||
|
||||
class I18n<T extends Record<string, any>> {
|
||||
public ts: T;
|
||||
|
||||
constructor(locale: T) {
|
||||
this.ts = locale;
|
||||
|
||||
// #region BIND
|
||||
this.t = this.t.bind(this);
|
||||
// #endregion
|
||||
}
|
||||
|
||||
// string にしているのは、ドット区切りでのパス指定を許可するため
|
||||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
public t(key: string, args?: Record<string, string | number>): string {
|
||||
try {
|
||||
let str = key
|
||||
.split(".")
|
||||
.reduce((o, i) => o[i], this.ts) as unknown as string;
|
||||
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
str = str.replaceAll(`{${k}}`, v.toString());
|
||||
}
|
||||
}
|
||||
return str;
|
||||
} catch (err) {
|
||||
console.warn(`missing localization '${key}'`);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
import { I18n } from "i18n";
|
||||
|
||||
export const i18n = markRaw(new I18n(locale));
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"dynamicImport": true,
|
||||
"decorators": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"experimental": {
|
||||
"emitAssertForImportAttributes": true
|
||||
},
|
||||
"target": "es2022"
|
||||
},
|
||||
"minify": false,
|
||||
"module": {
|
||||
"type": "es6",
|
||||
"strict": true,
|
||||
"resolveFully": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Firefish contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,3 @@
|
|||
# i18n
|
||||
|
||||
## Typed i18n for firefish
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "i18n",
|
||||
"version": "0.0.1",
|
||||
"description": "Firefish i18n for TypeScript",
|
||||
"homepage": "https://firefish.dev/firefish/firefish/-/tree/develop/packages/i18n",
|
||||
"main": "./built/index.js",
|
||||
"types": "./src/index.ts",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build:types": "node scripts/build.js",
|
||||
"build": "pnpm run build:types && pnpm swc src --out-dir built --source-maps false --copy-files --strip-leading-paths",
|
||||
"build:debug": "pnpm run build:types && pnpm swc src --out-dir built --source-maps true --copy-files --strip-leading-paths",
|
||||
"watch": "pnpm swc src --out-dir built --source-maps true --copy-files --strip-leading-paths --watch",
|
||||
"lint": "pnpm biome check --apply src",
|
||||
"format": "pnpm biome format --write src",
|
||||
"jest": "echo NOT_IMPLEMENTED",
|
||||
"test": "pnpm jest && pnpm tsd"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://firefish.dev/firefish/firefish.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "0.3.12",
|
||||
"@swc/core": "1.5.0",
|
||||
"@swc/types": "^0.1.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "20.12.7",
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"files": [
|
||||
"built", "src"
|
||||
],
|
||||
"dependencies": {
|
||||
"js-yaml": "4.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Build I18n types from en-US.yml
|
||||
*/
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import fs from "node:fs";
|
||||
|
||||
const locale = yaml.load(fs.readFileSync("../../locales/en-US.yml"));
|
||||
|
||||
function safeStr(str) {
|
||||
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(str) ? str : `"${str}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} obj
|
||||
* @param {string} prefix
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function generateKeys(obj, prefix = "") {
|
||||
const keys = [];
|
||||
for (const [key, o] of Object.entries(obj)) {
|
||||
const keyStr = safeStr(`${prefix}${key}`);
|
||||
if (typeof o === 'string') {
|
||||
const vars = o.match(/\{[A-Za-z0-9_]+\}/g);
|
||||
if (vars == null || vars.length === 0) {
|
||||
keys.push(`
|
||||
/** ${o.replaceAll("\n", " \n * ") } */
|
||||
${keyStr}: {
|
||||
param: Record<string, never>;
|
||||
result: ${JSON.stringify(o)};
|
||||
}`);
|
||||
} else {
|
||||
keys.push(`
|
||||
/** ${o.replaceAll("\n", " \n * ") } */
|
||||
${keyStr}: {
|
||||
param: {
|
||||
${
|
||||
[...new Set(vars)].map(v => `${v.slice(1, -1)}: any;`).join("\n\t\t")
|
||||
}
|
||||
};
|
||||
result: ${JSON.stringify(o)};
|
||||
}`);
|
||||
}
|
||||
} else if (typeof o === 'object') {
|
||||
keys.push(generateKeys(o, `${prefix}${key}.`));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function generateValues(obj) {
|
||||
const keys = [];
|
||||
for (const [key, o] of Object.entries(obj)) {
|
||||
const keyStr = safeStr(key);
|
||||
if (typeof o === 'string') {
|
||||
// const vars = o.match(/\{[A-Za-z0-9_]+\}/g);
|
||||
keys.push(`\n/** ${o.replaceAll("\n", " \n * ") } */\n${keyStr}: ${JSON.stringify(o)}`);
|
||||
} else if (typeof o === 'object') {
|
||||
keys.push(`\n${keyStr}: {\n\t${
|
||||
generateValues(o).replaceAll("\n", "\n\t")
|
||||
}\n}`);
|
||||
}
|
||||
}
|
||||
return keys.join("\n");
|
||||
}
|
||||
|
||||
const typestr = `
|
||||
export interface I18nKeys {
|
||||
${generateKeys(locale).flat(100).join("\n").replaceAll("\n", "\n\t")}
|
||||
}
|
||||
|
||||
export interface I18nValues {
|
||||
${generateValues(locale).replaceAll("\n", "\n\t")}
|
||||
}`;
|
||||
|
||||
fs.mkdirSync("built", { recursive: true });
|
||||
|
||||
fs.writeFileSync("built/types.d.ts", typestr);
|
|
@ -0,0 +1,63 @@
|
|||
import type { I18nKeys, I18nValues } from "../built/types.d.ts";
|
||||
|
||||
const undefinedProxy: object = new Proxy(
|
||||
{},
|
||||
{
|
||||
get() {
|
||||
return undefinedProxy;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: used intentially
|
||||
type ExplicitAny = any;
|
||||
|
||||
const makeProxy: <T extends object>(_any: T) => T = (target: ExplicitAny) =>
|
||||
new Proxy(target, {
|
||||
get(target, prop) {
|
||||
if (target[prop] == null) {
|
||||
return undefinedProxy;
|
||||
}
|
||||
if (typeof target[prop] === "object") {
|
||||
return makeProxy(target[prop]);
|
||||
}
|
||||
return target[prop];
|
||||
},
|
||||
});
|
||||
|
||||
export class I18n {
|
||||
public ts: I18nValues;
|
||||
private _ts: I18nValues;
|
||||
|
||||
constructor(locale: I18nValues) {
|
||||
this.ts = makeProxy(locale);
|
||||
this._ts = locale;
|
||||
|
||||
// #region BIND
|
||||
this.t = this.t.bind(this);
|
||||
// #endregion
|
||||
}
|
||||
|
||||
// string にしているのは、ドット区切りでのパス指定を許可するため
|
||||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
public t<K extends keyof I18nKeys>(
|
||||
key: K,
|
||||
args?: I18nKeys[K]["param"],
|
||||
): I18nKeys[K]["result"] {
|
||||
try {
|
||||
let str = key
|
||||
.split(".")
|
||||
.reduce((o, i) => o[i as never], this._ts) as unknown as string;
|
||||
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
str = str.replaceAll(`{${k}}`, v.toString());
|
||||
}
|
||||
}
|
||||
return str as never;
|
||||
} catch (err) {
|
||||
console.warn(`missing localization '${key}'`);
|
||||
return key as never;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"module": "es2022",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./built/",
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitReturns": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "test/**/*"]
|
||||
}
|
|
@ -659,6 +659,9 @@ importers:
|
|||
gsap:
|
||||
specifier: ^3.12.5
|
||||
version: 3.12.5
|
||||
i18n:
|
||||
specifier: workspace:*
|
||||
version: link:../i18n
|
||||
idb-keyval:
|
||||
specifier: 6.2.1
|
||||
version: 6.2.1
|
||||
|
@ -824,6 +827,35 @@ importers:
|
|||
specifier: 5.4.5
|
||||
version: 5.4.5
|
||||
|
||||
packages/i18n:
|
||||
dependencies:
|
||||
js-yaml:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
optionalDependencies:
|
||||
'@swc/core-android-arm64':
|
||||
specifier: 1.3.11
|
||||
version: 1.3.11
|
||||
devDependencies:
|
||||
'@swc/cli':
|
||||
specifier: 0.3.12
|
||||
version: 0.3.12(@swc/core@1.5.0)
|
||||
'@swc/core':
|
||||
specifier: 1.5.0
|
||||
version: 1.5.0
|
||||
'@swc/types':
|
||||
specifier: ^0.1.6
|
||||
version: 0.1.6
|
||||
'@types/js-yaml':
|
||||
specifier: ^4.0.9
|
||||
version: 4.0.9
|
||||
'@types/node':
|
||||
specifier: 20.12.7
|
||||
version: 20.12.7
|
||||
typescript:
|
||||
specifier: 5.4.5
|
||||
version: 5.4.5
|
||||
|
||||
packages/megalodon:
|
||||
dependencies:
|
||||
'@types/oauth':
|
||||
|
@ -2759,6 +2791,49 @@ packages:
|
|||
slash: 3.0.0
|
||||
dev: true
|
||||
|
||||
/@jest/core@29.7.0:
|
||||
resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
|
||||
peerDependenciesMeta:
|
||||
node-notifier:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
'@jest/reporters': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.12.7
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.7.0
|
||||
jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2)
|
||||
jest-haste-map: 29.7.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-resolve-dependencies: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-runtime: 29.7.0
|
||||
jest-snapshot: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
jest-watcher: 29.7.0
|
||||
micromatch: 4.0.5
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/@jest/core@29.7.0(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -2930,9 +3005,9 @@ packages:
|
|||
resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@babel/core': 7.23.2
|
||||
'@babel/core': 7.24.4
|
||||
'@jest/types': 29.6.3
|
||||
'@jridgewell/trace-mapping': 0.3.20
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
babel-plugin-istanbul: 6.1.1
|
||||
chalk: 4.1.2
|
||||
convert-source-map: 2.0.0
|
||||
|
@ -3678,7 +3753,7 @@ packages:
|
|||
'@swc/counter': 0.1.3
|
||||
commander: 8.3.0
|
||||
fast-glob: 3.3.2
|
||||
minimatch: 9.0.3
|
||||
minimatch: 9.0.4
|
||||
piscina: 4.4.0
|
||||
semver: 7.6.0
|
||||
slash: 3.0.0
|
||||
|
@ -4114,6 +4189,10 @@ packages:
|
|||
pretty-format: 29.7.0
|
||||
dev: true
|
||||
|
||||
/@types/js-yaml@4.0.9:
|
||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||
dev: true
|
||||
|
||||
/@types/json-schema@7.0.12:
|
||||
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||
dev: true
|
||||
|
@ -5158,7 +5237,6 @@ packages:
|
|||
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/adm-zip@0.5.10:
|
||||
resolution: {integrity: sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==}
|
||||
|
@ -9831,7 +9909,7 @@ packages:
|
|||
node-notifier:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2)
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
|
@ -10276,7 +10354,7 @@ packages:
|
|||
node-notifier:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2)
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.1.0
|
||||
jest-cli: 29.7.0(@types/node@18.11.18)
|
||||
|
@ -13921,7 +13999,7 @@ packages:
|
|||
'@tsconfig/node14': 1.0.3
|
||||
'@tsconfig/node16': 1.0.4
|
||||
'@types/node': 20.12.7
|
||||
acorn: 8.10.0
|
||||
acorn: 8.11.3
|
||||
acorn-walk: 8.2.0
|
||||
arg: 4.1.3
|
||||
create-require: 1.1.1
|
||||
|
|
|
@ -5,3 +5,4 @@ packages:
|
|||
- 'packages/sw'
|
||||
- 'packages/firefish-js'
|
||||
- 'packages/megalodon'
|
||||
- 'packages/i18n'
|
||||
|
|
|
@ -12,6 +12,7 @@ import { execa } from "execa";
|
|||
"--parallel",
|
||||
"--filter=backend-rs",
|
||||
"--filter=firefish-js",
|
||||
"--filter=i18n",
|
||||
"run",
|
||||
"build",
|
||||
], {
|
||||
|
@ -26,6 +27,7 @@ import { execa } from "execa";
|
|||
"--parallel",
|
||||
"--filter=!backend-rs",
|
||||
"--filter=!firefish-js",
|
||||
"--filter=!i18n",
|
||||
"run",
|
||||
"build",
|
||||
], {
|
||||
|
|
|
@ -25,6 +25,10 @@ import { fileURLToPath } from "node:url";
|
|||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
fs.rmSync(join(__dirname, "/../packages/i18n/built"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
fs.rmSync(join(__dirname, "/../packages/megalodon/lib"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
|
|
|
@ -26,6 +26,14 @@ import { execa } from "execa";
|
|||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
fs.rmSync(join(__dirname, "/../packages/i18n/node_modules"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
fs.rmSync(join(__dirname, "/../packages/firefish-js/node_modules"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
fs.rmSync(join(__dirname, "/../packages/megalodon/node_modules"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
|
|
|
@ -12,6 +12,7 @@ import fs from "node:fs";
|
|||
"--parallel",
|
||||
"--filter=backend-rs",
|
||||
"--filter=firefish-js",
|
||||
"--filter=i18n",
|
||||
"run",
|
||||
"build:debug",
|
||||
], {
|
||||
|
@ -26,6 +27,7 @@ import fs from "node:fs";
|
|||
"--parallel",
|
||||
"--filter=!backend-rs",
|
||||
"--filter=!firefish-js",
|
||||
"--filter=!i18n",
|
||||
"run",
|
||||
"build:debug",
|
||||
], {
|
||||
|
|
Loading…
Reference in New Issue