ExcelのOfficeアドインとして、ローカルで動作する翻訳アプリを作ってみました!
以前にExcel-DNA(C#)でxllアドインとしてローカルLLMを組み込んだモノを作りましたが、今回React + Transformers.jsでモダンなUIと柔軟な構成で実装しています。
以下では、開発環境から構築手順、翻訳モデルの組み込み、UIの工夫、そしてExcelへの統合まで紹介していきます。
テンプレートはYeomanジェネレーターで作っています。
最終的にエクセルのサイドパネルでこのようなものをつくります。

最終的なコードは以下です。
以前はExcel-DNA(C#)でローカルLLMのxllアドインを作りました。
環境
- Windows11
- Node.js v22.19.0
- npm 10.9.3
- VSCode
- Excel2024
プロジェクトの作成
-
React を使用して Excel 作業ウィンドウ アドインを構築する
これでReactでのテンプレートをつくります
yo office
? Choose a project type: Office Add-in Task Pane project using React framework
? Choose a script type: JavaScript
? What do you want to name your add-in? ReactTest
? Which Office client application would you like to support? Excel
transformers.jsで翻訳アプリ
- チュートリアル Building a React application
- サンプル transformers.js-examples/react-translator/
- 使用モデル Xenova/nllb-200-distilled-600M
翻訳アプリをOfficeアドインテンプレートへ
transformersを追加
npm i @huggingface/transformers
cssを追加
npm install --save-dev style-loader css-loader
- テンプレート
LocalTranslatorForExcel/src内のファイルは削除して、react-translator/src/内のファイルをコピーする
webpack.config.jsの設定
以下のように必要な設定を変更していきます。
- ファイル名が変わるのでentryの変更
- commandsは使わないので不要
entry: {
polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
react: ["react", "react-dom"],
// 変更
// taskpane: {
index: {
// import: ["./src/taskpane/index.jsx", "./src/taskpane/taskpane.html"],
import: ["./src/main.jsx", "./src/index.html"],
dependOn: "react",
},
// 不要
// commands: "./src/commands/commands.js",
},
// entryに合わせて変更
plugins: [
// new HtmlWebpackPlugin({
// filename: "taskpane.html",
// template: "./src/taskpane/taskpane.html",
// chunks: ["polyfill", "taskpane", "react"],
// }),
new HtmlWebpackPlugin({
filename: "index.html",
template: "./src/index.html",
chunks: ["polyfill", "index", "react"],
}),
// 不要
// new HtmlWebpackPlugin({
// filename: "commands.html",
// template: "./src/commands/commands.html",
// chunks: ["polyfill", "commands"],
// }),
- cssファイルを使うので追加
// cssを追加
resolve: {
extensions: [".js", ".jsx", ".html", ".css"],
},
module: {
rules: [
// cssについて追加
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
exclude: /node_modules/,
},
]
}
- WebSocketについて追加
devServer: {
// websocketの追加
client: {
webSocketURL: {
hostname: "localhost",
port: 3000,
pathname: "/ws",
protocol: "wss",
},
},
},
manifest.xml
- entry変更に合わせての変更
<DefaultSettings>
<!-- 変更 -->
<!-- <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/> -->
<SourceLocation DefaultValue="https://localhost:3000/index.html"/>
<bt:Urls>
<bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
<!-- 不要 -->
<!-- <bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/> -->
<!-- 変更 -->
<!-- <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/> -->
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/index.html"/>
</bt:Urls>
その他
- App.jsx(その他も)でreactが読み込めないエラーになるのでimportを追加
App.jsx
// エラー
// Uncaught ReferenceError: React is not defined
// at App (App.jsx:107:3)
import React from "react";
- favicon.icoを設定していないとブラウザが探してエラーになるのでindex.htmlへ追加
index.html
<!-- ブラウザが探すので設定していないとエラーになる -->
<!-- favicon.ico:1 Failed to load resource: the server responded with a status of 404 () -->
<link rel="icon" href="../assets/favicon.ico" type="image/x-icon">
- エクセルのサイドパネルに表示するので小さい幅へのレスポンシブ対応
App.css 広げる
App.css
#root {
/* レスポンシブ対応のために。固定ではなく100% */
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.language-container {
display: flex;
gap: 20px;
justify-content: center;
}
.textbox-container {
display: flex;
justify-content: center;
gap: 20px;
/* 固定値は入れない */
/* width: 800px; */
}
.textbox-container > textarea,
.language-selector {
width: 50%;
}
.language-selector > select {
width: 150px;
}
.progress-container {
position: relative;
font-size: 14px;
color: white;
background-color: #e9ecef;
border: solid 1px;
border-radius: 8px;
text-align: left;
overflow: hidden;
}
.progress-bar {
padding: 0 4px;
z-index: 0;
top: 0;
width: 1%;
overflow: hidden;
background-color: #007bff;
white-space: nowrap;
}
.progress-text {
z-index: 2;
}
.selector-container {
display: flex;
gap: 20px;
}
.progress-bars-container {
padding: 8px;
height: 140px;
}
.container {
margin: 25px;
display: flex;
flex-direction: column;
gap: 10px;
}
/* メディアクエリ対応 */
@media screen and (max-width: 768px) {
.textbox-container {
width: 100%;
flex-direction: column;
}
.textbox-container > textarea,
.language-selector {
width: 100%;
}
/* .language-container {
flex-direction: column;
} */
button {
width: 100%;
}
.progress-bars-container {
width: 100%;
}
}
.button-group {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1rem;
}
翻訳言語の入れ替え
- 翻訳対象を入れ替えるボタンを追加します
App.jsx 広げる
App.jsx
// sourceとtarget入れ替えの状態管理
const [shouldTranslateAfterSwap, setShouldTranslateAfterSwap] = useState(false);
useEffect(() => {
if (shouldTranslateAfterSwap) {
translate();
setShouldTranslateAfterSwap(false);
}
}, [sourceLanguage, targetLanguage, input]);
// sourceとtargetを入れ替える
// 翻訳結果を空白にする
// 翻訳の実行
const swapLanguages = () => {
setSourceLanguage(targetLanguage);
setTargetLanguage(sourceLanguage);
setInput(output);
setOutput("");
setShouldTranslateAfterSwap(true);
};
return (
<>
<h1>Local Translator for Excel!</h1>
<h2>エクセルでローカル翻訳!</h2>
<div className="container">
<div className="button-group">
<button disabled={disabled} onClick={translate}>
Translate
</button>
{/* 入れ替えボタン */}
<button onClick={swapLanguages}>⇔Swap⇔</button>
</div>
<div className="language-container">
<LanguageSelector
type={"Source"}
value={sourceLanguage}
onChange={(x) => setSourceLanguage(x.target.value)}
/>
<LanguageSelector
type={"Target"}
value={targetLanguage}
onChange={(x) => setTargetLanguage(x.target.value)}
/>
</div>
<div className="textbox-container">
<textarea value={input} rows={3} onChange={(e) => setInput(e.target.value)}></textarea>
<textarea value={output} rows={3} readOnly></textarea>
</div>
</div>
<div className="progress-bars-container">
{ready === false && <label>Loading models... (only run once)</label>}
{progressItems.map((data) => (
<div key={data.file}>
<Progress text={data.file} percentage={data.progress} />
</div>
))}
</div>
</>
);
}
- 変更が反映されるように
defaultLanguageからvalueに変更
LanguageSelector.jsx 広げる
LanguageSelector.jsx
export default function LanguageSelector({ type, onChange, value }) {
return (
<div className="language-selector">
<label>{type}: </label>
<select onChange={onChange} value={value}>
{Object.entries(LANGUAGES).map(([key, value]) => {
return (
<option key={key} value={value}>
{key}
</option>
);
})}
</select>
</div>
);
}
エクセルで使用
GitHub Pagesにホスト
-
npm run buildでdistフォルダに出力されます - 設定を変更する
ここを変えればbuild時にmanifest.xmlが変わる
webpack.config.js
// const urlProd = "https://www.contoso.com/"; // CHANGE THIS TO YOUR PRODUCTION DEPLOYMENT LOCATION
const urlProd = "https://msmsrep.github.io/LocalTranslatorForExcel/dist/";
- エクセルに読み込むのは
distに出力されたmanifest.xmlです
次回はサイドロードについての予定です
