Seize the day

POST : Android Dev Study

안드로이드 다국어 번역툴 #8

번역 툴 마지막은 Restring 개선이다. Restring을 적용해도 바뀌지 않은 문자열이 있었다. 

1. 액티비트 타이틀이 바뀌지 않는 경우가 있고

2. 커스텀 뷰를 layout xml에 사용한 경우에는 변경되지 않았고, 

3. 메뉴에서도 스트링이 바뀌지 않았다. 


Activity title과 CustomView의 처리는 BaseActivity의 onPostCreate에서 하는 것이 가장 좋을 것 같다. 

CustomViewTransformer.kt

class CustomViewTransformer(private val context: Context) {

fun transform(viewGroup: ViewGroup?) {
if (viewGroup == null) {
return
}
for (i in 0..viewGroup.childCount) {
val childView = viewGroup.getChildAt(i)
if (childView is CommonSettingsView) {
checkSettingsView(childView)
} else {
transform(childView as? ViewGroup)
}
}
}

private fun checkSettingsView(settingsView: CommonSettingsView) {
settingsView.transformRestring(context)
}
}


BaseActivity.java

@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);

if (BuildConfig.RESTRING) {
new CustomViewTransformer(this).transform((ViewGroup) getWindow().getDecorView());
try {
ActivityInfo activityInfo = getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
if (activityInfo != null) {
setTitle(getString(activityInfo.labelRes));
}
} catch (Exception ignored) {
}
}
}


커스텀뷰는 CommonSettingsView뿐이고, 미리 TextView와 string resource id를 가지고 있다가 업데이트 해준다. 

public class CommonSettingsView extends FrameLayout implements View.OnClickListener {

static class RestringItem {
TextView textView;
int viewId;

RestringItem(TextView textView, int viewId) {
this.textView = textView;
this.viewId = viewId;
}
}
final ArrayList<RestringItem> restringItemList = new ArrayList<>();

public CommonSettingsView(Context context) {
this(context, null);
}

public CommonSettingsView(Context context, AttributeSet attrs) {
super(context, attrs);

inflate(getContext(), R.layout.common_settings_view, this);

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.settings_custom);
if (typedArray != null) {
String value;

value = typedArray.getString(R.styleable.settings_custom_titleText);
if (StringUtils.isNotEmpty(value)) {
ViewUtils.setVisible(findViewById(R.id.title_layout));
title.setText(value);
}
if (BuildConfig.RESTRING) {
int viewId = typedArray.getResourceId(R.styleable.settings_custom_titleText, View.NO_ID);
if (viewId != View.NO_ID) {
restringItemList.add(new RestringItem(title, viewId));
}
}

value = typedArray.getString(R.styleable.settings_custom_subjectText);
if (StringUtils.isNotEmpty(value)) {
ViewUtils.setVisible(findViewById(R.id.subject_layout));
subjectText.setText(value);
}
if (BuildConfig.RESTRING) {
int viewId = typedArray.getResourceId(R.styleable.settings_custom_subjectText, View.NO_ID);
if (viewId != View.NO_ID) {
restringItemList.add(new RestringItem(subjectText, viewId));
}
}

value = typedArray.getString(R.styleable.settings_custom_valueText);
if (StringUtils.isNotEmpty(value)) {
ViewUtils.setVisible(valueText);
valueText.setText(value);
}
if (BuildConfig.RESTRING) {
int viewId = typedArray.getResourceId(R.styleable.settings_custom_valueText, View.NO_ID);
if (viewId != View.NO_ID) {
restringItemList.add(new RestringItem(valueText, viewId));
}
}


}
public void transformRestring(Context context) {
if (BuildConfig.RESTRING) {
for (RestringItem item : restringItemList) {
item.textView.setText(context.getString(item.viewId));
}
}
}
}


메뉴는 BaseActivity의 getMenuInflater를 override한다.

class RestringMenuInflater(
private val context: Context,
private val menuInflater: MenuInflater
) : MenuInflater(context) {

override fun inflate(@MenuRes menuRes: Int, menu: Menu) {
menuInflater.inflate(menuRes, menu)

MenuTransformer(context, menuRes) { menuId, stringId ->
menu.findItem(menuId)?.title = context.getString(stringId)
}.transform()
}
}


BaseActivity.java

@NonNull
@Override
public MenuInflater getMenuInflater() {
if (BuildConfig.RESTRING) {
return new RestringMenuInflater(this, super.getMenuInflater());
}
return super.getMenuInflater();
}


MenuTransformer.kt 주요 코드는 안드로이드에서 가져왔다. 

typealias MenuTransformerCallback = (menuId: Int, stringId: Int) -> Unit

class MenuTransformer(
private val context: Context,
private val menuRes: Int,
private val callback: MenuTransformerCallback
) {

companion object {
private const val XML_MENU = "menu"
private const val XML_GROUP = "group"
private const val XML_ITEM = "item"
}

fun transform() = apply {
var parser: XmlResourceParser? = null
try {
parser = context.resources.getLayout(menuRes)
val attrs = Xml.asAttributeSet(parser)

parseMenu(parser!!, attrs)
} catch (e: XmlPullParserException) {
throw InflateException("Error inflating menu XML", e)
} catch (e: IOException) {
throw InflateException("Error inflating menu XML", e)
} finally {
parser?.close()
}
}

@Throws(XmlPullParserException::class, IOException::class)
private fun parseMenu(parser: XmlPullParser, attrs: AttributeSet) {
//val menuState = MenuState(menu)

var eventType = parser.eventType
var tagName: String
var lookingForEndOfUnknownTag = false
var unknownTagName: String? = null

// This loop will skip to the menu start tag
do {
if (eventType == XmlPullParser.START_TAG) {
tagName = parser.name
if (tagName == XML_MENU) {
// Go to next tag
eventType = parser.next()
break
}

throw RuntimeException("Expecting menu, got $tagName")
}
eventType = parser.next()
} while (eventType != XmlPullParser.END_DOCUMENT)

var reachedEndOfMenu = false
while (!reachedEndOfMenu) {
when (eventType) {
XmlPullParser.START_TAG -> {
if (!lookingForEndOfUnknownTag) {

tagName = parser.name
when (tagName) {
XML_GROUP -> {
//menuState.readGroup(attrs)
}
XML_ITEM -> transformMenuItem(attrs)
XML_MENU -> // A menu start tag denotes a submenu for an item
//val subMenu = menuState.addSubMenuItem()
//registerMenu(subMenu, attrs)

// Parse the submenu into returned SubMenu
parseMenu(parser, attrs)
else -> {
lookingForEndOfUnknownTag = true
unknownTagName = tagName
}
}
}
}

XmlPullParser.END_TAG -> {
tagName = parser.name
if (lookingForEndOfUnknownTag && tagName == unknownTagName) {
lookingForEndOfUnknownTag = false
unknownTagName = null
} else if (tagName == XML_GROUP) {
//menuState.resetGroup()
} else if (tagName == XML_ITEM) {
// Add the item if it hasn't been added (if the item was
// a submenu, it would have been added already)
// if (!menuState.hasAddedItem()) {
// if (menuState.itemActionProvider != null && menuState.itemActionProvider.hasSubMenu()) {
// registerMenu(menuState.addSubMenuItem(), attrs)
// } else {
// registerMenu(menuState.addItem(), attrs)
// }
// }
} else if (tagName == XML_MENU) {
reachedEndOfMenu = true
}
}

XmlPullParser.END_DOCUMENT -> throw RuntimeException("Unexpected end of document")
}

eventType = parser.next()
}
}

@SuppressLint("PrivateResource")
private fun transformMenuItem(attrs: AttributeSet) {
val a = context.obtainStyledAttributes(
attrs,
R.styleable.MenuItem
)

val menuId = a.getResourceId(R.styleable.MenuItem_android_id, 0)
val stringId = a.getResourceId(R.styleable.MenuItem_android_title, View.NO_ID)
if (stringId != View.NO_ID) {
callback(menuId, stringId)
}

a.recycle()
}
}


Activity의 툴바의 메뉴는 자동으로 Restring을 적용하지만 팝업 메뉴는 직접 반영해야한다.

final PopupMenu popupMenu = new PopupMenu(context, multiView);
popupMenu.inflate(R.menu.menu_multiple);
if (BuildConfig.RESTRING) {
new MenuTransformer(context, R.menu.menu_multiple, (menuId, stringId) -> {
popupMenu.getMenu().findItem(menuId).setTitle(context.getString(stringId));
return null;
}).transform();
}


그리고 Notification의 스트링등은 별도 처리해야 한다. 기존 getString을 getRestring으로 변경하면 

public String getRestring(@StringRes int resourceId) {
if (BuildConfig.RESTRING) {
BaseRestringHelper restringHelper = ((BaseApplication) context.getApplicationContext()).getRestringHelper();
if (restringHelper != null) {
return restringHelper.getString(context, resourceId);
}
}
return context.getString(resourceId);
}


Toast의 메시지도 별도 처리해야 한다. 

public class ToastUtils {
public static void show(int resId) {
show(resId, false);
}

public static void show(int resId, boolean isLong) {
if (BuildConfig.RESTRING) {
BaseRestringHelper restringHelper = ((BaseApplication) ApplicationKeeper.get()).getRestringHelper();
if (restringHelper != null) {
show(restringHelper.getString(ApplicationKeeper.get(), resId), isLong);
return;
}
}
show(ApplicationKeeper.get().getResources().getString(resId), isLong);
}


top

posted at

2018. 12. 29. 22:40


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크