用語集を“自動で”守る:Google Apps Scriptで表記ブレ検出&一括置換

(例)AIライティング構成の7つの型:見出しテンプレと作り方のアイキャッチ

用語表記のブレ(例:ユーザ/ユーザー、ログイン/サインイン)は読みやすさも信頼性も落とします。本稿は、スプレッドシートの用語集をもとに、原稿の表記ブレを自動検出→一括置換するGoogle Apps Script(コピペ可)と、テンプレTSV運用ルールを一気に提供します。MTPE/外注/自分の過去記事にも効きます。

なぜ自動チェックが必要?

  • 再現性:人手だけだと漏れが出る。機械でゼロ漏れに。
  • スピード:MTPE/外注ドラフトを数秒で整形。
  • 共有知:用語集をSSOT(単一の真実源)化 → チームも迷わない。

準備:シート2枚だけ(glossary / draft)

  1. glossary シート:少なくとも下記4列
    列名備考
    term_jaユーザー日本語の優先形
    forbidden_synonyms_jaユーザ, 利用者カンマ/読点区切り
    term_ensign in英語の優先形(任意)
    forbidden_synonyms_enlog in, loginカンマ区切り(任意)
    statusApprovedDraftは無視
  2. draft シート:A列に原稿(1セル=1段落でOK)。
    スクリプトがB列に置換後を書き出します(A列は残す)。

ポイント:まずは2〜3語で動作確認→Approved語を少しずつ増やすのが安全。

コピペOK:GASコード(メニュー/検出/置換/レポート)

  1. スプレッドシート → 拡張機能Apps Script を開き、下のコードを貼って保存。
  2. 一度実行→権限を許可。
  3. シートに「Glossary Tools」メニューが出ます。
// ====== 設定 ======
const CONFIG = {
  glossarySheet: 'glossary',
  draftSheet: 'draft',
  outCol: 2,
  normalizeNFKC: true,
  includeEnglish: true
};

// ====== メニュー ======
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Glossary Tools')
    .addItem('検出のみ(レポート作成)', 'glossaryReport')
    .addItem('置換してB列へ出力', 'glossaryReplaceToB')
    .addToUi();
}

// ====== ユーティリティ ======
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const splitList = s => (s || '')
  .split(/[,、\n]/).map(x => x.trim()).filter(Boolean);

function getSheetByNameOrThrow(name) {
  const sh = SpreadsheetApp.getActive().getSheetByName(name);
  if (!sh) throw new Error(`シート「${name}」が見つかりません`);
  return sh;
}

function nfkc(s){ return CONFIG.normalizeNFKC && s ? s.normalize('NFKC') : (s||''); }

// ====== 用語集の読み込み ======
function loadGlossary() {
  const sh = getSheetByNameOrThrow(CONFIG.glossarySheet);
  const [head, ...rows] = sh.getDataRange().getValues();
  const idx = Object.fromEntries(head.map((h,i) => [String(h).trim(), i]));
  const pick = (r, k) => r[idx[k]] != null ? String(r[idx[k]]) : '';

  const dict = [];
  rows.forEach(r => {
    const status = pick(r, 'status').trim();
    if (status !== 'Approved') return;

    const termJa = nfkc(pick(r, 'term_ja'));
    const forbJa = splitList(pick(r, 'forbidden_synonyms_ja')).map(nfkc);
    const termEn = nfkc(pick(r, 'term_en'));
    const forbEn = splitList(pick(r, 'forbidden_synonyms_en')).map(nfkc);

    if (termJa && forbJa.length) forbJa.forEach(ng => dict.push({ from: ng, to: termJa, lang: 'ja' }));
    if (CONFIG.includeEnglish && termEn && forbEn.length) forbEn.forEach(ng => dict.push({ from: ng, to: termEn, lang: 'en' }));
  });
  return dict;
}

// ====== 検出レポート ======
function glossaryReport() {
  const dict = loadGlossary();
  const sh = getSheetByNameOrThrow(CONFIG.draftSheet);
  const last = sh.getLastRow();
  if (last <= 1) throw new Error('draftシートA列に原文を入れてください');

  const src = sh.getRange(2,1,last-1,1).getValues().map(r => nfkc(String(r[0])));
  const counts = new Map();

  dict.forEach(({from, to, lang}) => {
    const re = new RegExp(esc(from), lang === 'en' ? 'gi' : 'g');
    let total = 0;
    src.forEach(t => { total += (t.match(re) || []).length; });
    if (total > 0) counts.set(`${from}|${to}`, {from, to, total});
  });

  const book = SpreadsheetApp.getActive();
  const rsh = book.getSheetByName('glossary_report') || book.insertSheet('glossary_report');
  rsh.clear();
  rsh.getRange(1,1,1,4).setValues([['from(禁止語)','to(優先形)','件数','日時']]);
  const now = new Date();
  const rows = Array.from(counts.values()).sort((a,b)=>b.total-a.total).map(o => [o.from,o.to,o.total,now]);
  if (rows.length) rsh.getRange(2,1,rows.length,4).setValues(rows);
  SpreadsheetApp.getUi().alert(`検出完了:${rows.length}語にヒットしました(glossary_report参照)`);
}

// ====== 置換してB列へ出力 ======
function glossaryReplaceToB() {
  const dict = loadGlossary();
  const sh = getSheetByNameOrThrow(CONFIG.draftSheet);
  const last = sh.getLastRow();
  if (last <= 1) throw new Error('draftシートA列に原文を入れてください');

  const src = sh.getRange(2,1,last-1,1).getValues().map(r => nfkc(String(r[0])));
  const out = src.map(t => {
    let s = t;
    dict.forEach(({from, to, lang}) => {
      const re = new RegExp(esc(from), lang === 'en' ? 'gi' : 'g');
      s = s.replace(re, to);
    });
    return [s];
  });
  sh.getRange(2, CONFIG.outCol, out.length, 1).setValues(out);
  SpreadsheetApp.getUi().alert('置換完了:B列に出力しました(A列=原文は保持)');
}

使い方:手順(5分)

  1. glossaryに2〜3語をApprovedで登録(例:ユーザー / ユーザ)。
  2. draftのA列に原稿を数段落ペースト。
  3. Glossary Tools > 検出のみでヒット状況を確認。
  4. 問題なければ置換してB列へ。A列(原文)は保持。
  5. 最終的にB列をコピー→目的のエディタ/ブログに貼る。

運用ルール:誤置換を防ぐコツ

  • Approvedのみ適用(Draftは検討中語)。
  • 禁止語は実績ベースで最小限:闇雲に増やさない。
  • 英語UIは動詞句/名詞の方針を決めて一貫(例:ボタンはSign in)。
  • 週次でglossary_reportを見て追加/整理。
  • 迷う語は記事末に「本記事の表記方針」を1行明記。

Q&A

Q. 置換対象を単語だけに限定できますか?

A. 日本語は語境界の検出が難しいため、基本は部分一致が現実的です。固有名が巻き込まれそうな場合は、禁止語をもう少し具体化する(例:「ログイン」ではなく「ログインする」)などで誤爆を下げられます。

Q. 全角/半角/長音のゆれが気になります。

A. オプションのNFKC正規化(既定ON)で多くは吸収します。必要に応じてglossary側にバリエーションを追加してください。

Q. Googleドキュメントにも直接適用したい。

A. 本稿はシート内原稿を想定。Docs対応の拡張関数は別記事で配布予定です。まずはシートで原文→置換後の流れを固めるのがおすすめ。

付録:テンプレTSV(ヘッダー+サンプル)

下を丸ごとコピー→スプレッドシートA1へ貼付(タブ区切り)。シート名はglossaryに。

term_ja	forbidden_synonyms_ja	term_en	forbidden_synonyms_en	status
ユーザー	ユーザ, 利用者			
ログイン	サインイン	sign in	log in, login	Approved
無料トライアル	フリートライアル	free trial		free trial	Approved

draftシート(見本)

原文(A列の例)
ユーザはログイン後、無料トライアルを開始できます。
このプロダクトではサインインではなくログイン表記に統一します。

実行後、B列に「ユーザー」「ログイン」「free trial」など優先形が反映されます。

まとめ:ドラフトを“自動で整える”

Approved語だけで運用し、glossary_reportで定期見直し。最初の2〜3語で効果を実感したら、用語集テンプレと組み合わせて拡張してください。

コメント

このブログの人気の投稿

Bloggerインデックス完全ガイド(Search Console編)

2025年無料家計簿アプリ3選!マネーフォワード ME、シンプル家計簿、Zaimを比較。節約&ガジェット好きに最適。