Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package com.yugahashimoto.andcode.feature.browser

import android.annotation.SuppressLint
import android.view.ViewGroup
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.yugahashimoto.andcode.R

/** In-app browser for pages served inside the guest runtime (e.g. http://127.0.0.1:PORT/). */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GuestBrowserScreen(
initialUrl: String,
onBack: () -> Unit,
) {
var urlInput by remember { mutableStateOf(initialUrl) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
remember で保持しているため、画面回転などの設定変更で URL 入力値・進捗・履歴状態がすべてリセットされ、ブラウザが初期 URL に戻ってしまいます。urlInputrememberSaveable に変更するか、WebView の onSaveInstanceState/restoreState を利用して設定変更後も状態を復元できるようにしてください。

Suggestion:

Suggested change
var urlInput by remember { mutableStateOf(initialUrl) }
var urlInput by rememberSaveable { mutableStateOf(initialUrl) }

var progress by remember { mutableIntStateOf(0) }
var canGoBack by remember { mutableStateOf(false) }
var canGoForward by remember { mutableStateOf(false) }
var webView by remember { mutableStateOf<WebView?>(null) }

BackHandler {
val view = webView
if (view != null && view.canGoBack()) {
view.goBack()
} else {
onBack()
}
}

Scaffold(
topBar = {
GuestBrowserTopBar(
canGoBack = canGoBack,
canGoForward = canGoForward,
onBack = onBack,
onHistoryBack = { webView?.goBack() },
onForward = { webView?.goForward() },
onReload = { webView?.reload() },
)
},
) { padding ->
Column(modifier = Modifier.padding(padding).fillMaxSize()) {
GuestBrowserUrlBar(
urlInput = urlInput,
onUrlChange = { urlInput = it },
onGo = { webView?.loadUrl(normalizeUrl(urlInput)) },
)
if (progress in 1..99) {
LinearProgressIndicator(progress = { progress / 100f })
}
AndroidView(
modifier = Modifier.weight(1f),
factory = { context ->
WebView(context).apply {
Comment on lines +82 to +85

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · high]
WebView のライフサイクル管理が欠落しています。AndroidViewonRelease を指定していないため、この画面が composition から離脱しても WebView が destroy() されず、画面遷移を繰り返すたびに WebView インスタンスがリークします。また、Activity の onPause/onResume に追従する webView.onPause()/onResume() も呼ばれていないため、バックグラウンド時も CPU・バッテリーを消費し続けるリスクがあります。onRelease = { it.destroy() } を指定し、必要に応じて LifecycleEventObserver 等で onPause/onResume を連携してください。

Suggestion:

Suggested change
AndroidView(
modifier = Modifier.weight(1f),
factory = { context ->
WebView(context).apply {
AndroidView(
modifier = Modifier.weight(1f),
factory = { context ->
WebView(context).apply {
// ...
}
},
onRelease = { it.destroy() },
)

layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
configure(
onProgress = { progress = it },
onNavigationStateChange = { url, back, forward ->
urlInput = url
canGoBack = back
canGoForward = forward
},
)
webView = this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
WebViewはネイティブリソースを保持するため、画面離脱時にdestroy()を呼ぶ必要がありますが、本実装ではDisposableEffect等による破棄処理がありません。ComposeのAndroidViewはcomposableツリーから外れても自動的にdestroy()を呼ばないため、画面を開くたびにWebViewとそのネイティブリソースがリークします。また破棄後もwebViewのstate参照が残るため、BackHandler等で破棄済みインスタンスを参照する恐れもあります。DisposableEffectでonDispose内にstopLoading()→webChromeClient/webViewClientをnull化→destroy()→webView=nullの一連の処理を追加することを推奨します。

if (initialUrl.isNotBlank()) {
loadUrl(normalizeUrl(initialUrl))
}
Comment on lines +99 to +102

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · high]
AndroidView の factory 内で生成した WebView を Compose の state(webView)に保持していますが、画面離脱時の破棄処理(AndroidView の onRelease での WebView.destroy()、または DisposableEffect によるクリーンアップ)がありません。Compose のライフサイクルと同期しないため、GuestBrowserScreen を離れても WebView のネイティブリソースやレンダリングスレッドが解放されず、メモリリークや破棄後の WebView 操作によるクラッシュにつながります。また factory 内(Composition 中)に mutableStateOf への書込みを行うため、余分な再コンポジションも発生します。AndroidView に onRelease = { it.destroy() } を追加し、破棄を明示することを推奨します。

Suggestion:

Suggested change
webView = this
if (initialUrl.isNotBlank()) {
loadUrl(normalizeUrl(initialUrl))
}
factory = { context ->
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
configure(
onProgress = { progress = it },
onNavigationStateChange = { url, back, forward ->
urlInput = url
canGoBack = back
canGoForward = forward
},
)
webView = this
if (initialUrl.isNotBlank()) {
loadUrl(normalizeUrl(initialUrl))
}
}
},
onRelease = { it.destroy() },

}
},
)
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun GuestBrowserTopBar(
canGoBack: Boolean,
canGoForward: Boolean,
onBack: () -> Unit,
onHistoryBack: () -> Unit,
onForward: () -> Unit,
onReload: () -> Unit,
) {
TopAppBar(
title = { Text(stringResource(R.string.guest_browser_title)) },
navigationIcon = {
IconButton(onClick = onBack) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
システムバックと画面上部の戻るボタンで動作が不統一です。BackHandler は履歴があれば goBack() しますが、TopAppBar の navigationIcon は常に onBack() を呼ぶため、履歴が残っている状態で上部の戻るボタンを押すと画面が即座に閉じてしまいます。両者を同じ挙動(履歴があれば戻る、なければ画面を閉じる)に統一してください。

Suggestion:

Suggested change
IconButton(onClick = onBack) {
IconButton(onClick = { if (canGoBack) onHistoryBack() else onBack() }) {

Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.nav_back),
)
}
},
actions = {
IconButton(enabled = canGoBack, onClick = onHistoryBack) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.guest_browser_back),
)
}
Comment on lines +131 to +136

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[style · low]
navigationIcon(画面を閉じる)とactions内の履歴戻るボタン(WebView履歴を戻る)の両方に同一のIcons.AutoMirrored.Filled.ArrowBackを使用しているため、視覚的に操作を区別できません。履歴戻る側のアイコンを別のもの(例: Icons.Default.UndoやArrowUpward)に差し替える等して、視覚的・アクセシビリティ上で「画面を閉じる」と「履歴を戻る」を区別できるようにすることを推奨します。

IconButton(enabled = canGoForward, onClick = onForward) {
Icon(
Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = stringResource(R.string.guest_browser_forward),
)
}
IconButton(onClick = onReload) {
Icon(
Icons.Default.Refresh,
contentDescription = stringResource(R.string.guest_browser_reload),
)
}
},
)
}

@Composable
private fun GuestBrowserUrlBar(
urlInput: String,
onUrlChange: (String) -> Unit,
onGo: () -> Unit,
) {
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {
OutlinedTextField(
value = urlInput,
onValueChange = onUrlChange,
modifier = Modifier.weight(1f),
singleLine = true,
placeholder = { Text(stringResource(R.string.guest_browser_url_hint)) },
)
Button(onClick = onGo, modifier = Modifier.padding(start = 8.dp)) {
Text(stringResource(R.string.guest_browser_go))
}
}
}

private fun normalizeUrl(raw: String): String {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
normalizeUrlは空文字や空白のみの入力を "http://" として返すため、WebViewが不正なURLをロードしてエラーページを表示します。また javascript: 等のスキームを含む入力は先頭に http:// が付与され、予期しない挙動になります。入力の空チェック(空の場合はGoボタンを無効化する等)と、Uri.parse等によるスキーム/ホストの検証を行い、不正な入力はloadUrlしないようにすることを推奨します。

val trimmed = raw.trim()
return if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
trimmed
} else {
"http://$trimmed"
}
}
Comment on lines +173 to +180

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · low]
normalizeUrl() は空文字や空白のみの入力を「http://」に変換するため、アドレスバーを空にして Go ボタンを押すと不正な URL で loadUrl() が呼ばれエラーになります。また http/https 以外のスキーム(例: javascript:)にも先頭へ http:// を付与した誤った URL を生成します。isBlank() チェックで入力を検証し、不正な入力は無視するかデフォルト URL にフォールバックしてください。

Suggestion:

Suggested change
private fun normalizeUrl(raw: String): String {
val trimmed = raw.trim()
return if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
trimmed
} else {
"http://$trimmed"
}
}
private fun normalizeUrl(raw: String): String {
val trimmed = raw.trim()
return when {
trimmed.isBlank() -> trimmed
trimmed.startsWith("http://") || trimmed.startsWith("https://") -> trimmed
else -> "http://$trimmed"
}
}


@SuppressLint("SetJavaScriptEnabled")
private fun WebView.configure(
onProgress: (Int) -> Unit,
onNavigationStateChange: (url: String, canGoBack: Boolean, canGoForward: Boolean) -> Unit,
) {
// Guest pages (dev servers, dashboards, tool UIs) are interactive web apps that need JS.
settings.javaScriptEnabled = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
この画面はゲストランタイム用(例: http://127.0.0.1:PORT/)ですが、URLバーから入力された任意のURLをバリデーションなしでloadUrlし、かつjavaScriptEnabled=true / domStorageEnabled=trueを無条件で有効化しています。またページ内リンクやリダイレクトによる外部サイトへの遷移を制限するshouldOverrideUrlLoadingの実装もないため、悪意のあるページ上でJavaScriptが実行され、フィッシングやローカルデータへの不正アクセスにつながるリスクがあります。プロジェクトには既にOpenCodeUrl.normalize()(クリアテキストHTTPを信頼済みホストに制限)というセキュリティ検証があるため、URLバー入力時はスキーム/ホストを検証し、shouldOverrideUrlLoadingで信頼できるローカルオリジン(localhost/127.0.0.1)以外への遷移を遮断するか、外部オリジンではJSを無効化するなどの対策を推奨します。

settings.domStorageEnabled = true
Comment on lines +187 to +189

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · medium]
この画面はゲストランタイムのローカルページ(http://127.0.0.1:PORT/)の表示を想定していますが、アドレスバーから任意の外部サイトへ遷移でき、かつ JavaScript / DOM Storage を無条件で有効化しています。想定外の外部サイトが読み込まれた場合、JS 経由で WebView の Cookie や DOM Storage にアクセスでき、フィッシングやアプリ内データへの攻撃面になります。外部サイトへの遷移を意図しないのであれば WebViewClient.shouldOverrideUrlLoading 等でゲストランタイムのオリジンに遷移を制限するか、少なくとも遷移先のホスト検証と allowFileAccess の無効化を検討してください。

webChromeClient =
object : WebChromeClient() {
override fun onProgressChanged(
view: WebView?,
newProgress: Int,
) {
onProgress(newProgress)
}
}
webViewClient =
object : WebViewClient() {
override fun onPageFinished(
view: WebView?,
url: String?,
) {
onProgress(100)
onNavigationStateChange(
url.orEmpty(),
view?.canGoBack() == true,
view?.canGoForward() == true,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.OpenInBrowser
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Policy
import androidx.compose.material.icons.filled.Router
Expand Down Expand Up @@ -74,6 +75,7 @@ fun SettingsScreenV2(
onOpenAgentSettings: () -> Unit = {},
onOpenGitHubSettings: () -> Unit = {},
onOpenLocalRuntime: () -> Unit,
onOpenGuestBrowser: () -> Unit = {},
onOpenRemoteConnection: () -> Unit,
onOpenWorkspaces: () -> Unit,
onOpenDiagnostics: () -> Unit,
Expand Down Expand Up @@ -267,6 +269,12 @@ fun SettingsScreenV2(
onClick = onOpenLocalRuntime,
)
SettingsDivider()
SettingsRow(
icon = Icons.Default.OpenInBrowser,
title = stringResource(R.string.guest_browser_row),
onClick = onOpenGuestBrowser,
)
SettingsDivider()
SettingsRow(
icon = Icons.Default.Router,
title = stringResource(R.string.remote_connection_row),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const val SCHEDULE_EDIT_ARG_ID = "scheduleId"
const val SCHEDULE_EDIT_ROUTE_PATTERN = "$ROUTE_SCHEDULE_EDIT?$SCHEDULE_EDIT_ARG_ID={$SCHEDULE_EDIT_ARG_ID}"
const val ROUTE_CODE_VIEWER = "code-viewer"
const val ROUTE_TERMINAL = "terminal"
const val ROUTE_GUEST_BROWSER = "guest-browser"

const val CODE_VIEWER_ROUTE_PATTERN = "$ROUTE_CODE_VIEWER/{runtimeId}/{workspacePath}/{filePath}"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ fun NavGraphBuilder.settingsNavGraph(
onOpenAgentSettings = { navController.navigate(ROUTE_SETTINGS_AGENTS) },
onOpenGitHubSettings = { navController.navigate(ROUTE_SETTINGS_GITHUB) },
onOpenLocalRuntime = { navController.navigate(LOCAL_RUNTIME_MANAGEMENT_ROUTE) },
onOpenGuestBrowser = { navController.navigate(ROUTE_GUEST_BROWSER) },
onOpenRemoteConnection = { navController.navigate(ROUTE_REMOTE_CONNECTION) },
onOpenWorkspaces = { navController.navigate(ROUTE_WORKSPACES) },
onOpenDiagnostics = onShowDiagnostics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.yugahashimoto.andcode.AndCodeApplication
import com.yugahashimoto.andcode.feature.browser.GuestBrowserScreen
import com.yugahashimoto.andcode.feature.workspace.CodeViewerScreen
import com.yugahashimoto.andcode.feature.workspace.CodeViewerViewModel
import com.yugahashimoto.andcode.feature.workspace.LocalRuntimeManagementScreen
Expand Down Expand Up @@ -184,6 +185,13 @@ fun NavGraphBuilder.workspaceNavGraph(
)
}

composable(ROUTE_GUEST_BROWSER) {
GuestBrowserScreen(
initialUrl = app.localRuntimeManager.installedPort()?.let { "http://127.0.0.1:$it/" }.orEmpty(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[performance · low]
installedPort() は内部で readMetadata() を呼び、metadata.json を同期的にディスクから読み込みます。この式は composable ブロック内で評価されるため、Navigation がこのディスティネーションを再コンポーズするたびにメインスレッド上でファイル I/O が再実行されます。一方で URL の値は GuestBrowserScreen の WebView factory で一度だけ消費されるため、再読み込みは実質無駄になります。remember で一度だけ計算することを推奨します(ナビゲーション時に毎回最新のポートを読む必要がある場合は、LocalRuntimeManager.stateLocalRuntimeStatus.Ready.port を利用する方が Compose の状態と整合します)。

Suggestion:

Suggested change
initialUrl = app.localRuntimeManager.installedPort()?.let { "http://127.0.0.1:$it/" }.orEmpty(),
val initialUrl = remember { app.localRuntimeManager.installedPort()?.let { "http://127.0.0.1:$it/" }.orEmpty() }
GuestBrowserScreen(
initialUrl = initialUrl,
onBack = { navController.popBackStack() },
)

Comment on lines +188 to +190

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
このファイルのコメント(32〜34行目)にも記載されている通り、NavHost は destination のラムダを記憶するため、グラフ構築時に読み込んだ値は固定されてしまいます。ここで installedPort() を即座に評価すると、ユーザーが後からゲストブラウザを開いた時点では古い(あるいはまだインストール前で null の)ポートがキャプチャされ、http://127.0.0.1:/ のように無効な URL を読み込む恐れがあります。他の箇所と同じく getter(例: app.localRuntimeManager::installedPort)を渡し、画面側で読み取るのが安全です。

onBack = { navController.popBackStack() },
)
}

composable(CODE_VIEWER_ROUTE_PATTERN) { backStack ->
val arguments =
runCatching {
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1032,4 +1032,11 @@
<string name="activity_event_question">سؤال</string>
<string name="activity_event_unknown">حدث غير مدعوم</string>
<string name="connection_info_incomplete">معلومات الاتصال غير مكتملة</string>
<string name="guest_browser_title">المتصفح الضيف</string>
<string name="guest_browser_row">المتصفح الضيف</string>
<string name="guest_browser_url_hint">http://127.0.0.1:port</string>
<string name="guest_browser_go">فتح</string>
<string name="guest_browser_back">رجوع</string>
<string name="guest_browser_forward">تقدم</string>
<string name="guest_browser_reload">إعادة تحميل</string>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1032,4 +1032,11 @@
<string name="activity_event_question">Pregunta</string>
<string name="activity_event_unknown">Evento no compatible</string>
<string name="connection_info_incomplete">La información de conexión está incompleta</string>
<string name="guest_browser_title">Navegador invitado</string>
<string name="guest_browser_row">Navegador invitado</string>
<string name="guest_browser_url_hint">http://127.0.0.1:puerto</string>
<string name="guest_browser_go">Abrir</string>
<string name="guest_browser_back">Atrás</string>
<string name="guest_browser_forward">Avanzar</string>
<string name="guest_browser_reload">Recargar</string>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1032,4 +1032,11 @@
<string name="activity_event_question">Question</string>
<string name="activity_event_unknown">Événement non pris en charge</string>
<string name="connection_info_incomplete">Les informations de connexion sont incomplètes</string>
<string name="guest_browser_title">Navigateur invité</string>
<string name="guest_browser_row">Navigateur invité</string>
<string name="guest_browser_url_hint">http://127.0.0.1:port</string>
<string name="guest_browser_go">Ouvrir</string>
<string name="guest_browser_back">Retour</string>
<string name="guest_browser_forward">Avancer</string>
<string name="guest_browser_reload">Recharger</string>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1052,4 +1052,11 @@
<string name="pr_collapse">閉じる</string>
<string name="cd_pull_request_expand">他 %1$d 件のプルリクエストを表示</string>
<string name="cd_pull_request_collapse">プルリクエストの表示を減らす</string>
<string name="guest_browser_title">ゲストブラウザ</string>
<string name="guest_browser_row">ゲストブラウザ</string>
<string name="guest_browser_url_hint">http://127.0.0.1:ポート</string>
<string name="guest_browser_go">開く</string>
<string name="guest_browser_back">戻る</string>
<string name="guest_browser_forward">進む</string>
<string name="guest_browser_reload">再読み込み</string>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/res/values-pt-rBR/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1032,4 +1032,11 @@
<string name="activity_event_question">Pergunta</string>
<string name="activity_event_unknown">Evento não compatível</string>
<string name="connection_info_incomplete">As informações de conexão estão incompletas</string>
<string name="guest_browser_title">Navegador convidado</string>
<string name="guest_browser_row">Navegador convidado</string>
<string name="guest_browser_url_hint">http://127.0.0.1:porta</string>
<string name="guest_browser_go">Abrir</string>
<string name="guest_browser_back">Voltar</string>
<string name="guest_browser_forward">Avançar</string>
<string name="guest_browser_reload">Recarregar</string>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1032,4 +1032,11 @@
<string name="activity_event_question">Вопрос</string>
<string name="activity_event_unknown">Неподдерживаемое событие</string>
<string name="connection_info_incomplete">Недостаточно данных подключения</string>
<string name="guest_browser_title">Гостевой браузер</string>
<string name="guest_browser_row">Гостевой браузер</string>
<string name="guest_browser_url_hint">http://127.0.0.1:порт</string>
<string name="guest_browser_go">Открыть</string>
<string name="guest_browser_back">Назад</string>
<string name="guest_browser_forward">Вперёд</string>
<string name="guest_browser_reload">Обновить</string>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1032,4 +1032,11 @@
<string name="activity_event_question">问题</string>
<string name="activity_event_unknown">不支持的事件</string>
<string name="connection_info_incomplete">连接信息不完整</string>
<string name="guest_browser_title">访客浏览器</string>
<string name="guest_browser_row">访客浏览器</string>
<string name="guest_browser_url_hint">http://127.0.0.1:端口</string>
<string name="guest_browser_go">打开</string>
<string name="guest_browser_back">后退</string>
<string name="guest_browser_forward">前进</string>
<string name="guest_browser_reload">重新加载</string>
</resources>
7 changes: 7 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1052,4 +1052,11 @@
<string name="pr_collapse">Fewer</string>
<string name="cd_pull_request_expand">Show %1$d more pull requests</string>
<string name="cd_pull_request_collapse">Show fewer pull requests</string>
<string name="guest_browser_title">Guest Browser</string>
<string name="guest_browser_row">Guest Browser</string>
<string name="guest_browser_url_hint">http://127.0.0.1:port</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[maintainability · low]
URL形式の例示文字列(ヒント)が通常の翻訳対象になっています。その結果、他言語版では「http://127.0.0.1:ポート」「http://127.0.0.1:端口」のようにURL中に非ASCIIの「port」訳語が埋め込まれ、ユーザーが実際のURLを入力する際の見本として不自然になります(「端口」等をそのまま入力してしまう懸念もあります)。技術的な書式の見本は translatable="false" にするか、プレースホルダー(例: http://127.0.0.1:%1$s)を使うのが望ましいです。

<string name="guest_browser_go">Open</string>
<string name="guest_browser_back">Back</string>
<string name="guest_browser_forward">Forward</string>
<string name="guest_browser_reload">Reload</string>
</resources>
Loading