am 92b2d5a6: Merge "Pseudolocalizer improvements."

* commit '92b2d5a608f9fefc9a88eea03cd599a26c1b62ce':
  Pseudolocalizer improvements.
This commit is contained in:
Elliott Hughes
2014-04-18 22:36:32 +00:00
committed by Android Git Automerger
9 changed files with 337 additions and 106 deletions

View File

@@ -41,6 +41,15 @@ typedef enum Command {
kCommandSingleCrunch,
} Command;
/*
* Pseudolocalization methods
*/
typedef enum PseudolocalizationMethod {
NO_PSEUDOLOCALIZATION = 0,
PSEUDO_ACCENTED,
PSEUDO_BIDI,
} PseudolocalizationMethod;
/*
* Bundle of goodies, including everything specified on the command line.
*/
@@ -50,12 +59,12 @@ public:
: mCmd(kCommandUnknown), mVerbose(false), mAndroidList(false),
mForce(false), mGrayscaleTolerance(0), mMakePackageDirs(false),
mUpdate(false), mExtending(false),
mRequireLocalization(false), mPseudolocalize(false),
mRequireLocalization(false), mPseudolocalize(NO_PSEUDOLOCALIZATION),
mWantUTF16(false), mValues(false),
mCompressionMethod(0), mJunkPath(false), mOutputAPKFile(NULL),
mManifestPackageNameOverride(NULL), mInstrumentationPackageNameOverride(NULL),
mAutoAddOverlay(false), mGenDependencies(false),
mAssetSourceDir(NULL),
mAssetSourceDir(NULL),
mCrunchedOutputDir(NULL), mProguardFile(NULL),
mAndroidManifestFile(NULL), mPublicOutputFile(NULL),
mRClassDir(NULL), mResourceIntermediatesDir(NULL), mManifestMinSdkVersion(NULL),
@@ -94,8 +103,8 @@ public:
void setExtending(bool val) { mExtending = val; }
bool getRequireLocalization(void) const { return mRequireLocalization; }
void setRequireLocalization(bool val) { mRequireLocalization = val; }
bool getPseudolocalize(void) const { return mPseudolocalize; }
void setPseudolocalize(bool val) { mPseudolocalize = val; }
short getPseudolocalize(void) const { return mPseudolocalize; }
void setPseudolocalize(short val) { mPseudolocalize = val; }
void setWantUTF16(bool val) { mWantUTF16 = val; }
bool getValues(void) const { return mValues; }
void setValues(bool val) { mValues = val; }
@@ -250,7 +259,7 @@ private:
bool mUpdate;
bool mExtending;
bool mRequireLocalization;
bool mPseudolocalize;
short mPseudolocalize;
bool mWantUTF16;
bool mValues;
int mCompressionMethod;

View File

@@ -1885,14 +1885,17 @@ int doPackage(Bundle* bundle)
FILE* fp;
String8 dependencyFile;
// -c zz_ZZ means do pseudolocalization
// -c en_XA or/and ar_XB means do pseudolocalization
ResourceFilter filter;
err = filter.parse(bundle->getConfigurations());
if (err != NO_ERROR) {
goto bail;
}
if (filter.containsPseudo()) {
bundle->setPseudolocalize(true);
bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_ACCENTED);
}
if (filter.containsPseudoBidi()) {
bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_BIDI);
}
N = bundle->getFileSpecCount();

View File

@@ -24,8 +24,10 @@ ResourceFilter::parse(const char* arg)
String8 part(p, q-p);
if (part == "zz_ZZ") {
mContainsPseudo = true;
if (part == "en_XA") {
mContainsPseudoAccented = true;
} else if (part == "ar_XB") {
mContainsPseudoBidi = true;
}
int axis;
AxisValue value;

View File

@@ -16,19 +16,22 @@
class ResourceFilter
{
public:
ResourceFilter() : mData(), mContainsPseudo(false) {}
ResourceFilter() : mData(), mContainsPseudoAccented(false),
mContainsPseudoBidi(false) {}
status_t parse(const char* arg);
bool isEmpty() const;
bool match(int axis, const ResTable_config& config) const;
bool match(const ResTable_config& config) const;
const SortedVector<AxisValue>* configsForAxis(int axis) const;
inline bool containsPseudo() const { return mContainsPseudo; }
inline bool containsPseudo() const { return mContainsPseudoAccented; }
inline bool containsPseudoBidi() const { return mContainsPseudoBidi; }
private:
bool match(int axis, const AxisValue& value) const;
KeyedVector<int,SortedVector<AxisValue> > mData;
bool mContainsPseudo;
bool mContainsPseudoAccented;
bool mContainsPseudoBidi;
};
#endif

View File

@@ -25,7 +25,7 @@ status_t compileXmlFile(const sp<AaptAssets>& assets,
if (root == NULL) {
return UNKNOWN_ERROR;
}
return compileXmlFile(assets, root, target, table, options);
}
@@ -577,13 +577,13 @@ status_t parseAndAddBag(Bundle* bundle,
int32_t curFormat,
bool isFormatted,
const String16& product,
bool pseudolocalize,
PseudolocalizationMethod pseudolocalize,
const bool overwrite,
ResourceTable* outTable)
{
status_t err;
const String16 item16("item");
String16 str;
Vector<StringPool::entry_style_span> spans;
err = parseStyledString(bundle, in->getPrintableSource().string(),
@@ -672,7 +672,7 @@ status_t parseAndAddEntry(Bundle* bundle,
int32_t curFormat,
bool isFormatted,
const String16& product,
bool pseudolocalize,
PseudolocalizationMethod pseudolocalize,
const bool overwrite,
KeyedVector<type_ident_pair_t, bool>* skippedResourceNames,
ResourceTable* outTable)
@@ -854,10 +854,16 @@ status_t compileResourceFile(Bundle* bundle,
ResTable_config curParams(defParams);
ResTable_config pseudoParams(curParams);
pseudoParams.language[0] = 'z';
pseudoParams.language[1] = 'z';
pseudoParams.country[0] = 'Z';
pseudoParams.country[1] = 'Z';
pseudoParams.language[0] = 'e';
pseudoParams.language[1] = 'n';
pseudoParams.country[0] = 'X';
pseudoParams.country[1] = 'A';
ResTable_config pseudoBidiParams(curParams);
pseudoBidiParams.language[0] = 'a';
pseudoBidiParams.language[1] = 'r';
pseudoBidiParams.country[0] = 'X';
pseudoBidiParams.country[1] = 'B';
while ((code=block.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) {
if (code == ResXMLTree::START_TAG) {
@@ -1334,6 +1340,7 @@ status_t compileResourceFile(Bundle* bundle,
name,
locale,
SourcePos(in->getPrintableSource(), block.getLineNumber()));
curIsPseudolocalizable = fileIsTranslatable;
}
if (formatted == false16) {
@@ -1389,6 +1396,7 @@ status_t compileResourceFile(Bundle* bundle,
curTag = &plurals16;
curType = plurals16;
curIsBag = true;
curIsPseudolocalizable = fileIsTranslatable;
} else if (strcmp16(block.getElementName(&len), array16.string()) == 0) {
curTag = &array16;
curType = array16;
@@ -1410,26 +1418,24 @@ status_t compileResourceFile(Bundle* bundle,
} else if (strcmp16(block.getElementName(&len), string_array16.string()) == 0) {
// Check whether these strings need valid formats.
// (simplified form of what string16 does above)
bool isTranslatable = false;
size_t n = block.getAttributeCount();
// Pseudolocalizable by default, unless this string array isn't
// translatable.
curIsPseudolocalizable = true;
for (size_t i = 0; i < n; i++) {
size_t length;
const uint16_t* attr = block.getAttributeName(i, &length);
if (strcmp16(attr, translatable16.string()) == 0) {
const uint16_t* value = block.getAttributeStringValue(i, &length);
if (strcmp16(value, false16.string()) == 0) {
curIsPseudolocalizable = false;
}
}
if (strcmp16(attr, formatted16.string()) == 0) {
const uint16_t* value = block.getAttributeStringValue(i, &length);
if (strcmp16(value, false16.string()) == 0) {
curIsFormatted = false;
}
} else if (strcmp16(attr, translatable16.string()) == 0) {
const uint16_t* value = block.getAttributeStringValue(i, &length);
if (strcmp16(value, false16.string()) == 0) {
isTranslatable = false;
}
}
}
@@ -1438,6 +1444,7 @@ status_t compileResourceFile(Bundle* bundle,
curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_STRING;
curIsBag = true;
curIsBagReplaceOnOverwrite = true;
curIsPseudolocalizable = isTranslatable && fileIsTranslatable;
} else if (strcmp16(block.getElementName(&len), integer_array16.string()) == 0) {
curTag = &integer_array16;
curType = array16;
@@ -1559,19 +1566,29 @@ status_t compileResourceFile(Bundle* bundle,
err = parseAndAddBag(bundle, in, &block, curParams, myPackage, curType,
ident, parentIdent, itemIdent, curFormat, curIsFormatted,
product, false, overwrite, outTable);
product, NO_PSEUDOLOCALIZATION, overwrite, outTable);
if (err == NO_ERROR) {
if (curIsPseudolocalizable && localeIsDefined(curParams)
&& bundle->getPseudolocalize()) {
&& bundle->getPseudolocalize() > 0) {
// pseudolocalize here
#if 1
block.setPosition(parserPosition);
err = parseAndAddBag(bundle, in, &block, pseudoParams, myPackage,
curType, ident, parentIdent, itemIdent, curFormat,
curIsFormatted, product, true, overwrite, outTable);
#endif
if ((PSEUDO_ACCENTED & bundle->getPseudolocalize()) ==
PSEUDO_ACCENTED) {
block.setPosition(parserPosition);
err = parseAndAddBag(bundle, in, &block, pseudoParams, myPackage,
curType, ident, parentIdent, itemIdent, curFormat,
curIsFormatted, product, PSEUDO_ACCENTED,
overwrite, outTable);
}
if ((PSEUDO_BIDI & bundle->getPseudolocalize()) ==
PSEUDO_BIDI) {
block.setPosition(parserPosition);
err = parseAndAddBag(bundle, in, &block, pseudoBidiParams, myPackage,
curType, ident, parentIdent, itemIdent, curFormat,
curIsFormatted, product, PSEUDO_BIDI,
overwrite, outTable);
}
}
}
}
if (err != NO_ERROR) {
hasErrors = localHasErrors = true;
}
@@ -1592,20 +1609,31 @@ status_t compileResourceFile(Bundle* bundle,
err = parseAndAddEntry(bundle, in, &block, curParams, myPackage, curType, ident,
*curTag, curIsStyled, curFormat, curIsFormatted,
product, false, overwrite, &skippedResourceNames, outTable);
product, NO_PSEUDOLOCALIZATION, overwrite, &skippedResourceNames, outTable);
if (err < NO_ERROR) { // Why err < NO_ERROR instead of err != NO_ERROR?
hasErrors = localHasErrors = true;
}
else if (err == NO_ERROR) {
if (curIsPseudolocalizable && localeIsDefined(curParams)
&& bundle->getPseudolocalize()) {
&& bundle->getPseudolocalize() > 0) {
// pseudolocalize here
block.setPosition(parserPosition);
err = parseAndAddEntry(bundle, in, &block, pseudoParams, myPackage, curType,
ident, *curTag, curIsStyled, curFormat,
curIsFormatted, product,
true, overwrite, &skippedResourceNames, outTable);
if ((PSEUDO_ACCENTED & bundle->getPseudolocalize()) ==
PSEUDO_ACCENTED) {
block.setPosition(parserPosition);
err = parseAndAddEntry(bundle, in, &block, pseudoParams, myPackage, curType,
ident, *curTag, curIsStyled, curFormat,
curIsFormatted, product,
PSEUDO_ACCENTED, overwrite, &skippedResourceNames, outTable);
}
if ((PSEUDO_BIDI & bundle->getPseudolocalize()) ==
PSEUDO_BIDI) {
block.setPosition(parserPosition);
err = parseAndAddEntry(bundle, in, &block, pseudoBidiParams,
myPackage, curType, ident, *curTag, curIsStyled, curFormat,
curIsFormatted, product,
PSEUDO_BIDI, overwrite, &skippedResourceNames, outTable);
}
if (err != NO_ERROR) {
hasErrors = localHasErrors = true;
}
@@ -2636,8 +2664,8 @@ ResourceTable::validateLocalizations(void)
continue;
}
// don't bother with the pseudolocale "zz_ZZ"
if (config != "zz_ZZ") {
// don't bother with the pseudolocale "en_XA" or "ar_XB"
if (config != "en_XA" && config != "ar_XB") {
if (configSrcMap.find(config) == configSrcMap.end()) {
// okay, no specific localization found. it's possible that we are
// requiring a specific regional localization [e.g. de_DE] but there is an

View File

@@ -187,7 +187,7 @@ status_t parseStyledString(Bundle* bundle,
String16* outString,
Vector<StringPool::entry_style_span>* outSpans,
bool isFormatted,
bool pseudolocalize)
PseudolocalizationMethod pseudolocalize)
{
Vector<StringPool::entry_style_span> spanStack;
String16 curString;
@@ -198,21 +198,30 @@ status_t parseStyledString(Bundle* bundle,
size_t len;
ResXMLTree::event_code_t code;
// Bracketing if pseudolocalization accented method specified.
if (pseudolocalize == PSEUDO_ACCENTED) {
curString.append(String16(String8("[")));
}
while ((code=inXml->next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) {
if (code == ResXMLTree::TEXT) {
String16 text(inXml->getText(&len));
if (firstTime && text.size() > 0) {
firstTime = false;
if (text.string()[0] == '@') {
// If this is a resource reference, don't do the pseudoloc.
pseudolocalize = false;
pseudolocalize = NO_PSEUDOLOCALIZATION;
}
}
if (xliffDepth == 0 && pseudolocalize) {
std::string orig(String8(text).string());
std::string pseudo = pseudolocalize_string(orig);
curString.append(String16(String8(pseudo.c_str())));
if (xliffDepth == 0 && pseudolocalize > 0) {
String16 pseudo;
if (pseudolocalize == PSEUDO_ACCENTED) {
pseudo = pseudolocalize_string(text);
} else if (pseudolocalize == PSEUDO_BIDI) {
pseudo = pseudobidi_string(text);
} else {
pseudo = text;
}
curString.append(pseudo);
} else {
if (isFormatted && hasSubstitutionErrors(fileName, inXml, text) != NO_ERROR) {
return UNKNOWN_ERROR;
@@ -352,6 +361,25 @@ moveon:
}
}
// Bracketing if pseudolocalization accented method specified.
if (pseudolocalize == PSEUDO_ACCENTED) {
const char16_t* str = outString->string();
const char16_t* p = str;
const char16_t* e = p + outString->size();
int words_cnt = 0;
while (p < e) {
if (isspace(*p)) {
words_cnt++;
}
p++;
}
unsigned int length = words_cnt > 3 ? outString->size() :
outString->size() / 2;
curString.append(String16(String8(" ")));
curString.append(pseudo_generate_expansion(length));
curString.append(String16(String8("]")));
}
if (code == ResXMLTree::BAD_DOCUMENT) {
SourcePos(String8(fileName), inXml->getLineNumber()).error(
"Error parsing XML\n");

View File

@@ -26,7 +26,7 @@ status_t parseStyledString(Bundle* bundle,
String16* outString,
Vector<StringPool::entry_style_span>* outSpans,
bool isFormatted,
bool isPseudolocalizable);
PseudolocalizationMethod isPseudolocalizable);
void printXMLBlock(ResXMLTree* block);

View File

@@ -2,89 +2,155 @@
using namespace std;
// String basis to generate expansion
static const String16 k_expansion_string = String16("one two three "
"four five six seven eight nine ten eleven twelve thirteen "
"fourteen fiveteen sixteen seventeen nineteen twenty");
// Special unicode characters to override directionality of the words
static const String16 k_rlm = String16("\xe2\x80\x8f");
static const String16 k_rlo = String16("\xE2\x80\xae");
static const String16 k_pdf = String16("\xE2\x80\xac");
// Placeholder marks
static const String16 k_placeholder_open = String16("\xc2\xbb");
static const String16 k_placeholder_close = String16("\xc2\xab");
static const char*
pseudolocalize_char(char c)
pseudolocalize_char(const char16_t c)
{
switch (c) {
case 'a': return "\xc4\x83";
case 'b': return "\xcf\x84";
case 'c': return "\xc4\x8b";
case 'd': return "\xc4\x8f";
case 'e': return "\xc4\x99";
case 'a': return "\xc3\xa5";
case 'b': return "\xc9\x93";
case 'c': return "\xc3\xa7";
case 'd': return "\xc3\xb0";
case 'e': return "\xc3\xa9";
case 'f': return "\xc6\x92";
case 'g': return "\xc4\x9d";
case 'h': return "\xd1\x9b";
case 'i': return "\xcf\x8a";
case 'h': return "\xc4\xa5";
case 'i': return "\xc3\xae";
case 'j': return "\xc4\xb5";
case 'k': return "\xc4\xb8";
case 'l': return "\xc4\xba";
case 'k': return "\xc4\xb7";
case 'l': return "\xc4\xbc";
case 'm': return "\xe1\xb8\xbf";
case 'n': return "\xd0\xb8";
case 'o': return "\xcf\x8c";
case 'p': return "\xcf\x81";
case 'n': return "\xc3\xb1";
case 'o': return "\xc3\xb6";
case 'p': return "\xc3\xbe";
case 'q': return "\x51";
case 'r': return "\xd2\x91";
case 'r': return "\xc5\x95";
case 's': return "\xc5\xa1";
case 't': return "\xd1\x82";
case 'u': return "\xce\xb0";
case 't': return "\xc5\xa3";
case 'u': return "\xc3\xbb";
case 'v': return "\x56";
case 'w': return "\xe1\xba\x85";
case 'w': return "\xc5\xb5";
case 'x': return "\xd1\x85";
case 'y': return "\xe1\xbb\xb3";
case 'z': return "\xc5\xba";
case 'y': return "\xc3\xbd";
case 'z': return "\xc5\xbe";
case 'A': return "\xc3\x85";
case 'B': return "\xce\xb2";
case 'C': return "\xc4\x88";
case 'D': return "\xc4\x90";
case 'E': return "\xd0\x84";
case 'F': return "\xce\x93";
case 'G': return "\xc4\x9e";
case 'H': return "\xc4\xa6";
case 'I': return "\xd0\x87";
case 'J': return "\xc4\xb5";
case 'C': return "\xc3\x87";
case 'D': return "\xc3\x90";
case 'E': return "\xc3\x89";
case 'G': return "\xc4\x9c";
case 'H': return "\xc4\xa4";
case 'I': return "\xc3\x8e";
case 'J': return "\xc4\xb4";
case 'K': return "\xc4\xb6";
case 'L': return "\xc5\x81";
case 'L': return "\xc4\xbb";
case 'M': return "\xe1\xb8\xbe";
case 'N': return "\xc5\x83";
case 'O': return "\xce\x98";
case 'P': return "\xcf\x81";
case 'N': return "\xc3\x91";
case 'O': return "\xc3\x96";
case 'P': return "\xc3\x9e";
case 'Q': return "\x71";
case 'R': return "\xd0\xaf";
case 'S': return "\xc8\x98";
case 'T': return "\xc5\xa6";
case 'U': return "\xc5\xa8";
case 'R': return "\xc5\x94";
case 'S': return "\xc5\xa0";
case 'T': return "\xc5\xa2";
case 'U': return "\xc3\x9b";
case 'V': return "\xce\xbd";
case 'W': return "\xe1\xba\x84";
case 'W': return "\xc5\xb4";
case 'X': return "\xc3\x97";
case 'Y': return "\xc2\xa5";
case 'Y': return "\xc3\x9d";
case 'Z': return "\xc5\xbd";
case '!': return "\xc2\xa1";
case '?': return "\xc2\xbf";
case '$': return "\xe2\x82\xac";
default: return NULL;
}
}
static const bool
is_possible_normal_placeholder_end(const char16_t c) {
switch (c) {
case 's': return true;
case 'S': return true;
case 'c': return true;
case 'C': return true;
case 'd': return true;
case 'o': return true;
case 'x': return true;
case 'X': return true;
case 'f': return true;
case 'e': return true;
case 'E': return true;
case 'g': return true;
case 'G': return true;
case 'a': return true;
case 'A': return true;
case 'b': return true;
case 'B': return true;
case 'h': return true;
case 'H': return true;
case '%': return true;
case 'n': return true;
default: return false;
}
}
String16
pseudo_generate_expansion(const unsigned int length) {
String16 result = k_expansion_string;
const char16_t* s = result.string();
if (result.size() < length) {
result += String16(" ");
result += pseudo_generate_expansion(length - result.size());
} else {
int ext = 0;
// Should contain only whole words, so looking for a space
for (unsigned int i = length + 1; i < result.size(); ++i) {
++ext;
if (s[i] == ' ') {
break;
}
}
result.remove(length + ext, 0);
}
return result;
}
/**
* Converts characters so they look like they've been localized.
*
* Note: This leaves escape sequences untouched so they can later be
* processed by ResTable::collectString in the normal way.
*/
string
pseudolocalize_string(const string& source)
String16
pseudolocalize_string(const String16& source)
{
const char* s = source.c_str();
string result;
const size_t I = source.length();
const char16_t* s = source.string();
String16 result;
const size_t I = source.size();
for (size_t i=0; i<I; i++) {
char c = s[i];
char16_t c = s[i];
if (c == '\\') {
// Escape syntax, no need to pseudolocalize
if (i<I-1) {
result += '\\';
result += String16("\\");
i++;
c = s[i];
switch (c) {
case 'u':
// this one takes up 5 chars
result += string(s+i, 5);
result += String16(s+i, 5);
i += 4;
break;
case 't':
@@ -96,24 +162,107 @@ pseudolocalize_string(const string& source)
case '\'':
case '\\':
default:
result += c;
result.append(&c, 1);
break;
}
} else {
result += c;
result.append(&c, 1);
}
} else if (c == '%') {
// Placeholder syntax, no need to pseudolocalize
result += k_placeholder_open;
bool end = false;
result.append(&c, 1);
while (!end && i < I) {
++i;
c = s[i];
result.append(&c, 1);
if (is_possible_normal_placeholder_end(c)) {
end = true;
} else if (c == 't') {
++i;
c = s[i];
result.append(&c, 1);
end = true;
}
}
result += k_placeholder_close;
} else if (c == '<' || c == '&') {
// html syntax, no need to pseudolocalize
bool tag_closed = false;
while (!tag_closed && i < I) {
if (c == '&') {
String16 escape_text;
escape_text.append(&c, 1);
bool end = false;
size_t htmlCodePos = i;
while (!end && htmlCodePos < I) {
++htmlCodePos;
c = s[htmlCodePos];
escape_text.append(&c, 1);
// Valid html code
if (c == ';') {
end = true;
i = htmlCodePos;
}
// Wrong html code
else if (!((c == '#' ||
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9')))) {
end = true;
}
}
result += escape_text;
if (escape_text != String16("&lt;")) {
tag_closed = true;
}
continue;
}
if (c == '>') {
tag_closed = true;
result.append(&c, 1);
continue;
}
result.append(&c, 1);
i++;
c = s[i];
}
} else {
// This is a pure text that should be pseudolocalized
const char* p = pseudolocalize_char(c);
if (p != NULL) {
result += p;
result += String16(p);
} else {
result += c;
result.append(&c, 1);
}
}
}
//printf("result=\'%s\'\n", result.c_str());
return result;
}
String16
pseudobidi_string(const String16& source)
{
const char16_t* s = source.string();
String16 result;
result += k_rlm;
result += k_rlo;
for (size_t i=0; i<source.size(); i++) {
char16_t c = s[i];
switch(c) {
case ' ': result += k_pdf;
result += k_rlm;
result.append(&c, 1);
result += k_rlm;
result += k_rlo;
break;
default: result.append(&c, 1);
break;
}
}
result += k_pdf;
result += k_rlm;
return result;
}

View File

@@ -1,9 +1,18 @@
#ifndef HOST_PSEUDOLOCALIZE_H
#define HOST_PSEUDOLOCALIZE_H
#include "StringPool.h"
#include <string>
std::string pseudolocalize_string(const std::string& source);
String16 pseudolocalize_string(const String16& source);
// Surrounds every word in the sentance with specific characters that makes
// the word directionality RTL.
String16 pseudobidi_string(const String16& source);
// Generates expansion string based on the specified lenght.
// Generated string could not be shorter that length, but it could be slightly
// longer.
String16 pseudo_generate_expansion(const unsigned int length);
#endif // HOST_PSEUDOLOCALIZE_H