Compare commits

...

9 Commits

Author SHA1 Message Date
Linca 7eb666c381 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
2024-05-06 23:10:43 +00:00
naskya 82c98ae72f
ci: modify buildah args 2024-05-07 07:26:33 +09:00
naskya 5b3f93457b
dev: add renovate 2024-05-07 06:58:00 +09:00
naskya 4d9c0f8e7b
ci: fix syntax 2024-05-07 06:11:31 +09:00
naskya bf2b624bc9
ci: build OCI container image on develop 2024-05-07 05:52:43 +09:00
naskya 5261eb24b6
ci: restrict project path 2024-05-07 05:26:05 +09:00
naskya d440e9b388
ci: revise tasks 2024-05-07 04:58:59 +09:00
Lhcfl 4f5a6bd69b fix: https://firefish.dev/firefish/firefish/-/merge_requests/10773#note_5623 2024-04-27 21:28:35 +08:00
Lhcfl d135a362c7 dev: Add type annotations to i18n 2024-04-27 12:02:55 +08:00
18 changed files with 439 additions and 43 deletions

View File

@ -8,10 +8,11 @@ services:
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_PROJECT_PATH == 'firefish/firefish'
when: always
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
when: never
- if: $CI_MERGE_REQUEST_PROJECT_PATH == 'firefish/firefish'
when: always
- when: never
cache:
paths:
@ -23,6 +24,8 @@ cache:
stages:
- test
- build
- dependency
variables:
POSTGRES_DB: 'firefish_db'
@ -36,7 +39,6 @@ variables:
default:
before_script:
- mkdir -p "${CARGO_HOME}"
- apt-get update && apt-get -y upgrade
- apt-get -y --no-install-recommends install curl
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
@ -48,10 +50,71 @@ default:
- export PGPASSWORD="${POSTGRES_PASSWORD}"
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
build_and_cargo_unit_test:
build_test:
stage: test
script:
- pnpm install --frozen-lockfile
- pnpm run build:debug
- pnpm run migrate
container_image_build:
stage: build
image: docker.io/debian:bookworm-slim
services: []
before_script: []
rules:
- if: $CI_COMMIT_BRANCH == 'develop'
script:
- apt-get update && apt-get -y upgrade
- apt-get install -y --no-install-recommends buildah ca-certificates
- buildah login --username "${CI_REGISTRY_USER}" --password "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
- buildah build --security-opt seccomp=unconfined --cap-add all --tag "${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production" --platform linux/amd64 .
- buildah push "${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production" "docker://${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
cargo_unit_test:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
changes:
paths:
- packages/backend-rs/**/*
- packages/macro-rs/**/*
- Cargo.toml
- Cargo.lock
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
when: never
script:
- cargo check --features napi
- pnpm install --frozen-lockfile
- mkdir packages/backend-rs/built
- cp packages/backend-rs/index.js packages/backend-rs/built/index.js
- cp packages/backend-rs/index.d.ts packages/backend-rs/built/index.d.ts
- pnpm --filter='!backend-rs' run build:debug
- cargo test
cargo_clippy:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
changes:
paths:
- packages/backend-rs/**/*
- packages/macro-rs/**/*
- Cargo.toml
- Cargo.lock
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
when: never
script:
- cargo clippy -- -D warnings
renovate:
stage: dependency
image:
name: docker.io/renovate/renovate:37-slim
entrypoint: [""]
rules:
- if: $RENOVATE && $CI_PIPELINE_SOURCE == 'schedule'
services: []
before_script: []
script:
- renovate --platform gitlab --token "${API_TOKEN}" --endpoint "${CI_SERVER_URL}/api/v4" "${CI_PROJECT_PATH}"

View File

@ -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

View File

@ -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",

View File

@ -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));

24
packages/i18n/.swcrc Normal file
View File

@ -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
}
}

21
packages/i18n/LICENSE Normal file
View File

@ -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.

3
packages/i18n/README.md Normal file
View File

@ -0,0 +1,3 @@
# i18n
## Typed i18n for firefish

View File

@ -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"
}
}

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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/**/*"]
}

View File

@ -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

View File

@ -5,3 +5,4 @@ packages:
- 'packages/sw'
- 'packages/firefish-js'
- 'packages/megalodon'
- 'packages/i18n'

15
renovate.json Normal file
View File

@ -0,0 +1,15 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"rangeStrategy": "bump",
"branchConcurrentLimit": 5,
"enabledManagers": ["npm", "cargo"],
"baseBranches": ["develop"],
"lockFileMaintenance": {
"enabled": true,
"recreateWhen": "always",
"rebaseStalePrs": true,
"branchTopic": "lock-file-maintenance",
"commitMessageAction": "Lock file maintenance"
}
}

View File

@ -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",
], {

View File

@ -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,

View File

@ -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,

View File

@ -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",
], {