Introduces in-place phrase replacement.

Since we have implemented the functions to add and exlcude phrases, the
commit allows users to use a table to change the output of a phrase
without changing its BPMF reading and score, when the "phrase replacement"
mode is on.

It could help users to switch a specific input scenario and the ordinary
one. For example, if a user wants to work on financial Chinese numbers
like 壹、貳、參, he or she may want the characters to have higher score
as the normal numbers like 一、二、三. The commit can let the users to
temporarily replace 一、二、三 to 壹、貳、參 by just turn on "phrase
replacement" mode and prepare a custom table.

The conversion is not done on the output phase like how we do
Traditional/Simplified Chinese conversion. What the phrase replacement
table does is to slightly modify the language model. The replacement
takes place on walking the nodes and candidates list.

A user can enable the mode and edit the table from the input menu. Since
the function is quite advanced, the menu items are hidden until the user
holds the option key.

The table is a plain text file. Each line contains a "from" and "to".
For example

```
一 壹
```

However, if the user also want all other phrase contain 一 to become 壹,
all of the phrases have to be built into the table

```
一百 壹佰
一千 壹仟
一萬 壹萬
一百萬 壹百萬
```
This commit is contained in:
zonble 2022-01-15 06:23:09 +08:00
parent 825ed4f122
commit 136ac34f22
13 changed files with 263 additions and 31 deletions

View File

@ -50,6 +50,7 @@
D44FB74527915565003C80A6 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74427915555003C80A6 /* Preferences.swift */; };
D44FB74727919D35003C80A6 /* EmacsKeyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74627919C83003C80A6 /* EmacsKeyHelper.swift */; };
D44FB74A2791B829003C80A6 /* VXHanConvert in Frameworks */ = {isa = PBXBuildFile; productRef = D44FB7492791B829003C80A6 /* VXHanConvert */; };
D44FB74D2792189A003C80A6 /* PhraseReplacementMap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */; };
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; };
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; };
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
@ -181,6 +182,8 @@
D44FB74427915555003C80A6 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
D44FB74627919C83003C80A6 /* EmacsKeyHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmacsKeyHelper.swift; sourceTree = "<group>"; };
D44FB7482791B346003C80A6 /* VXHanConvert */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = VXHanConvert; path = Packages/VXHanConvert; sourceTree = "<group>"; };
D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PhraseReplacementMap.cpp; sourceTree = "<group>"; };
D44FB74C2792189A003C80A6 /* PhraseReplacementMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PhraseReplacementMap.h; sourceTree = "<group>"; };
D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = "<group>"; };
D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonModalAlertWindowController.swift; sourceTree = "<group>"; };
D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = "<group>"; };
@ -292,6 +295,8 @@
6ACC3D3C27914AAB00F1B140 /* KeyValueBlobReader.h */,
D41355DC278EA3ED005E5CBD /* UserPhrasesLM.cpp */,
D41355DD278EA3ED005E5CBD /* UserPhrasesLM.h */,
D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */,
D44FB74C2792189A003C80A6 /* PhraseReplacementMap.h */,
D41355D9278E6D17005E5CBD /* McBopomofoLM.cpp */,
D41355DA278E6D17005E5CBD /* McBopomofoLM.h */,
D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */,
@ -589,6 +594,7 @@
D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */,
D44FB74727919D35003C80A6 /* EmacsKeyHelper.swift in Sources */,
6A0D4ED315FC0D6400ABF4B3 /* main.m in Sources */,
D44FB74D2792189A003C80A6 /* PhraseReplacementMap.cpp in Sources */,
D44FB74527915565003C80A6 /* Preferences.swift in Sources */,
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */,
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */,

View File

@ -52,7 +52,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NonModalAlertWindowControlle
func applicationDidFinishLaunching(_ notification: Notification) {
LanguageModelManager.loadDataModels()
LanguageModelManager.loadUserPhrasesModel()
LanguageModelManager.loadUserPhrases()
LanguageModelManager.loadUserPhraseReplacement()
if UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) == nil {
UserDefaults.standard.set(true, forKey: kCheckUpdateAutomatically)

View File

@ -37,6 +37,7 @@ McBopomofoLM::~McBopomofoLM()
m_languageModel.close();
m_userPhrases.close();
m_excludedPhrases.close();
m_phraseReplacement.close();
}
void McBopomofoLM::loadLanguageModel(const char* languageModelDataPath)
@ -60,6 +61,13 @@ void McBopomofoLM::loadUserPhrases(const char* userPhrasesDataPath,
}
}
void McBopomofoLM::loadPhraseReplacementMap(const char* phraseReplacementPath) {
if (phraseReplacementPath) {
m_phraseReplacement.close();
m_phraseReplacement.open(phraseReplacementPath);
}
}
const vector<Bigram> McBopomofoLM::bigramsForKeys(const string& preceedingKey, const string& key)
{
return vector<Bigram>();
@ -83,24 +91,45 @@ const vector<Unigram> McBopomofoLM::unigramsForKey(const string& key)
if (m_userPhrases.hasUnigramsForKey(key)) {
vector<Unigram> rawUserUnigrams = m_userPhrases.unigramsForKey(key);
vector<Unigram> filterredUserUnigrams = m_userPhrases.unigramsForKey(key);
for (auto&& unigram : rawUserUnigrams) {
if (excludedValues.find(unigram.keyValue.value) == excludedValues.end()) {
userUnigrams.push_back(unigram);
filterredUserUnigrams.push_back(unigram);
}
}
transform(userUnigrams.begin(), userUnigrams.end(),
transform(filterredUserUnigrams.begin(), filterredUserUnigrams.end(),
inserter(userValues, userValues.end()),
[](const Unigram &u) { return u.keyValue.value; });
if (m_phraseReplacementEnabled) {
for (auto&& unigram : filterredUserUnigrams) {
string value = unigram.keyValue.value;
string replacement = m_phraseReplacement.valueForKey(value);
if (replacement != "") {
unigram.keyValue.value = replacement;
}
unigrams.push_back(unigram);
}
} else {
unigrams = filterredUserUnigrams;
}
}
if (m_languageModel.hasUnigramsForKey(key)) {
vector<Unigram> globalUnigrams = m_languageModel.unigramsForKey(key);
for (auto&& unigram : globalUnigrams) {
if (excludedValues.find(unigram.keyValue.value) == excludedValues.end() &&
userValues.find(unigram.keyValue.value) == userValues.end()) {
string value = unigram.keyValue.value;
if (excludedValues.find(value) == excludedValues.end() &&
userValues.find(value) == userValues.end()) {
if (m_phraseReplacementEnabled) {
string replacement = m_phraseReplacement.valueForKey(value);
if (replacement != "") {
unigram.keyValue.value = replacement;
}
}
unigrams.push_back(unigram);
}
}
@ -119,3 +148,14 @@ bool McBopomofoLM::hasUnigramsForKey(const string& key)
return unigramsForKey(key).size() > 0;
}
void McBopomofoLM::setPhraseReplacementEnabled(bool enabled)
{
m_phraseReplacementEnabled = enabled;
}
bool McBopomofoLM::phraseReplacementEnabled()
{
return m_phraseReplacementEnabled;
}

View File

@ -27,6 +27,7 @@
#include <stdio.h>
#include "FastLM.h"
#include "UserPhrasesLM.h"
#include "PhraseReplacementMap.h"
namespace McBopomofo {
@ -38,17 +39,23 @@ public:
~McBopomofoLM();
void loadLanguageModel(const char* languageModelDataPath);
void loadUserPhrases(const char* m_userPhrasesDataPath,
const char* m_excludedPhrasesDataPath);
void loadUserPhrases(const char* userPhrasesDataPath,
const char* excludedPhrasesDataPath);
void loadPhraseReplacementMap(const char* phraseReplacementPath);
const vector<Bigram> bigramsForKeys(const string& preceedingKey, const string& key);
const vector<Unigram> unigramsForKey(const string& key);
bool hasUnigramsForKey(const string& key);
void setPhraseReplacementEnabled(bool enabled);
bool phraseReplacementEnabled();
protected:
FastLM m_languageModel;
UserPhrasesLM m_userPhrases;
UserPhrasesLM m_excludedPhrases;
PhraseReplacementMap m_phraseReplacement;
bool m_phraseReplacementEnabled;
};
};

View File

@ -0,0 +1,91 @@
#include "PhraseReplacementMap.h"
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <fstream>
#include <unistd.h>
#include "KeyValueBlobReader.h"
namespace McBopomofo {
using std::string;
PhraseReplacementMap::PhraseReplacementMap()
: fd(-1)
, data(0)
, length(0)
{
}
PhraseReplacementMap::~PhraseReplacementMap()
{
if (data) {
close();
}
}
bool PhraseReplacementMap::open(const char *path)
{
if (data) {
return false;
}
fd = ::open(path, O_RDONLY);
if (fd == -1) {
printf("open:: file not exist");
return false;
}
struct stat sb;
if (fstat(fd, &sb) == -1) {
printf("open:: cannot open file");
return false;
}
length = (size_t)sb.st_size;
data = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, 0);
if (!data) {
::close(fd);
return false;
}
KeyValueBlobReader reader(static_cast<char*>(data), length);
KeyValueBlobReader::KeyValue keyValue;
KeyValueBlobReader::State state;
while ((state = reader.Next(&keyValue)) == KeyValueBlobReader::State::HAS_PAIR) {
keyValueMap[keyValue.key] = keyValue.value;
}
if (state == KeyValueBlobReader::State::ERROR) {
close();
return false;
}
return true;
}
void PhraseReplacementMap::close()
{
if (data) {
munmap(data, length);
::close(fd);
data = 0;
}
keyValueMap.clear();
}
const std::string PhraseReplacementMap::valueForKey(const std::string& key)
{
auto iter = keyValueMap.find(key);
if (iter != keyValueMap.end()) {
const std::string_view v = iter->second;
return {v.data(), v.size()};
}
return string("");
}
}

View File

@ -0,0 +1,29 @@
#ifndef PHRASEREPLACEMENTMAP_H
#define PHRASEREPLACEMENTMAP_H
#include <string>
#include <map>
#include <iostream>
namespace McBopomofo {
class PhraseReplacementMap
{
public:
PhraseReplacementMap();
~PhraseReplacementMap();
bool open(const char *path);
void close();
const std::string valueForKey(const std::string& key);
protected:
std::map<std::string_view, std::string_view> keyValueMap;
int fd;
void *data;
size_t length;
};
}
#endif

View File

@ -142,6 +142,7 @@ static double FindHighestScore(const vector<NodeAnchor>& nodes, double epsilon)
// create the lattice builder
_languageModel = [LanguageModelManager languageModelMcBopomofo];
_languageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled);
_userOverrideModel = [LanguageModelManager userOverrideModel];
_builder = new BlockReadingBuilder(_languageModel);
@ -165,14 +166,19 @@ static double FindHighestScore(const vector<NodeAnchor>& nodes, double epsilon)
[menu addItemWithTitle:NSLocalizedString(@"McBopomofo Preferences", @"") action:@selector(showPreferences:) keyEquivalent:@""];
NSMenuItem *chineseConversionMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Chinese Conversion", @"") action:@selector(toggleChineseConverter:) keyEquivalent:@"g"];
NSMenuItem *chineseConversionMenuItem = [menu addItemWithTitle:NSLocalizedString(@"Chinese Conversion", @"") action:@selector(toggleChineseConverter:) keyEquivalent:@"g"];
chineseConversionMenuItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagControl;
chineseConversionMenuItem.state = Preferences.chineseConversionEnabled ? NSControlStateValueOn : NSControlStateValueOff;
[menu addItem:chineseConversionMenuItem];
NSMenuItem *halfWidthPunctuationMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Use Half-Width Punctuations", @"") action:@selector(toggleHalfWidthPunctuation:) keyEquivalent:@""];
NSMenuItem *halfWidthPunctuationMenuItem = [menu addItemWithTitle:NSLocalizedString(@"Use Half-Width Punctuations", @"") action:@selector(toggleHalfWidthPunctuation:) keyEquivalent:@""];
halfWidthPunctuationMenuItem.state = Preferences.halfWidthPunctuationEnabled ? NSControlStateValueOn : NSControlStateValueOff;
[menu addItem:halfWidthPunctuationMenuItem];
BOOL optionKeyPressed = [[NSEvent class] respondsToSelector:@selector(modifierFlags)] && ([NSEvent modifierFlags] & NSAlternateKeyMask);
if (_inputMode == kBopomofoModeIdentifier && optionKeyPressed) {
NSMenuItem *phaseReplacementMenuItem = [menu addItemWithTitle:NSLocalizedString(@"Use Phrase Replacement", @"") action:@selector(togglePhraseReplacementEnabled:) keyEquivalent:@""];
phaseReplacementMenuItem.state = Preferences.phraseReplacementEnabled ? NSControlStateValueOn : NSControlStateValueOff;
}
[menu addItem:[NSMenuItem separatorItem]];
[menu addItemWithTitle:NSLocalizedString(@"User Phrases", @"") action:NULL keyEquivalent:@""];
@ -183,6 +189,9 @@ static double FindHighestScore(const vector<NodeAnchor>& nodes, double epsilon)
else {
[menu addItemWithTitle:NSLocalizedString(@"Edit User Phrases", @"") action:@selector(openUserPhrases:) keyEquivalent:@""];
[menu addItemWithTitle:NSLocalizedString(@"Edit Excluded Phrases", @"") action:@selector(openExcludedPhrasesMcBopomofo:) keyEquivalent:@""];
if (optionKeyPressed) {
[menu addItemWithTitle:NSLocalizedString(@"Edit Phrase Replacement Table", @"") action:@selector(openPhraseReplacementMcBopomofo:) keyEquivalent:@""];
}
}
[menu addItemWithTitle:NSLocalizedString(@"Reload User Phrases", @"") action:@selector(reloadUserPhrases:) keyEquivalent:@""];
[menu addItem:[NSMenuItem separatorItem]];
@ -270,6 +279,7 @@ static double FindHighestScore(const vector<NodeAnchor>& nodes, double epsilon)
else {
newInputMode = kBopomofoModeIdentifier;
newLanguageModel = [LanguageModelManager languageModelMcBopomofo];
newLanguageModel->setPhraseReplacementEnabled(Preferences.phraseReplacementEnabled);
}
// Only apply the changes if the value is changed
@ -1481,6 +1491,30 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
}
- (void)toggleChineseConverter:(id)sender
{
BOOL chineseConversionEnabled = [Preferences toggleChineseConversionEnabled];
[NotifierController notifyWithMessage:
chineseConversionEnabled ?
NSLocalizedString(@"Chinese conversion on", @"") :
NSLocalizedString(@"Chinese conversion off", @"") stay:NO];
}
- (void)toggleHalfWidthPunctuation:(id)sender
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
[Preferences tooglePhraseReplacementEnabled];
#pragma GCC diagnostic pop
}
- (void)togglePhraseReplacementEnabled:(id)sender
{
BOOL enabled = [Preferences tooglePhraseReplacementEnabled];
McBopomofoLM *lm = [LanguageModelManager languageModelMcBopomofo];
lm->setPhraseReplacementEnabled(enabled);
}
- (void)checkForUpdate:(id)sender
{
[(AppDelegate *)[[NSApplication sharedApplication] delegate] checkForUpdateForced:YES];
@ -1521,9 +1555,15 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
[self _openUserFile:[LanguageModelManager excludedPhrasesDataPathMcBopomofo]];
}
- (void)openPhraseReplacementMcBopomofo:(id)sender
{
[self _openUserFile:[LanguageModelManager phraseReplacementDataPathMcBopomofo]];
}
- (void)reloadUserPhrases:(id)sender
{
[LanguageModelManager loadUserPhrasesModel];
[LanguageModelManager loadUserPhrases];
[LanguageModelManager loadUserPhraseReplacement];
}
- (void)showAbout:(id)sender
@ -1532,22 +1572,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; }
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
}
- (void)toggleChineseConverter:(id)sender
{
BOOL chineseConversionEnabled = [Preferences toggleChineseConversionEnabled];
[NotifierController notifyWithMessage:
chineseConversionEnabled ?
NSLocalizedString(@"Chinese conversion on", @"") :
NSLocalizedString(@"Chinese conversion off", @"") stay:NO];
}
- (void)toggleHalfWidthPunctuation:(id)sender
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
[Preferences toogleHalfWidthPunctuationEnabled];
#pragma GCC diagnostic pop
}
@end

View File

@ -8,7 +8,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface LanguageModelManager : NSObject
+ (void)loadDataModels;
+ (void)loadUserPhrasesModel;
+ (void)loadUserPhrases;
+ (void)loadUserPhraseReplacement;
+ (BOOL)checkIfUserLanguageModelFilesExist;
+ (BOOL)writeUserPhrase:(NSString *)userPhrase;
@ -16,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (class, readonly, nonatomic) NSString *userPhrasesDataPathMcBopomofo;
@property (class, readonly, nonatomic) NSString *excludedPhrasesDataPathMcBopomofo;
@property (class, readonly, nonatomic) NSString *excludedPhrasesDataPathPlainBopomofo;
@property (class, readonly, nonatomic) NSString *phraseReplacementDataPathMcBopomofo;
@property (class, readonly, nonatomic) McBopomofo::McBopomofoLM *languageModelMcBopomofo;
@property (class, readonly, nonatomic) McBopomofo::McBopomofoLM *languageModelPlainBopomofo;
@property (class, readonly, nonatomic) McBopomofo::UserOverrideModel *userOverrideModel;

View File

@ -32,12 +32,17 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
LTLoadLanguageModelFile(@"data-plain-bpmf", gLanguageModelPlainBopomofo);
}
+ (void)loadUserPhrasesModel
+ (void)loadUserPhrases
{
gLanguageModelMcBopomofo.loadUserPhrases([[self userPhrasesDataPathMcBopomofo] UTF8String], [[self excludedPhrasesDataPathMcBopomofo] UTF8String]);
gLanguageModelPlainBopomofo.loadUserPhrases(NULL, [[self excludedPhrasesDataPathPlainBopomofo] UTF8String]);
}
+ (void)loadUserPhraseReplacement
{
gLanguageModelMcBopomofo.loadPhraseReplacementMap([[self phraseReplacementDataPathMcBopomofo] UTF8String]);
}
+ (BOOL)checkIfUserDataFolderExists
{
NSString *folderPath = [self dataFolderPath];
@ -89,6 +94,9 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
if (![self checkIfFileExist:[self excludedPhrasesDataPathPlainBopomofo]]) {
return NO;
}
if (![self checkIfFileExist:[self phraseReplacementDataPathMcBopomofo]]) {
return NO;
}
return YES;
}
@ -135,7 +143,7 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
[writeFile writeData:data];
[writeFile closeFile];
[self loadUserPhrasesModel];
[self loadUserPhrases];
return YES;
}
@ -162,6 +170,11 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, McBopomo
return [[self dataFolderPath] stringByAppendingPathComponent:@"exclude-phrases-plain-bpmf.txt"];
}
+ (NSString *)phraseReplacementDataPathMcBopomofo
{
return [[self dataFolderPath] stringByAppendingPathComponent:@"phrases-replacement.txt"];
}
+ (McBopomofoLM *)languageModelMcBopomofo
{
return &gLanguageModelMcBopomofo;

View File

@ -6,5 +6,6 @@
@interface LanguageModelManager : NSObject
+ (void)loadDataModels;
+ (void)loadUserPhrasesModel;
+ (void)loadUserPhrases;
+ (void)loadUserPhraseReplacement;
@end

View File

@ -51,6 +51,7 @@ private let kCandidateTextFontName = "CandidateTextFontName"
private let kCandidateKeyLabelFontName = "CandidateKeyLabelFontName"
private let kCandidateKeys = "CandidateKeys"
private let kChineseConversionEngineKey = "ChineseConversionEngine"
private let kPhraseReplacementEnabledKey = "PhraseReplacementEnabled"
private let kDefaultCandidateListTextSize: CGFloat = 16
private let kMinKeyLabelSize: CGFloat = 10
@ -291,4 +292,12 @@ class Preferences: NSObject {
return ChineseConversionEngine(rawValue: chineneConversionEngine)?.name
}
@UserDefault(key: kPhraseReplacementEnabledKey, defaultValue: false)
@objc static var phraseReplacementEnabled: Bool
@objc static func tooglePhraseReplacementEnabled() -> Bool {
phraseReplacementEnabled = !phraseReplacementEnabled
return phraseReplacementEnabled;
}
}

View File

@ -72,3 +72,7 @@
"Chinese conversion on" = "Chinese conversion on";
"Chinese conversion off" = "Chinese conversion off";
"Edit Phrase Replacement Table" = "Edit Phrase Replacement Table";
"Use Phrase Replacement" = "Use Phrase Replacement";

View File

@ -72,3 +72,7 @@
"Chinese conversion on" = "已經切換到簡體中文模式";
"Chinese conversion off" = "已經切換到繁體中文模式";
"Edit Phrase Replacement Table" = "編輯詞彙替換表格";
"Use Phrase Replacement" = "使用詞彙替換";