diff --git a/Makefile.toml b/Makefile.toml index e2a1ce952..5b16ce0f2 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -39,38 +39,31 @@ args = ["exec", "lint-staged"] [tasks.lint-staged.windows] command = "pnpm.cmd" +[tasks.i18n-format] +description = "Format i18n keys" +command = "pnpm" +args = ["i18n:format"] +[tasks.i18n-format.windows] +command = "pnpm.cmd" + +[tasks.i18n-types] +description = "Generate i18n key types" +command = "pnpm" +args = ["i18n:types"] +[tasks.i18n-types.windows] +command = "pnpm.cmd" + +[tasks.git-add] +description = "Add changed files to git" +command = "git" +args = ["add", "."] + # --- Jobs --- -# Rust format (for pre-commit) -[tasks.rust-format-check] -description = "Check Rust code formatting" -dependencies = ["rust-format"] -[tasks.rust-format-check.condition] -files_modified.input = [ - "./src-tauri/**/*.rs", - "./crates/**/*.rs", - "**/Cargo.toml", -] -files_modified.output = ["./target/debug/*", "./target/release/*"] - -# Rust lint (for pre-push) -[tasks.rust-lint] -description = "Run Rust linting" -dependencies = ["rust-clippy"] -[tasks.rust-lint.condition] -files_modified.input = [ - "./src-tauri/**/*.rs", - "./crates/**/*.rs", - "**/Cargo.toml", -] -files_modified.output = ["./target/debug/*", "./target/release/*"] - -# Frontend format (for pre-commit) [tasks.frontend-format] description = "Frontend format checks" -dependencies = ["lint-staged"] +dependencies = ["i18n-format", "i18n-types", "git-add", "lint-staged"] -# Frontend lint (for pre-push) [tasks.frontend-lint] description = "Frontend linting and type checking" dependencies = ["eslint", "typecheck"] @@ -79,8 +72,8 @@ dependencies = ["eslint", "typecheck"] [tasks.pre-commit] description = "Pre-commit checks: format only" -dependencies = ["rust-format-check", "frontend-format"] +dependencies = ["rust-format", "frontend-format"] [tasks.pre-push] description = "Pre-push checks: lint and typecheck" -dependencies = ["rust-lint", "frontend-lint"] +dependencies = ["rust-clippy", "frontend-lint"] diff --git a/eslint.config.ts b/eslint.config.ts index 1c9ab2027..dbfa80359 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -39,6 +39,7 @@ export default defineConfig([ 'eslint.config.ts', 'src/polyfills/*.js', 'src-tauri/src/enhance/builtin/*.js', + 'scripts/*.mjs', ], }, }, @@ -139,7 +140,7 @@ export default defineConfig([ }, }, { - files: ['scripts/**/*.{js,mjs,cjs}'], + files: ['scripts/*.mjs'], languageOptions: { globals: { diff --git a/scripts/cleanup-unused-i18n.mjs b/scripts/cleanup-unused-i18n.mjs index 60fb05927..98f9c581f 100644 --- a/scripts/cleanup-unused-i18n.mjs +++ b/scripts/cleanup-unused-i18n.mjs @@ -1122,17 +1122,18 @@ function regenerateLocaleIndex(localeDir, namespaces) { const filePath = path.join(localeDir, `${namespace}.json`) if (!fs.existsSync(filePath)) continue const identifier = toModuleIdentifier(namespace, seen) - imports.push(`import ${identifier} from "./${namespace}.json";`) - mappings.push(` "${namespace}": ${identifier},`) + const key = namespace === identifier ? namespace : `'${namespace}'` + imports.push(`import ${identifier} from './${namespace}.json'`) + mappings.push(` ${key}: ${identifier},`) } const content = `${imports.join('\n')} const resources = { ${mappings.join('\n')} -}; +} -export default resources; +export default resources ` fs.writeFileSync(path.join(localeDir, 'index.ts'), content, 'utf8') diff --git a/scripts/generate-i18n-keys.mjs b/scripts/generate-i18n-keys.mjs index d22a46e35..961faea2f 100644 --- a/scripts/generate-i18n-keys.mjs +++ b/scripts/generate-i18n-keys.mjs @@ -12,9 +12,21 @@ const RESOURCE_OUTPUT = path.resolve( ROOT_DIR, 'src/types/generated/i18n-resources.ts', ) +const GENERATED_HEADER_LINES = [ + '// This file is auto-generated by scripts/generate-i18n-keys.mjs', + '// Do not edit this file manually.', +] +const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value) +const getIndent = (size) => ' '.repeat(size) +const formatStringLiteral = (value) => + `'${JSON.stringify(value).slice(1, -1).replaceAll("'", "\\'")}'` +const formatPropertyKey = (key) => + IDENTIFIER_PATTERN.test(key) ? key : formatStringLiteral(key) +const buildGeneratedFile = (bodyLines) => + [...GENERATED_HEADER_LINES, '', ...bodyLines, ''].join('\n') const flattenKeys = (data, prefix = '') => { const keys = [] @@ -35,11 +47,11 @@ const buildType = (data, indent = 0) => { } const entries = Object.entries(data).sort(([a], [b]) => a.localeCompare(b)) - const pad = ' '.repeat(indent) + const pad = getIndent(indent) const inner = entries .map(([key, value]) => { const typeStr = buildType(value, indent + 2) - return `${' '.repeat(indent + 2)}${JSON.stringify(key)}: ${typeStr};` + return `${getIndent(indent + 2)}${formatPropertyKey(key)}: ${typeStr}` }) .join('\n') @@ -66,19 +78,30 @@ const loadNamespaceJson = async () => { } const buildKeysFile = (keys) => { - const arrayLiteral = keys.map((key) => ` "${key}"`).join(',\n') - return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport const translationKeys = [\n${arrayLiteral}\n] as const;\n\nexport type TranslationKey = typeof translationKeys[number];\n` + const keyLines = keys.map( + (key) => `${getIndent(2)}${formatStringLiteral(key)},`, + ) + return buildGeneratedFile([ + 'export const translationKeys = [', + ...keyLines, + '] as const', + '', + 'export type TranslationKey = (typeof translationKeys)[number]', + ]) } const buildResourcesFile = (namespaces) => { - const namespaceEntries = namespaces - .map(({ name, json }) => { - const typeStr = buildType(json, 4) - return ` ${JSON.stringify(name)}: ${typeStr};` - }) - .join('\n') - - return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport interface TranslationResources {\n translation: {\n${namespaceEntries}\n };\n}\n` + const namespaceLines = namespaces.map(({ name, json }) => { + const typeStr = buildType(json, 4) + return `${getIndent(4)}${formatPropertyKey(name)}: ${typeStr}` + }) + return buildGeneratedFile([ + 'export interface TranslationResources {', + ' translation: {', + ...namespaceLines, + ' }', + '}', + ]) } const main = async () => {