Skip to content
Merged
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
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,12 @@ jobs:
cd gui
bun install --frozen-lockfile

- name: GUI lint
if: needs.changes.outputs.gui == 'true'
run: |
cd gui
bun run lint

- name: Typecheck
run: |
bun x tsc --noEmit
Expand All @@ -418,12 +424,6 @@ jobs:
- name: Check release helper syntax
run: bun build scripts/release.ts --target=bun --outdir=.tmp/ci-release-script-check

- name: GUI lint
if: needs.changes.outputs.gui == 'true'
run: |
cd gui
bun run lint

- name: GUI build
if: needs.changes.outputs.gui == 'true'
run: |
Expand Down
50 changes: 42 additions & 8 deletions gui/.eslint/local-i18n-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import type { Rule } from "eslint";
import type { JSXAttribute, JSXElement, JSXText, Node, Property, TemplateElement } from "estree";
import type { Literal, Node, Property, TemplateElement } from "estree";
import type { JSXAttribute, JSXElement, JSXText } from "estree-jsx";
import { isBrandOrModelLiteral, isTechnicalLiteral } from "./i18n-allowlist.ts";
import { formatHardcodedSnippet, i18nLocaleFileHint } from "./i18n-locales.ts";

type LocalRuleContext = {
report(descriptor: {
node: Node;
messageId: "uiString" | "dataCopy";
data?: Record<string, string>;
}): void;
};

type LocalRuleListener = {
JSXText?(node: JSXText): void;
JSXAttribute?(node: JSXAttribute): void;
Literal?(node: Literal): void;
TemplateElement?(node: TemplateElement): void;
Property?(node: Property): void;
};

type LocalRuleModule = {
meta: {
type: "problem";
docs: {
description: string;
};
schema: readonly unknown[];
messages: Record<string, string>;
};
create(context: LocalRuleContext): LocalRuleListener;
};

const LITERAL_PATTERN =
/[A-Za-zÀ-ÖØ-öø-ÿ\u0100-\u024F\u1E00-\u1EFF\u0400-\u04FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uAC00-\uD7AF]/u;

const HTML_CHARACTER_REFERENCE_PATTERN =
/&(?:#\d+|#x[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]+);/g;

const UI_ATTRS = new Set([
"title",
"placeholder",
Expand Down Expand Up @@ -40,12 +71,13 @@ function isAllowedLiteral(value: string): boolean {
}

function reportLiteral(
context: Rule.RuleContext,
context: LocalRuleContext,
node: Node,
value: string,
messageId: "uiString" | "dataCopy",
comparisonValue = value,
) {
if (isAllowedLiteral(value)) return;
if (isAllowedLiteral(comparisonValue)) return;
context.report({
node,
messageId,
Expand Down Expand Up @@ -181,7 +213,7 @@ function isInsideNonUiContext(node: Node): boolean {
}
}
if (current.type === "Property") {
const key = propertyKeyName(current as Property);
const key = propertyKeyName((current as Property).key);
if (key && NON_UI_OBJECT_KEYS.has(key)) return true;
}
if (current.type === "CallExpression") {
Expand All @@ -201,7 +233,7 @@ function propertyKeyName(key: Property["key"]): string | null {
return null;
}

const noHardcodedUiStrings: Rule.RuleModule = {
const noHardcodedUiStrings: LocalRuleModule = {
meta: {
type: "problem",
docs: {
Expand All @@ -219,7 +251,9 @@ const noHardcodedUiStrings: Rule.RuleModule = {
if (isInsideNonUiContext(node)) return;
const value = node.value.replace(/\s+/g, " ").trim();
if (!value) return;
reportLiteral(context, node, value, "uiString");

const comparisonValue = value.replace(HTML_CHARACTER_REFERENCE_PATTERN, "");
reportLiteral(context, node, value, "uiString", comparisonValue);
},
JSXAttribute(node: JSXAttribute) {
if (node.name.type !== "JSXIdentifier") return;
Expand Down Expand Up @@ -262,7 +296,7 @@ const noHardcodedUiStrings: Rule.RuleModule = {
},
};

const noHardcodedDataCopy: Rule.RuleModule = {
const noHardcodedDataCopy: LocalRuleModule = {
meta: {
type: "problem",
docs: {
Expand Down
155 changes: 155 additions & 0 deletions gui/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [],
"jsPlugins": [
{
"name": "local-i18n",
"specifier": "./.eslint/local-i18n-plugin.ts"
}
],
"categories": {
"correctness": "off"
},
"env": {
"builtin": true
},
"ignorePatterns": [
"dist",
"src/i18n/**",
"**/*.test.ts",
"**/*.test.tsx",
"src/api.ts",
"src/format*.ts",
"src/icons.tsx",
"src/provider-icons.ts"
],
"overrides": [
{
"files": [
"**/*.{ts,tsx}"
],
"rules": {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-unassigned-vars": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-useless-assignment": "error",
"no-with": "error",
"preserve-caught-error": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",

"no-array-constructor": "error",
"no-unused-expressions": "error",

"typescript/ban-ts-comment": "error",
"typescript/no-duplicate-enum-values": "error",
"typescript/no-empty-object-type": "error",
"typescript/no-explicit-any": "error",
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-misused-new": "error",
"typescript/no-namespace": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-require-imports": "error",
"typescript/no-this-alias": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-declaration-merging": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/triple-slash-reference": "error",

"react/exhaustive-deps": "warn",
"react/rules-of-hooks": "error",
"react/react-compiler": "error",

"react/only-export-components": [
"error",
{
"allowConstantExport": true
}
]
},
"plugins": [
"typescript",
"react"
],
"env": {
"browser": true
}
},
{
"files": [
"src/App.tsx",
"src/main.tsx",
"src/ui.tsx",
"src/pages/**/*.{ts,tsx}",
"src/components/**/*.{ts,tsx}"
],
"rules": {
"local-i18n/no-hardcoded-ui-strings": "error"
}
},
{
"files": [
"src/provider-workspace-data.ts"
],
"rules": {
"local-i18n/no-hardcoded-data-copy": "error"
}
}
]
}
Loading
Loading