From f7d594e1d0ad043066a305814bb369ed07f07f70 Mon Sep 17 00:00:00 2001 From: KUSATAKE Daisuke Date: Thu, 19 Mar 2026 17:23:31 +0900 Subject: [PATCH 1/4] feat: add i18n support with i18next for Japanese and English localization Add multi-language support using i18next. Default language is English, switching to Japanese when terminal locale (LC_ALL/LC_MESSAGES/LANG) starts with "ja". All hardcoded strings in commands, utilities, and Markdown output are now translated via translation keys. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/onPushToMain.yml | 2 + .mocharc.json | 2 +- .vscode/settings.json | 20 +++ package-lock.json | 43 ++++++- package.json | 2 + src/commands/all/index.ts | 66 +++++----- src/commands/document/index.ts | 25 ++-- src/commands/issue/index.ts | 39 +++--- src/commands/update/index.ts | 87 +++++++------ src/commands/wiki/index.ts | 19 +-- src/i18next.d.ts | 12 ++ src/locales/README.md | 58 +++++++++ src/locales/en.json | 195 +++++++++++++++++++++++++++++ src/locales/ja.json | 195 +++++++++++++++++++++++++++++ src/utils/backlog-api.ts | 107 ++++++++-------- src/utils/backlog.ts | 9 +- src/utils/common.ts | 6 +- src/utils/i18n.ts | 63 ++++++++++ src/utils/log.ts | 4 +- src/utils/sleep.ts | 4 +- test/setup.ts | 3 + tsconfig.json | 3 +- 22 files changed, 787 insertions(+), 177 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/i18next.d.ts create mode 100644 src/locales/README.md create mode 100644 src/locales/en.json create mode 100644 src/locales/ja.json create mode 100644 src/utils/i18n.ts create mode 100644 test/setup.ts diff --git a/.github/workflows/onPushToMain.yml b/.github/workflows/onPushToMain.yml index b6e2968..6a40af6 100644 --- a/.github/workflows/onPushToMain.yml +++ b/.github/workflows/onPushToMain.yml @@ -39,6 +39,8 @@ jobs: - name: Generate oclif README if: ${{ steps.version-check.outputs.skipped == 'false' }} id: oclif-readme + env: + LANG: ja_JP.UTF-8 run: | npm install npm exec oclif readme diff --git a/.mocharc.json b/.mocharc.json index 54e8fa6..dd5ff72 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,5 +1,5 @@ { - "require": ["ts-node/register"], + "require": ["test/setup.ts", "ts-node/register"], "watch-extensions": ["ts"], "recursive": true, "reporter": "spec", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..86329e4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "i18n-ally.localesPaths": [ + "src/locales" + ], + "i18n-ally.enabledFrameworks": ["react-i18next"], + "i18n-ally.pathMatcher": "{locale}.json", + "i18n-ally.keystyle": "nested", + "i18n-ally.sourceLanguage": "ja", + "i18n-ally.keysInUse": [ + "description.part2_whatever" + ], + "i18n-ally.sortKeys": true, + "i18n-ally.keepFulfilled": true, + // 翻訳エンジンを指定する(ユーザースコープで設定することを推奨) + // "i18n-ally.translate.engines": [ + // "google", + // "openai", + // "deepl", + // ] +} diff --git a/package-lock.json b/package-lock.json index 6fd5197..8f9c6ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@oclif/plugin-help": "^6", "@oclif/plugin-plugins": "^5", "dotenv": "^16.4.7", + "i18next": "^25.8.18", "ky": "^1.7.5" }, "bin": { @@ -1017,6 +1018,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -6787,6 +6797,37 @@ "node": ">=10.19.0" } }, + "node_modules/i18next": { + "version": "25.8.18", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.18.tgz", + "integrity": "sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -12447,7 +12488,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 568ca06..7f7afe8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@oclif/plugin-help": "^6", "@oclif/plugin-plugins": "^5", "dotenv": "^16.4.7", + "i18next": "^25.8.18", "ky": "^1.7.5" }, "devDependencies": { @@ -39,6 +40,7 @@ "files": [ "./bin", "./dist", + "./src/locales", "./oclif.manifest.json" ], "homepage": "https://github.com/ShuntaToda/backlog-exporter", diff --git a/src/commands/all/index.ts b/src/commands/all/index.ts index d663165..2a3a55a 100644 --- a/src/commands/all/index.ts +++ b/src/commands/all/index.ts @@ -5,77 +5,78 @@ import path from 'node:path' import {downloadDocuments, downloadIssues, downloadWikis} from '../../utils/backlog-api.js' import {validateAndGetProjectId} from '../../utils/backlog.js' import {createOutputDirectory, getApiKey} from '../../utils/common.js' +import {t} from '../../utils/i18n.js' import {FolderType, updateSettings} from '../../utils/settings.js' // .envファイルを読み込む dotenv.config() export default class All extends Command { - static description = 'Backlogから課題・Wiki・ドキュメントを取得してMarkdownファイルとして保存する' + static description = t('commands.all.description') static examples = [ `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY -課題・Wiki・ドキュメントをMarkdownファイルとして保存する +${t('commands.all.examples.saveAll')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-project -指定したディレクトリに課題・Wiki・ドキュメントを保存する +${t('commands.all.examples.outputDir')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --only issues,wiki -課題とWikiのみを取得する +${t('commands.all.examples.onlyIssuesAndWiki')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --exclude documents -ドキュメント以外(課題とWiki)を取得する +${t('commands.all.examples.excludeDocuments')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --maxCount 1000 -最大1000件の課題を取得する(デフォルトは5000件) +${t('commands.all.examples.maxCount')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName -ファイル名を課題キーにする +${t('commands.all.examples.issueKeyFileName')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFolder -課題キーでフォルダを作成する +${t('commands.all.examples.issueKeyFolder')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName --issueKeyFolder -課題キーでフォルダを作成し、ファイル名も課題キーにする +${t('commands.all.examples.issueKeyFolderAndFileName')} `, ] static flags = { apiKey: Flags.string({ - description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)', + description: t('common.flags.apiKey'), required: false, }), domain: Flags.string({ - description: 'Backlog domain (e.g. example.backlog.jp)', + description: t('common.flags.domain'), required: true, }), exclude: Flags.string({ - description: "Exclude the specified types, separated by commas (e.g., 'documents,wiki')", + description: t('common.flags.exclude'), required: false, }), issueKeyFileName: Flags.boolean({ - description: 'ファイル名を課題キーにする', + description: t('common.flags.issueKeyFileName'), required: false, }), issueKeyFolder: Flags.boolean({ - description: '課題キーでフォルダを作成する', + description: t('common.flags.issueKeyFolder'), required: false, }), maxCount: Flags.integer({ char: 'm', default: 5000, - description: '一度に取得する課題の最大数(デフォルト: 5000)', + description: t('common.flags.maxCount'), required: false, }), only: Flags.string({ - description: "Export only the specified types, separated by commas (e.g., 'issues,wiki')", + description: t('common.flags.only'), required: false, }), output: Flags.string({ char: 'o', - description: '出力ディレクトリパス', + description: t('common.flags.output'), required: false, }), projectIdOrKey: Flags.string({ - description: 'Backlog project ID or key', + description: t('common.flags.projectIdOrKey'), required: true, }), } @@ -90,7 +91,7 @@ export default class All extends Command { // Check for conflicting flags if (only && exclude) { - this.error('Cannot use both --only and --exclude flags together. Please use only one.') + this.error(t('commands.all.messages.onlyExcludeConflict')) } // Determine targets based on flags @@ -110,13 +111,18 @@ export default class All extends Command { const inputTargets = only ? only.split(',') : exclude ? exclude.split(',') : [] for (const target of inputTargets) { if (!validTargets.includes(target)) { - this.error(`Invalid target '${target}'. Available targets are: ${validTargets.join(', ')}`) + this.error( + t('commands.all.messages.invalidTarget', { + availableTargets: validTargets.join(', '), + target, + }), + ) } } // Check if any targets remain after exclusion if (targets.length === 0) { - this.error('No targets remaining after exclusion. Please specify valid targets to export.') + this.error(t('commands.all.messages.noTargetsAfterExclusion')) } // 出力ディレクトリの作成 @@ -124,7 +130,7 @@ export default class All extends Command { // プロジェクトキーからプロジェクトIDを取得 const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey) - this.log(`プロジェクトID: ${projectId} を使用します`) + this.log(t('common.messages.usingProjectId', {projectId})) if (targets.includes('issues')) { // 課題の出力ディレクトリ @@ -143,7 +149,7 @@ export default class All extends Command { }) // 課題の取得と保存 - this.log('課題の取得を開始します...') + this.log(t('commands.all.messages.issuesFetchStart')) await downloadIssues(this, { apiKey, count: maxCount, @@ -158,7 +164,7 @@ export default class All extends Command { await updateSettings(issueOutput, { lastUpdated: new Date().toISOString(), }) - this.log('課題の取得が完了しました') + this.log(t('commands.all.messages.issuesCompleted')) } if (targets.includes('wiki')) { @@ -176,7 +182,7 @@ export default class All extends Command { }) // Wikiの取得と保存 - this.log('Wikiの取得を開始します...') + this.log(t('commands.all.messages.wikiFetchStart')) await downloadWikis(this, { apiKey, domain, @@ -188,7 +194,7 @@ export default class All extends Command { await updateSettings(wikiOutput, { lastUpdated: new Date().toISOString(), }) - this.log('Wikiの取得が完了しました') + this.log(t('commands.all.messages.wikiCompleted')) } if (targets.includes('documents')) { @@ -206,7 +212,7 @@ export default class All extends Command { }) // ドキュメントの取得と保存 - this.log('ドキュメントの取得を開始します...') + this.log(t('commands.all.messages.documentsFetchStart')) await downloadDocuments(this, { apiKey, domain, @@ -219,13 +225,13 @@ export default class All extends Command { await updateSettings(documentOutput, { lastUpdated: new Date().toISOString(), }) - this.log('ドキュメントの取得が完了しました') + this.log(t('commands.all.messages.documentsCompleted')) } - this.log('すべてのデータの取得が完了しました!') + this.log(t('commands.all.messages.allCompleted')) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.error(`データの取得に失敗しました: ${errorMessage}`) + this.error(t('commands.all.messages.fetchFailed', {errorMessage})) } } } diff --git a/src/commands/document/index.ts b/src/commands/document/index.ts index 5d4f9be..6c96e9f 100644 --- a/src/commands/document/index.ts +++ b/src/commands/document/index.ts @@ -4,44 +4,45 @@ import * as dotenv from 'dotenv' import {downloadDocuments} from '../../utils/backlog-api.js' import {validateAndGetProjectId} from '../../utils/backlog.js' import {createOutputDirectory, getApiKey} from '../../utils/common.js' +import {t} from '../../utils/i18n.js' import {FolderType, updateSettings} from '../../utils/settings.js' // .envファイルを読み込む dotenv.config() export default class Document extends Command { - static description = 'Backlogからドキュメントを取得してMarkdownファイルとして保存する' + static description = t('commands.document.description') static examples = [ `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY -ドキュメントをMarkdownファイルとして保存する +${t('commands.document.examples.saveDocuments')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-documents -指定したディレクトリにドキュメントを保存する +${t('commands.document.examples.outputDir')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --keyword 仕様書 -キーワード「仕様書」を含むドキュメントのみを取得する +${t('commands.document.examples.keyword')} `, ] static flags = { apiKey: Flags.string({ - description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)', + description: t('common.flags.apiKey'), required: false, }), domain: Flags.string({ - description: 'Backlog domain (e.g. example.backlog.jp)', + description: t('common.flags.domain'), required: true, }), keyword: Flags.string({ - description: '検索キーワード', + description: t('common.flags.keyword'), required: false, }), output: Flags.string({ char: 'o', - description: '出力ディレクトリパス', + description: t('common.flags.output'), required: false, }), projectIdOrKey: Flags.string({ - description: 'Backlog project ID or key', + description: t('common.flags.projectIdOrKey'), required: true, }), } @@ -59,7 +60,7 @@ export default class Document extends Command { // プロジェクトキーからプロジェクトIDを取得 const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey) - this.log(`プロジェクトID: ${projectId} を使用します`) + this.log(t('common.messages.usingProjectId', {projectId})) // 設定ファイルを保存 await updateSettings(outputDir, { @@ -85,10 +86,10 @@ export default class Document extends Command { lastUpdated: new Date().toISOString(), }) - this.log('ドキュメントの取得が完了しました!') + this.log(t('commands.document.messages.completed')) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.error(`ドキュメントの取得に失敗しました: ${errorMessage}`) + this.error(t('commands.document.messages.fetchFailed', {errorMessage})) } } } diff --git a/src/commands/issue/index.ts b/src/commands/issue/index.ts index 4ba381d..f7c0148 100644 --- a/src/commands/issue/index.ts +++ b/src/commands/issue/index.ts @@ -4,70 +4,71 @@ import * as dotenv from 'dotenv' import {downloadIssues} from '../../utils/backlog-api.js' import {validateAndGetProjectId} from '../../utils/backlog.js' import {createOutputDirectory, getApiKey} from '../../utils/common.js' +import {t} from '../../utils/i18n.js' import {FolderType, updateSettings} from '../../utils/settings.js' // .envファイルを読み込む dotenv.config() export default class Issue extends Command { - static description = 'Backlogから課題を取得してMarkdownファイルとして保存する' + static description = t('commands.issue.description') static examples = [ `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY -課題をMarkdownファイルとして保存する +${t('commands.issue.examples.saveIssues')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-issues -指定したディレクトリに課題を保存する +${t('commands.issue.examples.outputDir')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --statusId 1,2,3 -指定したステータスIDの課題のみを取得する +${t('commands.issue.examples.statusId')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --maxCount 10000 -最大10000件の課題を取得する(デフォルトは5000件) +${t('commands.issue.examples.maxCount')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName -ファイル名を課題キーにする +${t('commands.issue.examples.issueKeyFileName')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFolder -課題キーでフォルダを作成する +${t('commands.issue.examples.issueKeyFolder')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --issueKeyFileName --issueKeyFolder -課題キーでフォルダを作成し、ファイル名も課題キーにする +${t('commands.issue.examples.issueKeyFolderAndFileName')} `, ] static flags = { apiKey: Flags.string({ - description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)', + description: t('common.flags.apiKey'), required: false, }), domain: Flags.string({ - description: 'Backlog domain (e.g. example.backlog.jp)', + description: t('common.flags.domain'), required: true, }), issueKeyFileName: Flags.boolean({ - description: 'ファイル名を課題キーにする', + description: t('common.flags.issueKeyFileName'), required: false, }), issueKeyFolder: Flags.boolean({ - description: '課題キーでフォルダを作成する', + description: t('common.flags.issueKeyFolder'), required: false, }), maxCount: Flags.integer({ char: 'm', default: 5000, - description: '一度に取得する課題の最大数(デフォルト: 5000)', + description: t('common.flags.maxCount'), required: false, }), output: Flags.string({ char: 'o', - description: '出力ディレクトリパス', + description: t('common.flags.output'), required: false, }), projectIdOrKey: Flags.string({ - description: 'Backlog project ID or key', + description: t('common.flags.projectIdOrKey'), required: true, }), statusId: Flags.string({ - description: 'ステータスID(カンマ区切りで複数指定可能)', + description: t('common.flags.statusId'), required: false, }), } @@ -85,7 +86,7 @@ export default class Issue extends Command { // プロジェクトキーからプロジェクトIDを取得 const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey) - this.log(`プロジェクトID: ${projectId} を使用します`) + this.log(t('common.messages.usingProjectId', {projectId})) // 設定ファイルを保存 await updateSettings(outputDir, { @@ -115,10 +116,10 @@ export default class Issue extends Command { lastUpdated: new Date().toISOString(), }) - this.log('課題の取得が完了しました!') + this.log(t('commands.issue.messages.completed')) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.error(`課題の取得に失敗しました: ${errorMessage}`) + this.error(t('commands.issue.messages.fetchFailed', {errorMessage})) } } } diff --git a/src/commands/update/index.ts b/src/commands/update/index.ts index 03923f9..92f6663 100644 --- a/src/commands/update/index.ts +++ b/src/commands/update/index.ts @@ -6,6 +6,7 @@ import {join} from 'node:path' import {downloadDocuments, downloadIssues, downloadWikis} from '../../utils/backlog-api.js' import {validateAndGetProjectId} from '../../utils/backlog.js' import {createOutputDirectory, getApiKey} from '../../utils/common.js' +import {t} from '../../utils/i18n.js' import {FolderType, getSettingsFilePath, loadSettings, updateSettings} from '../../utils/settings.js' // .envファイルを読み込む @@ -27,70 +28,70 @@ interface UpdateFlags { export default class Update extends Command { static args = { directory: Args.string({ - description: '更新対象のディレクトリ(設定ファイルが保存されている場所)', + description: t('commands.update.args.directory'), required: false, }), } - static description = 'Backlogから最新データを取得して更新する' + static description = t('commands.update.description') static examples = [ `<%= config.bin %> <%= command.id %> -カレントディレクトリの設定を使用して更新する +${t('commands.update.examples.currentDir')} `, `<%= config.bin %> <%= command.id %> --force -確認プロンプトをスキップする +${t('commands.update.examples.force')} `, `<%= config.bin %> <%= command.id %> --apiKey YOUR_API_KEY --domain example.backlog.jp --projectIdOrKey PROJECT_KEY -指定したパラメータで更新する(設定ファイルが存在する場合は上書きされます) +${t('commands.update.examples.specifyParams')} `, `<%= config.bin %> <%= command.id %> ./my-project -指定したディレクトリの設定を使用して更新する +${t('commands.update.examples.specifyDir')} `, `<%= config.bin %> <%= command.id %> --issueKeyFileName -ファイル名を課題キーにする +${t('commands.update.examples.issueKeyFileName')} `, `<%= config.bin %> <%= command.id %> --issueKeyFolder -課題キーでフォルダを作成する +${t('commands.update.examples.issueKeyFolder')} `, `<%= config.bin %> <%= command.id %> --issueKeyFileName --issueKeyFolder -課題キーでフォルダを作成し、ファイル名も課題キーにする +${t('commands.update.examples.issueKeyFolderAndFileName')} `, ] static flags = { apiKey: Flags.string({ - description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)', + description: t('common.flags.apiKey'), required: false, }), documentsOnly: Flags.boolean({ - description: 'ドキュメントのみを更新する', + description: t('common.flags.documentsOnly'), required: false, }), domain: Flags.string({ - description: 'Backlog domain (e.g. example.backlog.jp)', + description: t('common.flags.domain'), required: false, }), force: Flags.boolean({ char: 'f', - description: '確認プロンプトをスキップする', + description: t('common.flags.force'), required: false, }), issueKeyFileName: Flags.boolean({ - description: 'ファイル名を課題キーにする', + description: t('common.flags.issueKeyFileName'), required: false, }), issueKeyFolder: Flags.boolean({ - description: '課題キーでフォルダを作成する', + description: t('common.flags.issueKeyFolder'), required: false, }), issuesOnly: Flags.boolean({ - description: '課題のみを更新する', + description: t('common.flags.issuesOnly'), required: false, }), projectIdOrKey: Flags.string({ - description: 'Backlog project ID or key', + description: t('common.flags.projectIdOrKey'), required: false, }), wikisOnly: Flags.boolean({ - description: 'Wikiのみを更新する', + description: t('common.flags.wikisOnly'), required: false, }), } @@ -106,7 +107,7 @@ export default class Update extends Command { await this.findAndUpdateSettings(targetDir, flags as UpdateFlags) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.error(`更新に失敗しました: ${errorMessage}`) + this.error(t('commands.update.messages.updateFailed', {errorMessage})) } } @@ -123,24 +124,24 @@ export default class Update extends Command { }): Promise { if (options.force) return true - this.log(`以下の設定で更新を実行します:`) - this.log(`- ディレクトリ: ${options.targetDir}`) - this.log(`- ドメイン: ${options.domain}`) - this.log(`- プロジェクト: ${options.projectIdOrKey}`) + this.log(t('commands.update.messages.confirmSettings')) + this.log(t('commands.update.messages.settingDirectory', {targetDir: options.targetDir})) + this.log(t('commands.update.messages.settingDomain', {domain: options.domain})) + this.log(t('commands.update.messages.settingProject', {projectIdOrKey: options.projectIdOrKey})) if (options.folderType) { - this.log(`- フォルダタイプ: ${options.folderType}`) + this.log(t('commands.update.messages.folderType', {folderType: options.folderType})) } const updateTargets = [] - if (options.updateIssues) updateTargets.push('課題') - if (options.updateWikis) updateTargets.push('Wiki') - if (options.updateDocuments) updateTargets.push('ドキュメント') + if (options.updateIssues) updateTargets.push(t('commands.update.messages.targetIssues')) + if (options.updateWikis) updateTargets.push(t('commands.update.messages.targetWiki')) + if (options.updateDocuments) updateTargets.push(t('commands.update.messages.targetDocuments')) - this.log(`- 更新対象: ${updateTargets.join('・')}`) + this.log(t('commands.update.messages.settingTargets', {targets: updateTargets.join('・')})) // 確認プロンプトを表示 - this.log('更新を実行しますか? (y/n)') + this.log(t('commands.update.messages.confirmPrompt')) process.stdin.resume() process.stdin.setEncoding('utf8') const response = await new Promise((resolve) => { @@ -152,7 +153,7 @@ export default class Update extends Command { }) if (!response) { - this.log('更新をキャンセルしました') + this.log(t('commands.update.messages.cancelled')) } return response @@ -230,7 +231,7 @@ export default class Update extends Command { } } } catch { - this.warn(`ディレクトリの読み取りに失敗しました: ${targetDir}`) + this.warn(t('commands.update.messages.directoryReadFailed', {targetDir})) } } @@ -251,12 +252,12 @@ export default class Update extends Command { // 必須パラメータの検証 if (!domain) { - this.warn(`${targetDir}: ドメインが指定されていません。スキップします。`) + this.warn(t('commands.update.messages.domainMissing', {targetDir})) return } if (!projectIdOrKey) { - this.warn(`${targetDir}: プロジェクトIDまたはキーが指定されていません。スキップします。`) + this.warn(t('commands.update.messages.projectMissing', {targetDir})) return } @@ -265,9 +266,7 @@ export default class Update extends Command { try { apiKey = flags.apiKey || settings.apiKey || getApiKey(this) } catch { - this.warn( - `${targetDir}: APIキーが指定されていません。--apiKey フラグまたは BACKLOG_API_KEY 環境変数で設定してください。`, - ) + this.warn(t('commands.update.messages.apiKeyMissing', {targetDir})) return } @@ -297,7 +296,7 @@ export default class Update extends Command { // プロジェクトキーからプロジェクトIDを取得 const projectId = await validateAndGetProjectId(domain, projectIdOrKey, apiKey) - this.log(`プロジェクトID: ${projectId} を使用します`) + this.log(t('common.messages.usingProjectId', {projectId})) // 課題の更新 if (updateIssues) { @@ -333,7 +332,7 @@ export default class Update extends Command { }) } - this.log(`${targetDir} の更新が完了しました!`) + this.log(t('commands.update.messages.updateCompleted', {targetDir})) } // ドキュメントの更新 @@ -344,7 +343,7 @@ export default class Update extends Command { projectIdOrKey: string targetDir: string }): Promise { - this.log('ドキュメントの更新を開始します...') + this.log(t('commands.update.messages.documentsStart')) // 設定ファイルから前回の更新日時を取得 const {lastUpdated} = await loadSettings(options.targetDir) @@ -368,7 +367,7 @@ export default class Update extends Command { projectIdOrKey: options.projectIdOrKey, }) - this.log('ドキュメントの更新が完了しました') + this.log(t('commands.update.messages.documentsCompleted')) } // 課題の更新 @@ -381,7 +380,7 @@ export default class Update extends Command { projectIdOrKey: string targetDir: string }): Promise { - this.log('課題の更新を開始します...') + this.log(t('commands.update.messages.issuesStart')) // 設定ファイルから前回の更新日時を取得 const {lastUpdated} = await loadSettings(options.targetDir) @@ -407,7 +406,7 @@ export default class Update extends Command { projectIdOrKey: options.projectIdOrKey, }) - this.log('課題の更新が完了しました') + this.log(t('commands.update.messages.issuesCompleted')) } // Wikiの更新 @@ -417,7 +416,7 @@ export default class Update extends Command { projectIdOrKey: string targetDir: string }): Promise { - this.log('Wikiの更新を開始します...') + this.log(t('commands.update.messages.wikiStart')) // 設定ファイルから前回の更新日時を取得 const {lastUpdated} = await loadSettings(options.targetDir) @@ -440,6 +439,6 @@ export default class Update extends Command { projectIdOrKey: options.projectIdOrKey, }) - this.log('Wikiの更新が完了しました') + this.log(t('commands.update.messages.wikiCompleted')) } } diff --git a/src/commands/wiki/index.ts b/src/commands/wiki/index.ts index d3281a3..fbaec7e 100644 --- a/src/commands/wiki/index.ts +++ b/src/commands/wiki/index.ts @@ -3,37 +3,38 @@ import * as dotenv from 'dotenv' import {downloadWikis} from '../../utils/backlog-api.js' import {createOutputDirectory, getApiKey} from '../../utils/common.js' +import {t} from '../../utils/i18n.js' import {FolderType, updateSettings} from '../../utils/settings.js' // .envファイルを読み込む dotenv.config() export default class Wiki extends Command { - static description = 'Backlogから Wiki を取得してMarkdownファイルとして保存する' + static description = t('commands.wiki.description') static examples = [ `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY -Wikiをダウンロードする +${t('commands.wiki.examples.saveWiki')} `, `<%= config.bin %> <%= command.id %> --domain example.backlog.jp --projectIdOrKey PROJECT_KEY --apiKey YOUR_API_KEY --output ./my-project -指定したディレクトリにWikiを保存する +${t('commands.wiki.examples.outputDir')} `, ] static flags = { apiKey: Flags.string({ - description: 'Backlog API key (環境変数 BACKLOG_API_KEY からも自動読み取り可能)', + description: t('common.flags.apiKey'), required: false, }), domain: Flags.string({ - description: 'Backlog domain (e.g. example.backlog.jp)', + description: t('common.flags.domain'), required: true, }), output: Flags.string({ char: 'o', - description: '出力ディレクトリパス', + description: t('common.flags.output'), required: false, }), projectIdOrKey: Flags.string({ - description: 'Backlog project ID or key', + description: t('common.flags.projectIdOrKey'), required: true, }), } @@ -71,10 +72,10 @@ Wikiをダウンロードする lastUpdated: new Date().toISOString(), }) - this.log('Wikiの取得が完了しました!') + this.log(t('commands.wiki.messages.completed')) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) - this.error(`Wikiの取得に失敗しました: ${errorMessage}`) + this.error(t('commands.wiki.messages.fetchFailed', {errorMessage})) } } } diff --git a/src/i18next.d.ts b/src/i18next.d.ts new file mode 100644 index 0000000..8dec18a --- /dev/null +++ b/src/i18next.d.ts @@ -0,0 +1,12 @@ +import 'i18next' + +import type ja from './locales/ja.json' + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: 'translation' + resources: { + translation: typeof ja + } + } +} diff --git a/src/locales/README.md b/src/locales/README.md new file mode 100644 index 0000000..fa98031 --- /dev/null +++ b/src/locales/README.md @@ -0,0 +1,58 @@ +# Locales + +[i18next](https://www.i18next.com/) を使用した多言語対応のための翻訳ファイルを管理するディレクトリです。 + +## 対応言語 + +| ファイル | 言語 | +| --------- | ------ | +| `ja.json` | 日本語 | +| `en.json` | 英語 | + +## ネームスペース構造 + +``` +├── commands/ +│ ├── all/ # all コマンド +│ ├── issue/ # issue コマンド +│ ├── document/ # document コマンド +│ ├── wiki/ # wiki コマンド +│ └── update/ # update コマンド +│ ├── description # コマンドの説明文 +│ ├── examples/ # ヘルプに表示する使用例 +│ ├── labels/ # エクスポートファイルに出力するラベル +│ ├── messages/ # 実行時のログ・エラーメッセージ +│ └── args/ # コマンド引数の説明(update のみ) +│ +└── common/ + ├── flags/ # 複数コマンドで共有するCLIフラグの説明 + ├── labels/ # 複数コマンドで共有するエクスポート用ラベル + └── messages/ # 複数コマンドで共有するログ・エラーメッセージ +``` + +## キーの配置基準 + +### `commands..*` vs `common.*` + +- そのコマンドでしか使わないキーは `commands..*` に配置する +- 複数のコマンドで横断的に使われるキーは `common.*` に配置する + +### サブカテゴリの使い分け + +| カテゴリ | 用途 | 例 | +| ---------- | -------------------------------------------- | -------------------------------- | +| `labels` | エクスポートされるファイルに出力するラベル文字列 | `status`, `createdAt`, `assignee` | +| `messages` | 実行時にコンソールに表示するメッセージ | `fetchStart`, `completed` | +| `examples` | `--help` に表示するコマンド使用例の説明 | `saveIssues`, `outputDir` | +| `flags` | CLIフラグの説明文 | `apiKey`, `domain` | + +### キーを追加するときの判断フロー + +1. そのキーは特定のコマンドでしか使わないか? + - Yes → `commands.` 配下に追加 + - No → `common` 配下に追加 +2. キーの用途は何か? + - エクスポートファイルのラベル → `labels` + - 実行時メッセージ → `messages` + - CLIフラグの説明 → `flags` + - 使用例の説明 → `examples` diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..a930683 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,195 @@ +{ + "commands": { + "all": { + "description": "Export issues, wikis, and documents from Backlog as Markdown files", + "examples": { + "excludeDocuments": "Export everything except documents (issues and wikis)", + "issueKeyFileName": "Use issue key as file name", + "issueKeyFolder": "Create folders by issue key", + "issueKeyFolderAndFileName": "Create folders by issue key and use issue key as file name", + "maxCount": "Export up to 1000 issues (default is 5000)", + "onlyIssuesAndWiki": "Export only issues and wikis", + "outputDir": "Save issues, wikis, and documents to the specified directory", + "saveAll": "Save issues, wikis, and documents as Markdown files" + }, + "messages": { + "allCompleted": "All data export completed!", + "documentsCompleted": "Document export completed", + "documentsFetchStart": "Starting document export...", + "fetchFailed": "Failed to export data: {{errorMessage}}", + "invalidTarget": "Invalid target '{{target}}'. Available targets are: {{availableTargets}}", + "issuesCompleted": "Issue export completed", + "issuesFetchStart": "Starting issue export...", + "noTargetsAfterExclusion": "No targets remaining after exclusion. Please specify valid targets to export.", + "onlyExcludeConflict": "Cannot use both --only and --exclude flags together. Please use only one.", + "wikiCompleted": "Wiki export completed", + "wikiFetchStart": "Starting wiki export..." + } + }, + "document": { + "description": "Export documents from Backlog as Markdown files", + "examples": { + "keyword": "Export only documents containing the keyword", + "outputDir": "Save documents to the specified directory", + "saveDocuments": "Save documents as Markdown files" + }, + "labels": { + "attachmentInfo": "Creator: {{creator}}, Created: {{date}}", + "attachments": "Attachments", + "content": "Content", + "creator": "Creator", + "tags": "Tags", + "updater": "Updated by" + }, + "messages": { + "completed": "Document export completed!", + "documentFetchFailed": "Failed to fetch document {{name}}: {{errorMessage}}", + "downloadCompleted": "Document download completed!", + "fetchFailed": "Failed to export documents: {{errorMessage}}", + "fetchStart": "Starting document download...", + "processed": "Total {{count}} documents processed.", + "processing": "Processing document \"{{name}}\"...", + "treeFetch": "Fetching document tree...", + "treeProcess": "Processing active document tree..." + } + }, + "issue": { + "description": "Export issues from Backlog as Markdown files", + "examples": { + "issueKeyFileName": "Use issue key as file name", + "issueKeyFolder": "Create folders by issue key", + "issueKeyFolderAndFileName": "Create folders by issue key and use issue key as file name", + "maxCount": "Export up to 10000 issues (default is 5000)", + "outputDir": "Save issues to the specified directory", + "saveIssues": "Save issues as Markdown files", + "statusId": "Export only issues with the specified status IDs" + }, + "labels": { + "assignee": "Assignee", + "basicInfo": "Basic Info", + "comment": "Comment {{index}}", + "comments": "Comments", + "customFields": "Custom Fields", + "details": "Details", + "fieldName": "Field Name", + "fieldValue": "Value", + "issueKey": "Issue Key", + "noDetail": "No details available", + "none": "N/A", + "poster": "Author", + "priority": "Priority", + "timestamp": "Date", + "unassigned": "Unassigned" + }, + "messages": { + "apiFetchFailed": "Failed to fetch issues: {{errorMessage}}", + "commentFetchFailed": "Failed to fetch comments for issue {{issueKey}}: {{errorMessage}}", + "completed": "Issue export completed!", + "downloadCompleted": "Issue download completed!", + "fetchFailed": "Failed to export issues: {{errorMessage}}", + "fetchStart": "Starting issue download...", + "fetching": "Fetching issues... ({{count}} fetched)", + "found": "Total {{count}} issues found.", + "noNeedUpdate": "No issues need updating.", + "processingSinceLastUpdate": "Processing {{count}} issues updated since last update ({{lastUpdated}}).", + "saveFailed": "Failed to save issue {{issueKey}}: {{errorMessage}}", + "saving": "Saving issues...", + "savingProgress": "Saving issues... ({{current}}/{{total}})" + } + }, + "update": { + "args": { + "directory": "Target directory for update (where settings file is stored)" + }, + "description": "Fetch and update the latest data from Backlog", + "examples": { + "currentDir": "Update using the settings in the current directory", + "force": "Skip confirmation prompt", + "issueKeyFileName": "Use issue key as file name", + "issueKeyFolder": "Create folders by issue key", + "issueKeyFolderAndFileName": "Create folders by issue key and use issue key as file name", + "specifyDir": "Update using the settings in the specified directory", + "specifyParams": "Update with specified parameters (overwrites settings file if it exists)" + }, + "messages": { + "apiKeyMissing": "{{targetDir}}: API key not specified. Set it with --apiKey flag or BACKLOG_API_KEY environment variable.", + "cancelled": "Update cancelled", + "confirmPrompt": "Proceed with update? (y/n)", + "confirmSettings": "Update will be executed with the following settings:", + "directoryReadFailed": "Failed to read directory: {{targetDir}}", + "documentsCompleted": "Document update completed", + "documentsStart": "Starting document update...", + "domainMissing": "{{targetDir}}: Domain not specified. Skipping.", + "folderType": "- Folder type: {{folderType}}", + "issuesCompleted": "Issue update completed", + "issuesStart": "Starting issue update...", + "projectMissing": "{{targetDir}}: Project ID or key not specified. Skipping.", + "settingDirectory": "- Directory: {{targetDir}}", + "settingDomain": "- Domain: {{domain}}", + "settingProject": "- Project: {{projectIdOrKey}}", + "settingTargets": "- Update targets: {{targets}}", + "targetDocuments": "Documents", + "targetIssues": "Issues", + "targetWiki": "Wiki", + "updateCompleted": "Update completed for {{targetDir}}!", + "updateFailed": "Update failed: {{errorMessage}}", + "wikiCompleted": "Wiki update completed", + "wikiStart": "Starting wiki update..." + } + }, + "wiki": { + "description": "Export wikis from Backlog as Markdown files", + "examples": { + "outputDir": "Save wikis to the specified directory", + "saveWiki": "Download wikis" + }, + "messages": { + "apiFetchFailed": "Failed to fetch wiki {{name}}: {{errorMessage}}", + "completed": "Wiki export completed!", + "detailsFetch": "Fetching wiki details...", + "downloadCompleted": "Wiki download completed!", + "fetchFailed": "Failed to export wikis: {{errorMessage}}", + "fetchStart": "Starting wiki download...", + "fetching": "Fetching wikis... ({{current}}/{{total}})", + "found": "{{count}} wikis found.", + "listFetch": "Fetching wiki list...", + "noNeedUpdate": "No wikis need updating.", + "processingSinceLastUpdate": "Processing {{count}} wikis updated since last update ({{lastUpdated}}).", + "saving": "Saving wikis... ({{current}}/{{total}})" + } + } + }, + "common": { + "flags": { + "apiKey": "Backlog API key (can also be read from BACKLOG_API_KEY environment variable)", + "documentsOnly": "Update documents only", + "domain": "Backlog domain (e.g. example.backlog.jp)", + "exclude": "Exclude the specified types, separated by commas (e.g., 'documents,wiki')", + "force": "Skip confirmation prompt", + "issueKeyFileName": "Use issue key as file name", + "issueKeyFolder": "Create folders by issue key", + "issuesOnly": "Update issues only", + "keyword": "Search keyword", + "maxCount": "Maximum number of issues to fetch at once (default: 5000)", + "only": "Export only the specified types, separated by commas (e.g., 'issues,wiki')", + "output": "Output directory path", + "projectIdOrKey": "Backlog project ID or key", + "statusId": "Status IDs (comma-separated for multiple values)", + "wikisOnly": "Update wikis only" + }, + "labels": { + "createdAt": "Created", + "noContent": "(No content)", + "status": "Status", + "updatedAt": "Updated" + }, + "messages": { + "apiKeyFromEnv": "Using API key from BACKLOG_API_KEY environment variable", + "apiKeyNotFound": "API key not found. Please provide it with --apiKey flag or BACKLOG_API_KEY environment variable", + "failedToGetProjectId": "Failed to get project ID from project key \"{{projectKey}}\": {{errorMessage}}", + "logFailed": "Failed to write log: {{errorMessage}}", + "rateLimitWait": "Waiting 15 seconds to avoid rate limiting...", + "usingProjectId": "Using project ID: {{projectId}}" + } + } +} diff --git a/src/locales/ja.json b/src/locales/ja.json new file mode 100644 index 0000000..600a90e --- /dev/null +++ b/src/locales/ja.json @@ -0,0 +1,195 @@ +{ + "commands": { + "all": { + "description": "Backlogから課題・Wiki・ドキュメントを取得してMarkdownファイルとして保存する", + "examples": { + "excludeDocuments": "ドキュメント以外(課題とWiki)を取得する", + "issueKeyFileName": "ファイル名を課題キーにする", + "issueKeyFolder": "課題キーでフォルダを作成する", + "issueKeyFolderAndFileName": "課題キーでフォルダを作成し、ファイル名も課題キーにする", + "maxCount": "最大1000件の課題を取得する(デフォルトは5000件)", + "onlyIssuesAndWiki": "課題とWikiのみを取得する", + "outputDir": "指定したディレクトリに課題・Wiki・ドキュメントを保存する", + "saveAll": "課題・Wiki・ドキュメントをMarkdownファイルとして保存する" + }, + "messages": { + "allCompleted": "すべてのデータの取得が完了しました!", + "documentsCompleted": "ドキュメントの取得が完了しました", + "documentsFetchStart": "ドキュメントの取得を開始します...", + "fetchFailed": "データの取得に失敗しました: {{errorMessage}}", + "invalidTarget": "無効なターゲット「{{target}}」です。指定できるターゲットは次のとおりです: {{availableTargets}}", + "issuesCompleted": "課題の取得が完了しました", + "issuesFetchStart": "課題の取得を開始します...", + "noTargetsAfterExclusion": "除外の結果、エクスポート対象がなくなりました。有効なターゲットを指定してください。", + "onlyExcludeConflict": "「--only」と「--exclude」は同時に指定できません。どちらか一方だけを指定してください。", + "wikiCompleted": "Wikiの取得が完了しました", + "wikiFetchStart": "Wikiの取得を開始します..." + } + }, + "document": { + "description": "Backlogからドキュメントを取得してMarkdownファイルとして保存する", + "examples": { + "keyword": "キーワード「仕様書」を含むドキュメントのみを取得する", + "outputDir": "指定したディレクトリにドキュメントを保存する", + "saveDocuments": "ドキュメントをMarkdownファイルとして保存する" + }, + "labels": { + "attachmentInfo": "作成者: {{creator}}, 作成日時: {{date}}", + "attachments": "添付ファイル", + "content": "内容", + "creator": "作成者", + "tags": "タグ", + "updater": "更新者" + }, + "messages": { + "completed": "ドキュメントの取得が完了しました!", + "documentFetchFailed": "ドキュメント {{name}} の取得に失敗しました: {{errorMessage}}", + "downloadCompleted": "ドキュメントのダウンロードが完了しました!", + "fetchFailed": "ドキュメントの取得に失敗しました: {{errorMessage}}", + "fetchStart": "ドキュメントの取得を開始します...", + "processed": "合計 {{count}}件のドキュメントが処理されました。", + "processing": "ドキュメント「{{name}}」を処理中...", + "treeFetch": "ドキュメントツリーを取得しています...", + "treeProcess": "アクティブなドキュメントツリーを処理します..." + } + }, + "issue": { + "description": "Backlogから課題を取得してMarkdownファイルとして保存する", + "examples": { + "issueKeyFileName": "ファイル名を課題キーにする", + "issueKeyFolder": "課題キーでフォルダを作成する", + "issueKeyFolderAndFileName": "課題キーでフォルダを作成し、ファイル名も課題キーにする", + "maxCount": "最大10000件の課題を取得する(デフォルトは5000件)", + "outputDir": "指定したディレクトリに課題を保存する", + "saveIssues": "課題をMarkdownファイルとして保存する", + "statusId": "指定したステータスIDの課題のみを取得する" + }, + "labels": { + "assignee": "担当者", + "basicInfo": "基本情報", + "comment": "コメント {{index}}", + "comments": "コメント", + "customFields": "カスタム属性", + "details": "詳細", + "fieldName": "属性名", + "fieldValue": "値", + "issueKey": "課題キー", + "noDetail": "詳細情報なし", + "none": "なし", + "poster": "投稿者", + "priority": "優先度", + "timestamp": "日時", + "unassigned": "未割り当て" + }, + "messages": { + "apiFetchFailed": "課題の取得に失敗しました: {{errorMessage}}", + "commentFetchFailed": "課題 {{issueKey}} のコメント取得に失敗しました: {{errorMessage}}", + "completed": "課題の取得が完了しました!", + "downloadCompleted": "課題のダウンロードが完了しました!", + "fetchFailed": "課題の取得に失敗しました: {{errorMessage}}", + "fetchStart": "課題の取得を開始します...", + "fetching": "課題を取得中... ({{count}}件取得済み)", + "found": "合計 {{count}}件の課題が見つかりました。", + "noNeedUpdate": "更新が必要な課題はありません。", + "processingSinceLastUpdate": "前回の更新日時({{lastUpdated}})以降に更新された{{count}}件の課題を処理します。", + "saveFailed": "課題 {{issueKey}} の保存に失敗しました: {{errorMessage}}", + "saving": "課題を保存しています...", + "savingProgress": "課題を保存中... ({{current}}/{{total}}件)" + } + }, + "update": { + "args": { + "directory": "更新対象のディレクトリ(設定ファイルが保存されている場所)" + }, + "description": "Backlogから最新データを取得して更新する", + "examples": { + "currentDir": "カレントディレクトリの設定を使用して更新する", + "force": "確認プロンプトをスキップする", + "issueKeyFileName": "ファイル名を課題キーにする", + "issueKeyFolder": "課題キーでフォルダを作成する", + "issueKeyFolderAndFileName": "課題キーでフォルダを作成し、ファイル名も課題キーにする", + "specifyDir": "指定したディレクトリの設定を使用して更新する", + "specifyParams": "指定したパラメータで更新する(設定ファイルが存在する場合は上書きされます)" + }, + "messages": { + "apiKeyMissing": "{{targetDir}}: APIキーが指定されていません。--apiKey フラグまたは BACKLOG_API_KEY 環境変数で設定してください。", + "cancelled": "更新をキャンセルしました", + "confirmPrompt": "更新を実行しますか? (y/n)", + "confirmSettings": "以下の設定で更新を実行します:", + "directoryReadFailed": "ディレクトリの読み取りに失敗しました: {{targetDir}}", + "documentsCompleted": "ドキュメントの更新が完了しました", + "documentsStart": "ドキュメントの更新を開始します...", + "domainMissing": "{{targetDir}}: ドメインが指定されていません。スキップします。", + "folderType": "- フォルダタイプ: {{folderType}}", + "issuesCompleted": "課題の更新が完了しました", + "issuesStart": "課題の更新を開始します...", + "projectMissing": "{{targetDir}}: プロジェクトIDまたはキーが指定されていません。スキップします。", + "settingDirectory": "- ディレクトリ: {{targetDir}}", + "settingDomain": "- ドメイン: {{domain}}", + "settingProject": "- プロジェクト: {{projectIdOrKey}}", + "settingTargets": "- 更新対象: {{targets}}", + "targetDocuments": "ドキュメント", + "targetIssues": "課題", + "targetWiki": "Wiki", + "updateCompleted": "{{targetDir}} の更新が完了しました!", + "updateFailed": "更新に失敗しました: {{errorMessage}}", + "wikiCompleted": "Wikiの更新が完了しました", + "wikiStart": "Wikiの更新を開始します..." + } + }, + "wiki": { + "description": "Backlogから Wiki を取得してMarkdownファイルとして保存する", + "examples": { + "outputDir": "指定したディレクトリにWikiを保存する", + "saveWiki": "Wikiをダウンロードする" + }, + "messages": { + "apiFetchFailed": "Wiki {{name}} の取得に失敗しました: {{errorMessage}}", + "completed": "Wikiの取得が完了しました!", + "detailsFetch": "Wiki詳細を取得しています...", + "downloadCompleted": "Wikiのダウンロードが完了しました!", + "fetchFailed": "Wikiの取得に失敗しました: {{errorMessage}}", + "fetchStart": "Wikiの取得を開始します...", + "fetching": "Wikiを取得中... ({{current}}/{{total}}件)", + "found": "{{count}}件のWikiが見つかりました。", + "listFetch": "Wiki一覧を取得しています...", + "noNeedUpdate": "更新が必要なWikiはありません。", + "processingSinceLastUpdate": "前回の更新日時({{lastUpdated}})以降に更新された{{count}}件のWikiを処理します。", + "saving": "Wikiを保存中... ({{current}}/{{total}}件)" + } + } + }, + "common": { + "flags": { + "apiKey": "Backlog APIキー(環境変数 BACKLOG_API_KEY からも自動読み取り可能)", + "documentsOnly": "ドキュメントのみを更新する", + "domain": "Backlogドメイン(例: example.backlog.jp)", + "exclude": "除外するタイプをカンマ区切りで指定(例: 'documents,wiki')", + "force": "確認プロンプトをスキップする", + "issueKeyFileName": "ファイル名を課題キーにする", + "issueKeyFolder": "課題キーでフォルダを作成する", + "issuesOnly": "課題のみを更新する", + "keyword": "検索キーワード", + "maxCount": "一度に取得する課題の最大数(デフォルト: 5000)", + "only": "指定したタイプのみをエクスポート、カンマ区切りで指定(例: 'issues,wiki')", + "output": "出力ディレクトリパス", + "projectIdOrKey": "BacklogプロジェクトIDまたはキー", + "statusId": "ステータスID(カンマ区切りで複数指定可能)", + "wikisOnly": "Wikiのみを更新する" + }, + "labels": { + "createdAt": "作成日時", + "noContent": "(内容なし)", + "status": "ステータス", + "updatedAt": "更新日時" + }, + "messages": { + "apiKeyFromEnv": "環境変数 BACKLOG_API_KEY からAPIキーを使用します", + "apiKeyNotFound": "APIキーが見つかりません。--apiKey フラグまたは BACKLOG_API_KEY 環境変数で提供してください", + "failedToGetProjectId": "プロジェクトキー \"{{projectKey}}\" からプロジェクトIDの取得に失敗しました: {{errorMessage}}", + "logFailed": "ログの記録に失敗しました: {{errorMessage}}", + "rateLimitWait": "レート制限を回避するため15秒間待機します...", + "usingProjectId": "プロジェクトID: {{projectId}} を使用します" + } + } +} diff --git a/src/utils/backlog-api.ts b/src/utils/backlog-api.ts index 4a621a9..c7f6165 100644 --- a/src/utils/backlog-api.ts +++ b/src/utils/backlog-api.ts @@ -5,6 +5,7 @@ import path from 'node:path' import process from 'node:process' import {sanitizeFileName, sanitizeWikiFileName} from './common.js' +import {t} from './i18n.js' import {appendLog} from './log.js' import {RateLimiter} from './sleep.js' @@ -24,10 +25,10 @@ export function createCustomFieldsSection( return '' } - let customFieldsSection = '\n\n## カスタム属性\n\n| 属性名 | 値 |\n|--------|----|\n' + let customFieldsSection = `\n\n## ${t('commands.issue.labels.customFields')}\n\n| ${t('commands.issue.labels.fieldName')} | ${t('commands.issue.labels.fieldValue')} |\n|--------|----|\n` for (const customField of customFields) { - let fieldValue = 'なし' + let fieldValue = t('commands.issue.labels.none') if (customField.value !== null && customField.value !== undefined) { if (Array.isArray(customField.value)) { // 配列の場合(複数選択など) @@ -83,7 +84,7 @@ export async function downloadIssues( ): Promise { const baseUrl = `https://${options.domain}/api/v2` - command.log('課題の取得を開始します...') + command.log(t('commands.issue.messages.fetchStart')) // 全ての課題を格納する配列 let allIssues: Array<{ @@ -148,7 +149,7 @@ export async function downloadIssues( } // 進捗状況を一行で更新 - process.stdout.write(`\r課題を取得中... (${allIssues.length}件取得済み)`) + process.stdout.write(`\r${t('commands.issue.messages.fetching', {count: allIssues.length})}`) return ky.get(`${baseUrl}/issues?${params.toString()}`).json< Array<{ @@ -184,14 +185,14 @@ export async function downloadIssues( await fetchAllIssues(offset + maxCount) } } catch (error) { - command.error(`課題の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`) + command.error(t('commands.issue.messages.apiFetchFailed', {errorMessage: error instanceof Error ? error.message : String(error)})) } } // 課題取得開始 await fetchAllIssues(0) - command.log(`\n合計 ${allIssues.length}件の課題が見つかりました。`) + command.log(`\n${t('commands.issue.messages.found', {count: allIssues.length})}`) // 前回の更新日時より新しい課題のみをフィルタリング let filteredIssues = allIssues @@ -201,23 +202,23 @@ export async function downloadIssues( const issueUpdatedDate = new Date(issue.updated) return issueUpdatedDate > lastUpdatedDate }) - command.log(`前回の更新日時(${options.lastUpdated})以降に更新された${filteredIssues.length}件の課題を処理します。`) + command.log(t('commands.issue.messages.processingSinceLastUpdate', {count: filteredIssues.length, lastUpdated: options.lastUpdated})) } if (filteredIssues.length === 0) { - command.log('更新が必要な課題はありません。') + command.log(t('commands.issue.messages.noNeedUpdate')) return } // 各課題の詳細情報を取得して保存 - command.log('課題を保存しています...') + command.log(t('commands.issue.messages.saving')) // 並列処理ではなく順次処理に変更 for (const issue of filteredIssues) { try { // 進捗状況を一行で更新 const currentIndex = filteredIssues.indexOf(issue) + 1 - process.stdout.write(`\r課題を保存中... (${currentIndex}/${filteredIssues.length}件)`) + process.stdout.write(`\r${t('commands.issue.messages.savingProgress', {current: currentIndex, total: filteredIssues.length})}`) // BacklogのIssueへのリンクを作成 const backlogIssueUrl = `https://${options.domain}/view/${issue.issueKey}` @@ -235,13 +236,13 @@ export async function downloadIssues( // コメントセクションを作成 let commentsSection = '' if (allComments.length > 0) { - commentsSection = '\n\n## コメント\n' + commentsSection = `\n\n## ${t('commands.issue.labels.comments')}\n` let commentIndex = 1 for (const comment of allComments) { const commentDate = new Date(comment.created).toLocaleString('ja-JP') - commentsSection += `\n### コメント ${commentIndex}\n- **投稿者**: ${ + commentsSection += `\n### ${t('commands.issue.labels.comment', {index: commentIndex})}\n- **${t('commands.issue.labels.poster')}**: ${ comment.createdUser.name - }\n- **日時**: ${commentDate}\n\n${comment.content || '(内容なし)'}\n\n---\n` + }\n- **${t('commands.issue.labels.timestamp')}**: ${commentDate}\n\n${comment.content || t('common.labels.noContent')}\n\n---\n` commentIndex++ } @@ -281,20 +282,20 @@ export async function downloadIssues( const customFieldsSection = createCustomFieldsSection(issue.customFields) // Markdownファイルに書き込む - const assigneeName = issue.assignee ? issue.assignee.name : '未割り当て' + const assigneeName = issue.assignee ? issue.assignee.name : t('commands.issue.labels.unassigned') const markdownContent = `# ${issue.summary} -## 基本情報 -- 課題キー: ${issue.issueKey} -- ステータス: ${issue.status.name} -- 優先度: ${issue.priority.name} -- 担当者: ${assigneeName} -- 作成日時: ${new Date(issue.created).toLocaleString('ja-JP')} -- 更新日時: ${new Date(issue.updated).toLocaleString('ja-JP')} +## ${t('commands.issue.labels.basicInfo')} +- ${t('commands.issue.labels.issueKey')}: ${issue.issueKey} +- ${t('common.labels.status')}: ${issue.status.name} +- ${t('commands.issue.labels.priority')}: ${issue.priority.name} +- ${t('commands.issue.labels.assignee')}: ${assigneeName} +- ${t('common.labels.createdAt')}: ${new Date(issue.created).toLocaleString('ja-JP')} +- ${t('common.labels.updatedAt')}: ${new Date(issue.updated).toLocaleString('ja-JP')} - [Backlog Issue Link](${backlogIssueUrl})${customFieldsSection} -## 詳細 -${issue.description || '詳細情報なし'}${commentsSection}` +## ${t('commands.issue.labels.details')} +${issue.description || t('commands.issue.labels.noDetail')}${commentsSection}` // eslint-disable-next-line no-await-in-loop await fs.writeFile(issueFilePath, markdownContent) @@ -304,12 +305,12 @@ ${issue.description || '詳細情報なし'}${commentsSection}` await appendLog(options.outputDir, `課題「${issue.summary}」を更新しました: ${backlogIssueUrl}`) } catch (error) { command.warn( - `課題 ${issue.issueKey} の保存に失敗しました: ${error instanceof Error ? error.message : String(error)}`, + t('commands.issue.messages.saveFailed', {errorMessage: error instanceof Error ? error.message : String(error), issueKey: issue.issueKey}), ) } } - command.log('\n課題のダウンロードが完了しました!') + command.log(`\n${t('commands.issue.messages.downloadCompleted')}`) } /** @@ -374,7 +375,7 @@ async function fetchAllCommentsForIssue({ return {comments: allComments} } catch (error) { command.warn( - `課題 ${issueKey} のコメント取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`, + t('commands.issue.messages.commentFetchFailed', {errorMessage: error instanceof Error ? error.message : String(error), issueKey}), ) return {comments: allComments} } @@ -402,13 +403,13 @@ export async function downloadWikis( ): Promise { const baseUrl = `https://${options.domain}/api/v2` - command.log('Wikiの取得を開始します...') + command.log(t('commands.wiki.messages.fetchStart')) // APIリクエスト数をカウントするためのRateLimiterを作成 const rateLimiter = new RateLimiter(command) // Wiki一覧の取得 - command.log('Wiki一覧を取得しています...') + command.log(t('commands.wiki.messages.listFetch')) // APIリクエスト数をインクリメント await rateLimiter.increment() @@ -417,7 +418,7 @@ export async function downloadWikis( .get(`${baseUrl}/wikis?apiKey=${options.apiKey}&projectIdOrKey=${options.projectIdOrKey}`) .json>() - command.log(`${wikis.length}件のWikiが見つかりました。`) + command.log(t('commands.wiki.messages.found', {count: wikis.length})) // 前回の更新日時より新しいWikiのみをフィルタリング let filteredWikis = wikis @@ -427,16 +428,16 @@ export async function downloadWikis( const wikiUpdatedDate = new Date(wiki.updated) return wikiUpdatedDate > lastUpdatedDate }) - command.log(`前回の更新日時(${options.lastUpdated})以降に更新された${filteredWikis.length}件のWikiを処理します。`) + command.log(t('commands.wiki.messages.processingSinceLastUpdate', {count: filteredWikis.length, lastUpdated: options.lastUpdated})) } if (filteredWikis.length === 0) { - command.log('更新が必要なWikiはありません。') + command.log(t('commands.wiki.messages.noNeedUpdate')) return } // 各Wikiの詳細情報を取得 - command.log('Wiki詳細を取得しています...') + command.log(t('commands.wiki.messages.detailsFetch')) // 並列処理ではなく順次処理に変更 for (const wiki of filteredWikis) { @@ -449,7 +450,7 @@ export async function downloadWikis( // 進捗状況を一行で更新 const currentIndex = filteredWikis.indexOf(wiki) + 1 - process.stdout.write(`\rWikiを取得中... (${currentIndex}/${filteredWikis.length}件)`) + process.stdout.write(`\r${t('commands.wiki.messages.fetching', {current: currentIndex, total: filteredWikis.length})}`) // eslint-disable-next-line no-await-in-loop const wikiDetail = await ky @@ -492,13 +493,13 @@ export async function downloadWikis( // 進捗状況を一行で更新 const wikiIndex = filteredWikis.indexOf(wiki) + 1 - process.stdout.write(`\rWikiを保存中... (${wikiIndex}/${filteredWikis.length}件)`) + process.stdout.write(`\r${t('commands.wiki.messages.saving', {current: wikiIndex, total: filteredWikis.length})}`) } catch (error) { - command.warn(`Wiki ${wiki.name} の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`) + command.warn(t('commands.wiki.messages.apiFetchFailed', {errorMessage: error instanceof Error ? error.message : String(error), name: wiki.name})) } } - command.log('\nWikiのダウンロードが完了しました!') + command.log(`\n${t('commands.wiki.messages.downloadCompleted')}`) } /** @@ -527,13 +528,13 @@ export async function downloadDocuments( ): Promise { const baseUrl = `https://${options.domain}/api/v2` - command.log('ドキュメントの取得を開始します...') + command.log(t('commands.document.messages.fetchStart')) // APIリクエスト数をカウントするためのRateLimiterを作成 const rateLimiter = new RateLimiter(command) // ドキュメントツリーの取得 - command.log('ドキュメントツリーを取得しています...') + command.log(t('commands.document.messages.treeFetch')) // APIリクエスト数をインクリメント await rateLimiter.increment() @@ -562,7 +563,7 @@ export async function downloadDocuments( } }>() - command.log('アクティブなドキュメントツリーを処理します...') + command.log(t('commands.document.messages.treeProcess')) // ツリー構造をトラバースして、各ドキュメントの詳細を取得・保存 const processedDocuments: string[] = [] @@ -596,7 +597,7 @@ export async function downloadDocuments( await rateLimiter.increment() // 進捗状況を表示 - process.stdout.write(`\rドキュメント「${node.name}」を処理中...`) + process.stdout.write(`\r${t('commands.document.messages.processing', {name: node.name})}`) // ドキュメント詳細を取得 const documentDetail = await ky.get(`${baseUrl}/documents/${node.id}?apiKey=${options.apiKey}`).json<{ @@ -656,18 +657,18 @@ export async function downloadDocuments( // 添付ファイルリストの作成 let attachmentsSection = '' if (documentDetail.attachments && documentDetail.attachments.length > 0) { - attachmentsSection = '\n\n## 添付ファイル\n' + attachmentsSection = `\n\n## ${t('commands.document.labels.attachments')}\n` for (const attachment of documentDetail.attachments) { const attachmentDate = new Date(attachment.created).toLocaleString('ja-JP') const fileSize = (attachment.size / 1024).toFixed(1) - attachmentsSection += `- **${attachment.name}** (${fileSize} KB) - 作成者: ${attachment.createdUser.name}, 作成日時: ${attachmentDate}\n` + attachmentsSection += `- **${attachment.name}** (${fileSize} KB) - ${t('commands.document.labels.attachmentInfo', {creator: attachment.createdUser.name, date: attachmentDate})}\n` } } // タグリストの作成 let tagsSection = '' if (documentDetail.tags && documentDetail.tags.length > 0) { - tagsSection = '\n\n## タグ\n' + tagsSection = `\n\n## ${t('commands.document.labels.tags')}\n` for (const tag of documentDetail.tags) { tagsSection += `- ${tag.name}\n` } @@ -682,15 +683,15 @@ export async function downloadDocuments( [Backlog Document Link](${backlogDocumentUrl}) -**ステータス**: ${documentDetail.statusId}${documentDetail.emoji ? ` ${documentDetail.emoji}` : ''} -**作成者**: ${documentDetail.createdUser.name} -**作成日時**: ${createdDate} -**更新者**: ${documentDetail.updatedUser.name} -**更新日時**: ${updatedDate} +**${t('common.labels.status')}**: ${documentDetail.statusId}${documentDetail.emoji ? ` ${documentDetail.emoji}` : ''} +**${t('commands.document.labels.creator')}**: ${documentDetail.createdUser.name} +**${t('common.labels.createdAt')}**: ${createdDate} +**${t('commands.document.labels.updater')}**: ${documentDetail.updatedUser.name} +**${t('common.labels.updatedAt')}**: ${updatedDate} -## 内容 +## ${t('commands.document.labels.content')} -${documentDetail.plain || '(内容なし)'}${attachmentsSection}${tagsSection}` +${documentDetail.plain || t('common.labels.noContent')}${attachmentsSection}${tagsSection}` await fs.writeFile(documentFilePath, markdownContent) @@ -701,7 +702,7 @@ ${documentDetail.plain || '(内容なし)'}${attachmentsSection}${tagsSectio ) } catch (error) { command.warn( - `ドキュメント ${node.name} の取得に失敗しました: ${error instanceof Error ? error.message : String(error)}`, + t('commands.document.messages.documentFetchFailed', {errorMessage: error instanceof Error ? error.message : String(error), name: node.name}), ) } } @@ -715,6 +716,6 @@ ${documentDetail.plain || '(内容なし)'}${attachmentsSection}${tagsSectio } } - command.log(`\n合計 ${processedDocuments.length}件のドキュメントが処理されました。`) - command.log('ドキュメントのダウンロードが完了しました!') + command.log(`\n${t('commands.document.messages.processed', {count: processedDocuments.length})}`) + command.log(t('commands.document.messages.downloadCompleted')) } diff --git a/src/utils/backlog.ts b/src/utils/backlog.ts index 71e6d00..df527c4 100644 --- a/src/utils/backlog.ts +++ b/src/utils/backlog.ts @@ -1,5 +1,7 @@ import ky from 'ky' +import {t} from './i18n.js' + /** * プロジェクトキーからプロジェクトIDを取得する * @param domain Backlogドメイン (例: example.backlog.jp) @@ -16,9 +18,10 @@ export async function getProjectIdFromKey(domain: string, projectKey: string, ap return projectData.id } catch (error) { throw new Error( - `プロジェクトキー "${projectKey}" からプロジェクトIDの取得に失敗しました: ${ - error instanceof Error ? error.message : String(error) - }`, + t('common.messages.failedToGetProjectId', { + errorMessage: error instanceof Error ? error.message : String(error), + projectKey, + }), ) } } diff --git a/src/utils/common.ts b/src/utils/common.ts index 601cfe2..c5eb164 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,6 +1,8 @@ import {Command} from '@oclif/core' import * as fs from 'node:fs/promises' +import {t} from './i18n.js' + /** * APIキーを取得する(優先順位: コマンドライン引数 > 環境変数) * @param command コマンドインスタンス @@ -16,12 +18,12 @@ export function getApiKey(command: Command, providedApiKey?: string): string { // 環境変数からのAPIキー const envApiKey = process.env.BACKLOG_API_KEY if (envApiKey) { - command.log('環境変数 BACKLOG_API_KEY からAPIキーを使用します') + command.log(t('common.messages.apiKeyFromEnv')) return envApiKey } // APIキーが見つからない場合はエラー - command.error('APIキーが見つかりません。--apiKey フラグまたは BACKLOG_API_KEY 環境変数で提供してください') + command.error(t('common.messages.apiKeyNotFound')) return '' // この行は実行されないが、TypeScriptのエラーを回避するために必要 } diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..d9e4798 --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,63 @@ +import i18next from 'i18next' +import {readdirSync, readFileSync} from 'node:fs' +import {dirname, join} from 'node:path' +import {fileURLToPath} from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const localesDir = join(__dirname, '..', 'locales') + +const FALLBACK_LOCALE = 'ja' + +/** + * 翻訳ファイルを同期的に読み込む + */ +function loadTranslation(locale: string): Record { + const filePath = join(localesDir, `${locale}.json`) + const content = readFileSync(filePath, 'utf8') + return JSON.parse(content) as Record +} + +/** + * localesディレクトリから全翻訳リソースを動的に読み込む + */ +function loadAllTranslations(): Record}> { + const files = readdirSync(localesDir).filter(f => f.endsWith('.json')) + const resources: Record}> = {} + for (const file of files) { + const locale = file.replace('.json', '') + resources[locale] = {translation: loadTranslation(locale)} + } + + return resources +} + +/** + * 環境変数からロケールを検出する + */ +function detectLocale(supportedLocales: string[]): string { + const langEnv = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '' + const lang = langEnv.split(/[._]/)[0] + return supportedLocales.includes(lang) ? lang : FALLBACK_LOCALE +} + +const resources = loadAllTranslations() +const locale = detectLocale(Object.keys(resources)) + +// eslint-disable-next-line import/no-named-as-default-member +i18next.init({ + fallbackLng: FALLBACK_LOCALE, + initImmediate: false, + interpolation: { + escapeValue: false, + }, + lng: locale, + resources, +}) + +// eslint-disable-next-line import/no-named-as-default-member +export const t = i18next.t.bind(i18next) + +// eslint-disable-next-line unicorn/prefer-export-from +export default i18next diff --git a/src/utils/log.ts b/src/utils/log.ts index a4ec4cf..5c39b03 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,6 +1,8 @@ import * as fs from 'node:fs/promises' import path from 'node:path' +import {t} from './i18n.js' + /** * 更新ログを記録する * @param outputDir 出力ディレクトリ @@ -14,6 +16,6 @@ export async function appendLog(outputDir: string, message: string): Promise 1 && this.requestCount % 100 === 0) { - this.command.log('レート制限を回避するため15秒間待機します...') + this.command.log(t('common.messages.rateLimitWait')) await sleep(15_000) } diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..0705517 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,3 @@ +// テスト環境のロケールを日本語に設定 +// i18nモジュールが読み込まれる前に設定する必要がある +process.env.LANG = 'ja_JP.UTF-8' diff --git a/tsconfig.json b/tsconfig.json index 198ecdc..361895f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,9 +7,10 @@ "strict": true, "target": "es2022", "moduleResolution": "node16", + "resolveJsonModule": true, "composite": true }, - "include": ["./src/**/*"], + "include": ["./src/**/*", "./src/locales/*.json"], "ts-node": { "esm": true } From 71e446acaec22c3772ef79185da48c476f2b0161 Mon Sep 17 00:00:00 2001 From: KUSATAKE Daisuke Date: Wed, 25 Mar 2026 14:44:19 +0900 Subject: [PATCH 2/4] feat: add i18n keys for issueType, startDate, dueDate, and notSet labels Replace hardcoded Japanese labels added in upstream merge with i18n t() calls Co-Authored-By: Claude Opus 4.6 --- src/locales/en.json | 4 ++++ src/locales/ja.json | 4 ++++ src/utils/backlog-api.ts | 10 +++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index a930683..5f785ca 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -67,6 +67,8 @@ "labels": { "assignee": "Assignee", "basicInfo": "Basic Info", + "dueDate": "Due Date", + "issueType": "Issue Type", "comment": "Comment {{index}}", "comments": "Comments", "customFields": "Custom Fields", @@ -77,7 +79,9 @@ "noDetail": "No details available", "none": "N/A", "poster": "Author", + "notSet": "Not set", "priority": "Priority", + "startDate": "Start Date", "timestamp": "Date", "unassigned": "Unassigned" }, diff --git a/src/locales/ja.json b/src/locales/ja.json index 600a90e..0cb59c9 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -67,6 +67,8 @@ "labels": { "assignee": "担当者", "basicInfo": "基本情報", + "dueDate": "期限日", + "issueType": "種別", "comment": "コメント {{index}}", "comments": "コメント", "customFields": "カスタム属性", @@ -77,7 +79,9 @@ "noDetail": "詳細情報なし", "none": "なし", "poster": "投稿者", + "notSet": "未設定", "priority": "優先度", + "startDate": "開始日", "timestamp": "日時", "unassigned": "未割り当て" }, diff --git a/src/utils/backlog-api.ts b/src/utils/backlog-api.ts index 371e832..7367bbc 100644 --- a/src/utils/backlog-api.ts +++ b/src/utils/backlog-api.ts @@ -292,18 +292,18 @@ export async function downloadIssues( // Markdownファイルに書き込む const assigneeName = issue.assignee ? issue.assignee.name : t('commands.issue.labels.unassigned') - const startDate = issue.startDate ? new Date(issue.startDate).toLocaleDateString('ja-JP') : '未設定' - const dueDate = issue.dueDate ? new Date(issue.dueDate).toLocaleDateString('ja-JP') : '未設定' + const startDate = issue.startDate ? new Date(issue.startDate).toLocaleDateString('ja-JP') : t('commands.issue.labels.notSet') + const dueDate = issue.dueDate ? new Date(issue.dueDate).toLocaleDateString('ja-JP') : t('commands.issue.labels.notSet') const markdownContent = `# ${issue.summary} ## ${t('commands.issue.labels.basicInfo')} - ${t('commands.issue.labels.issueKey')}: ${issue.issueKey} -- 種別: ${issue.issueType.name} +- ${t('commands.issue.labels.issueType')}: ${issue.issueType.name} - ${t('common.labels.status')}: ${issue.status.name} - ${t('commands.issue.labels.priority')}: ${issue.priority.name} - ${t('commands.issue.labels.assignee')}: ${assigneeName} -- 開始日: ${startDate} -- 期限日: ${dueDate} +- ${t('commands.issue.labels.startDate')}: ${startDate} +- ${t('commands.issue.labels.dueDate')}: ${dueDate} - ${t('common.labels.createdAt')}: ${new Date(issue.created).toLocaleString('ja-JP')} - ${t('common.labels.updatedAt')}: ${new Date(issue.updated).toLocaleString('ja-JP')} - [Backlog Issue Link](${backlogIssueUrl})${customFieldsSection} From 489e7e0d1df4ba89fc3b9ad280a485e2dd4a27f9 Mon Sep 17 00:00:00 2001 From: KUSATAKE Daisuke Date: Wed, 25 Mar 2026 14:48:42 +0900 Subject: [PATCH 3/4] feat: set locale detection fallback to en, i18next fallback to ja MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 開発第一言語がjaのため、jaロケールファイルには必ず全キーが存在する前提で i18nextのfallbackLngをjaに設定。一方、環境変数からロケールを検出できない 場合はenにフォールバックし、日本語話者以外には英語を表示する。 Co-Authored-By: Claude Opus 4.6 --- src/utils/i18n.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index d9e4798..51111d2 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -8,8 +8,6 @@ const __dirname = dirname(__filename) const localesDir = join(__dirname, '..', 'locales') -const FALLBACK_LOCALE = 'ja' - /** * 翻訳ファイルを同期的に読み込む */ @@ -39,7 +37,7 @@ function loadAllTranslations(): Record Date: Fri, 27 Mar 2026 18:01:04 +0900 Subject: [PATCH 4/4] feat: disable i18next support notice Co-Authored-By: Claude Opus 4.6 --- src/utils/i18n.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index 51111d2..264d1de 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -52,6 +52,7 @@ i18next.init({ }, lng: locale, resources, + showSupportNotice: false, }) // eslint-disable-next-line import/no-named-as-default-member