Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2019年05月07日] [更新 2019年06月23日] Delphi 一般・その他
Windows XP の元号追加と locale.nls ファイル
動作確認等 EXE 作成 : Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
実行環境 : Windows XP(SP3)
[整理番号 - 0480]







Windows XP の元号の情報は locale.nls ファイルにある


2019年5月1日、元号が令和になりました。Windows 7 以降では新しい元号に対応するための更新プログラムが提供されています。Windows Vista 以前の Windows はサポート期限が終了しているので、そのような更新プログラムの提供はありません。

そこで、Windows XP でもシステム的に新しい元号を追加して運用できないかをテストした結果が本ページの内容です。 テストの結果、可能ですが、現実的には厳しい状況があります。
Windows Vista 以前で新しい元号に対応するには、元号と西暦の日付の変換テーブル等を用意して、自前で処理した方が無難と思われます。実際、Excel 等、Micorosoft Office 製品は、和暦の変換を独自で実装していて、システムの設定を使用しません。

Windows 7 以降では、元号関係の情報はレジストリに格納されています。Windows XP では NLS の機能の一部として、以下の locale.nls ファイルに格納されています。


C:\Windows\System32\locale.nls


[備考 1]
NLS というのは National Language Support の略です。NLS 関係の操作には NLS 関係の API を使用します。JEDI プロジェクトが、それらの API を利用可能にする JwaWinNLS.pas というユニットを提供しています。

[備考 2]
locale.nls は言語と地域等、ロケール関係の設定ファイルです。元号以外の情報も格納されています。したがって、他のコンピュータの locale.nls ファイルを使用すると、関係する設定の一部が変わってしまうことがあります。また、他の方のコンピュータの locale.nls ファイルを使用することは、ライセンス的にも問題があるかも知れません。


図1
Windows XP の起動画面
  • Windows XP のリリースは
    2001年10月25日
    (平成13年10月25日)
  • 後継の Windows Vista のリリースは
    2007年1月30日




01_locale.nls ファイルに元号の情報を追加するプログラム
WindowsXP_AddEraName.zip [886 KB] 2019年06月03日版 (EXE 同梱)


図の下に掲載しているコードが locale.nls ファイルに元号を追加するプログラムのコードです。
本プログラムの EXE があるフォルダに [Origin] と言う名前のフォルダを作成します。そのフォルダに System32 フォルダ内にある locale.nls をコピーしてから Project1.exe 実行します。 [Origin] フォルダのファイルはバックアップ用として使用できます。元号を追加した locale.nls は、EXE と同じフォルダに保存するようになっています。

ここで保存したファイルを System32 フォルダ内の locale.nls と置き換えれば、日付関係の処理関数で、新しく追加した元号が扱えるようになります。



図2
[令和を追加] ボタンで処理実行
  • 何回実行しても、元号追加前の locale.nls から別の locale.nls を作成するので結果は同じ
  • [置き換え] は、元号を追加した locale.nls ファイルを System32 フォルフォルダ内のファイルに上書きコピーする
  • この [置き換え] の機能は、Windows PE で PC を起動した場合のみ有効
図3[
[旧 NLS 情報]
  • 変換元の locale.nls ファイルから元号の情報を取得して表示
図4
[新 NLS 情報]
  • 変換後の locale.nls ファイルから元号の情報を取得して表示
  • 先頭に新元号の「令和」が追加されている

リスト1
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Button4: TButton;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    FNlsOldPath : string;
    FNlsNewPath : string;
    FIsWinXP    : Boolean;
    procedure AddEraInfo(AEraName: string; AYear, AMonth, ADay: Word;
      ANlsOldFilePath, ANlsNewFilePath: string);
    procedure ListGengo(ANlsFilePath: string);
  end;

var
  Form1: TForm1;

implementation

uses
  System.IOUtils;

{$R *.dfm}

type
  TEraNameInfoXP = record
    Month   : Word;
    Day     : Word;
    Year    : Word;
    DataLen : Word;
    YearStr : array [0..4] of char;
    Name    : array [0..2] of Char;
  end;
  pEraNameInfoXP = ^TEraNameInfoXP;

  TOffsetTable = record
    TableNo : DWORD;
    Reserv  : Word;
    Offset  : array [0..5] of Word;
    digChar : array [0..4] of Char;
  end;
  pOffsetTable = ^TOffsetTable;

  TJumpTable = record
    Offset : array [0..8] of DWORD;
  end;
  pJumpTable = ^TJumpTable;

const
  FInfoStartPos = $3F8D4;
  FJumpOffsetStart = $3F5B2;
  FEraNameStart = FInfoStartPos + SizeOf(TOffsetTable);

//============================================================================
//  フォーム生成時の処理
//
//  元の locale.nls は System32 フォルダからコピーしてくる
//  情報を追加した locale.nls はこの EXE があるフォルダに保存
//============================================================================
procedure TForm1.FormCreate(Sender: TObject);
var
  LFileVer  : Cardinal;
  LMajorVer : Cardinal;
  LMinorVer : Cardinal;
begin
  FNlsOldPath := ExtractFilePath(Application.ExeName) + 'Origin\locale.nls';
  FNlsNewPath := ExtractFilePath(Application.ExeName) + 'locale.nls';

  Button1.Enabled := FileExists(FNlsOldPath);
  Button2.Enabled := FileExists(FNlsOldPath);
  Button3.Enabled := FileExists(FNlsNewPath);

  // Windows のバージョン判定
  LFileVer := GetFileVersion('C:\Windows\System32\Kernel32.dll');
  LMajorVer := LFileVer shr 16;
  LMinorVer := LFileVer and $FFFF;
  FIsWinXP := (LMajorVer = 5) and (LMinorVer = 1);
  Button4.Enabled := FIsWinXP and FileExists(FNlsNewPath);
end;

//============================================================================
//  locale.nls に令和の情報を追加
//============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.Clear;

  AddEraInfo('令和', 2019,  5,  1, FNlsOldPath, FNlsNewPath);

  Button3.Enabled := FileExists(FNlsNewPath);
  Button4.Enabled := FIsWinXP and FileExists(FNlsNewPath);
  Memo1.Lines.Add('locale.nls に元号を追加しました');
end;

//============================================================================
//  変換前の locale.nls の元号関係の情報を表示
//============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  ListGengo(FNlsOldPath);
end;

//============================================================================
//  変換後の locale.nls の元号関係の情報を表示
//============================================================================
procedure TForm1.Button3Click(Sender: TObject);
begin
  ListGengo(FNlsNewPath);
end;

//============================================================================
//  ファイルの置き換え
//============================================================================
procedure TForm1.Button4Click(Sender: TObject);
begin
  if not FIsWinXP then begin
    MessageBox(0, 'Windows XP のドライブではありません  ', '結果', MB_ICONWARNING);
    Exit;
  end;

  try
    TFile.Copy(FNlsNewPath,'C:\Windows\System32\locale.nls', True);
    MessageBox(0, 'ファイルを置き換えました  ', '結果', MB_ICONINFORMATION);
  except
    MessageBox(0, 'ファイルの置き換えに失敗しました  ','結果', MB_ICONWARNING);
  end;
end;

//----------------------------------------------------------------------------
//  Locale.nls ファイルに元号の情報を追加するルーチン
//
//  ANlsOldFilePath  : 元号追加前の locale.nls のフルパス
//  ANlsNewFilePath  : 元号追加後の locale.nls の保存先のフルパス
//
//  FInfoStartPos, FEraNameStart はグローバル変数
//----------------------------------------------------------------------------
procedure TForm1.AddEraInfo(AEraName: string; AYear, AMonth, ADay: Word;
  ANlsOldFilePath, ANlsNewFilePath: string);
var
  LSrcStream   : TBytesStream;
  LDstStream   : TBytesStream;
  LEraNameInfo : TEraNameInfoXP;
  LOffsetTable : TOffsetTable;
  LJmpTable    : TJumpTable;
  LEraWordSize : Word;
  LIndex       : Integer;
begin
  LSrcStream := TBytesStream.Create;
  LDstStream := TBytesStream.Create;
  try
    // locale.nls ファイルを読み込む
    LSrcStream.LoadFromFile(ANlsOldFilePath);

    // レコード型の初期化
    LEraNameInfo := System.Default(TEraNameInfoXP);
    // 令和の情報をレコード型のメンバにセット
    LEraWordSize := SizeOf(LEraNameInfo) div 2;
    LEraNameInfo.Year    := AYear;
    LEraNameInfo.Month   := AMonth;
    LEraNameInfo.Day     := ADay;
    LEraNameInfo.DataLen := LEraWordSize;
    StrPCopy(LEraNameInfo.Name, AEraName);
    StrPCopy(LEraNameInfo.YearStr, IntToStr(LEraNameInfo.Year));

    LDstStream.Size := LSrcStream.Size + SizeOf(LEraNameInfo);

    LDstStream.CopyFrom(LSrcStream, FEraNameStart);
    LDstStream.WriteBuffer(LEraNameInfo, SizeOf(LEraNameInfo));

    Move(LSrcStream.Bytes[FEraNameStart],
         LDstStream.Bytes[FEraNameStart + SizeOf(LEraNameInfo)],
         LSrcStream.Size - FEraNameStart);

    // 元号情報の最終位置のオフセットとその他のオフセット値を更新
    LDstStream.Seek(FInfoStartPos, soBeginning);
    LDstStream.ReadBuffer(LOffsetTable, SizeOf(LOffsetTable));
    for LIndex := 2 to 5 do begin
      LOffsetTable.Offset[LIndex] := LOffsetTable.Offset[LIndex] + LEraWordSize;
    end;
    LDstStream.Seek(FInfoStartPos, soBeginning);
    LDstStream.WriteBuffer(LOffsetTable, SizeOf(LOffsetTable));

    // もう 1 つのオフセット関係の値を更新
    LDstStream.Seek(FJumpOffsetStart, soBeginning);
    LDstStream.ReadBuffer(LJmpTable, SizeOf(LJmpTable));
    for LIndex := 0 to High(LJmpTable.Offset) do begin
      LJmpTable.Offset[LIndex] := LJmpTable.Offset[LIndex] + SizeOf(TEraNameInfoXP)
    end;
    LDstStream.Seek(FJumpOffsetStart, soBeginning);
    LDstStream.WriteBuffer(LJmpTable, SizeOf(LJmpTable));

    // 保存
    // System32 フォルダ内の locale.nls を,ここで保存したファイルに置き換える
    LDstStream.Position := 0;
    LDstStream.SaveToFile(ANlsNewFilePath);
  finally
    FreeAndNil(LSrcStream);
    FreeAndNil(LDstStream);
  end;
end;

//----------------------------------------------------------------------------
//  locale.nls の元号関係の情報を表示
//  FInfoStartPos, FEraNameStart はグローバル変数
//----------------------------------------------------------------------------
procedure TForm1.ListGengo(ANlsFilePath: string);
var
  LStream       : TBytesStream;
  LEraInfoStart : Word;
  LEraInfoEnd   : Word;
  LEraWordSize  : Word;
  LEraCount     : Integer;
  LEraNameInfo  : TEraNameInfoXP;
  LYear         : Integer;
  LMonth        : Integer;
  LDay          : Integer;
  LIndex        : Integer;
begin
  Memo1.Lines.Clear;

  LStream := TBytesStream.Create;
  try
    LStream.LoadFromFile(ANlsFilePath);

    LStream.Seek(FInfoStartPos + SizeOf(Word) * 4, soBeginning);
    LStream.ReadBuffer(LEraInfoStart, SizeOf(Word));
    LStream.ReadBuffer(LEraInfoEnd, SizeOf(Word));

    LEraWordSize := SizeOf(LEraNameInfo) div 2;
    LEraCount := (LEraInfoEnd - LEraInfoStart) div LEraWordSize;

    LStream.Seek(FEraNameStart, soBeginning);
    for LIndex := 0 to LEraCount - 1 do begin
      LStream.ReadBuffer(LEraNameInfo, SizeOf(LEraNameInfo));

      Memo1.Lines.Add(LEraNameInfo.Name);

      LYear  := LEraNameInfo.Year;
      LMonth := LEraNameInfo.Month;
      LDay   := LEraNameInfo.Day;

      Memo1.Lines.Add(IntToStr(LYear) + ' 年 '
                    + IntToStr(LMonth) + ' 月 '
                    + IntToStr(LDay) + ' 日 ~');
    end;
  finally
    FreeAndNil(LStream);
  end;
end;

end.


本プログラムでは「令和」という元号を追加しています。更に新しい元号を追加する場合は、今回元号を追加した locale.nls に元号を追加することになります。
例えば以下のコードは、3 つの元号を追加しています。

2019年5月1日に元号が「令和」になったわけですが、今後、Windows XP に更に元号を追加しなければならない可能性はかなり低いと思われます。下のコードは、上の元号追加のプログラムの動作確認の用途には利用できます。



図5
元号情報の追加例

リスト2
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.Clear;

  AddEraInfo('令和', 2019,  5,  1, FNlsOldPath, FNlsNewPath);
  AddEraInfo('正明', 2025,  6, 15, FNlsNewPath, FNlsNewPath);
  AddEraInfo('仁和', 2032, 10, 10, FNlsNewPath, FNlsNewPath);

  Button3.Enabled := FileExists(FNlsNewPath);
  Button4.Enabled := FIsWinXP and FileExists(FNlsNewPath);
  Memo1.Lines.Add('locale.nls に元号を追加しました');
end;


下図は、本プログラムで元号「令和」を追加した locale.nls ファイルを、バイナリエディタ BZ (BZ エディタ) で表示した画像てす。BZ エディタの [表示] [文字コード] で、文字コードを [Unicode UTF-16] にして表示しています。

$03F8DE からの 8 バイトが変更したデータ領域です。ここには、元号情報へのオフセット値と、その後に続く日付の形式の文字列へのオフセット値が WORD 単位 (2 バイト - 16 ビット符号なし整数値) で格納されています。全部で 8 バイトですが、実際に値が変わっているのは下位バイトの 4 バイトだけです。$03F8D4 が構造体の先頭のようです。そこからのオフセット値です。
$03F8F0 からの 24 バイトが追加挿入した元号のデータです。



図6

2019年06月01日の更新で、$03F5B2 からのオフセット関係の値を 9 バイト変更しています。
この変更がなくても、通常の用途では問題となることはないと思いますが、念のために変更処理を追加しておきました。



[備考]
上の図では「平成」という文字を検索していますが、BZ エディタでは「> 3F8D4」で検索すると、その番地にジャンプします。また「> 3F8D4 + E + E」で検索すると $03F8D4 から E ワード ( E * 2 バイト) 先の番地にジャンプできます。計算結果を求めるだけであれば「? 3F8D4 + E + E」とすれば、検索窓に計算結果を表示します。[Ctrl] + [R] でジャンプ前の位置に移動します。
上の図のバイナリエディタでのデータの表示は、バイトオーダが逆順の、リトルエンディアンであることに注意してください。計算式には通常の 16 進数表示を使用します。




02_変更後の locale.nls を使用した動作確認結果


System32 フォルダ内の locale.nls を、上のプログラムで作成した locale.nls ファイルに置き換えてテストした結果です。
下図は Delphi の元号名取得用の機能を使用した結果です。テストには図の下に掲載しているコードを使用しています。新規に追加した「令和」が取得できています。



図7
Delphi の元号名の取得機能を使用した結果
  • 新元号が追加されている
  • これは Delphi XE で作成した EXE での結果

リスト3
//============================================================================
//  元号名のリストを取得するサンプルコード
//  Delphi XE 用
//  Delphi XE では最大 7 つまで元号が使用可能
//============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LEraName : string;
  LIndex   : Integer;
begin
  Memo1.Lines.Clear;

  Memo1.Lines.Add('元号格納配列要素数 : ' + IntToStr(MaxEraCount));

  for LIndex := 1 to High(EraNames) do begin
    Memo1.Lines.Add(IntToStr(LIndex) + ' : ' + EraNames[LIndex]);
  end;
end;


リスト4
//============================================================================
//  元号名のリストを取得するサンプルコード
//  Delphi XE5 以降用
//============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LFmtSettings : TFormatSettings;
  LEraCount    : Integer;
  LEraName     : string;
  LIndex       : Integer;
begin
  Memo1.Lines.Clear;

  LFmtSettings := TFormatSettings.Create;
  LEraCount := Length(FormatSettings.EraInfo);

  Memo1.Lines.Add('元号格納配列要素数 : ' + IntToStr(LEraCount));

  for LIndex := 0 to LEraCount - 1 do begin
    LEraName := FormatSettings.EraInfo[LIndex].EraName;
    Memo1.Lines.Add(IntToStr(LIndex + 1) + ' : ' + LEraName);
  end;
end;


下図は、西暦を和暦表示にするテスト結果の画面です。



図8
西暦を和暦で表示

リスト5
//============================================================================
//  西暦から和暦名に変換するテスト
//  EXE は Delphi XE5 で作成
//============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LText : string;
begin
  Memo1.Lines.Clear;

  LText := FormatDateTime('gg e 年 m 月 d 日', StrToDate('2019/5/10'));
  Memo1.Lines.Add(LText);

  LText := FormatDateTime('gg e 年 m 月 d 日', StrToDate('1912/7/30'));
  Memo1.Lines.Add(LText);

  LText := FormatDateTime('gg e 年 m 月 d 日', StrToDate('1890/5/10'));
  Memo1.Lines.Add(LText);
end;


下図は拙作の「元号選択式日付入力コンポーネント」に添付のテストプログラムの実行画面です。



図9




03_System32 フォルダ内の locale.nls ファイルの置き換え


元号を追加した loclae.nls は、Windows XP の System32 フォルダ内の locale.nls ファイルと置き換えなければなりません。しかし、このファイルは Windows の起動と同時に使用中になります。使用中のファイルに対しては変更操作ができません。

locale.nls ファイルを置き換えるには、以下のような方法が考えられます。どちらの方法も、Windows XP の NTFS ファイルが操作可能であることが条件です。


  • 対象のコンピュータを 別の OS で起動する
  • ディスクを取り外して、別のコンピュータに接続する

別の OS として、Windows PE の CD を作成して使用する方法が現実的と言えます。以下の記事を参考にしてください。
LAN 上のクライアント PC で、CD ドライブが実装されていない場合、PXE サーバ (PXE ブート) 機能を導入して、Windows PE の ISO ファイルをクライアント PC から起動する方法 (ネットワークブート) も考えられます。



Windows XP のディスクを別のコンピュータに接続する操作は、仮想マシンであれば簡単に実現できます。今回は、VMware Workstation 12 Player で構成した仮想マシンでテストました。
Windows 7 の仮想マシンに 対象の Windows XP の仮想ディスクを接続して、 Windows 7 の仮想マシンを起動します。そして、Windows XP の仮想ディスクの System32 フォルダ内に、変更した locale.nls ファイルをコピーします。



[備考 1]
Windows XP の回復コンソールの COPY コマンドでも locale.nls ファイルを置き換えることができます。この回復コンソールの実体は Windows PE のコマンドプロンプトですが、ディスク内にあるプログラムの実行はできません。バッチファイルも実行できません。使用可能なコマンド類は限られています。
OEM 版の Windows XP では回復コンソールが利用できないことがあります。

[備考 2]
Windows XP には MS-DOS の起動ディスクを作成する機能がありますが、その MS-DOS からは Windows XP の NTFS ファイルにはアクセスできません。したがって、System32 フォルダ内のファイル操作はできません。





04_Windows 2000 そして Windows XP そして Windows Vista


Windows 2000 は、Windows XP の 1 つ前のバージョンの Windows です。Windows 2000 も元号関係のデータ構造は Windows XP と同じです。オフセットの値を以下に変更すれば新しい元号を追加することができます。ただし、2019年06月01日の更新で追加した、Windows XP 用の処理コードを実行しないようにします。


const
  FInfoStartPos  = $2D8DA;


図10
Windows 2000 の祈祷画面
  • Windows 2000 がリリースされたのは
    2000年2月18日
    (平成12年2月18日)

下図は Windows 2000 での動作確認の結果です。拙作の「元号選択式日付入力コンポーネント」に添付のテストプログラムの実行画面です。これも仮想マシンで実行しています。



図11

Windows XP の次にリリースされたのか Windows Vista です。現在 Windows Vista はほとんど使われていないようですが、Windows XP は今でも使われているようです。Windows Vista も元号の情報が locale.nls に格納されていますが、格納形式が Windows 2000 / XP とは全く違います。

Windows XP が使われ続けている理由に、UAC 関係があるようです。また、Windows XP がインストールされている PC に Windows 10 等をインストールするのは、スペック的にも無理があるため、PC の交換が必要という状況もあるようです。PC を交換すると、稼働中のアプリ類の再インストールが必要です。Windows のバージョンが異なれば、アプリの修正や変更が必要となることがあります。



[備考 1]
Windows XP が使われ続けているのは、リリース期間が長かったという見方があります。その意味では、Windows 7 も使われ続ける可能性があります。ただし、Windows 7 は、2019年05月17日現在でも Windows 10 への無料アップグレードが可能ですから、事情が違うかも知れません・

[備考 2]
Windows Vista の元号の情報は、locale.nls の $0027ECD0 バイト目あたりからあります。長い元号名のリスト、各元号の開始年月日等のリスト、短い元号名のリストの順になっているようですが、解析はかなり難しそうです。


図12
インストール直後の Windows Vista の画面




05_Windows の元号対応とアプリケーションの元号対応


Windows に元号を追加しても、その Windows で動作する全てのアプリケーションで、追加した元号が利用できるようになるわけではありません。アプリケーションとは、Excel 等のソフト、あるいは当サイトで提供しているようなプログラム全てを意味します。

Windows というシステムに追加した元号が利用できるのは、対象のアプリケーションが、システムの元号の処理機能を利用している場合だけです。システムの元号処理機能を使用しないアプリケーションで新しい元号を使用できようにするには、そのアプリケーション自身を、新しい元号に対応する必要があります。


.NET Framework 上で動作するアプリケーションの場合は、Windows の元号処理、.NET Framework のバージョンによる元号処理、対象アプリケーションの対応状況が関係してきます。
本ページの記事掲載時点では、Windows 7 以降については、新元号に対応するための更新プログラムをマイクロソフトが提供しています。



[備考]
新元号に対応するというのは、コンピュータの OS あるいはその OS で動作するアプリケーションに対する処理や操作です。パソコン (PC) を新元号に対応させるわけではありません。
パソコン (コンピュータ) を新元号に対応させるという処理や操作は、おそらく存在しません。

コンピュータと OS に関する知識がないエンドユーザに対しては、コンピュータあるいはパソコンを新元号に対応させる、という表現も必要と思われます。