diff --git a/.playwright-mcp/console-2026-05-20T07-14-08-098Z.log b/.playwright-mcp/console-2026-05-20T07-14-08-098Z.log new file mode 100644 index 0000000000..731ad15a0b --- /dev/null +++ b/.playwright-mcp/console-2026-05-20T07-14-08-098Z.log @@ -0,0 +1,2 @@ +[ 4319ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://github.com/warpdotdev/warp/issues/1194/agent_tasks:0 +[ 9204ms] [WARNING] The resource https://github.com/_graphql?body=%7B%22persistedQueryName%22%3A%22IssueViewerSecondaryViewQuery%22%2C%22query%22%3A%225055867f80c07e6224558ca214eece24%22%2C%22variables%22%3A%7B%22markAsRead%22%3Atrue%2C%22number%22%3A1194%2C%22owner%22%3A%22warpdotdev%22%2C%22repo%22%3A%22warp%22%7D%7D was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ https://github.com/warpdotdev/warp/issues/1194:0 diff --git a/.playwright-mcp/console-2026-05-20T07-14-34-986Z.log b/.playwright-mcp/console-2026-05-20T07-14-34-986Z.log new file mode 100644 index 0000000000..5bd00d828e --- /dev/null +++ b/.playwright-mcp/console-2026-05-20T07-14-34-986Z.log @@ -0,0 +1,2 @@ +[ 95412ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://github.com/warpdotdev/warp/pull/11382/partials/unread_timeline?since=2026-05-20T14%3A16%3A07.000000000%2B07%3A00:0 +[ 96469ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://github.com/warpdotdev/warp/pull/11382/partials/unread_timeline?since=2026-05-20T14%3A16%3A07.000000000%2B07%3A00:0 diff --git a/.playwright-mcp/console-2026-05-20T07-38-10-751Z.log b/.playwright-mcp/console-2026-05-20T07-38-10-751Z.log new file mode 100644 index 0000000000..ec6d01581f --- /dev/null +++ b/.playwright-mcp/console-2026-05-20T07-38-10-751Z.log @@ -0,0 +1,12 @@ +[ 4703494ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 4703755ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8303337ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8305401ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8309808ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8464147ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8464788ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8464993ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8465700ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8470650ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8470661ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 +[ 8472281ms] [ERROR] Failed to load resource: the server responded with a status of 503 () @ https://collector.github.com/github/collect:0 diff --git a/.playwright-mcp/console-2026-05-20T19-21-12-258Z.log b/.playwright-mcp/console-2026-05-20T19-21-12-258Z.log new file mode 100644 index 0000000000..77f99c131e --- /dev/null +++ b/.playwright-mcp/console-2026-05-20T19-21-12-258Z.log @@ -0,0 +1,2 @@ +[ 193502ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://github.com/warpdotdev/warp/pull/11382/partials/unread_timeline?since=2026-05-21T02%3A24%3A22.000000000%2B07%3A00:0 +[ 635063ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://github.com/warpdotdev/warp/pull/11382/partials/unread_timeline?since=2026-05-21T02%3A31%3A43.000000000%2B07%3A00:0 diff --git a/.playwright-mcp/console-2026-05-20T19-48-27-430Z.log b/.playwright-mcp/console-2026-05-20T19-48-27-430Z.log new file mode 100644 index 0000000000..49ffd56866 --- /dev/null +++ b/.playwright-mcp/console-2026-05-20T19-48-27-430Z.log @@ -0,0 +1,2 @@ +[ 29036ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://github.com/warpdotdev/warp/pull/11382/partials/unread_timeline?since=2026-05-21T02%3A48%3A51.000000000%2B07%3A00:0 +[ 4180493ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://github.com/warpdotdev/warp/pull/11382/partials/unread_timeline?since=2026-05-21T02%3A56%3A42.000000000%2B07%3A00:0 diff --git a/.playwright-mcp/console-2026-05-25T10-02-50-577Z.log b/.playwright-mcp/console-2026-05-25T10-02-50-577Z.log new file mode 100644 index 0000000000..f8ceb0b824 --- /dev/null +++ b/.playwright-mcp/console-2026-05-25T10-02-50-577Z.log @@ -0,0 +1,80 @@ +[ 4279982ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4339763ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4399793ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4459727ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5420551ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5479559ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5481157ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5540196ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5600272ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5660558ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5660599ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5719818ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5721475ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5780541ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5840460ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7203403ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7203403ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7203428ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7204050ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7204056ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7206062ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7206097ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7206097ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7726318ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7728360ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7728387ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7728395ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7731403ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7732617ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7733985ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7734983ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7734986ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7788587ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7969070ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8028675ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8089034ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8148331ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8150069ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8208176ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8210145ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8268975ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8449023ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8629422ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8689208ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8749295ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8809252ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8907560ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8929236ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9326445ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9326634ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9328916ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9329512ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9329512ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9330468ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9332482ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9336514ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9453496ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9469306ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9844765ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9882593ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9980270ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9981750ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9984757ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9984757ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10000597ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10038605ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10116609ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10272603ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10389585ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10802977ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10803038ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10804031ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10804031ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10806049ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10806050ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[11626987ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[11627929ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[11629484ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[11630488ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[11630488ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 diff --git a/.playwright-mcp/console-2026-05-25T10-02-50-588Z.log b/.playwright-mcp/console-2026-05-25T10-02-50-588Z.log new file mode 100644 index 0000000000..396facfc6a --- /dev/null +++ b/.playwright-mcp/console-2026-05-25T10-02-50-588Z.log @@ -0,0 +1,38 @@ +[ 5420310ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5420491ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5661281ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5720314ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5720464ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5780359ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5780499ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7203678ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7203738ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7203831ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7204380ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7204385ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7206040ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7206060ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7206064ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7726373ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7728579ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7733778ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7969051ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7969051ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8089023ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8089058ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8148942ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8149165ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8208893ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8209072ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8389117ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8629780ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8749302ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10803513ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10803561ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10804382ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10804383ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10806039ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[10806040ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[11624453ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[11624453ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[11624453ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 diff --git a/.playwright-mcp/console-2026-05-27T10-45-42-943Z.log b/.playwright-mcp/console-2026-05-27T10-45-42-943Z.log new file mode 100644 index 0000000000..8e5e903856 --- /dev/null +++ b/.playwright-mcp/console-2026-05-27T10-45-42-943Z.log @@ -0,0 +1,56 @@ +[ 1581ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1637ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1637ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1638ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1695ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1696ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1697ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1698ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1698ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1699ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 1700ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 2023ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 2049ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 2058ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 2374ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 2861ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 2861ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3281ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3282ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3283ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3437ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3438ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3441ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4517ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4746ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4747ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4747ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 5523ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 6523ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7531ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 7531ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8533ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9540ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 10540ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 11545ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 12550ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 13556ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 15567ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 16572ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 17578ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 18583ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 19590ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 20594ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 21599ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 22605ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 23610ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 24616ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 25621ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 26627ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 27632ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 28638ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 29643ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 30649ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 30651ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 151477ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 151892ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 diff --git a/.playwright-mcp/console-2026-05-27T10-45-42-957Z.log b/.playwright-mcp/console-2026-05-27T10-45-42-957Z.log new file mode 100644 index 0000000000..c656ffc18f --- /dev/null +++ b/.playwright-mcp/console-2026-05-27T10-45-42-957Z.log @@ -0,0 +1,21 @@ +[ 3535ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3538ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3540ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3574ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3579ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3593ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3970ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 3987ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4017ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4365ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4370ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4372ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4373ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 4374ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 6250ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 6252ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 6252ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 8576ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9502ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 9620ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 +[ 39921ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ https://collector.github.com/github/collect:0 diff --git a/.playwright-mcp/page-2026-05-20T07-12-08-429Z.yml b/.playwright-mcp/page-2026-05-20T07-12-08-429Z.yml new file mode 100644 index 0000000000..aada8a506d --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T07-12-08-429Z.yml @@ -0,0 +1,598 @@ +- generic [ref=e2]: + - generic: + - link "Skip to content" [ref=e3] [cursor=pointer]: + - /url: "#start-of-content" + - banner [ref=e5]: + - heading "Navigation Menu" [level=2] [ref=e6] + - generic [ref=e7]: + - link "Homepage" [ref=e9] [cursor=pointer]: + - /url: / + - img [ref=e10] + - generic [ref=e12]: + - navigation "Global" [ref=e15]: + - list [ref=e16]: + - listitem [ref=e17]: + - button "Platform" [ref=e19] [cursor=pointer]: + - text: Platform + - img [ref=e20] + - listitem [ref=e22]: + - button "Solutions" [ref=e24] [cursor=pointer]: + - text: Solutions + - img [ref=e25] + - listitem [ref=e27]: + - button "Resources" [ref=e29] [cursor=pointer]: + - text: Resources + - img [ref=e30] + - listitem [ref=e32]: + - button "Open Source" [ref=e34] [cursor=pointer]: + - text: Open Source + - img [ref=e35] + - listitem [ref=e37]: + - button "Enterprise" [ref=e39] [cursor=pointer]: + - text: Enterprise + - img [ref=e40] + - listitem [ref=e42]: + - link "Pricing" [ref=e43] [cursor=pointer]: + - /url: https://github.com/pricing + - generic [ref=e44]: Pricing + - generic [ref=e45]: + - button "Search or jump to…" [ref=e48] [cursor=pointer]: + - img [ref=e50] + - generic [ref=e52]: Search or jump to... + - img [ref=e54] + - link "Sign in" [ref=e58] [cursor=pointer]: + - /url: /login + - link "Sign up" [ref=e59] [cursor=pointer]: + - /url: /signup?ref_cta=Sign+up&ref_loc=header+logged+out&ref_page=%2F&source=header-home + - main [ref=e62]: + - generic [ref=e66]: + - generic [ref=e67]: + - generic [ref=e68]: + - generic [ref=e72]: + - region "The future of building happens together" [ref=e73]: + - generic [ref=e76]: + - heading "The future of building happens together" [level=1] [ref=e77] + - paragraph [ref=e78]: Tools and trends evolve, but collaboration endures. With GitHub, developers, agents, and code come together on one platform. + - generic [ref=e79]: + - form "Sign up for GitHub" [ref=e80]: + - generic [ref=e82]: + - generic [ref=e83]: + - generic [ref=e84]: Enter your email + - textbox "Enter your email" [ref=e86]: + - /placeholder: you@domain.com + - button "Sign up for GitHub" [ref=e87] [cursor=pointer]: + - generic [ref=e88]: Sign up for GitHub + - link "Try GitHub Copilot" [ref=e89] [cursor=pointer]: + - /url: /github-copilot/pro + - generic [ref=e90]: Try GitHub Copilot + - generic [ref=e92]: + - heading "GitHub features" [level=2] [ref=e93] + - button "Pause" [ref=e94] [cursor=pointer] + - generic [ref=e99]: A demonstration animation of a code editor using GitHub Copilot Chat, where the user requests GitHub Copilot to refactor duplicated logic and extract it into a reusable function for a given code snippet. + - generic [ref=e101]: + - tablist [ref=e103]: + - generic [ref=e104]: + - tab "Code" [ref=e106] [cursor=pointer] + - tab "Plan" [ref=e107] [cursor=pointer] + - tab "Collaborate" [ref=e108] [cursor=pointer] + - tab "Automate" [ref=e109] [cursor=pointer] + - tab "Secure" [ref=e110] [cursor=pointer] + - region [ref=e111]: Write, test, and fix code quickly with GitHub Copilot, from simple boilerplate to complex features. + - generic [ref=e113]: + - heading "GitHub customers" [level=2] [ref=e114] + - generic [ref=e115]: + - generic [ref=e116]: + - generic [ref=e117]: + - img "American Airlines" [ref=e118] + - img "Duolingo" [ref=e121] + - img "Ernst and Young" [ref=e123] + - img "Ford" [ref=e127] + - img "InfoSys" [ref=e130] + - img "Mercado Libre" [ref=e133] + - img "Mercedes-Benz" [ref=e149] + - img "Shopify" [ref=e152] + - img "Philips" [ref=e164] + - img "Société Générale" [ref=e167] + - img "Spotify" [ref=e185] + - img "Vodafone" [ref=e188] + - generic [ref=e199]: + - img [ref=e200] + - img [ref=e203] + - img [ref=e205] + - img [ref=e209] + - img [ref=e212] + - img [ref=e215] + - img [ref=e231] + - img [ref=e234] + - img [ref=e246] + - img [ref=e249] + - img [ref=e267] + - img [ref=e270] + - button "Pause animation" [ref=e281] [cursor=pointer]: + - img [ref=e282] + - generic [ref=e285]: + - generic [ref=e287]: + - generic [ref=e291]: + - heading "Accelerate your entire workflow" [level=2] [ref=e292]: + - generic [ref=e293]: Accelerate your entire workflow + - paragraph [ref=e294]: From your first line of code to final deployment, GitHub provides AI and automation tools to help you build and ship better software faster. + - generic [ref=e296]: + - img [ref=e298] + - button "Pause video" [ref=e302] [cursor=pointer] + - generic [ref=e308]: A Copilot chat window with the 'Ask' mode enabled. The user switches from 'Ask' mode to 'Agent' mode from a dropdown menu, then sends the prompt 'Update the website to allow searching for running races by name.' Copilot analyzes the codebase, then explains the required edits for three files before generating them. Copilot then confirms completion and summarizes the implemented changes for the new functionality allowing users to search races by name and view paginated, filtered results. + - generic [ref=e312]: + - generic [ref=e314]: + - heading "Your AI partner everywhere. Copilot is ready to work with you at each step of the software development lifecycle." [level=3] [ref=e315]: + - generic [ref=e316]: Your AI partner everywhere. Copilot is ready to work with you at each step of the software development lifecycle. + - link "Explore GitHub Copilot" [ref=e318] [cursor=pointer]: + - /url: /features/copilot + - generic [ref=e319]: Explore GitHub Copilot + - img [ref=e320] + - generic [ref=e322]: + - generic [ref=e324]: + - paragraph [ref=e325]: Duolingo boosts developer speed by 25% with GitHub Copilot + - link "Read customer story" [ref=e326] [cursor=pointer]: + - /url: /customer-stories/duolingo + - generic [ref=e327]: Read customer story + - img [ref=e328] + - generic [ref=e331]: + - paragraph [ref=e332]: 2025 Gartner® Magic Quadrant™ for AI Code Assistants + - link "Read industry report" [ref=e333] [cursor=pointer]: + - /url: https://www.gartner.com/reprints/?id=1-2LVTG7RP&ct=250915&st=sb + - generic [ref=e334]: Read industry report + - img [ref=e335] + - generic [ref=e342]: + - generic [ref=e343]: + - term [ref=e344]: + - button "Automate your path to production" [expanded] [ref=e345]: + - generic [ref=e346]: + - heading "Automate your path to production" [level=3] [ref=e347] + - img [ref=e348] + - definition [ref=e350]: + - generic [ref=e352]: + - paragraph [ref=e353]: Ship faster with secure, reliable CI/CD. + - link "Explore GitHub Actions" [ref=e355] [cursor=pointer]: + - /url: /features/actions + - generic [ref=e356]: Explore GitHub Actions + - img [ref=e357] + - generic [ref=e359]: + - term [ref=e360]: + - button "Code instantly from anywhere" [ref=e361] [cursor=pointer]: + - generic [ref=e362]: + - heading "Code instantly from anywhere" [level=3] [ref=e363] + - img [ref=e364] + - paragraph [ref=e366]: Launch a full, cloud-based development environment in seconds. + - link [ref=e368] [cursor=pointer]: + - /url: /features/codespaces + - generic [ref=e369]: Explore GitHub Codespaces + - img [ref=e370] + - generic [ref=e372]: + - term [ref=e373]: + - button "Keep momentum on the go" [ref=e374] [cursor=pointer]: + - generic [ref=e375]: + - heading "Keep momentum on the go" [level=3] [ref=e376] + - img [ref=e377] + - paragraph [ref=e379]: Manage projects and assign tasks to Copilot, all from your mobile device. + - link [ref=e381] [cursor=pointer]: + - /url: /mobile + - generic [ref=e382]: Explore GitHub Mobile + - img [ref=e383] + - generic [ref=e385]: + - term [ref=e386]: + - button "Shape your toolchain" [ref=e387] [cursor=pointer]: + - generic [ref=e388]: + - heading "Shape your toolchain" [level=3] [ref=e389] + - img [ref=e390] + - paragraph [ref=e392]: Extend your stack with apps, actions, and AI models. + - link [ref=e394] [cursor=pointer]: + - /url: /marketplace + - generic [ref=e395]: Explore GitHub Marketplace + - img [ref=e396] + - generic [ref=e398]: + - generic [ref=e400]: + - generic [ref=e404]: + - heading "Built-in application security where found means fixed" [level=2] [ref=e405]: + - generic [ref=e406]: Built-in application security where found means fixed + - paragraph [ref=e407]: Use AI to find and fix vulnerabilities so your team can ship more secure software faster. + - generic [ref=e409]: + - img [ref=e411] + - generic [ref=e414]: + - generic [ref=e415]: + - heading "Apply fixes in seconds. Spend less time debugging and more time building features with Copilot Autofix." [level=3] [ref=e416] + - link "Explore GitHub Advanced Security" [ref=e418] [cursor=pointer]: + - /url: /security/advanced-security + - generic [ref=e419]: Explore GitHub Advanced Security + - img [ref=e420] + - img "Copilot Autofix identifies vulnerable code and provides an explanation, together with a secure code suggestion to remediate the vulnerability." [ref=e425] + - generic [ref=e427]: + - generic [ref=e428]: + - generic [ref=e430]: + - generic [ref=e431]: + - paragraph [ref=e432]: + - generic [ref=e433]: Security debt, solved. Leverage security campaigns and Copilot Autofix to reduce application vulnerabilities. + - link "Learn about GitHub Code Security" [ref=e434] [cursor=pointer]: + - /url: /security/advanced-security/code-security + - generic [ref=e435]: Learn about GitHub Code Security + - img [ref=e436] + - generic [ref=e438]: + - img "A security campaign screen displays the campaign’s progress bar with 97% completed of 701 alerts. A total of 23 alerts are left with 13 in progress, and the campaign started 20 days ago. The status below shows that there are 7 days left in the campaign with a due date of November 15, 2024." + - generic [ref=e440]: + - generic [ref=e441]: + - paragraph [ref=e442]: + - generic [ref=e443]: Dependencies you can depend on. Update vulnerable dependencies with supported fixes for breaking changes. + - link "Learn about Dependabot" [ref=e444] [cursor=pointer]: + - /url: /security/advanced-security/software-supply-chain + - generic [ref=e445]: Learn about Dependabot + - img [ref=e446] + - generic [ref=e448]: + - img "List of dependencies defined in a requirements .txt file." + - generic [ref=e450]: + - generic [ref=e451]: + - paragraph [ref=e452]: + - generic [ref=e453]: Your secrets, your business. Detect, prevent, and remediate leaked secrets across your organization. + - link "Learn about GitHub Secret Protection" [ref=e454] [cursor=pointer]: + - /url: /security/advanced-security/secret-protection + - generic [ref=e455]: Learn about GitHub Secret Protection + - img [ref=e456] + - generic [ref=e458]: + - img "GitHub push protection confirms and displays an active secret, and blocks the push." + - generic [ref=e459]: + - paragraph [ref=e463]: + - text: 70% MTTR reduction + - generic [ref=e465]: + - text: with Copilot Autofix + - superscript [ref=e466]: + - link "Footnote 1" [ref=e467] [cursor=pointer]: + - /url: "#footnote-1" + - text: "1" + - paragraph [ref=e471]: + - text: 8.3M secret leaks stopped + - generic [ref=e473]: + - text: in the past 12 months with push protection + - superscript [ref=e474]: + - link "Footnote 1" [ref=e475] [cursor=pointer]: + - /url: "#footnote-1" + - text: "1" + - generic [ref=e477]: + - generic [ref=e479]: + - generic [ref=e483]: + - heading "Work together, achieve more" [level=2] [ref=e484]: + - generic [ref=e485]: Work together, achieve more + - paragraph [ref=e486]: From planning and discussion to code review, GitHub keeps your team’s conversation and context next to your code. + - generic [ref=e488]: + - img [ref=e490] + - img "A project management dashboard showing tasks for the ‘OctoArcade Invaders’ project, with tasks grouped under project phase categories like ‘Prototype,’ ‘Beta,’ and ‘Launch’ in a table layout. One of the columns displays sub-issue progress bars with percentages for each issue." [ref=e498] + - generic [ref=e501]: + - generic [ref=e503]: + - heading "Plan with clarity. Organize everything from high-level roadmaps to everyday tasks." [level=3] [ref=e504]: + - generic [ref=e505]: Plan with clarity. Organize everything from high-level roadmaps to everyday tasks. + - link "Explore GitHub Projects" [ref=e507] [cursor=pointer]: + - /url: /features/issues + - generic [ref=e508]: Explore GitHub Projects + - img [ref=e509] + - figure [ref=e513]: + - generic [ref=e514]: + - generic [ref=e516]: “ + - blockquote [ref=e517]: + - generic [ref=e518]: It helps us onboard new software engineers and get them productive right away. We have all our source code, issues, and pull requests in one place... GitHub is a complete platform that frees us from menial tasks and enables us to do our best work. + - generic [ref=e520]: + - generic [ref=e521]: Fabian Faulhaber + - generic [ref=e522]: Application manager at Mercedes-Benz + - generic [ref=e528]: + - generic [ref=e529]: + - term [ref=e530]: + - button "Keep track of your tasks" [expanded] [ref=e531]: + - generic [ref=e532]: + - heading "Keep track of your tasks" [level=3] [ref=e533] + - img [ref=e534] + - definition [ref=e536]: + - generic [ref=e538]: + - paragraph [ref=e539]: Create issues and manage projects with tools that adapt to your code. + - link "Explore GitHub Issues" [ref=e541] [cursor=pointer]: + - /url: /features/issues + - generic [ref=e542]: Explore GitHub Issues + - img [ref=e543] + - generic [ref=e545]: + - term [ref=e546]: + - button "Share ideas and ask questions" [ref=e547] [cursor=pointer]: + - generic [ref=e548]: + - heading "Share ideas and ask questions" [level=3] [ref=e549] + - img [ref=e550] + - paragraph [ref=e552]: Create space for open-ended conversations alongside your project. + - link [ref=e554] [cursor=pointer]: + - /url: /features/discussions + - generic [ref=e555]: Explore GitHub Discussions + - img [ref=e556] + - generic [ref=e558]: + - term [ref=e559]: + - button "Review code changes together" [ref=e560] [cursor=pointer]: + - generic [ref=e561]: + - heading "Review code changes together" [level=3] [ref=e562] + - img [ref=e563] + - paragraph [ref=e565]: Assign initial reviews to Copilot for greater speed and quality. + - link [ref=e567] [cursor=pointer]: + - /url: /features/code-review + - generic [ref=e568]: Explore code review + - img [ref=e569] + - generic [ref=e571]: + - term [ref=e572]: + - button "Fund open source projects" [ref=e573] [cursor=pointer]: + - generic [ref=e574]: + - heading "Fund open source projects" [level=3] [ref=e575] + - img [ref=e576] + - paragraph [ref=e578]: Become an open source partner and support the tools and libraries that power your work. + - link [ref=e580] [cursor=pointer]: + - /url: /sponsors + - generic [ref=e581]: Explore GitHub Sponsors + - img [ref=e582] + - generic [ref=e584]: + - generic [ref=e585]: + - heading "From startups to enterprises, GitHub scales with teams of any size in any industry." [level=2] [ref=e592]: + - generic [ref=e593]: From startups to enterprises, GitHub scales with teams of any size in any industry. + - tablist [ref=e597]: + - generic [ref=e598]: + - tab "By industry" [ref=e600] [cursor=pointer] + - tab "By size" [ref=e601] [cursor=pointer] + - tab "By use case" [ref=e602] [cursor=pointer] + - separator [ref=e604] + - generic [ref=e607]: + - link "Technology Figma streamlines development and strengthens security Read customer story" [ref=e609] [cursor=pointer]: + - /url: /customer-stories/figma + - generic [ref=e614]: + - generic [ref=e615]: Technology + - paragraph [ref=e616]: Figma streamlines development and strengthens security + - generic [ref=e618]: + - text: Read customer story + - img [ref=e619] + - link "Automotive Mercedes-Benz standardizes source code and automates onboarding Read customer story" [ref=e622] [cursor=pointer]: + - /url: /customer-stories/mercedes-benz + - generic [ref=e627]: + - generic [ref=e628]: Automotive + - paragraph [ref=e629]: Mercedes-Benz standardizes source code and automates onboarding + - generic [ref=e631]: + - text: Read customer story + - img [ref=e632] + - link "Financial services Mercado Libre cuts coding time by 50% Read customer story" [ref=e635] [cursor=pointer]: + - /url: /customer-stories/mercado-libre + - generic [ref=e640]: + - generic [ref=e641]: Financial services + - paragraph [ref=e642]: Mercado Libre cuts coding time by 50% + - generic [ref=e644]: + - text: Read customer story + - img [ref=e645] + - generic [ref=e650]: + - link "Explore customer stories" [ref=e652] [cursor=pointer]: + - /url: /customer-stories + - generic [ref=e653]: Explore customer stories + - img [ref=e654] + - separator [ref=e656] + - link "View all solutions" [ref=e658] [cursor=pointer]: + - /url: /solutions + - generic [ref=e659]: View all solutions + - img [ref=e660] + - generic [ref=e669]: + - heading "Millions of developers and businesses call GitHub home" [level=2] [ref=e670] + - paragraph [ref=e671]: Whether you’re scaling your development process or just learning how to code, GitHub is where you belong. Join the world’s most widely adopted developer platform to build the technologies that shape what’s next. + - generic [ref=e672]: + - form "Sign up for GitHub" [ref=e673]: + - generic [ref=e675]: + - generic [ref=e676]: + - generic [ref=e677]: Enter your email + - textbox "Enter your email" [ref=e679]: + - /placeholder: you@domain.com + - button "Sign up for GitHub" [ref=e680] [cursor=pointer]: + - generic [ref=e681]: Sign up for GitHub + - link "Try GitHub Copilot" [ref=e682] [cursor=pointer]: + - /url: /github-copilot/pro + - generic [ref=e683]: Try GitHub Copilot + - generic [ref=e685]: + - heading "Footnotes" [level=2] [ref=e686] + - list [ref=e687]: + - listitem [ref=e688]: + - paragraph [ref=e689]: + - text: GitHub internal customer data, 2025. + - link "Back to content" [ref=e690] [cursor=pointer]: + - /url: "#footnote-1-ref-0" + - img [ref=e691] + - generic: + - link "Back to top": + - /url: "#hero" + - img + - contentinfo [ref=e693]: + - heading "Site-wide Links" [level=2] [ref=e694] + - generic [ref=e696]: + - generic [ref=e697]: + - link "Go to GitHub homepage" [ref=e698] [cursor=pointer]: + - /url: / + - img [ref=e699] + - heading "Subscribe to our developer newsletter" [level=3] [ref=e703] + - paragraph [ref=e704]: Get tips, technical guides, and best practices. Twice a month. + - link "Subscribe" [ref=e705] [cursor=pointer]: + - /url: https://github.com/newsletter + - navigation "Platform" [ref=e706]: + - heading "Platform" [level=3] [ref=e707] + - list [ref=e708]: + - listitem [ref=e709]: + - link "Features" [ref=e710] [cursor=pointer]: + - /url: /features + - listitem [ref=e711]: + - link "Enterprise" [ref=e712] [cursor=pointer]: + - /url: /enterprise + - listitem [ref=e713]: + - link "Copilot" [ref=e714] [cursor=pointer]: + - /url: /features/copilot + - listitem [ref=e715]: + - link "AI" [ref=e716] [cursor=pointer]: + - /url: /features/ai + - listitem [ref=e717]: + - link "Security" [ref=e718] [cursor=pointer]: + - /url: /security + - listitem [ref=e719]: + - link "Pricing" [ref=e720] [cursor=pointer]: + - /url: /pricing + - listitem [ref=e721]: + - link "Team" [ref=e722] [cursor=pointer]: + - /url: /team + - listitem [ref=e723]: + - link "Resources" [ref=e724] [cursor=pointer]: + - /url: https://resources.github.com + - listitem [ref=e725]: + - link "Roadmap" [ref=e726] [cursor=pointer]: + - /url: https://github.com/github/roadmap + - listitem [ref=e727]: + - link "Compare GitHub" [ref=e728] [cursor=pointer]: + - /url: /resources/articles/devops-tools-comparison + - navigation "Ecosystem" [ref=e729]: + - heading "Ecosystem" [level=3] [ref=e730] + - list [ref=e731]: + - listitem [ref=e732]: + - link "Developer API" [ref=e733] [cursor=pointer]: + - /url: https://docs.github.com/get-started/exploring-integrations/about-building-integrations + - listitem [ref=e734]: + - link "Partners" [ref=e735] [cursor=pointer]: + - /url: https://partner.github.com + - listitem [ref=e736]: + - link "Education" [ref=e737] [cursor=pointer]: + - /url: https://github.com/edu + - listitem [ref=e738]: + - link "GitHub CLI" [ref=e739] [cursor=pointer]: + - /url: https://cli.github.com + - listitem [ref=e740]: + - link "GitHub Desktop" [ref=e741] [cursor=pointer]: + - /url: https://desktop.github.com + - listitem [ref=e742]: + - link "GitHub Mobile" [ref=e743] [cursor=pointer]: + - /url: https://github.com/mobile + - listitem [ref=e744]: + - link "GitHub Marketplace" [ref=e745] [cursor=pointer]: + - /url: https://github.com/marketplace + - listitem [ref=e746]: + - link "MCP Registry" [ref=e747] [cursor=pointer]: + - /url: https://github.com/mcp + - navigation "Support" [ref=e748]: + - heading "Support" [level=3] [ref=e749] + - list [ref=e750]: + - listitem [ref=e751]: + - link "Docs" [ref=e752] [cursor=pointer]: + - /url: https://docs.github.com + - listitem [ref=e753]: + - link "Community Forum" [ref=e754] [cursor=pointer]: + - /url: https://github.community + - listitem [ref=e755]: + - link "Professional Services" [ref=e756] [cursor=pointer]: + - /url: https://services.github.com + - listitem [ref=e757]: + - link "Premium Support" [ref=e758] [cursor=pointer]: + - /url: /enterprise/premium-support + - listitem [ref=e759]: + - link "Skills" [ref=e760] [cursor=pointer]: + - /url: https://skills.github.com + - listitem [ref=e761]: + - link "Status" [ref=e762] [cursor=pointer]: + - /url: https://www.githubstatus.com + - listitem [ref=e763]: + - link "Contact GitHub" [ref=e764] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - navigation "Company" [ref=e765]: + - heading "Company" [level=3] [ref=e766] + - list [ref=e767]: + - listitem [ref=e768]: + - link "About" [ref=e769] [cursor=pointer]: + - /url: https://github.com/about + - listitem [ref=e770]: + - link "Why GitHub" [ref=e771] [cursor=pointer]: + - /url: https://github.com/why-github + - listitem [ref=e772]: + - link "Customer stories" [ref=e773] [cursor=pointer]: + - /url: /customer-stories?type=enterprise + - listitem [ref=e774]: + - link "Blog" [ref=e775] [cursor=pointer]: + - /url: https://github.blog + - listitem [ref=e776]: + - link "The ReadME Project" [ref=e777] [cursor=pointer]: + - /url: /readme + - listitem [ref=e778]: + - link "Careers" [ref=e779] [cursor=pointer]: + - /url: https://github.careers + - listitem [ref=e780]: + - link "Newsroom" [ref=e781] [cursor=pointer]: + - /url: /newsroom + - listitem [ref=e782]: + - link "Inclusion" [ref=e783] [cursor=pointer]: + - /url: /about/diversity + - listitem [ref=e784]: + - link "Social Impact" [ref=e785] [cursor=pointer]: + - /url: https://socialimpact.github.com + - listitem [ref=e786]: + - link "Shop" [ref=e787] [cursor=pointer]: + - /url: https://shop.github.com + - generic [ref=e789]: + - navigation "Legal and Resource Links" [ref=e790]: + - list [ref=e791]: + - listitem [ref=e792]: + - text: © + - time [ref=e793]: "2026" + - text: GitHub, Inc. + - listitem [ref=e794]: + - link "Terms" [ref=e795] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e796]: + - link "Privacy" [ref=e797] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - link "(Updated 02/2024) 02/2024" [ref=e798] [cursor=pointer]: + - /url: https://github.com/github/site-policy/pull/582 + - text: (Updated 02/2024) + - time [ref=e799]: 02/2024 + - listitem [ref=e800]: + - link "Sitemap" [ref=e801] [cursor=pointer]: + - /url: /sitemap + - listitem [ref=e802]: + - link "What is Git?" [ref=e803] [cursor=pointer]: + - /url: /git-guides + - listitem [ref=e804]: + - button "Manage cookies" [ref=e806] [cursor=pointer] + - listitem [ref=e807]: + - button "Do not share my personal information" [ref=e809] [cursor=pointer] + - navigation "GitHub's Social Media Links" [ref=e810]: + - list [ref=e811]: + - listitem [ref=e812]: + - link "GitHub on LinkedIn" [ref=e813] [cursor=pointer]: + - /url: https://www.linkedin.com/company/github + - img [ref=e814] + - generic [ref=e816]: GitHub on LinkedIn + - listitem [ref=e817]: + - link "GitHub on Instagram" [ref=e818] [cursor=pointer]: + - /url: https://www.instagram.com/github + - img [ref=e819] + - generic [ref=e821]: GitHub on Instagram + - listitem [ref=e822]: + - link "GitHub on YouTube" [ref=e823] [cursor=pointer]: + - /url: https://www.youtube.com/github + - img [ref=e824] + - generic [ref=e826]: GitHub on YouTube + - listitem [ref=e827]: + - link "GitHub on X" [ref=e828] [cursor=pointer]: + - /url: https://x.com/github + - img [ref=e829] + - generic [ref=e831]: GitHub on X + - listitem [ref=e832]: + - link "GitHub on TikTok" [ref=e833] [cursor=pointer]: + - /url: https://www.tiktok.com/@github + - img [ref=e834] + - generic [ref=e836]: GitHub on TikTok + - listitem [ref=e837]: + - link "GitHub on Twitch" [ref=e838] [cursor=pointer]: + - /url: https://www.twitch.tv/github + - img [ref=e839] + - generic [ref=e841]: GitHub on Twitch + - listitem [ref=e842]: + - link "GitHub’s organization on GitHub" [ref=e843] [cursor=pointer]: + - /url: https://github.com/github + - img [ref=e844] + - generic [ref=e847]: GitHub’s organization on GitHub + - button "English Select language" [ref=e850] [cursor=pointer]: + - generic [ref=e853]: + - img [ref=e854] + - text: English + - generic [ref=e856]: Select language + - img [ref=e857] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-20T07-12-22-264Z.yml b/.playwright-mcp/page-2026-05-20T07-12-22-264Z.yml new file mode 100644 index 0000000000..bc11a3cbf3 --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T07-12-22-264Z.yml @@ -0,0 +1,42 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - banner [ref=e6] + - main [ref=e9]: + - generic [ref=e10]: + - generic [ref=e14]: + - img [ref=e17] + - heading "Sign in to GitHub" [level=1] [ref=e20] + - generic [ref=e22]: + - generic [ref=e23]: + - generic [ref=e24]: Username or email address + - textbox "Username or email address" [active] [ref=e25] + - generic [ref=e26]: + - generic [ref=e27]: Password + - textbox "Password" [ref=e28] + - link "Forgot password?" [ref=e29] [cursor=pointer]: + - /url: /password_reset + - button "Sign in" [ref=e31] [cursor=pointer] + - paragraph [ref=e34]: + - text: New to GitHub? + - link "Create an account" [ref=e35] [cursor=pointer]: + - /url: /signup?source=login + - contentinfo [ref=e36]: + - list [ref=e37]: + - listitem [ref=e38]: + - link "Terms" [ref=e39] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e40]: + - link "Privacy" [ref=e41] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e42]: + - link "Docs" [ref=e43] [cursor=pointer]: + - /url: https://docs.github.com + - listitem [ref=e44]: + - link "Contact GitHub Support" [ref=e45] [cursor=pointer]: + - /url: https://support.github.com + - listitem [ref=e46]: + - button "Manage cookies" [ref=e48] [cursor=pointer] + - listitem [ref=e49]: + - button "Do not share my personal information" [ref=e51] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-20T07-13-22-825Z.yml b/.playwright-mcp/page-2026-05-20T07-13-22-825Z.yml new file mode 100644 index 0000000000..381dc36fa7 --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T07-13-22-825Z.yml @@ -0,0 +1,1076 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - generic [ref=e7]: + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - link "You have no unread notifications (g then n)" [ref=e58] [cursor=pointer]: + - /url: /notifications + - img [ref=e59] + - button "Open user navigation menu" [ref=e62] [cursor=pointer]: + - img "User avatar" [ref=e63] + - heading "Repository navigation" [level=2] [ref=e64] + - navigation "Repository" [ref=e65]: + - list [ref=e66]: + - listitem [ref=e67]: + - link "Code" [ref=e68] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e70] + - generic [ref=e72]: Code + - listitem [ref=e73]: + - link "Issues (3.4k)" [ref=e74] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e76] + - generic [ref=e79]: Issues + - generic [ref=e80]: + - generic [ref=e81]: 3.4k + - generic [ref=e82]: (3.4k) + - listitem [ref=e83]: + - link "Pull requests (502)" [ref=e84] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e86] + - generic [ref=e88]: Pull requests + - generic [ref=e89]: + - generic [ref=e90]: "502" + - generic [ref=e91]: (502) + - listitem [ref=e92]: + - link "Agents" [ref=e93] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e95] + - generic [ref=e98]: Agents + - listitem [ref=e99]: + - link "Discussions" [ref=e100] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e102] + - generic [ref=e104]: Discussions + - listitem [ref=e105]: + - link "Actions" [ref=e106] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e108] + - generic [ref=e110]: Actions + - listitem [ref=e111]: + - link "Projects" [ref=e112] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e114] + - generic [ref=e116]: Projects + - listitem [ref=e117]: + - link "Security and quality" [ref=e118] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e120] + - generic [ref=e122]: Security and quality + - listitem [ref=e123]: + - link "Insights" [ref=e124] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e126] + - generic [ref=e128]: Insights + - region "Important update" [ref=e130]: + - img [ref=e132] + - generic [ref=e135]: + - heading "Important update" [level=2] [ref=e137] + - generic [ref=e139]: + - text: On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out. + - link "Review this update" [ref=e140] [cursor=pointer]: + - /url: https://gh.io/AAzfaht + - text: and manage your preferences in your + - link "GitHub account settings" [ref=e141] [cursor=pointer]: + - /url: https://github.com/settings/copilot/features + - text: . + - button "Dismiss banner" [ref=e142] [cursor=pointer]: + - img [ref=e143] + - main [ref=e148]: + - generic [ref=e155]: + - generic [ref=e158]: + - 'heading "I18n #11382 Edit title" [level=1] [ref=e160]': + - text: I18n + - generic [ref=e161]: + - generic [ref=e162]: "#11382" + - button "Edit title" [ref=e163] [cursor=pointer]: + - img [ref=e164] + - generic [ref=e167]: + - generic [ref=e168]: + - button "View status View status" [disabled] [ref=e169]: + - generic [ref=e170]: + - generic: + - generic: + - img + - generic: Loading + - generic [ref=e172]: Loading merge status + - button "Code" [ref=e173] [cursor=pointer]: + - generic [ref=e174]: + - generic [ref=e175]: Code + - generic: + - img + - generic [ref=e177]: + - generic [ref=e179]: + - img "Pull request" [ref=e180] + - text: Open + - generic [ref=e183]: + - link "ErshovDmitry" [ref=e184] [cursor=pointer]: + - /url: /ErshovDmitry + - text: wants to merge 8 commits into + - generic [ref=e185]: + - link "warpdotdev:master" [ref=e186] [cursor=pointer]: + - /url: /warpdotdev/warp/tree/master + - generic [ref=e187]: from + - generic [ref=e188]: + - link "ErshovDmitry:i18n" [ref=e189] [cursor=pointer]: + - /url: /ErshovDmitry/warp-i18n/tree/i18n + - button "Copy head branch name to clipboard" [ref=e190] [cursor=pointer]: + - img [ref=e191] + - navigation "Pull request navigation tabs" [ref=e197]: + - tablist [ref=e198]: + - tab "Conversation" [selected] [ref=e199] [cursor=pointer]: + - img [ref=e200] + - text: Conversation + - tab "Commits (8)" [ref=e202] [cursor=pointer]: + - img [ref=e203] + - text: Commits + - generic [ref=e205]: "8" + - generic [ref=e206]: (8) + - tab "Checks" [ref=e207] [cursor=pointer]: + - img [ref=e208] + - text: Checks + - tab "Files changed" [ref=e210] [cursor=pointer]: + - img [ref=e211] + - text: Files changed + - generic [ref=e217]: + - generic [ref=e219]: + - heading "Conversation" [level=2] [ref=e220] + - generic [ref=e221]: + - generic [ref=e222]: + - generic [ref=e224]: + - link "@ErshovDmitry" [ref=e225] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e226] + - generic [ref=e228]: + - generic [ref=e229]: + - group [ref=e232]: + - button "Show options" [ref=e233] [cursor=pointer]: + - img "Show options" [ref=e236] + - heading "ErshovDmitry commented May 20, 20263 hours ago" [level=3] [ref=e238]: + - generic [ref=e239]: + - strong [ref=e240]: + - link "ErshovDmitry" [ref=e241] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 hours ago" [ref=e242] [cursor=pointer]: + - /url: "#issue-4483039846" + - generic [ref=e246]: + - paragraph [ref=e247]: Hello! My name is Dmitry. + - paragraph [ref=e248]: I've been using Warp and love it. I wanted to add i18n support so the terminal could speak Russian (and other languages in the future). I'm not a developer myself — this was built with DeepSeek AI, but I reviewed everything personally. + - paragraph [ref=e249]: I'd much rather this live in the official repo than a fork. If the team is interested in i18n, I'm happy to help however I can — and I'll gladly delete my fork once upstreamed. + - paragraph [ref=e250]: + - text: Cheers, + - text: Dmitry + - separator [ref=e251] + - heading "What" [level=2] [ref=e252] + - paragraph [ref=e253]: Adds i18n (internationalization) support to Warp. + - heading "Changes" [level=2] [ref=e254] + - 'heading "New crate: warp_i18n" [level=3] [ref=e255]': + - text: "New crate:" + - code [ref=e256]: warp_i18n + - list [ref=e257]: + - listitem [ref=e258]: + - code [ref=e259]: t!() + - text: macro for translating UI strings + - listitem [ref=e260]: + - code [ref=e261]: current_locale() + - text: / + - code [ref=e262]: set_current_locale() + - text: for runtime language switching + - listitem [ref=e263]: + - code [ref=e264]: init_from_json() + - text: for WASM compatibility + - listitem [ref=e265]: Build script for compile-time key validation + - listitem [ref=e266]: 21 tests + - heading "Feature flag" [level=3] [ref=e267] + - list [ref=e268]: + - listitem [ref=e269]: + - code [ref=e270]: FeatureFlag::I18n + - text: in + - code [ref=e271]: DOGFOOD_FLAGS + - heading "Settings" [level=3] [ref=e272] + - list [ref=e273]: + - listitem [ref=e274]: + - code [ref=e275]: Locale + - text: enum (En, Ru) + - listitem [ref=e276]: Language dropdown in Settings → Appearance + - listitem [ref=e277]: Auto-detect system locale on first run + - heading "Migrated UI" [level=3] [ref=e278] + - list [ref=e279]: + - listitem [ref=e280]: 10 top-level menu labels + - listitem [ref=e281]: 14 settings section names + - listitem [ref=e282]: 11 settings category labels + - heading "Locales" [level=3] [ref=e283] + - list [ref=e284]: + - listitem [ref=e285]: + - code [ref=e286]: en.json + - text: — 51 keys (mandatory fallback) + - listitem [ref=e287]: + - code [ref=e288]: ru.json + - text: — 51 keys (Russian translation) + - paragraph [ref=e289]: + - emphasis [ref=e290]: + - text: Built with DeepSeek AI — human-reviewed, all tests pass ( + - code [ref=e291]: cargo check --workspace + - text: "," + - code [ref=e292]: cargo test -p warp_i18n + - text: ). + - group [ref=e296]: + - generic "Add or remove reactions" [ref=e297] [cursor=pointer]: + - img [ref=e298] + - generic [ref=e300]: + - generic [ref=e302]: + - generic [ref=e303]: + - img [ref=e305] + - generic [ref=e307]: + - link "ErshovDmitry" [ref=e308] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 5 commits + - link "May 20, 2026 09:155 hours ago" [ref=e309] [cursor=pointer]: + - /url: "#commits-pushed-f06dc63" + - generic [ref=e310]: + - generic [ref=e311]: + - img [ref=e313] + - generic [ref=e318]: + - link "@ErshovDmitry" [ref=e321] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e322] + - code [ref=e324]: + - 'link "feat: add warp_i18n crate with t!() macro and en/ru locales" [ref=e325] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - code [ref=e333]: + - link "f06dc63" [ref=e334] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - generic [ref=e335]: + - img [ref=e337] + - generic [ref=e342]: + - link "@ErshovDmitry" [ref=e345] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e346] + - generic [ref=e347]: + - code [ref=e348]: + - 'link "feat: i18n Phase 2 — FeatureFlag, Locale setting, app_menus migration…" [ref=e349] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - button "Commit message body" [ref=e351] [cursor=pointer]: … + - code [ref=e359]: + - link "643f6c3" [ref=e360] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - generic [ref=e361]: + - img [ref=e363] + - generic [ref=e368]: + - link "@ErshovDmitry" [ref=e371] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e372] + - generic [ref=e373]: + - code [ref=e374]: + - 'link "fix: wire warp_i18n::init() at startup, sync LocaleSettings with i18n…" [ref=e375] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - button "Commit message body" [ref=e377] [cursor=pointer]: … + - code [ref=e385]: + - link "a9e9e26" [ref=e386] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - generic [ref=e387]: + - img [ref=e389] + - generic [ref=e394]: + - link "@ErshovDmitry" [ref=e397] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e398] + - code [ref=e400]: + - 'link "docs: add English translation of the fork note in README" [ref=e401] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - code [ref=e409]: + - link "3911714" [ref=e410] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - generic [ref=e411]: + - img [ref=e413] + - generic [ref=e418]: + - link "@ErshovDmitry" [ref=e421] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e422] + - generic [ref=e423]: + - code [ref=e424]: + - 'link "feat: i18n Phase 3 — build script key validation, WASM support, setti…" [ref=e425] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - button "Commit message body" [ref=e427] [cursor=pointer]: … + - code [ref=e435]: + - link "169dfa6" [ref=e436] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e438]: + - link "@cla-bot" [ref=e440] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e441] + - generic [ref=e443]: + - generic [ref=e444]: + - group [ref=e447]: + - button "Show options" [ref=e448] [cursor=pointer]: + - img "Show options" [ref=e451] + - heading "cla-bot Bot commented May 20, 20263 hours ago" [level=3] [ref=e453]: + - generic [ref=e454]: + - strong [ref=e455]: + - link "cla-bot" [ref=e456] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e457]: Bot + - text: commented + - link "May 20, 20263 hours ago" [ref=e458] [cursor=pointer]: + - /url: "#issuecomment-4494411354" + - generic [ref=e459]: + - table [ref=e461]: + - rowgroup [ref=e462]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e463]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e464]': + - paragraph [ref=e465]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e466]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e467]: + - listitem [ref=e468]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e469]: git config --list | grep email + - listitem [ref=e470]: + - text: If not, set it up using + - code [ref=e471]: git config --global user.email email@example.com + - listitem [ref=e472]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e473] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e478]: + - generic "Add or remove reactions" [ref=e479] [cursor=pointer]: + - img [ref=e480] + - generic [ref=e483]: + - link "@oz-for-oss" [ref=e485] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e486] + - generic [ref=e488]: + - generic [ref=e489]: + - generic [ref=e490]: + - group [ref=e492]: + - button "Show options" [ref=e493] [cursor=pointer]: + - img "Show options" [ref=e496] + - generic "This user has previously committed to the warp repository." [ref=e499]: + - generic [ref=e500]: Contributor + - heading "oz-for-oss Bot commented May 20, 20263 hours ago" [level=3] [ref=e501]: + - generic [ref=e502]: + - strong [ref=e503]: + - link "oz-for-oss" [ref=e504] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e505]: Bot + - text: commented + - link "May 20, 20263 hours ago" [ref=e506] [cursor=pointer]: + - /url: "#issuecomment-4494411637" + - generic [ref=e507]: + - table [ref=e509]: + - rowgroup [ref=e510]: + - 'row "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e511]': + - 'cell "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e512]': + - paragraph [ref=e513]: + - link "@ErshovDmitry" [ref=e514] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e515]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e516]: ready-to-implement + - text: . + - paragraph [ref=e517]: "Issue-state enforcement details:" + - list [ref=e518]: + - listitem [ref=e519]: + - paragraph [ref=e520]: "Associated same-repo issues checked: none" + - listitem [ref=e521]: + - paragraph [ref=e522]: + - text: "Required readiness label:" + - code [ref=e523]: ready-to-implement + - paragraph [ref=e524]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e525]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e526]: ready-to-implement + - text: . + - paragraph [ref=e527]: + - emphasis [ref=e528]: + - text: Powered by + - link "Oz" [ref=e529] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e534]: + - generic "Add or remove reactions" [ref=e535] [cursor=pointer]: + - img [ref=e536] + - generic [ref=e539]: + - img [ref=e541] + - generic [ref=e543]: + - link "@github-actions" [ref=e544] [cursor=pointer]: + - /url: /apps/github-actions + - img "@github-actions" [ref=e545] + - link "github-actions" [ref=e546] [cursor=pointer]: + - /url: /apps/github-actions + - generic [ref=e547]: Bot + - text: added the + - link "external-contributor" [ref=e548] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - text: label + - link "May 20, 20263 hours ago" [ref=e549] [cursor=pointer]: + - /url: "#event-25738319166" + - generic [ref=e552]: + - generic [ref=e553]: + - link "oz-for-oss[bot]" [ref=e554] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "oz-for-oss[bot]" [ref=e555] + - img [ref=e557] + - generic [ref=e559]: + - generic [ref=e560]: + - strong [ref=e561]: + - link "oz-for-oss" [ref=e562] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e563]: Bot + - text: requested changes + - link "May 20, 20263 hours ago" [ref=e565] [cursor=pointer]: + - /url: "#pullrequestreview-4325084051" + - link "View reviewed changes" [ref=e567] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e569]: View reviewed changes + - generic [ref=e572]: + - generic [ref=e573]: + - generic [ref=e574]: + - group [ref=e576]: + - button "Show options" [ref=e577] [cursor=pointer]: + - img "Show options" [ref=e580] + - generic "This user has previously committed to the warp repository." [ref=e583]: + - generic [ref=e584]: Contributor + - heading "oz-for-oss Bot left a comment" [level=3] [ref=e585]: + - generic [ref=e586]: + - strong [ref=e587]: + - link "oz-for-oss" [ref=e588] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e589]: Bot + - text: left a comment + - generic [ref=e592]: + - paragraph [ref=e593]: + - link "@ErshovDmitry" [ref=e594] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e595]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e596]: ready-to-implement + - text: . + - paragraph [ref=e597]: "Issue-state enforcement details:" + - list [ref=e598]: + - listitem [ref=e599]: + - paragraph [ref=e600]: "Associated same-repo issues checked: none" + - listitem [ref=e601]: + - paragraph [ref=e602]: + - text: "Required readiness label:" + - code [ref=e603]: ready-to-implement + - paragraph [ref=e604]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e605]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e606]: ready-to-implement + - text: . + - paragraph [ref=e607]: + - emphasis [ref=e608]: + - text: Powered by + - link "Oz" [ref=e609] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e613]: + - generic "Add or remove reactions" [ref=e614] [cursor=pointer]: + - img [ref=e615] + - generic [ref=e618]: + - link "@ErshovDmitry" [ref=e620] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e621] + - generic [ref=e623]: + - generic [ref=e624]: + - generic [ref=e625]: + - group [ref=e627]: + - button "Show options" [ref=e628] [cursor=pointer]: + - img "Show options" [ref=e631] + - generic "You are the author of this pull request." [ref=e634]: + - generic [ref=e635]: Author + - heading "ErshovDmitry commented May 20, 20263 hours ago" [level=3] [ref=e636]: + - generic [ref=e637]: + - strong [ref=e638]: + - link "ErshovDmitry" [ref=e639] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 hours ago" [ref=e640] [cursor=pointer]: + - /url: "#issuecomment-4494430862" + - generic [ref=e641]: + - table [ref=e643]: + - rowgroup [ref=e644]: + - 'row "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e645]': + - 'cell "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e646]': + - paragraph [ref=e647]: + - text: Oh wow — I just noticed there are already several i18n PRs open ( + - link "#10630" [ref=e648] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "," + - link "#10990" [ref=e649] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: "," + - link "#9458" [ref=e650] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9458 + - text: "," + - link "#9922" [ref=e651] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9922 + - text: ). I should have checked first before opening mine. My apologies for the noise! + - blockquote [ref=e652]: + - paragraph [ref=e653]: + - text: That said, I see none of them include Russian yet. If the team settles on one i18n approach (looks like + - link "#10630" [ref=e654] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e655] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: is the leading one?), I'd be happy to contribute Russian translations to that effort instead. + - paragraph [ref=e656]: I'll close this PR if it's just adding clutter — just let me know. + - paragraph [ref=e657]: + - text: Cheers, + - text: Dmitry + - group [ref=e662]: + - generic "Add or remove reactions" [ref=e663] [cursor=pointer]: + - img [ref=e664] + - generic [ref=e669]: + - img [ref=e671] + - generic [ref=e676]: + - link "@ErshovDmitry" [ref=e679] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e680] + - generic [ref=e681]: + - code [ref=e682]: + - 'link "refactor: migrate to ZacharyZcR-compatible YAML i18n with Russian locale" [ref=e683] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - button "Commit message body" [ref=e685] [cursor=pointer]: … + - code [ref=e693]: + - link "04801f0" [ref=e694] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - generic [ref=e696]: + - link "@cla-bot" [ref=e698] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e699] + - generic [ref=e701]: + - generic [ref=e702]: + - group [ref=e705]: + - button "Show options" [ref=e706] [cursor=pointer]: + - img "Show options" [ref=e709] + - heading "cla-bot Bot commented May 20, 20261 hour ago" [level=3] [ref=e711]: + - generic [ref=e712]: + - strong [ref=e713]: + - link "cla-bot" [ref=e714] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e715]: Bot + - text: commented + - link "May 20, 20261 hour ago" [ref=e716] [cursor=pointer]: + - /url: "#issuecomment-4494962896" + - generic [ref=e717]: + - table [ref=e719]: + - rowgroup [ref=e720]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e721]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e722]': + - paragraph [ref=e723]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e724]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e725]: + - listitem [ref=e726]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e727]: git config --list | grep email + - listitem [ref=e728]: + - text: If not, set it up using + - code [ref=e729]: git config --global user.email email@example.com + - listitem [ref=e730]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e731] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e736]: + - generic "Add or remove reactions" [ref=e737] [cursor=pointer]: + - img [ref=e738] + - generic [ref=e743]: + - img [ref=e745] + - generic [ref=e750]: + - link "@ErshovDmitry" [ref=e753] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e754] + - code [ref=e756]: + - 'link "fix: init_locale before menu bar, cleanup dead code, harden set_locale" [ref=e757] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - code [ref=e765]: + - link "1caedf3" [ref=e766] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - generic [ref=e768]: + - link "@cla-bot" [ref=e770] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e771] + - generic [ref=e773]: + - generic [ref=e774]: + - group [ref=e777]: + - button "Show options" [ref=e778] [cursor=pointer]: + - img "Show options" [ref=e781] + - heading "cla-bot Bot commented May 20, 202649 minutes ago" [level=3] [ref=e783]: + - generic [ref=e784]: + - strong [ref=e785]: + - link "cla-bot" [ref=e786] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e787]: Bot + - text: commented + - link "May 20, 202649 minutes ago" [ref=e788] [cursor=pointer]: + - /url: "#issuecomment-4495220665" + - generic [ref=e789]: + - table [ref=e791]: + - rowgroup [ref=e792]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e793]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e794]': + - paragraph [ref=e795]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e796]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e797]: + - listitem [ref=e798]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e799]: git config --list | grep email + - listitem [ref=e800]: + - text: If not, set it up using + - code [ref=e801]: git config --global user.email email@example.com + - listitem [ref=e802]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e803] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e808]: + - generic "Add or remove reactions" [ref=e809] [cursor=pointer]: + - img [ref=e810] + - generic [ref=e815]: + - img [ref=e817] + - generic [ref=e822]: + - link "@ErshovDmitry" [ref=e825] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e826] + - generic [ref=e827]: + - code [ref=e828]: + - 'link "fix(i18n): Round 2 review — internationalize all hardcoded menu strings" [ref=e829] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - button "Commit message body" [ref=e831] [cursor=pointer]: … + - code [ref=e839]: + - link "1b026c7" [ref=e840] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - generic [ref=e842]: + - link "@cla-bot" [ref=e844] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e845] + - generic [ref=e847]: + - generic [ref=e848]: + - group [ref=e851]: + - button "Show options" [ref=e852] [cursor=pointer]: + - img "Show options" [ref=e855] + - heading "cla-bot Bot commented May 20, 202620 minutes ago" [level=3] [ref=e857]: + - generic [ref=e858]: + - strong [ref=e859]: + - link "cla-bot" [ref=e860] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e861]: Bot + - text: commented + - link "May 20, 202620 minutes ago" [ref=e862] [cursor=pointer]: + - /url: "#issuecomment-4495434298" + - generic [ref=e863]: + - table [ref=e865]: + - rowgroup [ref=e866]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e867]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e868]': + - paragraph [ref=e869]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e870]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e871]: + - listitem [ref=e872]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e873]: git config --list | grep email + - listitem [ref=e874]: + - text: If not, set it up using + - code [ref=e875]: git config --global user.email email@example.com + - listitem [ref=e876]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e877] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e882]: + - generic "Add or remove reactions" [ref=e883] [cursor=pointer]: + - img [ref=e884] + - generic [ref=e887]: + - link "@ErshovDmitry" [ref=e889] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e890] + - generic [ref=e892]: + - generic [ref=e893]: + - generic [ref=e894]: + - group [ref=e896]: + - button "Show options" [ref=e897] [cursor=pointer]: + - img "Show options" [ref=e900] + - generic "You are the author of this pull request." [ref=e903]: + - generic [ref=e904]: Author + - heading "ErshovDmitry commented May 20, 20269 minutes ago" [level=3] [ref=e905]: + - generic [ref=e906]: + - strong [ref=e907]: + - link "ErshovDmitry" [ref=e908] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20269 minutes ago" [ref=e909] [cursor=pointer]: + - /url: "#issuecomment-4495523677" + - generic [ref=e910]: + - table [ref=e912]: + - rowgroup [ref=e913]: + - 'row "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e914]': + - 'cell "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e915]': + - paragraph [ref=e916]: + - text: Update:** I've rewritten this PR to match + - link "@ZacharyZcR" [ref=e917] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: "'s YAML approach from" + - link "#10630" [ref=e918] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e919] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: ":" + - blockquote [ref=e920]: + - list [ref=e921]: + - listitem [ref=e922]: + - code [ref=e923]: crates/i18n/ + - text: with + - code [ref=e924]: t!() + - text: / + - code [ref=e925]: t_required!() + - text: macros and + - code [ref=e926]: TranslationLookup + - listitem [ref=e927]: + - code [ref=e928]: "resources/bundled/locales/{en,ru}.yml" + - text: (247 keys each, YAML) + - listitem [ref=e929]: + - code [ref=e930]: WARP_LANG + - text: env var → system locale → + - code [ref=e931]: en + - text: fallback + - listitem [ref=e932]: + - text: No feature flag, no settings UI — same design as + - generic [ref=e933]: + - img [ref=e934] + - 'link "Add i18n framework with Simplified Chinese (zh-CN) support #10630" [ref=e936] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/pull/10630 + - paragraph [ref=e937]: + - text: The Russian + - code [ref=e938]: ru.yml + - text: should be a drop-in addition to the framework once it's approved. Happy to rebase onto whichever PR lands first. + - paragraph [ref=e939]: Dmitry + - group [ref=e944]: + - generic "Add or remove reactions" [ref=e945] [cursor=pointer]: + - img [ref=e946] + - generic [ref=e949]: + - link "@ErshovDmitry" [ref=e951] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e952] + - generic [ref=e954]: + - generic [ref=e955]: + - generic [ref=e956]: + - group [ref=e958]: + - button "Show options" [ref=e959] [cursor=pointer]: + - img "Show options" [ref=e962] + - generic "You are the author of this pull request." [ref=e965]: + - generic [ref=e966]: Author + - heading "ErshovDmitry commented May 20, 20262 minutes ago" [level=3] [ref=e967]: + - generic [ref=e968]: + - strong [ref=e969]: + - link "ErshovDmitry" [ref=e970] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20262 minutes ago" [ref=e971] [cursor=pointer]: + - /url: "#issuecomment-4495572049" + - generic [ref=e972]: + - table [ref=e974]: + - rowgroup [ref=e975]: + - row "@cla-bot check" [ref=e976]: + - cell "@cla-bot check" [ref=e977]: + - paragraph [ref=e978]: + - link "@cla-bot" [ref=e979] [cursor=pointer]: + - /url: https://github.com/cla-bot + - text: check + - group [ref=e984]: + - generic "Add or remove reactions" [ref=e985] [cursor=pointer]: + - img [ref=e986] + - generic [ref=e989]: + - img [ref=e991] + - generic [ref=e993]: + - link "@cla-bot" [ref=e994] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e995] + - link "cla-bot" [ref=e996] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e997]: Bot + - text: added the + - link "cla-signed" [ref=e998] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - text: label + - link "May 20, 20262 minutes ago" [ref=e999] [cursor=pointer]: + - /url: "#event-25743817179" + - generic [ref=e1001]: + - link "@cla-bot" [ref=e1003] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e1004] + - generic [ref=e1006]: + - generic [ref=e1007]: + - group [ref=e1010]: + - button "Show options" [ref=e1011] [cursor=pointer]: + - img "Show options" [ref=e1014] + - heading "cla-bot Bot commented May 20, 20261 minute ago" [level=3] [ref=e1016]: + - generic [ref=e1017]: + - strong [ref=e1018]: + - link "cla-bot" [ref=e1019] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e1020]: Bot + - text: commented + - link "May 20, 20261 minute ago" [ref=e1021] [cursor=pointer]: + - /url: "#issuecomment-4495573189" + - generic [ref=e1022]: + - table [ref=e1024]: + - rowgroup [ref=e1025]: + - row "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1026]: + - cell "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1027]: + - paragraph [ref=e1028]: The cla-bot has been summoned, and re-checked this pull request! + - group [ref=e1033]: + - generic "Add or remove reactions" [ref=e1034] [cursor=pointer]: + - img [ref=e1035] + - generic [ref=e1040]: + - img [ref=e1041] + - generic [ref=e1044]: Loading + - generic [ref=e1047]: + - link "@ErshovDmitry" [ref=e1049] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1050] + - form "Add a comment" [ref=e1052]: + - group [ref=e1053]: + - heading "Add a comment" [level=4] [ref=e1056] + - generic [ref=e1057]: Comment + - generic [ref=e1058]: + - generic [ref=e1059]: + - tablist "Add a comment" [ref=e1060]: + - tab "Write" [selected] [ref=e1061] [cursor=pointer] + - tab "Preview" [ref=e1062] [cursor=pointer] + - toolbar [ref=e1063]: + - generic [ref=e1064]: + - button "Heading" [ref=e1066] [cursor=pointer]: + - img + - button "Bold" [ref=e1068] [cursor=pointer]: + - img + - button "Italic" [ref=e1070] [cursor=pointer]: + - img + - button "Quote" [ref=e1072] [cursor=pointer]: + - img + - button "Code" [ref=e1074] [cursor=pointer]: + - img + - button "Link" [ref=e1076] [cursor=pointer]: + - img + - separator [ref=e1077] + - button "Numbered list" [ref=e1079] [cursor=pointer]: + - img + - button "Unordered list" [ref=e1081] [cursor=pointer]: + - img + - button "Task list" [ref=e1083] [cursor=pointer]: + - img + - separator [ref=e1084] + - button "Attach files" [ref=e1086] [cursor=pointer]: + - img + - button "Mention" [ref=e1088] [cursor=pointer]: + - img + - button "Reference" [ref=e1090] [cursor=pointer]: + - img + - button "Saved replies" [ref=e1092] [cursor=pointer]: + - img + - button "Slash commands" [ref=e1094] [cursor=pointer]: + - img + - tabpanel "Write" [ref=e1095]: + - generic [ref=e1099]: + - textbox "Comment" [ref=e1100]: + - /placeholder: " " + - paragraph: Add your comment here... + - generic [ref=e1101]: + - link "Markdown is supported" [ref=e1103] [cursor=pointer]: + - /url: https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax + - generic [ref=e1104]: + - generic: + - img + - generic [ref=e1105]: Markdown is supported + - button "Paste, drop, or click to add files" [ref=e1106] [cursor=pointer]: + - generic [ref=e1107]: + - generic: + - img + - generic [ref=e1108]: Paste, drop, or click to add files + - generic [ref=e1111]: + - button "Close pull request" [ref=e1113] [cursor=pointer]: + - img [ref=e1114] + - text: Close pull request + - button "Comment" [disabled] [ref=e1117] + - generic [ref=e1118]: + - img [ref=e1119] + - generic [ref=e1121]: + - text: Remember, contributions to this repository should follow its + - link "contributing guidelines" [ref=e1122] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/CONTRIBUTING.md + - text: "," + - link "security policy" [ref=e1123] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/SECURITY.md + - text: ", and" + - link "code of conduct" [ref=e1124] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/CODE_OF_CONDUCT.md + - text: . + - generic [ref=e1125]: + - img [ref=e1126] + - strong [ref=e1128]: ProTip! + - text: Add comments to specific lines under + - link "Files changed" [ref=e1129] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files + - text: . + - generic [ref=e1133]: + - form "Select reviewers" [ref=e1135]: + - heading "Reviewers" [level=3] [ref=e1136] + - generic [ref=e1137]: + - paragraph [ref=e1138]: + - generic [ref=e1139]: + - link "@oz-for-oss" [ref=e1140] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e1141] + - link "oz-for-oss[bot]" [ref=e1142] [cursor=pointer]: + - /url: /apps/oz-for-oss + - link "oz-for-oss[bot] requested changes" [ref=e1143] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes/BASE..169dfa663dd4c533eb32daf850f25d6b9c123521 + - img [ref=e1145] + - paragraph [ref=e1147]: Requested changes must be addressed to merge this pull request. + - generic [ref=e1148]: + - generic [ref=e1149]: Still in progress? + - group [ref=e1150]: + - button "Convert to draft" [ref=e1151] [cursor=pointer] + - form "Select assignees" [ref=e1153]: + - heading "Assignees" [level=3] [ref=e1154] + - text: No one assigned + - generic [ref=e1155]: + - heading "Labels" [level=3] [ref=e1156] + - generic [ref=e1157]: + - link "cla-signed" [ref=e1158] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - generic [ref=e1159]: cla-signed + - link "external-contributor" [ref=e1160] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - generic [ref=e1161]: external-contributor + - form "Select projects" [ref=e1163]: + - heading "Projects" [level=3] [ref=e1164] + - text: None yet + - form "Select milestones" [ref=e1166]: + - heading "Milestone" [level=3] [ref=e1167] + - text: No milestone + - form "Link issues" [ref=e1173]: + - heading "Development" [level=3] [ref=e1174] + - paragraph [ref=e1175]: Successfully merging this pull request may close these issues. + - generic [ref=e1177]: + - button "Notifications Customize" [ref=e1180] [cursor=pointer]: + - generic [ref=e1181]: + - generic [ref=e1182]: Notifications + - generic [ref=e1183]: Customize + - button "Unsubscribe" [ref=e1185] [cursor=pointer]: + - generic [ref=e1186]: + - generic: + - img + - generic [ref=e1187]: Unsubscribe + - paragraph [ref=e1188]: You’re receiving notifications because you were mentioned. + - generic [ref=e1190]: + - heading "1 participant" [level=3] [ref=e1191] + - link "@ErshovDmitry" [ref=e1193] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1194] + - generic [ref=e1197]: + - generic [ref=e1198]: + - checkbox "Allow edits by maintainers" [checked] [ref=e1199] + - text: Allow edits by maintainers + - button "Learn more about allowing maintainer edits" [ref=e1200] [cursor=pointer]: + - img [ref=e1203] + - contentinfo [ref=e1206]: + - heading "Footer" [level=2] [ref=e1207] + - generic [ref=e1208]: + - generic [ref=e1209]: + - link "GitHub Homepage" [ref=e1210] [cursor=pointer]: + - /url: https://github.com + - img [ref=e1211] + - generic [ref=e1213]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e1214]: + - heading "Footer navigation" [level=3] [ref=e1215] + - list "Footer navigation" [ref=e1216]: + - listitem [ref=e1217]: + - link "Terms" [ref=e1218] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e1219]: + - link "Privacy" [ref=e1220] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e1221]: + - link "Security" [ref=e1222] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e1223]: + - link "Status" [ref=e1224] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e1225]: + - link "Community" [ref=e1226] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e1227]: + - link "Docs" [ref=e1228] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e1229]: + - link "Contact" [ref=e1230] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e1231]: + - button "Manage cookies" [ref=e1233] [cursor=pointer] + - listitem [ref=e1234]: + - button "Do not share my personal information" [ref=e1236] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-20T07-13-50-884Z.yml b/.playwright-mcp/page-2026-05-20T07-13-50-884Z.yml new file mode 100644 index 0000000000..5f858466ea --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T07-13-50-884Z.yml @@ -0,0 +1,168 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - generic [ref=e7]: + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - link "You have no unread notifications (g then n)" [ref=e58] [cursor=pointer]: + - /url: /notifications + - img [ref=e59] + - button "Open user navigation menu" [ref=e62] [cursor=pointer]: + - img "User avatar" [ref=e63] + - heading "Repository navigation" [level=2] [ref=e64] + - navigation "Repository" [ref=e65]: + - list [ref=e66]: + - listitem [ref=e67]: + - link "Code" [ref=e68] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e70] + - generic [ref=e72]: Code + - listitem [ref=e73]: + - link "Issues (3.4k)" [ref=e74] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e76] + - generic [ref=e79]: Issues + - generic [ref=e80]: + - generic [ref=e81]: 3.4k + - generic [ref=e82]: (3.4k) + - listitem [ref=e83]: + - link "Pull requests (502)" [ref=e84] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e86] + - generic [ref=e88]: Pull requests + - generic [ref=e89]: + - generic [ref=e90]: "502" + - generic [ref=e91]: (502) + - listitem [ref=e92]: + - link "Agents" [ref=e93] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e95] + - generic [ref=e98]: Agents + - listitem [ref=e99]: + - link "Discussions" [ref=e100] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e102] + - generic [ref=e104]: Discussions + - listitem [ref=e105]: + - link "Actions" [ref=e106] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e108] + - generic [ref=e110]: Actions + - listitem [ref=e111]: + - link "Projects" [ref=e112] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e114] + - generic [ref=e116]: Projects + - listitem [ref=e117]: + - link "Security and quality" [ref=e118] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e120] + - generic [ref=e122]: Security and quality + - listitem [ref=e123]: + - link "Insights" [ref=e124] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e126] + - generic [ref=e128]: Insights + - region "Important update" [ref=e130]: + - img [ref=e132] + - generic [ref=e135]: + - heading "Important update" [level=2] [ref=e137] + - generic [ref=e139]: + - text: On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out. + - link "Review this update" [ref=e140] [cursor=pointer]: + - /url: https://gh.io/AAzfaht + - text: and manage your preferences in your + - link "GitHub account settings" [ref=e141] [cursor=pointer]: + - /url: https://github.com/settings/copilot/features + - text: . + - button "Dismiss banner" [ref=e142] [cursor=pointer]: + - img [ref=e143] + - main [ref=e147] + - contentinfo [ref=e150]: + - heading "Footer" [level=2] [ref=e151] + - generic [ref=e152]: + - generic [ref=e153]: + - link "GitHub Homepage" [ref=e154] [cursor=pointer]: + - /url: https://github.com + - img [ref=e155] + - generic [ref=e157]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e158]: + - heading "Footer navigation" [level=3] [ref=e159] + - list "Footer navigation" [ref=e160]: + - listitem [ref=e161]: + - link "Terms" [ref=e162] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e163]: + - link "Privacy" [ref=e164] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e165]: + - link "Security" [ref=e166] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e167]: + - link "Status" [ref=e168] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e169]: + - link "Community" [ref=e170] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e171]: + - link "Docs" [ref=e172] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e173]: + - link "Contact" [ref=e174] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e175]: + - button "Manage cookies" [ref=e177] [cursor=pointer] + - listitem [ref=e178]: + - button "Do not share my personal information" [ref=e180] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-20T07-14-12-943Z.yml b/.playwright-mcp/page-2026-05-20T07-14-12-943Z.yml new file mode 100644 index 0000000000..de967f3037 --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T07-14-12-943Z.yml @@ -0,0 +1,751 @@ +- generic [ref=e2]: + - generic: + - generic: + - tooltip "This user is the author of this issue" + - generic: + - tooltip "This user is a member of the warpdotdev organization." + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - generic [ref=e7]: + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - link "You have no unread notifications (g then n)" [ref=e58] [cursor=pointer]: + - /url: /notifications + - img [ref=e59] + - button "Open user navigation menu" [ref=e62] [cursor=pointer]: + - img "User avatar" [ref=e63] + - heading "Repository navigation" [level=2] [ref=e64] + - navigation "Repository" [ref=e65]: + - list [ref=e66]: + - listitem [ref=e67]: + - link "Code" [ref=e68] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e70] + - generic [ref=e72]: Code + - listitem [ref=e73]: + - link "Issues (3.4k)" [ref=e74] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e76] + - generic [ref=e79]: Issues + - generic [ref=e80]: + - generic [ref=e81]: 3.4k + - generic [ref=e82]: (3.4k) + - listitem [ref=e83]: + - link "Pull requests (502)" [ref=e84] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e86] + - generic [ref=e88]: Pull requests + - generic [ref=e89]: + - generic [ref=e90]: "502" + - generic [ref=e91]: (502) + - listitem [ref=e92]: + - link "Agents" [ref=e93] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e95] + - generic [ref=e98]: Agents + - listitem [ref=e99]: + - link "Discussions" [ref=e100] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e102] + - generic [ref=e104]: Discussions + - listitem [ref=e105]: + - link "Actions" [ref=e106] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e108] + - generic [ref=e110]: Actions + - listitem [ref=e111]: + - link "Projects" [ref=e112] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e114] + - generic [ref=e116]: Projects + - listitem [ref=e117]: + - link "Security and quality" [ref=e118] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e120] + - generic [ref=e122]: Security and quality + - listitem [ref=e123]: + - link "Insights" [ref=e124] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e126] + - generic [ref=e128]: Insights + - region "Important update" [ref=e130]: + - img [ref=e132] + - generic [ref=e135]: + - heading "Important update" [level=2] [ref=e137] + - generic [ref=e139]: + - text: On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out. + - link "Review this update" [ref=e140] [cursor=pointer]: + - /url: https://gh.io/AAzfaht + - text: and manage your preferences in your + - link "GitHub account settings" [ref=e141] [cursor=pointer]: + - /url: https://github.com/settings/copilot/features + - text: . + - button "Dismiss banner" [ref=e142] [cursor=pointer]: + - img [ref=e143] + - main [ref=e148]: + - generic [ref=e154]: + - region "Header" [ref=e156]: + - generic [ref=e157]: + - 'heading "Application language switch / i18n Support multiple languages and localizations #1194" [level=1] [ref=e159]' + - generic [ref=e161]: + - link "New issue" [ref=e163] [cursor=pointer]: + - /url: /warpdotdev/warp/issues/new/choose + - generic [ref=e165]: New issue + - button "Copy link" [ref=e166] [cursor=pointer]: + - img [ref=e167] + - generic [ref=e173]: + - generic [ref=e175]: + - img "Issue" [ref=e176] + - text: Open + - 'link "Open #10990" [ref=e180] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/pull/10990 + - img "Open" [ref=e181] + - generic [ref=e183]: "#10990" + - generic [ref=e185]: + - generic [ref=e186]: + - generic [ref=e188]: + - link "@Quinlivanner's profile" [ref=e189] [cursor=pointer]: + - /url: https://github.com/Quinlivanner + - img "@Quinlivanner" [ref=e190] + - generic [ref=e191]: + - heading "Description" [level=2] [ref=e192] + - generic [ref=e194]: + - generic [ref=e196]: + - generic [ref=e197]: + - link "Quinlivanner" [ref=e199] [cursor=pointer]: + - /url: https://github.com/Quinlivanner + - generic [ref=e200]: + - text: opened + - link "on Apr 26, 2022on Apr 26, 2022" [ref=e201] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issue-1215319490 + - generic "Apr 26, 2022, 10:06 AM GMT+7" [ref=e202]: on Apr 26, 2022on Apr 26, 2022 + - button "Issue body actions" [ref=e206] [cursor=pointer]: + - img [ref=e207] + - generic [ref=e209]: + - generic [ref=e211]: + - heading "Discord username (optional)" [level=3] [ref=e212] + - paragraph [ref=e213]: Quickk#0001 + - heading "Describe the solution you'd like?" [level=3] [ref=e214] + - generic [ref=e215]: + - code [ref=e217]: Provide other language options for application and optimize for language localization. (It is the interface language, not the language content support), allowing users to choose their own applicable language in the Setting, such as Chinese, so that users can better experience the product and find problems. + - button "Copy code to clipboard" [ref=e219] [cursor=pointer]: + - img [ref=e220] + - heading "Is your feature request related to a problem? Please describe." [level=3] [ref=e223] + - generic [ref=e224]: + - code [ref=e226]: No, I'm not currently having problems + - button "Copy code to clipboard" [ref=e228] [cursor=pointer]: + - img [ref=e229] + - heading "Additional context" [level=3] [ref=e232] + - paragraph [ref=e233]: "We are tracking different language support in the links below:" + - list [ref=e234]: + - listitem [ref=e235]: + - link "Chinese support" [ref=e236] [cursor=pointer]: + - /url: https://github.com/warpdotdev/Warp/issues/1823 + - listitem [ref=e237]: + - link "Korean support" [ref=e238] [cursor=pointer]: + - /url: https://github.com/warpdotdev/Warp/issues/3127 + - listitem [ref=e239]: + - link "Japanese support" [ref=e240] [cursor=pointer]: + - /url: https://github.com/warpdotdev/Warp/issues/6581 + - toolbar "Reactions" [ref=e242]: + - button "React" [ref=e243] [cursor=pointer]: + - img [ref=e244] + - switch "👍 223 reactions" [ref=e246] [cursor=pointer]: + - generic [ref=e247]: + - generic: 👍 + - generic [ref=e248]: + - generic [ref=e249]: React with 👍 + - text: "223" + - switch "👎 3 reactions" [ref=e250] [cursor=pointer]: + - generic [ref=e251]: + - generic: 👎 + - generic [ref=e252]: + - generic [ref=e253]: React with 👎 + - text: "3" + - switch "❤️ 2 reactions" [ref=e254] [cursor=pointer]: + - generic [ref=e255]: + - generic: ❤️ + - generic [ref=e256]: + - generic [ref=e257]: React with ❤️ + - text: "2" + - switch "👀 6 reactions" [ref=e258] [cursor=pointer]: + - generic [ref=e259]: + - generic: 👀 + - generic [ref=e260]: + - generic [ref=e261]: React with 👀 + - text: "6" + - generic [ref=e263]: + - generic [ref=e264]: + - heading "Activity" [level=2] [ref=e265] + - link [ref=e266] [cursor=pointer]: + - /url: /warpdotdev/warp/issues/1194?timeline_page=1 + - text: Next + - generic [ref=e267]: + - region "Events" [ref=e268]: + - generic [ref=e275]: + - img [ref=e279] + - generic [ref=e281]: + - generic [ref=e283]: + - link "elviskahoro" [ref=e285] [cursor=pointer]: + - /url: /elviskahoro + - generic [ref=e286]: elviskahoro + - text: mentioned this + - link "on Jul 9, 2022on Jul 9, 2022" [ref=e287] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#event-1199167149 + - generic "Jul 9, 2022, 3:50 AM GMT+7" [ref=e288]: on Jul 9, 2022on Jul 9, 2022 + - region "Issues mentioned" [ref=e291]: + - list [ref=e292]: + - listitem [ref=e293]: + - generic [ref=e294]: + - img "Completed" [ref=e295] + - 'link "Will you consider the Chinese version? #1132" [ref=e298] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/issues/1132 + - text: Will you consider the Chinese version? + - generic [ref=e299]: "#1132" + - generic [ref=e303]: + - img [ref=e307] + - generic [ref=e309]: + - generic [ref=e311]: + - link "dannyneira" [ref=e313] [cursor=pointer]: + - /url: /dannyneira + - generic [ref=e314]: dannyneira + - text: mentioned this + - link "on Sep 10, 2022on Sep 10, 2022" [ref=e315] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#event-1213033965 + - generic "Sep 10, 2022, 2:12 AM GMT+7" [ref=e316]: on Sep 10, 2022on Sep 10, 2022 + - region "Issues mentioned" [ref=e319]: + - list [ref=e320]: + - listitem [ref=e321]: + - generic [ref=e322]: + - img "Not planned" [ref=e323] + - 'link "Support for Chinese language #1823" [ref=e325] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/issues/1823 + - text: Support for Chinese language + - generic [ref=e326]: "#1823" + - generic [ref=e327]: + - link "@77321660's profile" [ref=e328] [cursor=pointer]: + - /url: https://github.com/77321660 + - img "77321660" [ref=e329] + - generic [ref=e336]: + - generic [ref=e338]: + - heading "77321660 commented on Oct 17, 2022 on Oct 17, 2022" [level=3] [ref=e339]: + - text: 77321660 commented + - generic "Oct 17, 2022, 12:56 PM GMT+7" [ref=e340]: on Oct 17, 2022 on Oct 17, 2022 + - generic [ref=e341]: + - link "77321660" [ref=e343] [cursor=pointer]: + - /url: /77321660 + - link "on Oct 17, 2022on Oct 17, 2022" [ref=e346] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1280326947 + - generic "Oct 17, 2022, 12:56 PM GMT+7" [ref=e347]: on Oct 17, 2022on Oct 17, 2022 + - button "Actions for 77321660's comment, 12:56 PM on October 17, 2022" [ref=e351] [cursor=pointer]: + - img [ref=e352] + - generic [ref=e354]: + - paragraph [ref=e357]: "+1" + - toolbar "Reactions" [ref=e358]: + - button "React" [ref=e359] [cursor=pointer]: + - img [ref=e360] + - generic [ref=e362]: + - link "@mingzaily's profile" [ref=e363] [cursor=pointer]: + - /url: https://github.com/mingzaily + - img "mingzaily" [ref=e364] + - generic [ref=e371]: + - generic [ref=e373]: + - heading "mingzaily commented on Nov 11, 2022 on Nov 11, 2022" [level=3] [ref=e374]: + - text: mingzaily commented + - generic "Nov 11, 2022, 3:31 PM GMT+7" [ref=e375]: on Nov 11, 2022 on Nov 11, 2022 + - generic [ref=e376]: + - link "mingzaily" [ref=e378] [cursor=pointer]: + - /url: /mingzaily + - link "on Nov 11, 2022on Nov 11, 2022" [ref=e381] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1311388252 + - generic "Nov 11, 2022, 3:31 PM GMT+7" [ref=e382]: on Nov 11, 2022on Nov 11, 2022 + - button "Actions for mingzaily's comment, 3:31 PM on November 11, 2022" [ref=e386] [cursor=pointer]: + - img [ref=e387] + - generic [ref=e389]: + - paragraph [ref=e392]: "+1" + - toolbar "Reactions" [ref=e393]: + - button "React" [ref=e394] [cursor=pointer]: + - img [ref=e395] + - generic [ref=e397]: + - link "@MacAndTea's profile" [ref=e398] [cursor=pointer]: + - /url: https://github.com/MacAndTea + - img "MacAndTea" [ref=e399] + - generic [ref=e406]: + - generic [ref=e408]: + - heading "MacAndTea commented on Nov 23, 2022 on Nov 23, 2022" [level=3] [ref=e409]: + - text: MacAndTea commented + - generic "Nov 23, 2022, 2:57 PM GMT+7" [ref=e410]: on Nov 23, 2022 on Nov 23, 2022 + - generic [ref=e411]: + - link "MacAndTea" [ref=e413] [cursor=pointer]: + - /url: /MacAndTea + - link "on Nov 23, 2022on Nov 23, 2022" [ref=e416] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1324672708 + - generic "Nov 23, 2022, 2:57 PM GMT+7" [ref=e417]: on Nov 23, 2022on Nov 23, 2022 + - button "Actions for MacAndTea's comment, 2:57 PM on November 23, 2022" [ref=e421] [cursor=pointer]: + - img [ref=e422] + - generic [ref=e424]: + - paragraph [ref=e427]: "+1" + - toolbar "Reactions" [ref=e428]: + - button "React" [ref=e429] [cursor=pointer]: + - img [ref=e430] + - generic [ref=e432]: + - link "@fby0394's profile" [ref=e433] [cursor=pointer]: + - /url: https://github.com/fby0394 + - img "fby0394" [ref=e434] + - generic [ref=e441]: + - generic [ref=e443]: + - heading "fby0394 commented on Dec 2, 2022 on Dec 2, 2022" [level=3] [ref=e444]: + - text: fby0394 commented + - generic "Dec 2, 2022, 9:20 AM GMT+7" [ref=e445]: on Dec 2, 2022 on Dec 2, 2022 + - generic [ref=e446]: + - link "fby0394" [ref=e448] [cursor=pointer]: + - /url: /fby0394 + - link "on Dec 2, 2022on Dec 2, 2022" [ref=e451] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1334681180 + - generic "Dec 2, 2022, 9:20 AM GMT+7" [ref=e452]: on Dec 2, 2022on Dec 2, 2022 + - button "Actions for fby0394's comment, 9:20 AM on December 2, 2022" [ref=e456] [cursor=pointer]: + - img [ref=e457] + - generic [ref=e459]: + - paragraph [ref=e462]: Will there be a Chinese version in the future? + - toolbar "Reactions" [ref=e463]: + - button "React" [ref=e464] [cursor=pointer]: + - img [ref=e465] + - switch "❤️ 5 reactions" [ref=e467] [cursor=pointer]: + - generic [ref=e468]: + - generic: ❤️ + - generic [ref=e469]: + - generic [ref=e470]: React with ❤️ + - text: "5" + - generic [ref=e471]: + - link "@Quinlivanner's profile" [ref=e472] [cursor=pointer]: + - /url: https://github.com/Quinlivanner + - img "Quinlivanner" [ref=e473] + - generic [ref=e480]: + - generic [ref=e482]: + - heading "Quinlivanner commented on Dec 2, 2022 on Dec 2, 2022" [level=3] [ref=e483]: + - text: Quinlivanner commented + - generic "Dec 2, 2022, 1:40 PM GMT+7" [ref=e484]: on Dec 2, 2022 on Dec 2, 2022 + - generic [ref=e485]: + - link "Quinlivanner" [ref=e487] [cursor=pointer]: + - /url: /Quinlivanner + - link "on Dec 2, 2022on Dec 2, 2022" [ref=e490] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1334823738 + - generic "Dec 2, 2022, 1:40 PM GMT+7" [ref=e491]: on Dec 2, 2022on Dec 2, 2022 + - generic [ref=e492]: + - button "Last edited by Quinlivanner" [ref=e495] [cursor=pointer]: + - generic [ref=e498]: Last edited by Quinlivanner + - generic: + - img + - generic [ref=e499]: + - generic "This user is the author of this issue" [ref=e501]: Author + - button "Actions for Quinlivanner's comment, 1:40 PM on December 2, 2022" [ref=e503] [cursor=pointer]: + - img [ref=e504] + - generic [ref=e506]: + - generic [ref=e508]: + - blockquote [ref=e509]: + - paragraph [ref=e510]: Will there be a Chinese version in the future? + - paragraph [ref=e511]: Tbh i think yes if have enough check + - toolbar "Reactions" [ref=e512]: + - button "React" [ref=e513] [cursor=pointer]: + - img [ref=e514] + - switch "👍 2 reactions" [ref=e516] [cursor=pointer]: + - generic [ref=e517]: + - generic: 👍 + - generic [ref=e518]: + - generic [ref=e519]: React with 👍 + - text: "2" + - generic [ref=e520]: + - link "@ujjboy's profile" [ref=e521] [cursor=pointer]: + - /url: https://github.com/ujjboy + - img "ujjboy" [ref=e522] + - generic [ref=e529]: + - generic [ref=e531]: + - heading "ujjboy commented on Dec 16, 2022 on Dec 16, 2022" [level=3] [ref=e532]: + - text: ujjboy commented + - generic "Dec 16, 2022, 2:47 PM GMT+7" [ref=e533]: on Dec 16, 2022 on Dec 16, 2022 + - generic [ref=e534]: + - link "ujjboy" [ref=e536] [cursor=pointer]: + - /url: /ujjboy + - link "on Dec 16, 2022on Dec 16, 2022" [ref=e539] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1354341409 + - generic "Dec 16, 2022, 2:47 PM GMT+7" [ref=e540]: on Dec 16, 2022on Dec 16, 2022 + - button "Actions for ujjboy's comment, 2:47 PM on December 16, 2022" [ref=e544] [cursor=pointer]: + - img [ref=e545] + - generic [ref=e547]: + - paragraph [ref=e550]: "+1" + - toolbar "Reactions" [ref=e551]: + - button "React" [ref=e552] [cursor=pointer]: + - img [ref=e553] + - switch "👍 2 reactions" [ref=e555] [cursor=pointer]: + - generic [ref=e556]: + - generic: 👍 + - generic [ref=e557]: + - generic [ref=e558]: React with 👍 + - text: "2" + - region "Events" [ref=e559]: + - generic [ref=e566]: + - img [ref=e570] + - generic [ref=e574]: + - link "warpdotdev-devx" [ref=e576] [cursor=pointer]: + - /url: /apps/warpdotdev-devx + - generic [ref=e577]: warpdotdev-devx + - text: added + - link "Feature" [ref=e579] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3A%22Feature%22 + - generic [ref=e581]: Feature + - link "on Jan 10, 2023on Jan 10, 2023" [ref=e583] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#event-8186658613 + - generic "Jan 10, 2023, 6:02 AM GMT+7" [ref=e584]: on Jan 10, 2023on Jan 10, 2023 + - generic [ref=e591]: + - img [ref=e595] + - generic [ref=e597]: + - generic [ref=e599]: + - link "dannyneira" [ref=e601] [cursor=pointer]: + - /url: /dannyneira + - generic [ref=e602]: dannyneira + - text: mentioned this in 2 issues + - link "on Mar 24, 2023on Mar 24, 2023" [ref=e603] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#event-1274189649 + - generic "Mar 24, 2023, 11:24 PM GMT+7" [ref=e604]: on Mar 24, 2023on Mar 24, 2023 + - region "Issues mentioned" [ref=e607]: + - list [ref=e608]: + - listitem [ref=e609]: + - generic [ref=e610]: + - img "Not planned" [ref=e611] + - 'link "什么时候可以出ui的中文版本 / Release Chinese version of Warp UI #2789" [ref=e613] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/issues/2789 + - text: 什么时候可以出ui的中文版本 / Release Chinese version of Warp UI + - generic [ref=e614]: "#2789" + - listitem [ref=e615]: + - generic [ref=e616]: + - img "Not planned" [ref=e617] + - 'link "When will warp support Chinese? #2829" [ref=e619] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/issues/2829 + - text: When will warp support Chinese? + - generic [ref=e620]: "#2829" + - generic [ref=e621]: + - link "@dannyneira's profile" [ref=e622] [cursor=pointer]: + - /url: https://github.com/dannyneira + - img "dannyneira" [ref=e623] + - generic [ref=e630]: + - generic [ref=e632]: + - heading "dannyneira commented on Mar 24, 2023 on Mar 24, 2023" [level=3] [ref=e633]: + - text: dannyneira commented + - generic "Mar 24, 2023, 11:24 PM GMT+7" [ref=e634]: on Mar 24, 2023 on Mar 24, 2023 + - generic [ref=e635]: + - link "dannyneira" [ref=e637] [cursor=pointer]: + - /url: /dannyneira + - link "on Mar 24, 2023on Mar 24, 2023" [ref=e640] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1483079449 + - generic "Mar 24, 2023, 11:24 PM GMT+7" [ref=e641]: on Mar 24, 2023on Mar 24, 2023 + - generic [ref=e643]: + - generic "This user is a member of the warpdotdev organization." [ref=e645]: Member + - button "Actions for dannyneira's comment, 11:24 PM on March 24, 2023" [ref=e647] [cursor=pointer]: + - img [ref=e648] + - generic [ref=e650]: + - paragraph [ref=e653]: Two more users have expressed interest in Chinese UI support. + - toolbar "Reactions" [ref=e654]: + - button "React" [ref=e655] [cursor=pointer]: + - img [ref=e656] + - switch "👍 1 reaction" [ref=e658] [cursor=pointer]: + - generic [ref=e659]: + - generic: 👍 + - generic [ref=e660]: + - generic [ref=e661]: React with 👍 + - text: "1" + - generic [ref=e662]: + - link "@wenhongquan's profile" [ref=e663] [cursor=pointer]: + - /url: https://github.com/wenhongquan + - img "wenhongquan" [ref=e664] + - generic [ref=e671]: + - generic [ref=e673]: + - heading "wenhongquan commented on Mar 31, 2023 on Mar 31, 2023" [level=3] [ref=e674]: + - text: wenhongquan commented + - generic "Mar 31, 2023, 2:45 PM GMT+7" [ref=e675]: on Mar 31, 2023 on Mar 31, 2023 + - generic [ref=e676]: + - link "wenhongquan" [ref=e678] [cursor=pointer]: + - /url: /wenhongquan + - link "on Mar 31, 2023on Mar 31, 2023" [ref=e681] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1491459742 + - generic "Mar 31, 2023, 2:45 PM GMT+7" [ref=e682]: on Mar 31, 2023on Mar 31, 2023 + - button "Actions for wenhongquan's comment, 2:45 PM on March 31, 2023" [ref=e686] [cursor=pointer]: + - img [ref=e687] + - generic [ref=e689]: + - paragraph [ref=e692]: "+1" + - toolbar "Reactions" [ref=e693]: + - button "React" [ref=e694] [cursor=pointer]: + - img [ref=e695] + - generic [ref=e697]: + - link "@tree-white's profile" [ref=e698] [cursor=pointer]: + - /url: https://github.com/tree-white + - img "tree-white" [ref=e699] + - generic [ref=e706]: + - generic [ref=e708]: + - heading "tree-white commented on Apr 12, 2023 on Apr 12, 2023" [level=3] [ref=e709]: + - text: tree-white commented + - generic "Apr 12, 2023, 7:19 AM GMT+7" [ref=e710]: on Apr 12, 2023 on Apr 12, 2023 + - generic [ref=e711]: + - link "tree-white" [ref=e713] [cursor=pointer]: + - /url: /tree-white + - link "on Apr 12, 2023on Apr 12, 2023" [ref=e716] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1504319611 + - generic "Apr 12, 2023, 7:19 AM GMT+7" [ref=e717]: on Apr 12, 2023on Apr 12, 2023 + - button "Actions for tree-white's comment, 7:19 AM on April 12, 2023" [ref=e721] [cursor=pointer]: + - img [ref=e722] + - generic [ref=e724]: + - paragraph [ref=e727]: +1 !!! + - toolbar "Reactions" [ref=e728]: + - button "React" [ref=e729] [cursor=pointer]: + - img [ref=e730] + - generic [ref=e732]: + - link "@suhengli's profile" [ref=e733] [cursor=pointer]: + - /url: https://github.com/suhengli + - img "suhengli" [ref=e734] + - generic [ref=e741]: + - generic [ref=e743]: + - heading "suhengli commented on Apr 14, 2023 on Apr 14, 2023" [level=3] [ref=e744]: + - text: suhengli commented + - generic "Apr 14, 2023, 9:52 AM GMT+7" [ref=e745]: on Apr 14, 2023 on Apr 14, 2023 + - generic [ref=e746]: + - link "suhengli" [ref=e748] [cursor=pointer]: + - /url: /suhengli + - link "on Apr 14, 2023on Apr 14, 2023" [ref=e751] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194#issuecomment-1507856622 + - generic "Apr 14, 2023, 9:52 AM GMT+7" [ref=e752]: on Apr 14, 2023on Apr 14, 2023 + - button "Actions for suhengli's comment, 9:52 AM on April 14, 2023" [ref=e756] [cursor=pointer]: + - img [ref=e757] + - generic [ref=e759]: + - paragraph [ref=e762]: "+1" + - toolbar "Reactions" [ref=e763]: + - button "React" [ref=e764] [cursor=pointer]: + - img [ref=e765] + - generic [ref=e769]: + - generic [ref=e774]: + - generic [ref=e775]: + - heading "207 remaining items" [level=3] [ref=e777]: + - text: "207" + - generic [ref=e778]: remaining items + - button "Load more" [ref=e780] [cursor=pointer]: + - generic [ref=e783]: Load more + - button "Load more actions" [ref=e785] [cursor=pointer]: + - img [ref=e786] + - generic [ref=e789]: + - img [ref=e790] + - generic [ref=e793]: Loading + - generic [ref=e795]: + - link "@ErshovDmitry's profile" [ref=e796] [cursor=pointer]: + - /url: /ErshovDmitry + - img "ErshovDmitry" [ref=e797] + - heading "Add a comment" [level=2] [ref=e798] + - group "new Comment" [ref=e801]: + - generic [ref=e802] [cursor=pointer]: new Comment + - generic [ref=e803]: + - generic [ref=e804]: "Markdown input: edit mode selected." + - generic [ref=e805]: + - generic [ref=e806]: + - navigation "View mode" [ref=e810]: + - tablist [ref=e811]: + - tab "Write" [selected] [ref=e812] [cursor=pointer] + - tab "Preview" [ref=e813] [cursor=pointer] + - generic [ref=e814]: + - toolbar "Formatting tools" [ref=e816]: + - button "Heading" [ref=e817] [cursor=pointer]: + - img [ref=e818] + - button "Bold" [ref=e820] [cursor=pointer]: + - img [ref=e821] + - button "Italic" [ref=e823] [cursor=pointer]: + - img [ref=e824] + - button "Quote" [ref=e826] [cursor=pointer]: + - img [ref=e827] + - button "Code" [ref=e829] [cursor=pointer]: + - img [ref=e830] + - button "Link" [ref=e832] [cursor=pointer]: + - img [ref=e833] + - generic [ref=e835]: + - button "Unordered list" [ref=e837] [cursor=pointer]: + - img [ref=e838] + - button "Numbered list" [ref=e840] [cursor=pointer]: + - img [ref=e841] + - button "Task list" [ref=e843] [cursor=pointer]: + - img [ref=e844] + - generic [ref=e846]: + - button "Mention" [ref=e848] [cursor=pointer]: + - img [ref=e849] + - button "Reference" [ref=e851] [cursor=pointer]: + - img [ref=e852] + - button "Saved replies" [ref=e854] [cursor=pointer]: + - img [ref=e855] + - textbox "Add a comment" [ref=e860]: + - /placeholder: Use Markdown to format your comment + - status + - generic [ref=e861]: + - button "Paste, drop, or click to add files" [ref=e864] [cursor=pointer]: + - generic [ref=e865]: + - generic: + - img + - generic [ref=e867]: Paste, drop, or click to add files + - generic [ref=e868]: + - generic [ref=e869]: + - tooltip "You do not have permissions to close this issue" [ref=e871]: + - button "Close issue" [disabled] [ref=e872]: + - generic [ref=e873]: + - generic: + - img + - generic [ref=e874]: Close issue + - button "More options" [disabled] [ref=e876]: + - img [ref=e877] + - tooltip "Comment can not be empty" [ref=e879]: + - button "Comment" [disabled] [ref=e880]: + - generic [ref=e882]: Comment + - generic [ref=e884]: + - heading "Metadata" [level=2] [ref=e885] + - generic [ref=e887]: + - generic [ref=e889]: + - generic [ref=e890]: + - heading "Assignees" [level=3] + - generic [ref=e891]: No one assigned + - generic [ref=e892]: + - generic [ref=e894]: + - heading "Labels" [level=3] + - generic [ref=e896]: + - link "area:ui-framework Core Warp UI framework, rendering, layout, and windowing infrastructure." [ref=e897] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues?q=state%3Aopen%20label%3A%22area%3Aui-framework%22 + - generic [ref=e899]: area:ui-framework + - generic [ref=e900]: Core Warp UI framework, rendering, layout, and windowing infrastructure. + - link "enhancement New feature or request." [ref=e901] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues?q=state%3Aopen%20label%3A%22enhancement%22 + - generic [ref=e903]: enhancement + - generic [ref=e904]: New feature or request. + - link "ready-to-spec The issue is ready for a product and technical spec." [ref=e905] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues?q=state%3Aopen%20label%3A%22ready-to-spec%22 + - generic [ref=e907]: ready-to-spec + - generic [ref=e908]: The issue is ready for a product and technical spec. + - link "triaged Issue has received an initial automated triage pass." [ref=e909] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues?q=state%3Aopen%20label%3A%22triaged%22 + - generic [ref=e911]: triaged + - generic [ref=e912]: Issue has received an initial automated triage pass. + - generic [ref=e914]: + - generic [ref=e915]: + - heading "Type" [level=3] + - generic [ref=e916]: No type + - generic [ref=e918]: + - generic [ref=e919]: + - heading "Projects" [level=3] + - generic [ref=e920]: No projects + - generic [ref=e922]: + - generic [ref=e923]: + - heading "Milestone" [level=3] + - generic [ref=e924]: No milestone + - generic [ref=e926]: + - generic [ref=e927]: + - heading "Relationships" [level=3] + - generic [ref=e928]: None yet + - generic [ref=e930]: + - generic [ref=e931]: + - heading "Development" [level=3] + - generic [ref=e932]: No branches or pull requests + - generic [ref=e933]: + - generic [ref=e935]: + - heading "Notifications" [level=3] + - generic [ref=e937]: + - button "Subscribe" [ref=e938] [cursor=pointer]: + - generic [ref=e939]: + - generic: + - img + - generic [ref=e940]: Subscribe + - paragraph [ref=e941]: You're not receiving notifications from this thread. + - heading "Issue actions" [level=2] [ref=e942] + - list [ref=e943]: + - listitem [ref=e944]: + - button "Give feedback" [ref=e945] [cursor=pointer]: + - generic: + - img + - generic [ref=e947]: Give feedback + - contentinfo [ref=e948]: + - heading "Footer" [level=2] [ref=e949] + - generic [ref=e950]: + - generic [ref=e951]: + - link "GitHub Homepage" [ref=e952] [cursor=pointer]: + - /url: https://github.com + - img [ref=e953] + - generic [ref=e955]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e956]: + - heading "Footer navigation" [level=3] [ref=e957] + - list "Footer navigation" [ref=e958]: + - listitem [ref=e959]: + - link "Terms" [ref=e960] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e961]: + - link "Privacy" [ref=e962] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e963]: + - link "Security" [ref=e964] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e965]: + - link "Status" [ref=e966] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e967]: + - link "Community" [ref=e968] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e969]: + - link "Docs" [ref=e970] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e971]: + - link "Contact" [ref=e972] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e973]: + - button "Manage cookies" [ref=e975] [cursor=pointer] + - listitem [ref=e976]: + - button "Do not share my personal information" [ref=e978] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-20T07-14-37-592Z.yml b/.playwright-mcp/page-2026-05-20T07-14-37-592Z.yml new file mode 100644 index 0000000000..b71fd62038 --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T07-14-37-592Z.yml @@ -0,0 +1,1076 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - generic [ref=e7]: + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - link "You have no unread notifications (g then n)" [ref=e58] [cursor=pointer]: + - /url: /notifications + - img [ref=e59] + - button "Open user navigation menu" [ref=e62] [cursor=pointer]: + - img "User avatar" [ref=e63] + - heading "Repository navigation" [level=2] [ref=e64] + - navigation "Repository" [ref=e65]: + - list [ref=e66]: + - listitem [ref=e67]: + - link "Code" [ref=e68] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e70] + - generic [ref=e72]: Code + - listitem [ref=e73]: + - link "Issues (3.4k)" [ref=e74] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e76] + - generic [ref=e79]: Issues + - generic [ref=e80]: + - generic [ref=e81]: 3.4k + - generic [ref=e82]: (3.4k) + - listitem [ref=e83]: + - link "Pull requests (502)" [ref=e84] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e86] + - generic [ref=e88]: Pull requests + - generic [ref=e89]: + - generic [ref=e90]: "502" + - generic [ref=e91]: (502) + - listitem [ref=e92]: + - link "Agents" [ref=e93] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e95] + - generic [ref=e98]: Agents + - listitem [ref=e99]: + - link "Discussions" [ref=e100] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e102] + - generic [ref=e104]: Discussions + - listitem [ref=e105]: + - link "Actions" [ref=e106] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e108] + - generic [ref=e110]: Actions + - listitem [ref=e111]: + - link "Projects" [ref=e112] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e114] + - generic [ref=e116]: Projects + - listitem [ref=e117]: + - link "Security and quality" [ref=e118] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e120] + - generic [ref=e122]: Security and quality + - listitem [ref=e123]: + - link "Insights" [ref=e124] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e126] + - generic [ref=e128]: Insights + - region "Important update" [ref=e130]: + - img [ref=e132] + - generic [ref=e135]: + - heading "Important update" [level=2] [ref=e137] + - generic [ref=e139]: + - text: On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out. + - link "Review this update" [ref=e140] [cursor=pointer]: + - /url: https://gh.io/AAzfaht + - text: and manage your preferences in your + - link "GitHub account settings" [ref=e141] [cursor=pointer]: + - /url: https://github.com/settings/copilot/features + - text: . + - button "Dismiss banner" [ref=e142] [cursor=pointer]: + - img [ref=e143] + - main [ref=e148]: + - generic [ref=e155]: + - generic [ref=e158]: + - 'heading "I18n #11382 Edit title" [level=1] [ref=e160]': + - text: I18n + - generic [ref=e161]: + - generic [ref=e162]: "#11382" + - button "Edit title" [ref=e163] [cursor=pointer]: + - img [ref=e164] + - generic [ref=e167]: + - generic [ref=e168]: + - button "View status View status" [disabled] [ref=e169]: + - generic [ref=e170]: + - generic: + - generic: + - img + - generic: Loading + - generic [ref=e172]: Loading merge status + - button "Code" [ref=e173] [cursor=pointer]: + - generic [ref=e174]: + - generic [ref=e175]: Code + - generic: + - img + - generic [ref=e177]: + - generic [ref=e179]: + - img "Pull request" [ref=e180] + - text: Open + - generic [ref=e183]: + - link "ErshovDmitry" [ref=e184] [cursor=pointer]: + - /url: /ErshovDmitry + - text: wants to merge 8 commits into + - generic [ref=e185]: + - link "warpdotdev:master" [ref=e186] [cursor=pointer]: + - /url: /warpdotdev/warp/tree/master + - generic [ref=e187]: from + - generic [ref=e188]: + - link "ErshovDmitry:i18n" [ref=e189] [cursor=pointer]: + - /url: /ErshovDmitry/warp-i18n/tree/i18n + - button "Copy head branch name to clipboard" [ref=e190] [cursor=pointer]: + - img [ref=e191] + - navigation "Pull request navigation tabs" [ref=e197]: + - tablist [ref=e198]: + - tab "Conversation" [selected] [ref=e199] [cursor=pointer]: + - img [ref=e200] + - text: Conversation + - tab "Commits (8)" [ref=e202] [cursor=pointer]: + - img [ref=e203] + - text: Commits + - generic [ref=e205]: "8" + - generic [ref=e206]: (8) + - tab "Checks" [ref=e207] [cursor=pointer]: + - img [ref=e208] + - text: Checks + - tab "Files changed" [ref=e210] [cursor=pointer]: + - img [ref=e211] + - text: Files changed + - generic [ref=e217]: + - generic [ref=e219]: + - heading "Conversation" [level=2] [ref=e220] + - generic [ref=e221]: + - generic [ref=e222]: + - generic [ref=e224]: + - link "@ErshovDmitry" [ref=e225] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e226] + - generic [ref=e228]: + - generic [ref=e229]: + - group [ref=e232]: + - button "Show options" [ref=e233] [cursor=pointer]: + - img "Show options" [ref=e236] + - heading "ErshovDmitry commented May 20, 20263 hours ago" [level=3] [ref=e238]: + - generic [ref=e239]: + - strong [ref=e240]: + - link "ErshovDmitry" [ref=e241] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 hours ago" [ref=e242] [cursor=pointer]: + - /url: "#issue-4483039846" + - generic [ref=e246]: + - paragraph [ref=e247]: Hello! My name is Dmitry. + - paragraph [ref=e248]: I've been using Warp and love it. I wanted to add i18n support so the terminal could speak Russian (and other languages in the future). I'm not a developer myself — this was built with DeepSeek AI, but I reviewed everything personally. + - paragraph [ref=e249]: I'd much rather this live in the official repo than a fork. If the team is interested in i18n, I'm happy to help however I can — and I'll gladly delete my fork once upstreamed. + - paragraph [ref=e250]: + - text: Cheers, + - text: Dmitry + - separator [ref=e251] + - heading "What" [level=2] [ref=e252] + - paragraph [ref=e253]: Adds i18n (internationalization) support to Warp. + - heading "Changes" [level=2] [ref=e254] + - 'heading "New crate: warp_i18n" [level=3] [ref=e255]': + - text: "New crate:" + - code [ref=e256]: warp_i18n + - list [ref=e257]: + - listitem [ref=e258]: + - code [ref=e259]: t!() + - text: macro for translating UI strings + - listitem [ref=e260]: + - code [ref=e261]: current_locale() + - text: / + - code [ref=e262]: set_current_locale() + - text: for runtime language switching + - listitem [ref=e263]: + - code [ref=e264]: init_from_json() + - text: for WASM compatibility + - listitem [ref=e265]: Build script for compile-time key validation + - listitem [ref=e266]: 21 tests + - heading "Feature flag" [level=3] [ref=e267] + - list [ref=e268]: + - listitem [ref=e269]: + - code [ref=e270]: FeatureFlag::I18n + - text: in + - code [ref=e271]: DOGFOOD_FLAGS + - heading "Settings" [level=3] [ref=e272] + - list [ref=e273]: + - listitem [ref=e274]: + - code [ref=e275]: Locale + - text: enum (En, Ru) + - listitem [ref=e276]: Language dropdown in Settings → Appearance + - listitem [ref=e277]: Auto-detect system locale on first run + - heading "Migrated UI" [level=3] [ref=e278] + - list [ref=e279]: + - listitem [ref=e280]: 10 top-level menu labels + - listitem [ref=e281]: 14 settings section names + - listitem [ref=e282]: 11 settings category labels + - heading "Locales" [level=3] [ref=e283] + - list [ref=e284]: + - listitem [ref=e285]: + - code [ref=e286]: en.json + - text: — 51 keys (mandatory fallback) + - listitem [ref=e287]: + - code [ref=e288]: ru.json + - text: — 51 keys (Russian translation) + - paragraph [ref=e289]: + - emphasis [ref=e290]: + - text: Built with DeepSeek AI — human-reviewed, all tests pass ( + - code [ref=e291]: cargo check --workspace + - text: "," + - code [ref=e292]: cargo test -p warp_i18n + - text: ). + - group [ref=e296]: + - generic "Add or remove reactions" [ref=e297] [cursor=pointer]: + - img [ref=e298] + - generic [ref=e300]: + - generic [ref=e302]: + - generic [ref=e303]: + - img [ref=e305] + - generic [ref=e307]: + - link "ErshovDmitry" [ref=e308] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 5 commits + - link "May 20, 2026 09:155 hours ago" [ref=e309] [cursor=pointer]: + - /url: "#commits-pushed-f06dc63" + - generic [ref=e310]: + - generic [ref=e311]: + - img [ref=e313] + - generic [ref=e318]: + - link "@ErshovDmitry" [ref=e321] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e322] + - code [ref=e324]: + - 'link "feat: add warp_i18n crate with t!() macro and en/ru locales" [ref=e325] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - code [ref=e333]: + - link "f06dc63" [ref=e334] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - generic [ref=e335]: + - img [ref=e337] + - generic [ref=e342]: + - link "@ErshovDmitry" [ref=e345] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e346] + - generic [ref=e347]: + - code [ref=e348]: + - 'link "feat: i18n Phase 2 — FeatureFlag, Locale setting, app_menus migration…" [ref=e349] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - button "Commit message body" [ref=e351] [cursor=pointer]: … + - code [ref=e359]: + - link "643f6c3" [ref=e360] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - generic [ref=e361]: + - img [ref=e363] + - generic [ref=e368]: + - link "@ErshovDmitry" [ref=e371] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e372] + - generic [ref=e373]: + - code [ref=e374]: + - 'link "fix: wire warp_i18n::init() at startup, sync LocaleSettings with i18n…" [ref=e375] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - button "Commit message body" [ref=e377] [cursor=pointer]: … + - code [ref=e385]: + - link "a9e9e26" [ref=e386] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - generic [ref=e387]: + - img [ref=e389] + - generic [ref=e394]: + - link "@ErshovDmitry" [ref=e397] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e398] + - code [ref=e400]: + - 'link "docs: add English translation of the fork note in README" [ref=e401] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - code [ref=e409]: + - link "3911714" [ref=e410] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - generic [ref=e411]: + - img [ref=e413] + - generic [ref=e418]: + - link "@ErshovDmitry" [ref=e421] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e422] + - generic [ref=e423]: + - code [ref=e424]: + - 'link "feat: i18n Phase 3 — build script key validation, WASM support, setti…" [ref=e425] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - button "Commit message body" [ref=e427] [cursor=pointer]: … + - code [ref=e435]: + - link "169dfa6" [ref=e436] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e438]: + - link "@cla-bot" [ref=e440] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e441] + - generic [ref=e443]: + - generic [ref=e444]: + - group [ref=e447]: + - button "Show options" [ref=e448] [cursor=pointer]: + - img "Show options" [ref=e451] + - heading "cla-bot Bot commented May 20, 20263 hours ago" [level=3] [ref=e453]: + - generic [ref=e454]: + - strong [ref=e455]: + - link "cla-bot" [ref=e456] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e457]: Bot + - text: commented + - link "May 20, 20263 hours ago" [ref=e458] [cursor=pointer]: + - /url: "#issuecomment-4494411354" + - generic [ref=e459]: + - table [ref=e461]: + - rowgroup [ref=e462]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e463]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e464]': + - paragraph [ref=e465]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e466]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e467]: + - listitem [ref=e468]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e469]: git config --list | grep email + - listitem [ref=e470]: + - text: If not, set it up using + - code [ref=e471]: git config --global user.email email@example.com + - listitem [ref=e472]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e473] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e478]: + - generic "Add or remove reactions" [ref=e479] [cursor=pointer]: + - img [ref=e480] + - generic [ref=e483]: + - link "@oz-for-oss" [ref=e485] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e486] + - generic [ref=e488]: + - generic [ref=e489]: + - generic [ref=e490]: + - group [ref=e492]: + - button "Show options" [ref=e493] [cursor=pointer]: + - img "Show options" [ref=e496] + - generic "This user has previously committed to the warp repository." [ref=e499]: + - generic [ref=e500]: Contributor + - heading "oz-for-oss Bot commented May 20, 20263 hours ago" [level=3] [ref=e501]: + - generic [ref=e502]: + - strong [ref=e503]: + - link "oz-for-oss" [ref=e504] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e505]: Bot + - text: commented + - link "May 20, 20263 hours ago" [ref=e506] [cursor=pointer]: + - /url: "#issuecomment-4494411637" + - generic [ref=e507]: + - table [ref=e509]: + - rowgroup [ref=e510]: + - 'row "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e511]': + - 'cell "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e512]': + - paragraph [ref=e513]: + - link "@ErshovDmitry" [ref=e514] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e515]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e516]: ready-to-implement + - text: . + - paragraph [ref=e517]: "Issue-state enforcement details:" + - list [ref=e518]: + - listitem [ref=e519]: + - paragraph [ref=e520]: "Associated same-repo issues checked: none" + - listitem [ref=e521]: + - paragraph [ref=e522]: + - text: "Required readiness label:" + - code [ref=e523]: ready-to-implement + - paragraph [ref=e524]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e525]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e526]: ready-to-implement + - text: . + - paragraph [ref=e527]: + - emphasis [ref=e528]: + - text: Powered by + - link "Oz" [ref=e529] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e534]: + - generic "Add or remove reactions" [ref=e535] [cursor=pointer]: + - img [ref=e536] + - generic [ref=e539]: + - img [ref=e541] + - generic [ref=e543]: + - link "@github-actions" [ref=e544] [cursor=pointer]: + - /url: /apps/github-actions + - img "@github-actions" [ref=e545] + - link "github-actions" [ref=e546] [cursor=pointer]: + - /url: /apps/github-actions + - generic [ref=e547]: Bot + - text: added the + - link "external-contributor" [ref=e548] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - text: label + - link "May 20, 20263 hours ago" [ref=e549] [cursor=pointer]: + - /url: "#event-25738319166" + - generic [ref=e552]: + - generic [ref=e553]: + - link "oz-for-oss[bot]" [ref=e554] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "oz-for-oss[bot]" [ref=e555] + - img [ref=e557] + - generic [ref=e559]: + - generic [ref=e560]: + - strong [ref=e561]: + - link "oz-for-oss" [ref=e562] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e563]: Bot + - text: requested changes + - link "May 20, 20263 hours ago" [ref=e565] [cursor=pointer]: + - /url: "#pullrequestreview-4325084051" + - link "View reviewed changes" [ref=e567] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e569]: View reviewed changes + - generic [ref=e572]: + - generic [ref=e573]: + - generic [ref=e574]: + - group [ref=e576]: + - button "Show options" [ref=e577] [cursor=pointer]: + - img "Show options" [ref=e580] + - generic "This user has previously committed to the warp repository." [ref=e583]: + - generic [ref=e584]: Contributor + - heading "oz-for-oss Bot left a comment" [level=3] [ref=e585]: + - generic [ref=e586]: + - strong [ref=e587]: + - link "oz-for-oss" [ref=e588] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e589]: Bot + - text: left a comment + - generic [ref=e592]: + - paragraph [ref=e593]: + - link "@ErshovDmitry" [ref=e594] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e595]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e596]: ready-to-implement + - text: . + - paragraph [ref=e597]: "Issue-state enforcement details:" + - list [ref=e598]: + - listitem [ref=e599]: + - paragraph [ref=e600]: "Associated same-repo issues checked: none" + - listitem [ref=e601]: + - paragraph [ref=e602]: + - text: "Required readiness label:" + - code [ref=e603]: ready-to-implement + - paragraph [ref=e604]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e605]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e606]: ready-to-implement + - text: . + - paragraph [ref=e607]: + - emphasis [ref=e608]: + - text: Powered by + - link "Oz" [ref=e609] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e613]: + - generic "Add or remove reactions" [ref=e614] [cursor=pointer]: + - img [ref=e615] + - generic [ref=e618]: + - link "@ErshovDmitry" [ref=e620] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e621] + - generic [ref=e623]: + - generic [ref=e624]: + - generic [ref=e625]: + - group [ref=e627]: + - button "Show options" [ref=e628] [cursor=pointer]: + - img "Show options" [ref=e631] + - generic "You are the author of this pull request." [ref=e634]: + - generic [ref=e635]: Author + - heading "ErshovDmitry commented May 20, 20263 hours ago" [level=3] [ref=e636]: + - generic [ref=e637]: + - strong [ref=e638]: + - link "ErshovDmitry" [ref=e639] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 hours ago" [ref=e640] [cursor=pointer]: + - /url: "#issuecomment-4494430862" + - generic [ref=e641]: + - table [ref=e643]: + - rowgroup [ref=e644]: + - 'row "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e645]': + - 'cell "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e646]': + - paragraph [ref=e647]: + - text: Oh wow — I just noticed there are already several i18n PRs open ( + - link "#10630" [ref=e648] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "," + - link "#10990" [ref=e649] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: "," + - link "#9458" [ref=e650] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9458 + - text: "," + - link "#9922" [ref=e651] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9922 + - text: ). I should have checked first before opening mine. My apologies for the noise! + - blockquote [ref=e652]: + - paragraph [ref=e653]: + - text: That said, I see none of them include Russian yet. If the team settles on one i18n approach (looks like + - link "#10630" [ref=e654] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e655] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: is the leading one?), I'd be happy to contribute Russian translations to that effort instead. + - paragraph [ref=e656]: I'll close this PR if it's just adding clutter — just let me know. + - paragraph [ref=e657]: + - text: Cheers, + - text: Dmitry + - group [ref=e662]: + - generic "Add or remove reactions" [ref=e663] [cursor=pointer]: + - img [ref=e664] + - generic [ref=e669]: + - img [ref=e671] + - generic [ref=e676]: + - link "@ErshovDmitry" [ref=e679] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e680] + - generic [ref=e681]: + - code [ref=e682]: + - 'link "refactor: migrate to ZacharyZcR-compatible YAML i18n with Russian locale" [ref=e683] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - button "Commit message body" [ref=e685] [cursor=pointer]: … + - code [ref=e693]: + - link "04801f0" [ref=e694] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - generic [ref=e696]: + - link "@cla-bot" [ref=e698] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e699] + - generic [ref=e701]: + - generic [ref=e702]: + - group [ref=e705]: + - button "Show options" [ref=e706] [cursor=pointer]: + - img "Show options" [ref=e709] + - heading "cla-bot Bot commented May 20, 20261 hour ago" [level=3] [ref=e711]: + - generic [ref=e712]: + - strong [ref=e713]: + - link "cla-bot" [ref=e714] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e715]: Bot + - text: commented + - link "May 20, 20261 hour ago" [ref=e716] [cursor=pointer]: + - /url: "#issuecomment-4494962896" + - generic [ref=e717]: + - table [ref=e719]: + - rowgroup [ref=e720]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e721]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e722]': + - paragraph [ref=e723]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e724]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e725]: + - listitem [ref=e726]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e727]: git config --list | grep email + - listitem [ref=e728]: + - text: If not, set it up using + - code [ref=e729]: git config --global user.email email@example.com + - listitem [ref=e730]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e731] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e736]: + - generic "Add or remove reactions" [ref=e737] [cursor=pointer]: + - img [ref=e738] + - generic [ref=e743]: + - img [ref=e745] + - generic [ref=e750]: + - link "@ErshovDmitry" [ref=e753] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e754] + - code [ref=e756]: + - 'link "fix: init_locale before menu bar, cleanup dead code, harden set_locale" [ref=e757] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - code [ref=e765]: + - link "1caedf3" [ref=e766] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - generic [ref=e768]: + - link "@cla-bot" [ref=e770] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e771] + - generic [ref=e773]: + - generic [ref=e774]: + - group [ref=e777]: + - button "Show options" [ref=e778] [cursor=pointer]: + - img "Show options" [ref=e781] + - heading "cla-bot Bot commented May 20, 202650 minutes ago" [level=3] [ref=e783]: + - generic [ref=e784]: + - strong [ref=e785]: + - link "cla-bot" [ref=e786] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e787]: Bot + - text: commented + - link "May 20, 202650 minutes ago" [ref=e788] [cursor=pointer]: + - /url: "#issuecomment-4495220665" + - generic [ref=e789]: + - table [ref=e791]: + - rowgroup [ref=e792]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e793]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e794]': + - paragraph [ref=e795]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e796]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e797]: + - listitem [ref=e798]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e799]: git config --list | grep email + - listitem [ref=e800]: + - text: If not, set it up using + - code [ref=e801]: git config --global user.email email@example.com + - listitem [ref=e802]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e803] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e808]: + - generic "Add or remove reactions" [ref=e809] [cursor=pointer]: + - img [ref=e810] + - generic [ref=e815]: + - img [ref=e817] + - generic [ref=e822]: + - link "@ErshovDmitry" [ref=e825] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e826] + - generic [ref=e827]: + - code [ref=e828]: + - 'link "fix(i18n): Round 2 review — internationalize all hardcoded menu strings" [ref=e829] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - button "Commit message body" [ref=e831] [cursor=pointer]: … + - code [ref=e839]: + - link "1b026c7" [ref=e840] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - generic [ref=e842]: + - link "@cla-bot" [ref=e844] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e845] + - generic [ref=e847]: + - generic [ref=e848]: + - group [ref=e851]: + - button "Show options" [ref=e852] [cursor=pointer]: + - img "Show options" [ref=e855] + - heading "cla-bot Bot commented May 20, 202621 minutes ago" [level=3] [ref=e857]: + - generic [ref=e858]: + - strong [ref=e859]: + - link "cla-bot" [ref=e860] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e861]: Bot + - text: commented + - link "May 20, 202621 minutes ago" [ref=e862] [cursor=pointer]: + - /url: "#issuecomment-4495434298" + - generic [ref=e863]: + - table [ref=e865]: + - rowgroup [ref=e866]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e867]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e868]': + - paragraph [ref=e869]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e870]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e871]: + - listitem [ref=e872]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e873]: git config --list | grep email + - listitem [ref=e874]: + - text: If not, set it up using + - code [ref=e875]: git config --global user.email email@example.com + - listitem [ref=e876]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e877] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e882]: + - generic "Add or remove reactions" [ref=e883] [cursor=pointer]: + - img [ref=e884] + - generic [ref=e887]: + - link "@ErshovDmitry" [ref=e889] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e890] + - generic [ref=e892]: + - generic [ref=e893]: + - generic [ref=e894]: + - group [ref=e896]: + - button "Show options" [ref=e897] [cursor=pointer]: + - img "Show options" [ref=e900] + - generic "You are the author of this pull request." [ref=e903]: + - generic [ref=e904]: Author + - heading "ErshovDmitry commented May 20, 202610 minutes ago" [level=3] [ref=e905]: + - generic [ref=e906]: + - strong [ref=e907]: + - link "ErshovDmitry" [ref=e908] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202610 minutes ago" [ref=e909] [cursor=pointer]: + - /url: "#issuecomment-4495523677" + - generic [ref=e910]: + - table [ref=e912]: + - rowgroup [ref=e913]: + - 'row "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e914]': + - 'cell "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e915]': + - paragraph [ref=e916]: + - text: Update:** I've rewritten this PR to match + - link "@ZacharyZcR" [ref=e917] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: "'s YAML approach from" + - link "#10630" [ref=e918] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e919] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: ":" + - blockquote [ref=e920]: + - list [ref=e921]: + - listitem [ref=e922]: + - code [ref=e923]: crates/i18n/ + - text: with + - code [ref=e924]: t!() + - text: / + - code [ref=e925]: t_required!() + - text: macros and + - code [ref=e926]: TranslationLookup + - listitem [ref=e927]: + - code [ref=e928]: "resources/bundled/locales/{en,ru}.yml" + - text: (247 keys each, YAML) + - listitem [ref=e929]: + - code [ref=e930]: WARP_LANG + - text: env var → system locale → + - code [ref=e931]: en + - text: fallback + - listitem [ref=e932]: + - text: No feature flag, no settings UI — same design as + - generic [ref=e933]: + - img [ref=e934] + - 'link "Add i18n framework with Simplified Chinese (zh-CN) support #10630" [ref=e936] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/pull/10630 + - paragraph [ref=e937]: + - text: The Russian + - code [ref=e938]: ru.yml + - text: should be a drop-in addition to the framework once it's approved. Happy to rebase onto whichever PR lands first. + - paragraph [ref=e939]: Dmitry + - group [ref=e944]: + - generic "Add or remove reactions" [ref=e945] [cursor=pointer]: + - img [ref=e946] + - generic [ref=e949]: + - link "@ErshovDmitry" [ref=e951] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e952] + - generic [ref=e954]: + - generic [ref=e955]: + - generic [ref=e956]: + - group [ref=e958]: + - button "Show options" [ref=e959] [cursor=pointer]: + - img "Show options" [ref=e962] + - generic "You are the author of this pull request." [ref=e965]: + - generic [ref=e966]: Author + - heading "ErshovDmitry commented May 20, 20263 minutes ago" [level=3] [ref=e967]: + - generic [ref=e968]: + - strong [ref=e969]: + - link "ErshovDmitry" [ref=e970] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 minutes ago" [ref=e971] [cursor=pointer]: + - /url: "#issuecomment-4495572049" + - generic [ref=e972]: + - table [ref=e974]: + - rowgroup [ref=e975]: + - row "@cla-bot check" [ref=e976]: + - cell "@cla-bot check" [ref=e977]: + - paragraph [ref=e978]: + - link "@cla-bot" [ref=e979] [cursor=pointer]: + - /url: https://github.com/cla-bot + - text: check + - group [ref=e984]: + - generic "Add or remove reactions" [ref=e985] [cursor=pointer]: + - img [ref=e986] + - generic [ref=e989]: + - img [ref=e991] + - generic [ref=e993]: + - link "@cla-bot" [ref=e994] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e995] + - link "cla-bot" [ref=e996] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e997]: Bot + - text: added the + - link "cla-signed" [ref=e998] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - text: label + - link "May 20, 20263 minutes ago" [ref=e999] [cursor=pointer]: + - /url: "#event-25743817179" + - generic [ref=e1001]: + - link "@cla-bot" [ref=e1003] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e1004] + - generic [ref=e1006]: + - generic [ref=e1007]: + - group [ref=e1010]: + - button "Show options" [ref=e1011] [cursor=pointer]: + - img "Show options" [ref=e1014] + - heading "cla-bot Bot commented May 20, 20263 minutes ago" [level=3] [ref=e1016]: + - generic [ref=e1017]: + - strong [ref=e1018]: + - link "cla-bot" [ref=e1019] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e1020]: Bot + - text: commented + - link "May 20, 20263 minutes ago" [ref=e1021] [cursor=pointer]: + - /url: "#issuecomment-4495573189" + - generic [ref=e1022]: + - table [ref=e1024]: + - rowgroup [ref=e1025]: + - row "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1026]: + - cell "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1027]: + - paragraph [ref=e1028]: The cla-bot has been summoned, and re-checked this pull request! + - group [ref=e1033]: + - generic "Add or remove reactions" [ref=e1034] [cursor=pointer]: + - img [ref=e1035] + - generic [ref=e1040]: + - img [ref=e1041] + - generic [ref=e1044]: Loading + - generic [ref=e1047]: + - link "@ErshovDmitry" [ref=e1049] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1050] + - form "Add a comment" [ref=e1052]: + - group [ref=e1053]: + - heading "Add a comment" [level=4] [ref=e1056] + - generic [ref=e1057]: Comment + - generic [ref=e1058]: + - generic [ref=e1059]: + - tablist "Add a comment" [ref=e1060]: + - tab "Write" [selected] [ref=e1061] [cursor=pointer] + - tab "Preview" [ref=e1062] [cursor=pointer] + - toolbar [ref=e1063]: + - generic [ref=e1064]: + - button "Heading" [ref=e1066] [cursor=pointer]: + - img + - button "Bold" [ref=e1068] [cursor=pointer]: + - img + - button "Italic" [ref=e1070] [cursor=pointer]: + - img + - button "Quote" [ref=e1072] [cursor=pointer]: + - img + - button "Code" [ref=e1074] [cursor=pointer]: + - img + - button "Link" [ref=e1076] [cursor=pointer]: + - img + - separator [ref=e1077] + - button "Numbered list" [ref=e1079] [cursor=pointer]: + - img + - button "Unordered list" [ref=e1081] [cursor=pointer]: + - img + - button "Task list" [ref=e1083] [cursor=pointer]: + - img + - separator [ref=e1084] + - button "Attach files" [ref=e1086] [cursor=pointer]: + - img + - button "Mention" [ref=e1088] [cursor=pointer]: + - img + - button "Reference" [ref=e1090] [cursor=pointer]: + - img + - button "Saved replies" [ref=e1092] [cursor=pointer]: + - img + - button "Slash commands" [ref=e1094] [cursor=pointer]: + - img + - tabpanel "Write" [ref=e1095]: + - generic [ref=e1099]: + - textbox "Comment" [ref=e1100]: + - /placeholder: " " + - paragraph: Add your comment here... + - generic [ref=e1101]: + - link "Markdown is supported" [ref=e1103] [cursor=pointer]: + - /url: https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax + - generic [ref=e1104]: + - generic: + - img + - generic [ref=e1105]: Markdown is supported + - button "Paste, drop, or click to add files" [ref=e1106] [cursor=pointer]: + - generic [ref=e1107]: + - generic: + - img + - generic [ref=e1108]: Paste, drop, or click to add files + - generic [ref=e1111]: + - button "Close pull request" [ref=e1113] [cursor=pointer]: + - img [ref=e1114] + - text: Close pull request + - button "Comment" [disabled] [ref=e1117] + - generic [ref=e1118]: + - img [ref=e1119] + - generic [ref=e1121]: + - text: Remember, contributions to this repository should follow its + - link "contributing guidelines" [ref=e1122] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/CONTRIBUTING.md + - text: "," + - link "security policy" [ref=e1123] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/SECURITY.md + - text: ", and" + - link "code of conduct" [ref=e1124] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/CODE_OF_CONDUCT.md + - text: . + - generic [ref=e1125]: + - img [ref=e1126] + - strong [ref=e1128]: ProTip! + - text: Add comments to specific lines under + - link "Files changed" [ref=e1129] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files + - text: . + - generic [ref=e1133]: + - form "Select reviewers" [ref=e1135]: + - heading "Reviewers" [level=3] [ref=e1136] + - generic [ref=e1137]: + - paragraph [ref=e1138]: + - generic [ref=e1139]: + - link "@oz-for-oss" [ref=e1140] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e1141] + - link "oz-for-oss[bot]" [ref=e1142] [cursor=pointer]: + - /url: /apps/oz-for-oss + - link "oz-for-oss[bot] requested changes" [ref=e1143] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes/BASE..169dfa663dd4c533eb32daf850f25d6b9c123521 + - img [ref=e1145] + - paragraph [ref=e1147]: Requested changes must be addressed to merge this pull request. + - generic [ref=e1148]: + - generic [ref=e1149]: Still in progress? + - group [ref=e1150]: + - button "Convert to draft" [ref=e1151] [cursor=pointer] + - form "Select assignees" [ref=e1153]: + - heading "Assignees" [level=3] [ref=e1154] + - text: No one assigned + - generic [ref=e1155]: + - heading "Labels" [level=3] [ref=e1156] + - generic [ref=e1157]: + - link "cla-signed" [ref=e1158] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - generic [ref=e1159]: cla-signed + - link "external-contributor" [ref=e1160] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - generic [ref=e1161]: external-contributor + - form "Select projects" [ref=e1163]: + - heading "Projects" [level=3] [ref=e1164] + - text: None yet + - form "Select milestones" [ref=e1166]: + - heading "Milestone" [level=3] [ref=e1167] + - text: No milestone + - form "Link issues" [ref=e1173]: + - heading "Development" [level=3] [ref=e1174] + - paragraph [ref=e1175]: Successfully merging this pull request may close these issues. + - generic [ref=e1177]: + - button "Notifications Customize" [ref=e1180] [cursor=pointer]: + - generic [ref=e1181]: + - generic [ref=e1182]: Notifications + - generic [ref=e1183]: Customize + - button "Unsubscribe" [ref=e1185] [cursor=pointer]: + - generic [ref=e1186]: + - generic: + - img + - generic [ref=e1187]: Unsubscribe + - paragraph [ref=e1188]: You’re receiving notifications because you were mentioned. + - generic [ref=e1190]: + - heading "1 participant" [level=3] [ref=e1191] + - link "@ErshovDmitry" [ref=e1193] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1194] + - generic [ref=e1197]: + - generic [ref=e1198]: + - checkbox "Allow edits by maintainers" [checked] [ref=e1199] + - text: Allow edits by maintainers + - button "Learn more about allowing maintainer edits" [ref=e1200] [cursor=pointer]: + - img [ref=e1203] + - contentinfo [ref=e1206]: + - heading "Footer" [level=2] [ref=e1207] + - generic [ref=e1208]: + - generic [ref=e1209]: + - link "GitHub Homepage" [ref=e1210] [cursor=pointer]: + - /url: https://github.com + - img [ref=e1211] + - generic [ref=e1213]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e1214]: + - heading "Footer navigation" [level=3] [ref=e1215] + - list "Footer navigation" [ref=e1216]: + - listitem [ref=e1217]: + - link "Terms" [ref=e1218] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e1219]: + - link "Privacy" [ref=e1220] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e1221]: + - link "Security" [ref=e1222] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e1223]: + - link "Status" [ref=e1224] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e1225]: + - link "Community" [ref=e1226] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e1227]: + - link "Docs" [ref=e1228] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e1229]: + - link "Contact" [ref=e1230] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e1231]: + - button "Manage cookies" [ref=e1233] [cursor=pointer] + - listitem [ref=e1234]: + - button "Do not share my personal information" [ref=e1236] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-20T07-15-19-645Z.yml b/.playwright-mcp/page-2026-05-20T07-15-19-645Z.yml new file mode 100644 index 0000000000..215ca39365 --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T07-15-19-645Z.yml @@ -0,0 +1,1201 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - generic [ref=e7]: + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - button "Open user navigation menu" [ref=e62] [cursor=pointer]: + - img "User avatar" [ref=e63] + - heading "Repository navigation" [level=2] [ref=e64] + - navigation "Repository" [ref=e65]: + - list [ref=e66]: + - listitem [ref=e67]: + - link "Code" [ref=e68] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e70] + - generic [ref=e72]: Code + - listitem [ref=e73]: + - link "Issues (3.4k)" [ref=e74] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e76] + - generic [ref=e79]: Issues + - generic [ref=e80]: + - generic [ref=e81]: 3.4k + - generic [ref=e82]: (3.4k) + - listitem [ref=e83]: + - link "Pull requests (502)" [ref=e84] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e86] + - generic [ref=e88]: Pull requests + - generic [ref=e89]: + - generic [ref=e90]: "502" + - generic [ref=e91]: (502) + - listitem [ref=e92]: + - link "Agents" [ref=e93] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e95] + - generic [ref=e98]: Agents + - listitem [ref=e99]: + - link "Discussions" [ref=e100] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e102] + - generic [ref=e104]: Discussions + - listitem [ref=e105]: + - link "Actions" [ref=e106] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e108] + - generic [ref=e110]: Actions + - listitem [ref=e111]: + - link "Projects" [ref=e112] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e114] + - generic [ref=e116]: Projects + - listitem [ref=e117]: + - link "Security and quality" [ref=e118] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e120] + - generic [ref=e122]: Security and quality + - listitem [ref=e123]: + - link "Insights" [ref=e124] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e126] + - generic [ref=e128]: Insights + - region "Important update" [ref=e130]: + - img [ref=e132] + - generic [ref=e135]: + - heading "Important update" [level=2] [ref=e137] + - generic [ref=e139]: + - text: On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out. + - link "Review this update" [ref=e140] [cursor=pointer]: + - /url: https://gh.io/AAzfaht + - text: and manage your preferences in your + - link "GitHub account settings" [ref=e141] [cursor=pointer]: + - /url: https://github.com/settings/copilot/features + - text: . + - button "Dismiss banner" [ref=e142] [cursor=pointer]: + - img [ref=e143] + - main [ref=e148]: + - generic [ref=e155]: + - generic [ref=e157]: + - generic [ref=e158]: + - 'heading "I18n #11382 Edit title" [level=1] [ref=e160]': + - text: I18n + - generic [ref=e161]: + - generic [ref=e162]: "#11382" + - button "Edit title" [ref=e163] [cursor=pointer]: + - img [ref=e164] + - generic [ref=e167]: + - button "View status" [ref=e1239] [cursor=pointer]: + - generic [ref=e1241]: View status + - button "Code" [ref=e173] [cursor=pointer]: + - generic [ref=e174]: + - generic [ref=e175]: Code + - generic: + - img + - generic [ref=e177]: + - generic [ref=e179]: + - img "Pull request" [ref=e180] + - text: Open + - generic [ref=e183]: + - link "ErshovDmitry" [ref=e184] [cursor=pointer]: + - /url: /ErshovDmitry + - text: wants to merge 8 commits into + - generic [ref=e185]: + - link "warpdotdev:master" [ref=e186] [cursor=pointer]: + - /url: /warpdotdev/warp/tree/master + - generic [ref=e187]: from + - generic [ref=e188]: + - link "ErshovDmitry:i18n" [ref=e189] [cursor=pointer]: + - /url: /ErshovDmitry/warp-i18n/tree/i18n + - button "Copy head branch name to clipboard" [ref=e190] [cursor=pointer]: + - img [ref=e191] + - generic [ref=e194]: + - generic [ref=e1243]: + - generic [ref=e1244]: +1 191 + - generic [ref=e1245]: "-106" + - generic [ref=e1246]: "Lines changed: 1191 additions & 106 deletions" + - navigation "Pull request navigation tabs" [ref=e197]: + - tablist [ref=e198]: + - tab "Conversation (9)" [selected] [ref=e1253] [cursor=pointer]: + - img [ref=e200] + - text: Conversation + - generic [ref=e1254]: "9" + - generic [ref=e1255]: (9) + - tab "Commits (8)" [ref=e202] [cursor=pointer]: + - img [ref=e203] + - text: Commits + - generic [ref=e205]: "8" + - generic [ref=e206]: (8) + - tab "Checks (0)" [ref=e1256] [cursor=pointer]: + - img [ref=e208] + - text: Checks + - generic [ref=e1257]: "0" + - generic [ref=e1258]: (0) + - tab "Files changed (16)" [ref=e1259] [cursor=pointer]: + - img [ref=e211] + - text: Files changed + - generic [ref=e1260]: "16" + - generic [ref=e1261]: (16) + - generic [ref=e1409]: + - generic [ref=e1411]: + - img "Pull request" [ref=e1412] + - text: Open + - heading "I18n#11382 ErshovDmitry wants to merge 8 commits into warpdotdev:master from ErshovDmitry:i18n Copy head branch name to clipboard" [level=2] [ref=e1414]: + - generic [ref=e1415]: + - link "I18n" [ref=e1416] [cursor=pointer]: + - /url: "#top" + - generic [ref=e1417]: "#11382" + - generic [ref=e1419]: + - link "ErshovDmitry" [ref=e1420] [cursor=pointer]: + - /url: /ErshovDmitry + - text: wants to merge 8 commits into + - generic [ref=e1421]: + - link "warpdotdev:master" [ref=e1422] [cursor=pointer]: + - /url: /warpdotdev/warp/tree/master + - generic [ref=e1423]: from + - generic [ref=e1424]: + - link "ErshovDmitry:i18n" [ref=e1425] [cursor=pointer]: + - /url: /ErshovDmitry/warp-i18n/tree/i18n + - button "Copy head branch name to clipboard" [ref=e1426] [cursor=pointer]: + - img [ref=e1427] + - generic [ref=e217]: + - generic [ref=e219]: + - heading "Conversation" [level=2] [ref=e220] + - generic [ref=e221]: + - generic [ref=e222]: + - generic [ref=e224]: + - link "@ErshovDmitry" [ref=e225] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e226] + - generic [ref=e228]: + - generic [ref=e229]: + - group [ref=e232]: + - button "Show options" [ref=e233] [cursor=pointer]: + - img "Show options" [ref=e236] + - heading "ErshovDmitry commented May 20, 20263 hours ago" [level=3] [ref=e238]: + - generic [ref=e239]: + - strong [ref=e240]: + - link "ErshovDmitry" [ref=e241] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 hours ago" [ref=e242] [cursor=pointer]: + - /url: "#issue-4483039846" + - generic [ref=e246]: + - paragraph [ref=e247]: Hello! My name is Dmitry. + - paragraph [ref=e248]: I've been using Warp and love it. I wanted to add i18n support so the terminal could speak Russian (and other languages in the future). I'm not a developer myself — this was built with DeepSeek AI, but I reviewed everything personally. + - paragraph [ref=e249]: I'd much rather this live in the official repo than a fork. If the team is interested in i18n, I'm happy to help however I can — and I'll gladly delete my fork once upstreamed. + - paragraph [ref=e250]: + - text: Cheers, + - text: Dmitry + - separator [ref=e251] + - heading "What" [level=2] [ref=e252] + - paragraph [ref=e253]: Adds i18n (internationalization) support to Warp. + - heading "Changes" [level=2] [ref=e254] + - 'heading "New crate: warp_i18n" [level=3] [ref=e255]': + - text: "New crate:" + - code [ref=e256]: warp_i18n + - list [ref=e257]: + - listitem [ref=e258]: + - code [ref=e259]: t!() + - text: macro for translating UI strings + - listitem [ref=e260]: + - code [ref=e261]: current_locale() + - text: / + - code [ref=e262]: set_current_locale() + - text: for runtime language switching + - listitem [ref=e263]: + - code [ref=e264]: init_from_json() + - text: for WASM compatibility + - listitem [ref=e265]: Build script for compile-time key validation + - listitem [ref=e266]: 21 tests + - heading "Feature flag" [level=3] [ref=e267] + - list [ref=e268]: + - listitem [ref=e269]: + - code [ref=e270]: FeatureFlag::I18n + - text: in + - code [ref=e271]: DOGFOOD_FLAGS + - heading "Settings" [level=3] [ref=e272] + - list [ref=e273]: + - listitem [ref=e274]: + - code [ref=e275]: Locale + - text: enum (En, Ru) + - listitem [ref=e276]: Language dropdown in Settings → Appearance + - listitem [ref=e277]: Auto-detect system locale on first run + - heading "Migrated UI" [level=3] [ref=e278] + - list [ref=e279]: + - listitem [ref=e280]: 10 top-level menu labels + - listitem [ref=e281]: 14 settings section names + - listitem [ref=e282]: 11 settings category labels + - heading "Locales" [level=3] [ref=e283] + - list [ref=e284]: + - listitem [ref=e285]: + - code [ref=e286]: en.json + - text: — 51 keys (mandatory fallback) + - listitem [ref=e287]: + - code [ref=e288]: ru.json + - text: — 51 keys (Russian translation) + - paragraph [ref=e289]: + - emphasis [ref=e290]: + - text: Built with DeepSeek AI — human-reviewed, all tests pass ( + - code [ref=e291]: cargo check --workspace + - text: "," + - code [ref=e292]: cargo test -p warp_i18n + - text: ). + - group [ref=e296]: + - generic "Add or remove reactions" [ref=e297] [cursor=pointer]: + - img [ref=e298] + - generic [ref=e300]: + - generic [ref=e302]: + - generic [ref=e303]: + - img [ref=e305] + - generic [ref=e307]: + - link "ErshovDmitry" [ref=e308] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 5 commits + - link "May 20, 2026 09:155 hours ago" [ref=e309] [cursor=pointer]: + - /url: "#commits-pushed-f06dc63" + - generic [ref=e310]: + - generic [ref=e311]: + - img [ref=e313] + - generic [ref=e318]: + - link "@ErshovDmitry" [ref=e321] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e322] + - code [ref=e324]: + - 'link "feat: add warp_i18n crate with t!() macro and en/ru locales" [ref=e325] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - code [ref=e333]: + - link "f06dc63" [ref=e334] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - generic [ref=e335]: + - img [ref=e337] + - generic [ref=e342]: + - link "@ErshovDmitry" [ref=e345] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e346] + - generic [ref=e347]: + - code [ref=e348]: + - 'link "feat: i18n Phase 2 — FeatureFlag, Locale setting, app_menus migration…" [ref=e349] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - button "Commit message body" [ref=e351] [cursor=pointer]: … + - code [ref=e359]: + - link "643f6c3" [ref=e360] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - generic [ref=e361]: + - img [ref=e363] + - generic [ref=e368]: + - link "@ErshovDmitry" [ref=e371] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e372] + - generic [ref=e373]: + - code [ref=e374]: + - 'link "fix: wire warp_i18n::init() at startup, sync LocaleSettings with i18n…" [ref=e375] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - button "Commit message body" [ref=e377] [cursor=pointer]: … + - code [ref=e385]: + - link "a9e9e26" [ref=e386] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - generic [ref=e387]: + - img [ref=e389] + - generic [ref=e394]: + - link "@ErshovDmitry" [ref=e397] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e398] + - code [ref=e400]: + - 'link "docs: add English translation of the fork note in README" [ref=e401] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - code [ref=e409]: + - link "3911714" [ref=e410] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - generic [ref=e411]: + - img [ref=e413] + - generic [ref=e418]: + - link "@ErshovDmitry" [ref=e421] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e422] + - generic [ref=e423]: + - code [ref=e424]: + - 'link "feat: i18n Phase 3 — build script key validation, WASM support, setti…" [ref=e425] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - button "Commit message body" [ref=e427] [cursor=pointer]: … + - group [ref=e1262]: + - generic "1 / 2 checks OK" [ref=e1263] [cursor=pointer]: + - img "1 / 2 checks OK" [ref=e1264] + - code [ref=e435]: + - link "169dfa6" [ref=e436] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e438]: + - link "@cla-bot" [ref=e440] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e441] + - generic [ref=e443]: + - generic [ref=e444]: + - group [ref=e447]: + - button "Show options" [ref=e448] [cursor=pointer]: + - img "Show options" [ref=e451] + - heading "cla-bot Bot commented May 20, 20263 hours ago" [level=3] [ref=e453]: + - generic [ref=e454]: + - strong [ref=e455]: + - link "cla-bot" [ref=e456] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e457]: Bot + - text: commented + - link "May 20, 20263 hours ago" [ref=e458] [cursor=pointer]: + - /url: "#issuecomment-4494411354" + - generic [ref=e459]: + - table [ref=e461]: + - rowgroup [ref=e462]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e463]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e464]': + - paragraph [ref=e465]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e466]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e467]: + - listitem [ref=e468]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e469]: git config --list | grep email + - listitem [ref=e470]: + - text: If not, set it up using + - code [ref=e471]: git config --global user.email email@example.com + - listitem [ref=e472]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e473] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e478]: + - generic "Add or remove reactions" [ref=e479] [cursor=pointer]: + - img [ref=e480] + - generic [ref=e483]: + - link "@oz-for-oss" [ref=e485] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e486] + - generic [ref=e488]: + - generic [ref=e489]: + - generic [ref=e490]: + - group [ref=e492]: + - button "Show options" [ref=e493] [cursor=pointer]: + - img "Show options" [ref=e496] + - generic "This user has previously committed to the warp repository." [ref=e499]: + - generic [ref=e500]: Contributor + - heading "oz-for-oss Bot commented May 20, 20263 hours ago" [level=3] [ref=e501]: + - generic [ref=e502]: + - strong [ref=e503]: + - link "oz-for-oss" [ref=e504] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e505]: Bot + - text: commented + - link "May 20, 20263 hours ago" [ref=e506] [cursor=pointer]: + - /url: "#issuecomment-4494411637" + - generic [ref=e507]: + - table [ref=e509]: + - rowgroup [ref=e510]: + - 'row "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e511]': + - 'cell "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e512]': + - paragraph [ref=e513]: + - link "@ErshovDmitry" [ref=e514] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e515]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e516]: ready-to-implement + - text: . + - paragraph [ref=e517]: "Issue-state enforcement details:" + - list [ref=e518]: + - listitem [ref=e519]: + - paragraph [ref=e520]: "Associated same-repo issues checked: none" + - listitem [ref=e521]: + - paragraph [ref=e522]: + - text: "Required readiness label:" + - code [ref=e523]: ready-to-implement + - paragraph [ref=e524]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e525]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e526]: ready-to-implement + - text: . + - paragraph [ref=e527]: + - emphasis [ref=e528]: + - text: Powered by + - link "Oz" [ref=e529] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e534]: + - generic "Add or remove reactions" [ref=e535] [cursor=pointer]: + - img [ref=e536] + - generic [ref=e539]: + - img [ref=e541] + - generic [ref=e543]: + - link "@github-actions" [ref=e544] [cursor=pointer]: + - /url: /apps/github-actions + - img "@github-actions" [ref=e545] + - link "github-actions" [ref=e546] [cursor=pointer]: + - /url: /apps/github-actions + - generic [ref=e547]: Bot + - text: added the + - link "external-contributor" [ref=e548] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - text: label + - link "May 20, 20263 hours ago" [ref=e549] [cursor=pointer]: + - /url: "#event-25738319166" + - generic [ref=e552]: + - generic [ref=e553]: + - link "oz-for-oss[bot]" [ref=e554] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "oz-for-oss[bot]" [ref=e555] + - img [ref=e557] + - generic [ref=e559]: + - generic [ref=e560]: + - strong [ref=e561]: + - link "oz-for-oss" [ref=e562] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e563]: Bot + - text: requested changes + - link "May 20, 20263 hours ago" [ref=e565] [cursor=pointer]: + - /url: "#pullrequestreview-4325084051" + - link "View reviewed changes" [ref=e567] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e569]: View reviewed changes + - generic [ref=e572]: + - generic [ref=e573]: + - generic [ref=e574]: + - group [ref=e576]: + - button "Show options" [ref=e577] [cursor=pointer]: + - img "Show options" [ref=e580] + - generic "This user has previously committed to the warp repository." [ref=e583]: + - generic [ref=e584]: Contributor + - heading "oz-for-oss Bot left a comment" [level=3] [ref=e585]: + - generic [ref=e586]: + - strong [ref=e587]: + - link "oz-for-oss" [ref=e588] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e589]: Bot + - text: left a comment + - generic [ref=e592]: + - paragraph [ref=e593]: + - link "@ErshovDmitry" [ref=e594] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e595]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e596]: ready-to-implement + - text: . + - paragraph [ref=e597]: "Issue-state enforcement details:" + - list [ref=e598]: + - listitem [ref=e599]: + - paragraph [ref=e600]: "Associated same-repo issues checked: none" + - listitem [ref=e601]: + - paragraph [ref=e602]: + - text: "Required readiness label:" + - code [ref=e603]: ready-to-implement + - paragraph [ref=e604]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e605]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e606]: ready-to-implement + - text: . + - paragraph [ref=e607]: + - emphasis [ref=e608]: + - text: Powered by + - link "Oz" [ref=e609] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e613]: + - generic "Add or remove reactions" [ref=e614] [cursor=pointer]: + - img [ref=e615] + - generic [ref=e618]: + - link "@ErshovDmitry" [ref=e620] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e621] + - generic [ref=e623]: + - generic [ref=e624]: + - generic [ref=e625]: + - group [ref=e627]: + - button "Show options" [ref=e628] [cursor=pointer]: + - img "Show options" [ref=e631] + - generic "You are the author of this pull request." [ref=e634]: + - generic [ref=e635]: Author + - heading "ErshovDmitry commented May 20, 20263 hours ago" [level=3] [ref=e636]: + - generic [ref=e637]: + - strong [ref=e638]: + - link "ErshovDmitry" [ref=e639] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 hours ago" [ref=e640] [cursor=pointer]: + - /url: "#issuecomment-4494430862" + - generic [ref=e641]: + - table [ref=e643]: + - rowgroup [ref=e644]: + - 'row "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e645]': + - 'cell "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e646]': + - paragraph [ref=e647]: + - text: Oh wow — I just noticed there are already several i18n PRs open ( + - link "#10630" [ref=e648] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "," + - link "#10990" [ref=e649] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: "," + - link "#9458" [ref=e650] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9458 + - text: "," + - link "#9922" [ref=e651] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9922 + - text: ). I should have checked first before opening mine. My apologies for the noise! + - blockquote [ref=e652]: + - paragraph [ref=e653]: + - text: That said, I see none of them include Russian yet. If the team settles on one i18n approach (looks like + - link "#10630" [ref=e654] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e655] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: is the leading one?), I'd be happy to contribute Russian translations to that effort instead. + - paragraph [ref=e656]: I'll close this PR if it's just adding clutter — just let me know. + - paragraph [ref=e657]: + - text: Cheers, + - text: Dmitry + - group [ref=e662]: + - generic "Add or remove reactions" [ref=e663] [cursor=pointer]: + - img [ref=e664] + - generic [ref=e669]: + - img [ref=e671] + - generic [ref=e676]: + - link "@ErshovDmitry" [ref=e679] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e680] + - generic [ref=e681]: + - code [ref=e682]: + - 'link "refactor: migrate to ZacharyZcR-compatible YAML i18n with Russian locale" [ref=e683] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - button "Commit message body" [ref=e685] [cursor=pointer]: … + - group [ref=e1266]: + - generic "0 / 1 checks OK" [ref=e1267] [cursor=pointer]: + - img "0 / 1 checks OK" [ref=e1268] + - code [ref=e693]: + - link "04801f0" [ref=e694] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - generic [ref=e696]: + - link "@cla-bot" [ref=e698] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e699] + - generic [ref=e701]: + - generic [ref=e702]: + - group [ref=e705]: + - button "Show options" [ref=e706] [cursor=pointer]: + - img "Show options" [ref=e709] + - heading "cla-bot Bot commented May 20, 20261 hour ago" [level=3] [ref=e711]: + - generic [ref=e712]: + - strong [ref=e713]: + - link "cla-bot" [ref=e714] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e715]: Bot + - text: commented + - link "May 20, 20261 hour ago" [ref=e716] [cursor=pointer]: + - /url: "#issuecomment-4494962896" + - generic [ref=e717]: + - table [ref=e719]: + - rowgroup [ref=e720]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e721]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e722]': + - paragraph [ref=e723]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e724]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e725]: + - listitem [ref=e726]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e727]: git config --list | grep email + - listitem [ref=e728]: + - text: If not, set it up using + - code [ref=e729]: git config --global user.email email@example.com + - listitem [ref=e730]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e731] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e736]: + - generic "Add or remove reactions" [ref=e737] [cursor=pointer]: + - img [ref=e738] + - generic [ref=e743]: + - img [ref=e745] + - generic [ref=e750]: + - link "@ErshovDmitry" [ref=e753] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e754] + - code [ref=e756]: + - 'link "fix: init_locale before menu bar, cleanup dead code, harden set_locale" [ref=e757] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - group [ref=e1270]: + - generic "0 / 1 checks OK" [ref=e1271] [cursor=pointer]: + - img "0 / 1 checks OK" [ref=e1272] + - code [ref=e765]: + - link "1caedf3" [ref=e766] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - generic [ref=e768]: + - link "@cla-bot" [ref=e770] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e771] + - generic [ref=e773]: + - generic [ref=e774]: + - group [ref=e777]: + - button "Show options" [ref=e778] [cursor=pointer]: + - img "Show options" [ref=e781] + - heading "cla-bot Bot commented May 20, 202650 minutes ago" [level=3] [ref=e783]: + - generic [ref=e784]: + - strong [ref=e785]: + - link "cla-bot" [ref=e786] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e787]: Bot + - text: commented + - link "May 20, 202650 minutes ago" [ref=e788] [cursor=pointer]: + - /url: "#issuecomment-4495220665" + - generic [ref=e789]: + - table [ref=e791]: + - rowgroup [ref=e792]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e793]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e794]': + - paragraph [ref=e795]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e796]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e797]: + - listitem [ref=e798]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e799]: git config --list | grep email + - listitem [ref=e800]: + - text: If not, set it up using + - code [ref=e801]: git config --global user.email email@example.com + - listitem [ref=e802]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e803] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e808]: + - generic "Add or remove reactions" [ref=e809] [cursor=pointer]: + - img [ref=e810] + - generic [ref=e815]: + - img [ref=e817] + - generic [ref=e822]: + - link "@ErshovDmitry" [ref=e825] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e826] + - generic [ref=e827]: + - code [ref=e828]: + - 'link "fix(i18n): Round 2 review — internationalize all hardcoded menu strings" [ref=e829] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - button "Commit message body" [ref=e831] [cursor=pointer]: … + - group [ref=e1274]: + - generic "1 / 1 checks OK" [ref=e1275] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1276] + - code [ref=e839]: + - link "1b026c7" [ref=e840] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - generic [ref=e842]: + - link "@cla-bot" [ref=e844] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e845] + - generic [ref=e847]: + - generic [ref=e848]: + - group [ref=e851]: + - button "Show options" [ref=e852] [cursor=pointer]: + - img "Show options" [ref=e855] + - heading "cla-bot Bot commented May 20, 202621 minutes ago" [level=3] [ref=e857]: + - generic [ref=e858]: + - strong [ref=e859]: + - link "cla-bot" [ref=e860] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e861]: Bot + - text: commented + - link "May 20, 202621 minutes ago" [ref=e862] [cursor=pointer]: + - /url: "#issuecomment-4495434298" + - generic [ref=e863]: + - table [ref=e865]: + - rowgroup [ref=e866]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e867]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e868]': + - paragraph [ref=e869]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e870]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e871]: + - listitem [ref=e872]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e873]: git config --list | grep email + - listitem [ref=e874]: + - text: If not, set it up using + - code [ref=e875]: git config --global user.email email@example.com + - listitem [ref=e876]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e877] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e882]: + - generic "Add or remove reactions" [ref=e883] [cursor=pointer]: + - img [ref=e884] + - generic [ref=e887]: + - link "@ErshovDmitry" [ref=e889] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e890] + - generic [ref=e892]: + - generic [ref=e893]: + - generic [ref=e894]: + - group [ref=e896]: + - button "Show options" [ref=e897] [cursor=pointer]: + - img "Show options" [ref=e900] + - generic "You are the author of this pull request." [ref=e903]: + - generic [ref=e904]: Author + - heading "ErshovDmitry commented May 20, 202610 minutes ago" [level=3] [ref=e905]: + - generic [ref=e906]: + - strong [ref=e907]: + - link "ErshovDmitry" [ref=e908] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202610 minutes ago" [ref=e909] [cursor=pointer]: + - /url: "#issuecomment-4495523677" + - generic [ref=e910]: + - table [ref=e912]: + - rowgroup [ref=e913]: + - 'row "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e914]': + - 'cell "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e915]': + - paragraph [ref=e916]: + - text: Update:** I've rewritten this PR to match + - link "@ZacharyZcR" [ref=e917] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: "'s YAML approach from" + - link "#10630" [ref=e918] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e919] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: ":" + - blockquote [ref=e920]: + - list [ref=e921]: + - listitem [ref=e922]: + - code [ref=e923]: crates/i18n/ + - text: with + - code [ref=e924]: t!() + - text: / + - code [ref=e925]: t_required!() + - text: macros and + - code [ref=e926]: TranslationLookup + - listitem [ref=e927]: + - code [ref=e928]: "resources/bundled/locales/{en,ru}.yml" + - text: (247 keys each, YAML) + - listitem [ref=e929]: + - code [ref=e930]: WARP_LANG + - text: env var → system locale → + - code [ref=e931]: en + - text: fallback + - listitem [ref=e932]: + - text: No feature flag, no settings UI — same design as + - generic [ref=e933]: + - img [ref=e934] + - 'link "Add i18n framework with Simplified Chinese (zh-CN) support #10630" [ref=e936] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/pull/10630 + - paragraph [ref=e937]: + - text: The Russian + - code [ref=e938]: ru.yml + - text: should be a drop-in addition to the framework once it's approved. Happy to rebase onto whichever PR lands first. + - paragraph [ref=e939]: Dmitry + - group [ref=e944]: + - generic "Add or remove reactions" [ref=e945] [cursor=pointer]: + - img [ref=e946] + - generic [ref=e949]: + - link "@ErshovDmitry" [ref=e951] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e952] + - generic [ref=e954]: + - generic [ref=e955]: + - generic [ref=e956]: + - group [ref=e958]: + - button "Show options" [ref=e959] [cursor=pointer]: + - img "Show options" [ref=e962] + - generic "You are the author of this pull request." [ref=e965]: + - generic [ref=e966]: Author + - heading "ErshovDmitry commented May 20, 20263 minutes ago" [level=3] [ref=e967]: + - generic [ref=e968]: + - strong [ref=e969]: + - link "ErshovDmitry" [ref=e970] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 minutes ago" [ref=e971] [cursor=pointer]: + - /url: "#issuecomment-4495572049" + - generic [ref=e972]: + - table [ref=e974]: + - rowgroup [ref=e975]: + - row "@cla-bot check" [ref=e976]: + - cell "@cla-bot check" [ref=e977]: + - paragraph [ref=e978]: + - link "@cla-bot" [ref=e979] [cursor=pointer]: + - /url: https://github.com/cla-bot + - text: check + - group [ref=e984]: + - generic "Add or remove reactions" [ref=e985] [cursor=pointer]: + - img [ref=e986] + - generic [ref=e989]: + - img [ref=e991] + - generic [ref=e993]: + - link "@cla-bot" [ref=e994] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e995] + - link "cla-bot" [ref=e996] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e997]: Bot + - text: added the + - link "cla-signed" [ref=e998] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - text: label + - link "May 20, 20263 minutes ago" [ref=e999] [cursor=pointer]: + - /url: "#event-25743817179" + - generic [ref=e1001]: + - link "@cla-bot" [ref=e1003] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e1004] + - generic [ref=e1006]: + - generic [ref=e1007]: + - group [ref=e1010]: + - button "Show options" [ref=e1011] [cursor=pointer]: + - img "Show options" [ref=e1014] + - heading "cla-bot Bot commented May 20, 20263 minutes ago" [level=3] [ref=e1016]: + - generic [ref=e1017]: + - strong [ref=e1018]: + - link "cla-bot" [ref=e1019] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e1020]: Bot + - text: commented + - link "May 20, 20263 minutes ago" [ref=e1021] [cursor=pointer]: + - /url: "#issuecomment-4495573189" + - generic [ref=e1022]: + - table [ref=e1024]: + - rowgroup [ref=e1025]: + - row "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1026]: + - cell "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1027]: + - paragraph [ref=e1028]: The cla-bot has been summoned, and re-checked this pull request! + - group [ref=e1033]: + - generic "Add or remove reactions" [ref=e1034] [cursor=pointer]: + - img [ref=e1035] + - generic [ref=e1038]: + - heading "Merge info" [level=2] [ref=e1278] + - generic [ref=e1279]: + - img "Unable to merge" [ref=e1281] + - generic [ref=e1285]: + - region "Reviews" [ref=e1286]: + - generic [ref=e1287]: + - generic [ref=e1288]: + - img [ref=e1292] + - generic [ref=e1295]: + - heading "Changes requested" [level=3] [ref=e1296] + - paragraph [ref=e1297]: 1 change requested by reviewers with write access. + - button "Changes requested" [expanded] [ref=e1298] [cursor=pointer] + - img [ref=e1301] + - generic [ref=e1305]: + - button "Expand 1 requested change group" [ref=e1306] [cursor=pointer]: + - generic [ref=e1307]: + - generic [ref=e1308]: + - img [ref=e1309] + - text: 1 requested change + - generic: + - generic: + - img + - group "1 requested change" + - region "Checks" [ref=e1311]: + - generic [ref=e1313]: + - img [ref=e1317] + - generic [ref=e1320]: + - heading "5 workflows awaiting approval" [level=3] [ref=e1321] + - paragraph [ref=e1322]: + - text: This workflow requires approval from a maintainer. + - link "Learn more about approving workflows." [ref=e1323] [cursor=pointer]: + - /url: https://docs.github.com/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks + - generic [ref=e1326]: + - generic [ref=e1327]: + - generic [ref=e1328]: + - button "Collapse 1 pending check group" [expanded] [ref=e1329] [cursor=pointer]: + - generic [ref=e1330]: + - generic [ref=e1331]: 1 pending check + - generic: + - generic: + - img + - button "Checks settings" [ref=e1332] [cursor=pointer]: + - img [ref=e1333] + - group "pending checks" [ref=e1335]: + - generic [ref=e1337]: + - heading "pending checks" [level=3] [ref=e1338] + - list "pending checks" [ref=e1339]: + - listitem "Check CI results expected" [ref=e1340]: + - generic [ref=e1344]: + - img [ref=e1346] + - img "Check CI results" [ref=e1348] + - generic [ref=e1349]: + - heading "Check CI results" [level=4] [ref=e1350] + - generic [ref=e1351]: Expected — Waiting for status to be reported + - generic [ref=e1354]: Required + - generic [ref=e1356]: + - button "Collapse 1 successful check group" [expanded] [ref=e1358] [cursor=pointer]: + - generic [ref=e1359]: + - generic [ref=e1360]: 1 successful check + - generic: + - generic: + - img + - group "successful checks" [ref=e1361]: + - generic [ref=e1363]: + - heading "successful checks" [level=3] [ref=e1364] + - list "successful checks" [ref=e1365]: + - listitem "verification/cla-signed" [ref=e1366]: + - generic [ref=e1370]: + - img [ref=e1372] + - img "verification/cla-signed" [ref=e1374] + - heading "verification/cla-signed" [level=4] [ref=e1376]: + - link "verification/cla-signed" [ref=e1377] [cursor=pointer]: + - /url: https://s3.amazonaws.com/cla-bot-logs/warpdotdev-2d8e78d8-133e-497d-9968-a5abd1c7dd28 + - generic [ref=e1380]: Required + - button "More actions" [ref=e1383] [cursor=pointer]: + - img [ref=e1384] + - region "Merging is blocked" [ref=e1386]: + - generic [ref=e1388]: + - img [ref=e1392] + - generic [ref=e1395]: + - heading "Merging is blocked" [level=3] [ref=e1396] + - paragraph + - list [ref=e1397]: + - listitem [ref=e1398]: + - generic [ref=e1400]: 1 review requesting changes by reviewers with write access. + - generic [ref=e1401]: + - generic [ref=e1402]: Still in progress? + - button "Convert to draft" [ref=e1403] [cursor=pointer]: + - generic [ref=e1405]: Convert to draft + - generic [ref=e1047]: + - link "@ErshovDmitry" [ref=e1049] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1050] + - form "Add a comment" [ref=e1052]: + - group [ref=e1053]: + - heading "Add a comment" [level=4] [ref=e1056] + - generic [ref=e1057]: Comment + - generic [ref=e1058]: + - generic [ref=e1059]: + - tablist "Add a comment" [ref=e1060]: + - tab "Write" [selected] [ref=e1061] [cursor=pointer] + - tab "Preview" [ref=e1062] [cursor=pointer] + - toolbar [ref=e1063]: + - generic [ref=e1064]: + - button "Heading" [ref=e1066] [cursor=pointer]: + - img + - button "Bold" [ref=e1068] [cursor=pointer]: + - img + - button "Italic" [ref=e1070] [cursor=pointer]: + - img + - button "Quote" [ref=e1072] [cursor=pointer]: + - img + - button "Code" [ref=e1074] [cursor=pointer]: + - img + - button "Link" [ref=e1076] [cursor=pointer]: + - img + - separator [ref=e1077] + - button "Numbered list" [ref=e1079] [cursor=pointer]: + - img + - button "Unordered list" [ref=e1081] [cursor=pointer]: + - img + - button "Task list" [ref=e1083] [cursor=pointer]: + - img + - separator [ref=e1084] + - button "Attach files" [ref=e1086] [cursor=pointer]: + - img + - button "Mention" [ref=e1088] [cursor=pointer]: + - img + - button "Reference" [ref=e1090] [cursor=pointer]: + - img + - button "Saved replies" [ref=e1092] [cursor=pointer]: + - img + - button "Slash commands" [ref=e1094] [cursor=pointer]: + - img + - tabpanel "Write" [ref=e1095]: + - generic [ref=e1099]: + - textbox "Comment" [active] [ref=e1100]: + - /placeholder: " " + - paragraph: Add your comment here... + - generic [ref=e1101]: + - link "Markdown is supported" [ref=e1103] [cursor=pointer]: + - /url: https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax + - generic [ref=e1104]: + - generic: + - img + - generic [ref=e1105]: Markdown is supported + - button "Paste, drop, or click to add files" [ref=e1106] [cursor=pointer]: + - generic [ref=e1107]: + - generic: + - img + - generic [ref=e1108]: Paste, drop, or click to add files + - generic [ref=e1111]: + - button "Close pull request" [ref=e1113] [cursor=pointer]: + - img [ref=e1114] + - text: Close pull request + - button "Comment" [disabled] [ref=e1117] + - generic [ref=e1118]: + - img [ref=e1119] + - generic [ref=e1121]: + - text: Remember, contributions to this repository should follow its + - link "contributing guidelines" [ref=e1122] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/CONTRIBUTING.md + - text: "," + - link "security policy" [ref=e1123] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/SECURITY.md + - text: ", and" + - link "code of conduct" [ref=e1124] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/CODE_OF_CONDUCT.md + - text: . + - generic [ref=e1125]: + - img [ref=e1126] + - strong [ref=e1128]: ProTip! + - text: Add comments to specific lines under + - link "Files changed" [ref=e1129] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files + - text: . + - generic [ref=e1133]: + - form "Select reviewers" [ref=e1135]: + - heading "Reviewers" [level=3] [ref=e1136] + - generic [ref=e1137]: + - paragraph [ref=e1138]: + - generic [ref=e1139]: + - link "@oz-for-oss" [ref=e1140] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e1141] + - link "oz-for-oss[bot]" [ref=e1142] [cursor=pointer]: + - /url: /apps/oz-for-oss + - link "oz-for-oss[bot] requested changes" [ref=e1143] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes/BASE..169dfa663dd4c533eb32daf850f25d6b9c123521 + - img [ref=e1145] + - paragraph [ref=e1147]: Requested changes must be addressed to merge this pull request. + - generic [ref=e1148]: + - generic [ref=e1149]: Still in progress? + - group [ref=e1150]: + - button "Convert to draft" [ref=e1151] [cursor=pointer] + - form "Select assignees" [ref=e1153]: + - heading "Assignees" [level=3] [ref=e1154] + - text: No one assigned + - generic [ref=e1155]: + - heading "Labels" [level=3] [ref=e1156] + - generic [ref=e1157]: + - link "cla-signed" [ref=e1158] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - generic [ref=e1159]: cla-signed + - link "external-contributor" [ref=e1160] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - generic [ref=e1161]: external-contributor + - form "Select projects" [ref=e1163]: + - heading "Projects" [level=3] [ref=e1164] + - text: None yet + - form "Select milestones" [ref=e1166]: + - heading "Milestone" [level=3] [ref=e1167] + - text: No milestone + - form "Link issues" [ref=e1173]: + - heading "Development" [level=3] [ref=e1174] + - paragraph [ref=e1175]: Successfully merging this pull request may close these issues. + - paragraph [ref=e1407]: None yet + - generic [ref=e1177]: + - button "Notifications Customize" [ref=e1180] [cursor=pointer]: + - generic [ref=e1181]: + - generic [ref=e1182]: Notifications + - generic [ref=e1183]: Customize + - button "Unsubscribe" [ref=e1185] [cursor=pointer]: + - generic [ref=e1186]: + - generic: + - img + - generic [ref=e1187]: Unsubscribe + - paragraph [ref=e1188]: You’re receiving notifications because you were mentioned. + - generic [ref=e1190]: + - heading "1 participant" [level=3] [ref=e1191] + - link "@ErshovDmitry" [ref=e1193] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1194] + - generic [ref=e1197]: + - generic [ref=e1198]: + - checkbox "Allow edits by maintainers" [checked] [ref=e1199] + - text: Allow edits by maintainers + - button "Learn more about allowing maintainer edits" [ref=e1200] [cursor=pointer]: + - img [ref=e1203] + - contentinfo [ref=e1206]: + - heading "Footer" [level=2] [ref=e1207] + - generic [ref=e1208]: + - generic [ref=e1209]: + - link "GitHub Homepage" [ref=e1210] [cursor=pointer]: + - /url: https://github.com + - img [ref=e1211] + - generic [ref=e1213]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e1214]: + - heading "Footer navigation" [level=3] [ref=e1215] + - list "Footer navigation" [ref=e1216]: + - listitem [ref=e1217]: + - link "Terms" [ref=e1218] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e1219]: + - link "Privacy" [ref=e1220] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e1221]: + - link "Security" [ref=e1222] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e1223]: + - link "Status" [ref=e1224] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e1225]: + - link "Community" [ref=e1226] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e1227]: + - link "Docs" [ref=e1228] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e1229]: + - link "Contact" [ref=e1230] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e1231]: + - button "Manage cookies" [ref=e1233] [cursor=pointer] + - listitem [ref=e1234]: + - button "Do not share my personal information" [ref=e1236] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-20T07-38-21-430Z.yml b/.playwright-mcp/page-2026-05-20T07-38-21-430Z.yml new file mode 100644 index 0000000000..7882edf95f --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T07-38-21-430Z.yml @@ -0,0 +1,1204 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - generic [ref=e7]: + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - button "Open user navigation menu" [ref=e59] [cursor=pointer]: + - img "User avatar" [ref=e60] + - heading "Repository navigation" [level=2] [ref=e61] + - navigation "Repository" [ref=e62]: + - list [ref=e63]: + - listitem [ref=e64]: + - link "Code" [ref=e65] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e67] + - generic [ref=e69]: Code + - listitem [ref=e70]: + - link "Issues (3.4k)" [ref=e71] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e73] + - generic [ref=e76]: Issues + - generic [ref=e77]: + - generic [ref=e78]: 3.4k + - generic [ref=e79]: (3.4k) + - listitem [ref=e80]: + - link "Pull requests (502)" [ref=e81] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e83] + - generic [ref=e85]: Pull requests + - generic [ref=e86]: + - generic [ref=e87]: "502" + - generic [ref=e88]: (502) + - listitem [ref=e89]: + - link "Agents" [ref=e90] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e92] + - generic [ref=e95]: Agents + - listitem [ref=e96]: + - link "Discussions" [ref=e97] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e99] + - generic [ref=e101]: Discussions + - listitem [ref=e102]: + - link "Actions" [ref=e103] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e105] + - generic [ref=e107]: Actions + - listitem [ref=e108]: + - link "Projects" [ref=e109] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e111] + - generic [ref=e113]: Projects + - listitem [ref=e114]: + - link "Security and quality" [ref=e115] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e117] + - generic [ref=e119]: Security and quality + - listitem [ref=e120]: + - link "Insights" [ref=e121] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e123] + - generic [ref=e125]: Insights + - region "Important update" [ref=e127]: + - img [ref=e129] + - generic [ref=e132]: + - heading "Important update" [level=2] [ref=e134] + - generic [ref=e136]: + - text: On April 24 we'll start using GitHub Copilot interaction data for AI model training unless you opt out. + - link "Review this update" [ref=e137] [cursor=pointer]: + - /url: https://gh.io/AAzfaht + - text: and manage your preferences in your + - link "GitHub account settings" [ref=e138] [cursor=pointer]: + - /url: https://github.com/settings/copilot/features + - text: . + - button "Dismiss banner" [ref=e139] [cursor=pointer]: + - img [ref=e140] + - main [ref=e145]: + - generic [ref=e152]: + - generic [ref=e155]: + - 'heading "I18n #11382 Edit title" [level=1] [ref=e157]': + - text: I18n + - generic [ref=e158]: + - generic [ref=e159]: "#11382" + - button "Edit title" [ref=e160] [cursor=pointer]: + - img [ref=e161] + - generic [ref=e164]: + - generic [ref=e165]: + - button "View status View status" [disabled] [ref=e166]: + - generic [ref=e167]: + - generic: + - generic: + - img + - generic: Loading + - generic [ref=e169]: Loading merge status + - button "Code" [ref=e170] [cursor=pointer]: + - generic [ref=e171]: + - generic [ref=e172]: Code + - generic: + - img + - generic [ref=e174]: + - generic [ref=e176]: + - img "Pull request" [ref=e177] + - text: Open + - generic [ref=e180]: + - link "ErshovDmitry" [ref=e181] [cursor=pointer]: + - /url: /ErshovDmitry + - text: wants to merge 9 commits into + - generic [ref=e182]: + - link "warpdotdev:master" [ref=e183] [cursor=pointer]: + - /url: /warpdotdev/warp/tree/master + - generic [ref=e184]: from + - generic [ref=e185]: + - link "ErshovDmitry:i18n" [ref=e186] [cursor=pointer]: + - /url: /ErshovDmitry/warp-i18n/tree/i18n + - button "Copy head branch name to clipboard" [ref=e187] [cursor=pointer]: + - img [ref=e188] + - generic [ref=e191]: + - generic [ref=e193]: + - generic [ref=e194]: +1 197 + - generic [ref=e195]: "-106" + - generic [ref=e196]: "Lines changed: 1197 additions & 106 deletions" + - navigation "Pull request navigation tabs" [ref=e205]: + - tablist [ref=e206]: + - tab "Conversation (10)" [selected] [ref=e207] [cursor=pointer]: + - img [ref=e208] + - text: Conversation + - generic [ref=e210]: "10" + - generic [ref=e211]: (10) + - tab "Commits (9)" [ref=e212] [cursor=pointer]: + - img [ref=e213] + - text: Commits + - generic [ref=e215]: "9" + - generic [ref=e216]: (9) + - tab "Checks (0)" [ref=e217] [cursor=pointer]: + - img [ref=e218] + - text: Checks + - generic [ref=e220]: "0" + - generic [ref=e221]: (0) + - tab "Files changed (16)" [ref=e222] [cursor=pointer]: + - img [ref=e223] + - text: Files changed + - generic [ref=e225]: "16" + - generic [ref=e226]: (16) + - generic [ref=e231]: + - generic [ref=e233]: + - heading "Conversation" [level=2] [ref=e234] + - generic [ref=e235]: + - generic [ref=e236]: + - generic [ref=e238]: + - link "@ErshovDmitry" [ref=e239] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e240] + - generic [ref=e242]: + - generic [ref=e243]: + - group [ref=e246]: + - button "Show options" [ref=e247] [cursor=pointer]: + - img "Show options" [ref=e250] + - heading "ErshovDmitry commented May 20, 20263 hours ago" [level=3] [ref=e252]: + - generic [ref=e253]: + - strong [ref=e254]: + - link "ErshovDmitry" [ref=e255] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 hours ago" [ref=e256] [cursor=pointer]: + - /url: "#issue-4483039846" + - generic [ref=e260]: + - paragraph [ref=e261]: Hello! My name is Dmitry. + - paragraph [ref=e262]: I've been using Warp and love it. I wanted to add i18n support so the terminal could speak Russian (and other languages in the future). I'm not a developer myself — this was built with DeepSeek AI, but I reviewed everything personally. + - paragraph [ref=e263]: I'd much rather this live in the official repo than a fork. If the team is interested in i18n, I'm happy to help however I can — and I'll gladly delete my fork once upstreamed. + - paragraph [ref=e264]: + - text: Cheers, + - text: Dmitry + - separator [ref=e265] + - heading "What" [level=2] [ref=e266] + - paragraph [ref=e267]: Adds i18n (internationalization) support to Warp. + - heading "Changes" [level=2] [ref=e268] + - 'heading "New crate: warp_i18n" [level=3] [ref=e269]': + - text: "New crate:" + - code [ref=e270]: warp_i18n + - list [ref=e271]: + - listitem [ref=e272]: + - code [ref=e273]: t!() + - text: macro for translating UI strings + - listitem [ref=e274]: + - code [ref=e275]: current_locale() + - text: / + - code [ref=e276]: set_current_locale() + - text: for runtime language switching + - listitem [ref=e277]: + - code [ref=e278]: init_from_json() + - text: for WASM compatibility + - listitem [ref=e279]: Build script for compile-time key validation + - listitem [ref=e280]: 21 tests + - heading "Feature flag" [level=3] [ref=e281] + - list [ref=e282]: + - listitem [ref=e283]: + - code [ref=e284]: FeatureFlag::I18n + - text: in + - code [ref=e285]: DOGFOOD_FLAGS + - heading "Settings" [level=3] [ref=e286] + - list [ref=e287]: + - listitem [ref=e288]: + - code [ref=e289]: Locale + - text: enum (En, Ru) + - listitem [ref=e290]: Language dropdown in Settings → Appearance + - listitem [ref=e291]: Auto-detect system locale on first run + - heading "Migrated UI" [level=3] [ref=e292] + - list [ref=e293]: + - listitem [ref=e294]: 10 top-level menu labels + - listitem [ref=e295]: 14 settings section names + - listitem [ref=e296]: 11 settings category labels + - heading "Locales" [level=3] [ref=e297] + - list [ref=e298]: + - listitem [ref=e299]: + - code [ref=e300]: en.json + - text: — 51 keys (mandatory fallback) + - listitem [ref=e301]: + - code [ref=e302]: ru.json + - text: — 51 keys (Russian translation) + - paragraph [ref=e303]: + - emphasis [ref=e304]: + - text: Built with DeepSeek AI — human-reviewed, all tests pass ( + - code [ref=e305]: cargo check --workspace + - text: "," + - code [ref=e306]: cargo test -p warp_i18n + - text: ). + - group [ref=e310]: + - generic "Add or remove reactions" [ref=e311] [cursor=pointer]: + - img [ref=e312] + - generic [ref=e314]: + - generic [ref=e316]: + - generic [ref=e317]: + - img [ref=e319] + - generic [ref=e321]: + - link "ErshovDmitry" [ref=e322] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 5 commits + - link "May 20, 2026 09:155 hours ago" [ref=e323] [cursor=pointer]: + - /url: "#commits-pushed-f06dc63" + - generic [ref=e324]: + - generic [ref=e325]: + - img [ref=e327] + - generic [ref=e332]: + - link "@ErshovDmitry" [ref=e335] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e336] + - code [ref=e338]: + - 'link "feat: add warp_i18n crate with t!() macro and en/ru locales" [ref=e339] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - code [ref=e343]: + - link "f06dc63" [ref=e344] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - generic [ref=e345]: + - img [ref=e347] + - generic [ref=e352]: + - link "@ErshovDmitry" [ref=e355] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e356] + - generic [ref=e357]: + - code [ref=e358]: + - 'link "feat: i18n Phase 2 — FeatureFlag, Locale setting, app_menus migration…" [ref=e359] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - button "Commit message body" [ref=e361] [cursor=pointer]: … + - code [ref=e365]: + - link "643f6c3" [ref=e366] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - generic [ref=e367]: + - img [ref=e369] + - generic [ref=e374]: + - link "@ErshovDmitry" [ref=e377] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e378] + - generic [ref=e379]: + - code [ref=e380]: + - 'link "fix: wire warp_i18n::init() at startup, sync LocaleSettings with i18n…" [ref=e381] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - button "Commit message body" [ref=e383] [cursor=pointer]: … + - code [ref=e387]: + - link "a9e9e26" [ref=e388] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - generic [ref=e389]: + - img [ref=e391] + - generic [ref=e396]: + - link "@ErshovDmitry" [ref=e399] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e400] + - code [ref=e402]: + - 'link "docs: add English translation of the fork note in README" [ref=e403] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - code [ref=e407]: + - link "3911714" [ref=e408] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - generic [ref=e409]: + - img [ref=e411] + - generic [ref=e416]: + - link "@ErshovDmitry" [ref=e419] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e420] + - generic [ref=e421]: + - code [ref=e422]: + - 'link "feat: i18n Phase 3 — build script key validation, WASM support, setti…" [ref=e423] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - button "Commit message body" [ref=e425] [cursor=pointer]: … + - group [ref=e429]: + - generic "1 / 2 checks OK" [ref=e430] [cursor=pointer]: + - img "1 / 2 checks OK" [ref=e431] + - code [ref=e434]: + - link "169dfa6" [ref=e435] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e437]: + - link "@cla-bot" [ref=e439] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e440] + - generic [ref=e442]: + - generic [ref=e443]: + - group [ref=e446]: + - button "Show options" [ref=e447] [cursor=pointer]: + - img "Show options" [ref=e450] + - heading "cla-bot Bot commented May 20, 20263 hours ago" [level=3] [ref=e452]: + - generic [ref=e453]: + - strong [ref=e454]: + - link "cla-bot" [ref=e455] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e456]: Bot + - text: commented + - link "May 20, 20263 hours ago" [ref=e457] [cursor=pointer]: + - /url: "#issuecomment-4494411354" + - generic [ref=e458]: + - table [ref=e460]: + - rowgroup [ref=e461]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e462]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e463]': + - paragraph [ref=e464]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e465]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e466]: + - listitem [ref=e467]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e468]: git config --list | grep email + - listitem [ref=e469]: + - text: If not, set it up using + - code [ref=e470]: git config --global user.email email@example.com + - listitem [ref=e471]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e472] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e477]: + - generic "Add or remove reactions" [ref=e478] [cursor=pointer]: + - img [ref=e479] + - generic [ref=e482]: + - link "@oz-for-oss" [ref=e484] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e485] + - generic [ref=e487]: + - generic [ref=e488]: + - generic [ref=e489]: + - group [ref=e491]: + - button "Show options" [ref=e492] [cursor=pointer]: + - img "Show options" [ref=e495] + - generic "This user has previously committed to the warp repository." [ref=e498]: + - generic [ref=e499]: Contributor + - heading "oz-for-oss Bot commented May 20, 20263 hours ago" [level=3] [ref=e500]: + - generic [ref=e501]: + - strong [ref=e502]: + - link "oz-for-oss" [ref=e503] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e504]: Bot + - text: commented + - link "May 20, 20263 hours ago" [ref=e505] [cursor=pointer]: + - /url: "#issuecomment-4494411637" + - generic [ref=e506]: + - table [ref=e508]: + - rowgroup [ref=e509]: + - 'row "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e510]': + - 'cell "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e511]': + - paragraph [ref=e512]: + - link "@ErshovDmitry" [ref=e513] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e514]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e515]: ready-to-implement + - text: . + - paragraph [ref=e516]: "Issue-state enforcement details:" + - list [ref=e517]: + - listitem [ref=e518]: + - paragraph [ref=e519]: "Associated same-repo issues checked: none" + - listitem [ref=e520]: + - paragraph [ref=e521]: + - text: "Required readiness label:" + - code [ref=e522]: ready-to-implement + - paragraph [ref=e523]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e524]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e525]: ready-to-implement + - text: . + - paragraph [ref=e526]: + - emphasis [ref=e527]: + - text: Powered by + - link "Oz" [ref=e528] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e533]: + - generic "Add or remove reactions" [ref=e534] [cursor=pointer]: + - img [ref=e535] + - generic [ref=e538]: + - img [ref=e540] + - generic [ref=e542]: + - link "@github-actions" [ref=e543] [cursor=pointer]: + - /url: /apps/github-actions + - img "@github-actions" [ref=e544] + - link "github-actions" [ref=e545] [cursor=pointer]: + - /url: /apps/github-actions + - generic [ref=e546]: Bot + - text: added the + - link "external-contributor" [ref=e547] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - text: label + - link "May 20, 20263 hours ago" [ref=e548] [cursor=pointer]: + - /url: "#event-25738319166" + - generic [ref=e551]: + - generic [ref=e552]: + - link "oz-for-oss[bot]" [ref=e553] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "oz-for-oss[bot]" [ref=e554] + - img [ref=e556] + - generic [ref=e558]: + - generic [ref=e559]: + - strong [ref=e560]: + - link "oz-for-oss" [ref=e561] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e562]: Bot + - text: requested changes + - link "May 20, 20263 hours ago" [ref=e564] [cursor=pointer]: + - /url: "#pullrequestreview-4325084051" + - link "View reviewed changes" [ref=e566] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e568]: View reviewed changes + - generic [ref=e571]: + - generic [ref=e572]: + - generic [ref=e573]: + - group [ref=e575]: + - button "Show options" [ref=e576] [cursor=pointer]: + - img "Show options" [ref=e579] + - generic "This user has previously committed to the warp repository." [ref=e582]: + - generic [ref=e583]: Contributor + - heading "oz-for-oss Bot left a comment" [level=3] [ref=e584]: + - generic [ref=e585]: + - strong [ref=e586]: + - link "oz-for-oss" [ref=e587] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e588]: Bot + - text: left a comment + - generic [ref=e591]: + - paragraph [ref=e592]: + - link "@ErshovDmitry" [ref=e593] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e594]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e595]: ready-to-implement + - text: . + - paragraph [ref=e596]: "Issue-state enforcement details:" + - list [ref=e597]: + - listitem [ref=e598]: + - paragraph [ref=e599]: "Associated same-repo issues checked: none" + - listitem [ref=e600]: + - paragraph [ref=e601]: + - text: "Required readiness label:" + - code [ref=e602]: ready-to-implement + - paragraph [ref=e603]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e604]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e605]: ready-to-implement + - text: . + - paragraph [ref=e606]: + - emphasis [ref=e607]: + - text: Powered by + - link "Oz" [ref=e608] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e612]: + - generic "Add or remove reactions" [ref=e613] [cursor=pointer]: + - img [ref=e614] + - generic [ref=e617]: + - link "@ErshovDmitry" [ref=e619] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e620] + - generic [ref=e622]: + - generic [ref=e623]: + - generic [ref=e624]: + - group [ref=e626]: + - button "Show options" [ref=e627] [cursor=pointer]: + - img "Show options" [ref=e630] + - generic "You are the author of this pull request." [ref=e633]: + - generic [ref=e634]: Author + - heading "ErshovDmitry commented May 20, 20263 hours ago" [level=3] [ref=e635]: + - generic [ref=e636]: + - strong [ref=e637]: + - link "ErshovDmitry" [ref=e638] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20263 hours ago" [ref=e639] [cursor=pointer]: + - /url: "#issuecomment-4494430862" + - generic [ref=e640]: + - table [ref=e642]: + - rowgroup [ref=e643]: + - 'row "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e644]': + - 'cell "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e645]': + - paragraph [ref=e646]: + - text: Oh wow — I just noticed there are already several i18n PRs open ( + - link "#10630" [ref=e647] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "," + - link "#10990" [ref=e648] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: "," + - link "#9458" [ref=e649] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9458 + - text: "," + - link "#9922" [ref=e650] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9922 + - text: ). I should have checked first before opening mine. My apologies for the noise! + - blockquote [ref=e651]: + - paragraph [ref=e652]: + - text: That said, I see none of them include Russian yet. If the team settles on one i18n approach (looks like + - link "#10630" [ref=e653] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e654] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: is the leading one?), I'd be happy to contribute Russian translations to that effort instead. + - paragraph [ref=e655]: I'll close this PR if it's just adding clutter — just let me know. + - paragraph [ref=e656]: + - text: Cheers, + - text: Dmitry + - group [ref=e661]: + - generic "Add or remove reactions" [ref=e662] [cursor=pointer]: + - img [ref=e663] + - generic [ref=e668]: + - img [ref=e670] + - generic [ref=e675]: + - link "@ErshovDmitry" [ref=e678] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e679] + - generic [ref=e680]: + - code [ref=e681]: + - 'link "refactor: migrate to ZacharyZcR-compatible YAML i18n with Russian locale" [ref=e682] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - button "Commit message body" [ref=e684] [cursor=pointer]: … + - group [ref=e688]: + - generic "0 / 1 checks OK" [ref=e689] [cursor=pointer]: + - img "0 / 1 checks OK" [ref=e690] + - code [ref=e693]: + - link "04801f0" [ref=e694] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - generic [ref=e696]: + - link "@cla-bot" [ref=e698] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e699] + - generic [ref=e701]: + - generic [ref=e702]: + - group [ref=e705]: + - button "Show options" [ref=e706] [cursor=pointer]: + - img "Show options" [ref=e709] + - heading "cla-bot Bot commented May 20, 20261 hour ago" [level=3] [ref=e711]: + - generic [ref=e712]: + - strong [ref=e713]: + - link "cla-bot" [ref=e714] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e715]: Bot + - text: commented + - link "May 20, 20261 hour ago" [ref=e716] [cursor=pointer]: + - /url: "#issuecomment-4494962896" + - generic [ref=e717]: + - table [ref=e719]: + - rowgroup [ref=e720]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e721]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e722]': + - paragraph [ref=e723]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e724]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e725]: + - listitem [ref=e726]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e727]: git config --list | grep email + - listitem [ref=e728]: + - text: If not, set it up using + - code [ref=e729]: git config --global user.email email@example.com + - listitem [ref=e730]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e731] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e736]: + - generic "Add or remove reactions" [ref=e737] [cursor=pointer]: + - img [ref=e738] + - generic [ref=e743]: + - img [ref=e745] + - generic [ref=e750]: + - link "@ErshovDmitry" [ref=e753] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e754] + - code [ref=e756]: + - 'link "fix: init_locale before menu bar, cleanup dead code, harden set_locale" [ref=e757] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - group [ref=e761]: + - generic "0 / 1 checks OK" [ref=e762] [cursor=pointer]: + - img "0 / 1 checks OK" [ref=e763] + - code [ref=e766]: + - link "1caedf3" [ref=e767] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - generic [ref=e769]: + - link "@cla-bot" [ref=e771] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e772] + - generic [ref=e774]: + - generic [ref=e775]: + - group [ref=e778]: + - button "Show options" [ref=e779] [cursor=pointer]: + - img "Show options" [ref=e782] + - heading "cla-bot Bot commented May 20, 20261 hour ago" [level=3] [ref=e784]: + - generic [ref=e785]: + - strong [ref=e786]: + - link "cla-bot" [ref=e787] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e788]: Bot + - text: commented + - link "May 20, 20261 hour ago" [ref=e789] [cursor=pointer]: + - /url: "#issuecomment-4495220665" + - generic [ref=e790]: + - table [ref=e792]: + - rowgroup [ref=e793]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e794]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e795]': + - paragraph [ref=e796]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e797]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e798]: + - listitem [ref=e799]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e800]: git config --list | grep email + - listitem [ref=e801]: + - text: If not, set it up using + - code [ref=e802]: git config --global user.email email@example.com + - listitem [ref=e803]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e804] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e809]: + - generic "Add or remove reactions" [ref=e810] [cursor=pointer]: + - img [ref=e811] + - generic [ref=e816]: + - img [ref=e818] + - generic [ref=e823]: + - link "@ErshovDmitry" [ref=e826] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e827] + - generic [ref=e828]: + - code [ref=e829]: + - 'link "fix(i18n): Round 2 review — internationalize all hardcoded menu strings" [ref=e830] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - button "Commit message body" [ref=e832] [cursor=pointer]: … + - group [ref=e836]: + - generic "1 / 1 checks OK" [ref=e837] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e838] + - code [ref=e841]: + - link "1b026c7" [ref=e842] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - generic [ref=e844]: + - link "@cla-bot" [ref=e846] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e847] + - generic [ref=e849]: + - generic [ref=e850]: + - group [ref=e853]: + - button "Show options" [ref=e854] [cursor=pointer]: + - img "Show options" [ref=e857] + - heading "cla-bot Bot commented May 20, 202645 minutes ago" [level=3] [ref=e859]: + - generic [ref=e860]: + - strong [ref=e861]: + - link "cla-bot" [ref=e862] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e863]: Bot + - text: commented + - link "May 20, 202645 minutes ago" [ref=e864] [cursor=pointer]: + - /url: "#issuecomment-4495434298" + - generic [ref=e865]: + - table [ref=e867]: + - rowgroup [ref=e868]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e869]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e870]': + - paragraph [ref=e871]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e872]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e873]: + - listitem [ref=e874]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e875]: git config --list | grep email + - listitem [ref=e876]: + - text: If not, set it up using + - code [ref=e877]: git config --global user.email email@example.com + - listitem [ref=e878]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e879] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e884]: + - generic "Add or remove reactions" [ref=e885] [cursor=pointer]: + - img [ref=e886] + - generic [ref=e889]: + - link "@ErshovDmitry" [ref=e891] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e892] + - generic [ref=e894]: + - generic [ref=e895]: + - generic [ref=e896]: + - group [ref=e898]: + - button "Show options" [ref=e899] [cursor=pointer]: + - img "Show options" [ref=e902] + - generic "You are the author of this pull request." [ref=e905]: + - generic [ref=e906]: Author + - heading "ErshovDmitry commented May 20, 202634 minutes ago" [level=3] [ref=e907]: + - generic [ref=e908]: + - strong [ref=e909]: + - link "ErshovDmitry" [ref=e910] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202634 minutes ago" [ref=e911] [cursor=pointer]: + - /url: "#issuecomment-4495523677" + - generic [ref=e912]: + - table [ref=e914]: + - rowgroup [ref=e915]: + - 'row "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e916]': + - 'cell "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e917]': + - paragraph [ref=e918]: + - text: Update:** I've rewritten this PR to match + - link "@ZacharyZcR" [ref=e919] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: "'s YAML approach from" + - link "#10630" [ref=e920] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e921] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: ":" + - blockquote [ref=e922]: + - list [ref=e923]: + - listitem [ref=e924]: + - code [ref=e925]: crates/i18n/ + - text: with + - code [ref=e926]: t!() + - text: / + - code [ref=e927]: t_required!() + - text: macros and + - code [ref=e928]: TranslationLookup + - listitem [ref=e929]: + - code [ref=e930]: "resources/bundled/locales/{en,ru}.yml" + - text: (247 keys each, YAML) + - listitem [ref=e931]: + - code [ref=e932]: WARP_LANG + - text: env var → system locale → + - code [ref=e933]: en + - text: fallback + - listitem [ref=e934]: + - text: No feature flag, no settings UI — same design as + - generic [ref=e935]: + - img [ref=e936] + - 'link "Add i18n framework with Simplified Chinese (zh-CN) support #10630" [ref=e938] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/pull/10630 + - paragraph [ref=e939]: + - text: The Russian + - code [ref=e940]: ru.yml + - text: should be a drop-in addition to the framework once it's approved. Happy to rebase onto whichever PR lands first. + - paragraph [ref=e941]: Dmitry + - group [ref=e946]: + - generic "Add or remove reactions" [ref=e947] [cursor=pointer]: + - img [ref=e948] + - generic [ref=e951]: + - link "@ErshovDmitry" [ref=e953] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e954] + - generic [ref=e956]: + - generic [ref=e957]: + - generic [ref=e958]: + - group [ref=e960]: + - button "Show options" [ref=e961] [cursor=pointer]: + - img "Show options" [ref=e964] + - generic "You are the author of this pull request." [ref=e967]: + - generic [ref=e968]: Author + - heading "ErshovDmitry commented May 20, 202627 minutes ago" [level=3] [ref=e969]: + - generic [ref=e970]: + - strong [ref=e971]: + - link "ErshovDmitry" [ref=e972] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202627 minutes ago" [ref=e973] [cursor=pointer]: + - /url: "#issuecomment-4495572049" + - generic [ref=e974]: + - table [ref=e976]: + - rowgroup [ref=e977]: + - row "@cla-bot check" [ref=e978]: + - cell "@cla-bot check" [ref=e979]: + - paragraph [ref=e980]: + - link "@cla-bot" [ref=e981] [cursor=pointer]: + - /url: https://github.com/cla-bot + - text: check + - group [ref=e986]: + - generic "Add or remove reactions" [ref=e987] [cursor=pointer]: + - img [ref=e988] + - generic [ref=e991]: + - img [ref=e993] + - generic [ref=e995]: + - link "@cla-bot" [ref=e996] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e997] + - link "cla-bot" [ref=e998] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e999]: Bot + - text: added the + - link "cla-signed" [ref=e1000] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - text: label + - link "May 20, 202626 minutes ago" [ref=e1001] [cursor=pointer]: + - /url: "#event-25743817179" + - generic [ref=e1003]: + - link "@cla-bot" [ref=e1005] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e1006] + - generic [ref=e1008]: + - generic [ref=e1009]: + - group [ref=e1012]: + - button "Show options" [ref=e1013] [cursor=pointer]: + - img "Show options" [ref=e1016] + - heading "cla-bot Bot commented May 20, 202626 minutes ago" [level=3] [ref=e1018]: + - generic [ref=e1019]: + - strong [ref=e1020]: + - link "cla-bot" [ref=e1021] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e1022]: Bot + - text: commented + - link "May 20, 202626 minutes ago" [ref=e1023] [cursor=pointer]: + - /url: "#issuecomment-4495573189" + - generic [ref=e1024]: + - table [ref=e1026]: + - rowgroup [ref=e1027]: + - row "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1028]: + - cell "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1029]: + - paragraph [ref=e1030]: The cla-bot has been summoned, and re-checked this pull request! + - group [ref=e1035]: + - generic "Add or remove reactions" [ref=e1036] [cursor=pointer]: + - img [ref=e1037] + - generic [ref=e1040]: + - link "@ErshovDmitry" [ref=e1042] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1043] + - generic [ref=e1045]: + - generic [ref=e1046]: + - generic [ref=e1047]: + - group [ref=e1049]: + - button "Show options" [ref=e1050] [cursor=pointer]: + - img "Show options" [ref=e1053] + - generic "You are the author of this pull request." [ref=e1056]: + - generic [ref=e1057]: Author + - heading "ErshovDmitry commented May 20, 202622 minutes ago" [level=3] [ref=e1058]: + - generic [ref=e1059]: + - strong [ref=e1060]: + - link "ErshovDmitry" [ref=e1061] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202622 minutes ago" [ref=e1062] [cursor=pointer]: + - /url: "#issuecomment-4495607261" + - generic [ref=e1063]: + - table [ref=e1065]: + - rowgroup [ref=e1066]: + - 'row "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e1067]': + - 'cell "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e1068]': + - paragraph [ref=e1069]: + - link "@oss-maintainers" [ref=e1070] [cursor=pointer]: + - /url: https://github.com/oss-maintainers + - text: This PR now follows the same YAML-based approach as + - link "#10630" [ref=e1071] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e1072] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: by + - link "@ZacharyZcR" [ref=e1073] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: — matching + - code [ref=e1074]: crates/i18n/ + - text: API, + - code [ref=e1075]: "resources/bundled/locales/{en,ru}.yml" + - text: "," + - code [ref=e1076]: t!() + - text: / + - code [ref=e1077]: t_required!() + - text: macros, and + - code [ref=e1078]: WARP_LANG + - text: env var detection. + - paragraph [ref=e1079]: + - text: It's blocked by the issue-state enforcement check (needs a + - code [ref=e1080]: ready-to-implement + - text: issue). Could a maintainer please add + - code [ref=e1081]: ready-to-implement + - text: to + - link "#1194" [ref=e1082] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194 + - text: ", or let me know if I should create a separate tracking issue?" + - paragraph [ref=e1083]: + - text: Russian + - code [ref=e1084]: ru.yml + - text: (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. + - paragraph [ref=e1085]: + - text: Cheers, + - text: Dmitry + - group [ref=e1090]: + - generic "Add or remove reactions" [ref=e1091] [cursor=pointer]: + - img [ref=e1092] + - generic [ref=e1097]: + - img [ref=e1099] + - generic [ref=e1104]: + - link "@ErshovDmitry" [ref=e1107] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1108] + - code [ref=e1110]: + - 'link "chore: add .playwright-mcp to .gitignore" [ref=e1111] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/d67453f73b0087f3a931fd06e958ab9cebb3be0c + - group [ref=e1115]: + - generic "1 / 1 checks OK" [ref=e1116] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1117] + - code [ref=e1120]: + - link "d67453f" [ref=e1121] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d67453f73b0087f3a931fd06e958ab9cebb3be0c + - generic [ref=e1123]: + - img [ref=e1125] + - generic [ref=e1127]: + - link "@ErshovDmitry" [ref=e1128] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1129] + - link "ErshovDmitry" [ref=e1130] [cursor=pointer]: + - /url: /ErshovDmitry + - link "force-pushed" [ref=e1131] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - text: the + - generic [ref=e1133]: i18n + - text: branch from + - link "2196591" [ref=e1134] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/219659155d8415b5773506f3306f6339e1300b60 + - code [ref=e1135]: "2196591" + - text: to + - link "d67453f" [ref=e1136] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/d67453f73b0087f3a931fd06e958ab9cebb3be0c + - code [ref=e1137]: d67453f + - link "Compare" [ref=e1138] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - generic [ref=e1140]: Compare + - link "May 20, 2026 14:362 minutes ago" [ref=e1141] [cursor=pointer]: + - /url: "#event-25744804343" + - generic [ref=e1145]: + - img [ref=e1146] + - generic [ref=e1149]: Loading + - generic [ref=e1152]: + - link "@ErshovDmitry" [ref=e1154] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1155] + - form "Add a comment" [ref=e1157]: + - group [ref=e1158]: + - heading "Add a comment" [level=4] [ref=e1161] + - generic [ref=e1162]: Comment + - generic [ref=e1163]: + - generic [ref=e1164]: + - tablist "Add a comment" [ref=e1165]: + - tab "Write" [selected] [ref=e1166] [cursor=pointer] + - tab "Preview" [ref=e1167] [cursor=pointer] + - toolbar [ref=e1168]: + - generic [ref=e1169]: + - button "Heading" [ref=e1171] [cursor=pointer]: + - img + - button "Bold" [ref=e1173] [cursor=pointer]: + - img + - button "Italic" [ref=e1175] [cursor=pointer]: + - img + - button "Quote" [ref=e1177] [cursor=pointer]: + - img + - button "Code" [ref=e1179] [cursor=pointer]: + - img + - button "Link" [ref=e1181] [cursor=pointer]: + - img + - separator [ref=e1182] + - button "Numbered list" [ref=e1184] [cursor=pointer]: + - img + - button "Unordered list" [ref=e1186] [cursor=pointer]: + - img + - button "Task list" [ref=e1188] [cursor=pointer]: + - img + - separator [ref=e1189] + - button "Attach files" [ref=e1191] [cursor=pointer]: + - img + - button "Mention" [ref=e1193] [cursor=pointer]: + - img + - button "Reference" [ref=e1195] [cursor=pointer]: + - img + - button "Saved replies" [ref=e1197] [cursor=pointer]: + - img + - button "Slash commands" [ref=e1199] [cursor=pointer]: + - img + - tabpanel "Write" [ref=e1200]: + - generic [ref=e1204]: + - textbox "Comment" [ref=e1205]: + - /placeholder: " " + - paragraph: Add your comment here... + - generic [ref=e1206]: + - link "Markdown is supported" [ref=e1208] [cursor=pointer]: + - /url: https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax + - generic [ref=e1209]: + - generic: + - img + - generic [ref=e1210]: Markdown is supported + - button "Paste, drop, or click to add files" [ref=e1211] [cursor=pointer]: + - generic [ref=e1212]: + - generic: + - img + - generic [ref=e1213]: Paste, drop, or click to add files + - generic [ref=e1216]: + - button "Close pull request" [ref=e1218] [cursor=pointer]: + - img [ref=e1219] + - text: Close pull request + - button "Comment" [disabled] [ref=e1222] + - generic [ref=e1223]: + - img [ref=e1224] + - generic [ref=e1226]: + - text: Remember, contributions to this repository should follow its + - link "contributing guidelines" [ref=e1227] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/CONTRIBUTING.md + - text: "," + - link "security policy" [ref=e1228] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/SECURITY.md + - text: ", and" + - link "code of conduct" [ref=e1229] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/eaf2758697e4990505dadc35a522f0a8a2a9748a/CODE_OF_CONDUCT.md + - text: . + - generic [ref=e1230]: + - img [ref=e1231] + - strong [ref=e1233]: ProTip! + - text: Add comments to specific lines under + - link "Files changed" [ref=e1234] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files + - text: . + - generic [ref=e1238]: + - form "Select reviewers" [ref=e1240]: + - heading "Reviewers" [level=3] [ref=e1241] + - generic [ref=e1242]: + - paragraph [ref=e1243]: + - generic [ref=e1244]: + - link "@oz-for-oss" [ref=e1245] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e1246] + - link "oz-for-oss[bot]" [ref=e1247] [cursor=pointer]: + - /url: /apps/oz-for-oss + - link "oz-for-oss[bot] requested changes" [ref=e1248] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes/BASE..169dfa663dd4c533eb32daf850f25d6b9c123521 + - img [ref=e1250] + - paragraph [ref=e1252]: Requested changes must be addressed to merge this pull request. + - generic [ref=e1253]: + - generic [ref=e1254]: Still in progress? + - group [ref=e1255]: + - button "Convert to draft" [ref=e1256] [cursor=pointer] + - form "Select assignees" [ref=e1258]: + - heading "Assignees" [level=3] [ref=e1259] + - text: No one assigned + - generic [ref=e1260]: + - heading "Labels" [level=3] [ref=e1261] + - generic [ref=e1262]: + - link "cla-signed" [ref=e1263] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - generic [ref=e1264]: cla-signed + - link "external-contributor" [ref=e1265] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - generic [ref=e1266]: external-contributor + - form "Select projects" [ref=e1268]: + - heading "Projects" [level=3] [ref=e1269] + - text: None yet + - form "Select milestones" [ref=e1271]: + - heading "Milestone" [level=3] [ref=e1272] + - text: No milestone + - form "Link issues" [ref=e1278]: + - heading "Development" [level=3] [ref=e1279] + - paragraph [ref=e1280]: Successfully merging this pull request may close these issues. + - paragraph [ref=e1282]: None yet + - generic [ref=e1284]: + - button "Notifications Customize" [ref=e1287] [cursor=pointer]: + - generic [ref=e1288]: + - generic [ref=e1289]: Notifications + - generic [ref=e1290]: Customize + - button "Unsubscribe" [ref=e1292] [cursor=pointer]: + - generic [ref=e1293]: + - generic: + - img + - generic [ref=e1294]: Unsubscribe + - paragraph [ref=e1295]: You’re receiving notifications because you were mentioned. + - generic [ref=e1297]: + - heading "1 participant" [level=3] [ref=e1298] + - link "@ErshovDmitry" [ref=e1300] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1301] + - generic [ref=e1304]: + - generic [ref=e1305]: + - checkbox "Allow edits by maintainers" [checked] [ref=e1306] + - text: Allow edits by maintainers + - button "Learn more about allowing maintainer edits" [ref=e1307] [cursor=pointer]: + - img [ref=e1310] + - contentinfo [ref=e1313]: + - heading "Footer" [level=2] [ref=e1314] + - generic [ref=e1315]: + - generic [ref=e1316]: + - link "GitHub Homepage" [ref=e1317] [cursor=pointer]: + - /url: https://github.com + - img [ref=e1318] + - generic [ref=e1320]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e1321]: + - heading "Footer navigation" [level=3] [ref=e1322] + - list "Footer navigation" [ref=e1323]: + - listitem [ref=e1324]: + - link "Terms" [ref=e1325] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e1326]: + - link "Privacy" [ref=e1327] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e1328]: + - link "Security" [ref=e1329] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e1330]: + - link "Status" [ref=e1331] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e1332]: + - link "Community" [ref=e1333] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e1334]: + - link "Docs" [ref=e1335] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e1336]: + - link "Contact" [ref=e1337] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e1338]: + - button "Manage cookies" [ref=e1340] [cursor=pointer] + - listitem [ref=e1341]: + - button "Do not share my personal information" [ref=e1343] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-20T19-21-15-047Z.yml b/.playwright-mcp/page-2026-05-20T19-21-15-047Z.yml new file mode 100644 index 0000000000..0eddad2d79 --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T19-21-15-047Z.yml @@ -0,0 +1,1381 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - link "You have no unread notifications (g then n)" [ref=e58] [cursor=pointer]: + - /url: /notifications + - img [ref=e59] + - button "Open user navigation menu" [ref=e62] [cursor=pointer]: + - img "User avatar" [ref=e63] + - heading "Repository navigation" [level=2] [ref=e64] + - navigation "Repository" [ref=e65]: + - list [ref=e66]: + - listitem [ref=e67]: + - link "Code" [ref=e68] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e70] + - generic [ref=e72]: Code + - listitem [ref=e73]: + - link "Issues (3.4k)" [ref=e74] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e76] + - generic [ref=e79]: Issues + - generic [ref=e80]: + - generic [ref=e81]: 3.4k + - generic [ref=e82]: (3.4k) + - listitem [ref=e83]: + - link "Pull requests (491)" [ref=e84] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e86] + - generic [ref=e88]: Pull requests + - generic [ref=e89]: + - generic [ref=e90]: "491" + - generic [ref=e91]: (491) + - listitem [ref=e92]: + - link "Agents" [ref=e93] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e95] + - generic [ref=e98]: Agents + - listitem [ref=e99]: + - link "Discussions" [ref=e100] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e102] + - generic [ref=e104]: Discussions + - listitem [ref=e105]: + - link "Actions" [ref=e106] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e108] + - generic [ref=e110]: Actions + - listitem [ref=e111]: + - link "Projects" [ref=e112] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e114] + - generic [ref=e116]: Projects + - listitem [ref=e117]: + - link "Security and quality" [ref=e118] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e120] + - generic [ref=e122]: Security and quality + - listitem [ref=e123]: + - link "Insights" [ref=e124] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e126] + - generic [ref=e128]: Insights + - main [ref=e132]: + - generic [ref=e139]: + - generic [ref=e142]: + - 'heading "I18n #11382 Edit title" [level=1] [ref=e144]': + - text: I18n + - generic [ref=e145]: + - generic [ref=e146]: "#11382" + - button "Edit title" [ref=e147] [cursor=pointer]: + - img [ref=e148] + - generic [ref=e151]: + - generic [ref=e152]: + - button "View status View status" [disabled] [ref=e153]: + - generic [ref=e154]: + - generic: + - generic: + - img + - generic: Loading + - generic [ref=e156]: Loading merge status + - button "Code" [ref=e157] [cursor=pointer]: + - generic [ref=e158]: + - generic [ref=e159]: Code + - generic: + - img + - generic [ref=e161]: + - generic [ref=e163]: + - img "Pull request" [ref=e164] + - text: Open + - generic [ref=e167]: + - link "ErshovDmitry" [ref=e168] [cursor=pointer]: + - /url: /ErshovDmitry + - text: wants to merge 21 commits into + - generic [ref=e169]: + - link "warpdotdev:master" [ref=e170] [cursor=pointer]: + - /url: /warpdotdev/warp/tree/master + - generic [ref=e171]: from + - generic [ref=e172]: + - link "ErshovDmitry:i18n" [ref=e173] [cursor=pointer]: + - /url: /ErshovDmitry/warp-i18n/tree/i18n + - button "Copy head branch name to clipboard" [ref=e174] [cursor=pointer]: + - img [ref=e175] + - navigation "Pull request navigation tabs" [ref=e181]: + - tablist [ref=e182]: + - tab "Conversation" [selected] [ref=e183] [cursor=pointer]: + - img [ref=e184] + - text: Conversation + - tab "Commits (21)" [ref=e186] [cursor=pointer]: + - img [ref=e187] + - text: Commits + - generic [ref=e189]: "21" + - generic [ref=e190]: (21) + - tab "Checks" [ref=e191] [cursor=pointer]: + - img [ref=e192] + - text: Checks + - tab "Files changed" [ref=e194] [cursor=pointer]: + - img [ref=e195] + - text: Files changed + - generic [ref=e201]: + - generic [ref=e203]: + - heading "Conversation" [level=2] [ref=e204] + - generic [ref=e205]: + - generic [ref=e206]: + - generic [ref=e208]: + - link "@ErshovDmitry" [ref=e209] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e210] + - generic [ref=e212]: + - generic [ref=e213]: + - group [ref=e216]: + - button "Show options" [ref=e217] [cursor=pointer]: + - img "Show options" [ref=e220] + - heading "ErshovDmitry commented May 20, 202615 hours ago" [level=3] [ref=e222]: + - generic [ref=e223]: + - strong [ref=e224]: + - link "ErshovDmitry" [ref=e225] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202615 hours ago" [ref=e226] [cursor=pointer]: + - /url: "#issue-4483039846" + - generic [ref=e230]: + - paragraph [ref=e231]: Hello! My name is Dmitry. + - paragraph [ref=e232]: I've been using Warp and love it. I wanted to add i18n support so the terminal could speak Russian (and other languages in the future). I'm not a developer myself — this was built with DeepSeek AI, but I reviewed everything personally. + - paragraph [ref=e233]: I'd much rather this live in the official repo than a fork. If the team is interested in i18n, I'm happy to help however I can — and I'll gladly delete my fork once upstreamed. + - paragraph [ref=e234]: + - text: Cheers, + - text: Dmitry + - separator [ref=e235] + - heading "What" [level=2] [ref=e236] + - paragraph [ref=e237]: Adds i18n (internationalization) support to Warp. + - heading "Changes" [level=2] [ref=e238] + - 'heading "New crate: warp_i18n" [level=3] [ref=e239]': + - text: "New crate:" + - code [ref=e240]: warp_i18n + - list [ref=e241]: + - listitem [ref=e242]: + - code [ref=e243]: t!() + - text: macro for translating UI strings + - listitem [ref=e244]: + - code [ref=e245]: current_locale() + - text: / + - code [ref=e246]: set_current_locale() + - text: for runtime language switching + - listitem [ref=e247]: + - code [ref=e248]: init_from_json() + - text: for WASM compatibility + - listitem [ref=e249]: Build script for compile-time key validation + - listitem [ref=e250]: 21 tests + - heading "Feature flag" [level=3] [ref=e251] + - list [ref=e252]: + - listitem [ref=e253]: + - code [ref=e254]: FeatureFlag::I18n + - text: in + - code [ref=e255]: DOGFOOD_FLAGS + - heading "Settings" [level=3] [ref=e256] + - list [ref=e257]: + - listitem [ref=e258]: + - code [ref=e259]: Locale + - text: enum (En, Ru) + - listitem [ref=e260]: Language dropdown in Settings → Appearance + - listitem [ref=e261]: Auto-detect system locale on first run + - heading "Migrated UI" [level=3] [ref=e262] + - list [ref=e263]: + - listitem [ref=e264]: 10 top-level menu labels + - listitem [ref=e265]: 14 settings section names + - listitem [ref=e266]: 11 settings category labels + - heading "Locales" [level=3] [ref=e267] + - list [ref=e268]: + - listitem [ref=e269]: + - code [ref=e270]: en.json + - text: — 51 keys (mandatory fallback) + - listitem [ref=e271]: + - code [ref=e272]: ru.json + - text: — 51 keys (Russian translation) + - paragraph [ref=e273]: + - emphasis [ref=e274]: + - text: Built with DeepSeek AI — human-reviewed, all tests pass ( + - code [ref=e275]: cargo check --workspace + - text: "," + - code [ref=e276]: cargo test -p warp_i18n + - text: ). + - group [ref=e280]: + - generic "Add or remove reactions" [ref=e281] [cursor=pointer]: + - img [ref=e282] + - generic [ref=e284]: + - generic [ref=e286]: + - generic [ref=e287]: + - img [ref=e289] + - generic [ref=e291]: + - link "ErshovDmitry" [ref=e292] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 5 commits + - link "May 20, 2026 09:1517 hours ago" [ref=e293] [cursor=pointer]: + - /url: "#commits-pushed-f06dc63" + - generic [ref=e294]: + - generic [ref=e295]: + - img [ref=e297] + - generic [ref=e302]: + - link "@ErshovDmitry" [ref=e305] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e306] + - code [ref=e308]: + - 'link "feat: add warp_i18n crate with t!() macro and en/ru locales" [ref=e309] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - code [ref=e317]: + - link "f06dc63" [ref=e318] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f06dc6330a0585daf34c7614877e67eb40c1044d + - generic [ref=e319]: + - img [ref=e321] + - generic [ref=e326]: + - link "@ErshovDmitry" [ref=e329] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e330] + - generic [ref=e331]: + - code [ref=e332]: + - 'link "feat: i18n Phase 2 — FeatureFlag, Locale setting, app_menus migration…" [ref=e333] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - button "Commit message body" [ref=e335] [cursor=pointer]: … + - code [ref=e343]: + - link "643f6c3" [ref=e344] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/643f6c3998f83bba65e9d7ce59903e584a7fde4f + - generic [ref=e345]: + - img [ref=e347] + - generic [ref=e352]: + - link "@ErshovDmitry" [ref=e355] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e356] + - generic [ref=e357]: + - code [ref=e358]: + - 'link "fix: wire warp_i18n::init() at startup, sync LocaleSettings with i18n…" [ref=e359] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - button "Commit message body" [ref=e361] [cursor=pointer]: … + - code [ref=e369]: + - link "a9e9e26" [ref=e370] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/a9e9e26dcc1fbe397120d6fd2cd4f28a69c8b843 + - generic [ref=e371]: + - img [ref=e373] + - generic [ref=e378]: + - link "@ErshovDmitry" [ref=e381] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e382] + - code [ref=e384]: + - 'link "docs: add English translation of the fork note in README" [ref=e385] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - code [ref=e393]: + - link "3911714" [ref=e394] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/3911714c37b1d8609cf42df1b770099c73e32e24 + - generic [ref=e395]: + - img [ref=e397] + - generic [ref=e402]: + - link "@ErshovDmitry" [ref=e405] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e406] + - generic [ref=e407]: + - code [ref=e408]: + - 'link "feat: i18n Phase 3 — build script key validation, WASM support, setti…" [ref=e409] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - button "Commit message body" [ref=e411] [cursor=pointer]: … + - code [ref=e419]: + - link "169dfa6" [ref=e420] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e422]: + - link "@cla-bot" [ref=e424] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e425] + - generic [ref=e427]: + - generic [ref=e428]: + - group [ref=e431]: + - button "Show options" [ref=e432] [cursor=pointer]: + - img "Show options" [ref=e435] + - heading "cla-bot Bot commented May 20, 202615 hours ago" [level=3] [ref=e437]: + - generic [ref=e438]: + - strong [ref=e439]: + - link "cla-bot" [ref=e440] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e441]: Bot + - text: commented + - link "May 20, 202615 hours ago" [ref=e442] [cursor=pointer]: + - /url: "#issuecomment-4494411354" + - generic [ref=e443]: + - table [ref=e445]: + - rowgroup [ref=e446]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e447]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e448]': + - paragraph [ref=e449]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e450]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e451]: + - listitem [ref=e452]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e453]: git config --list | grep email + - listitem [ref=e454]: + - text: If not, set it up using + - code [ref=e455]: git config --global user.email email@example.com + - listitem [ref=e456]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e457] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e462]: + - generic "Add or remove reactions" [ref=e463] [cursor=pointer]: + - img [ref=e464] + - generic [ref=e467]: + - link "@oz-for-oss" [ref=e469] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e470] + - generic [ref=e472]: + - generic [ref=e473]: + - generic [ref=e474]: + - group [ref=e476]: + - button "Show options" [ref=e477] [cursor=pointer]: + - img "Show options" [ref=e480] + - generic "This user has previously committed to the warp repository." [ref=e483]: + - generic [ref=e484]: Contributor + - heading "oz-for-oss Bot commented May 20, 202615 hours ago" [level=3] [ref=e485]: + - generic [ref=e486]: + - strong [ref=e487]: + - link "oz-for-oss" [ref=e488] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e489]: Bot + - text: commented + - link "May 20, 202615 hours ago" [ref=e490] [cursor=pointer]: + - /url: "#issuecomment-4494411637" + - generic [ref=e491]: + - table [ref=e493]: + - rowgroup [ref=e494]: + - 'row "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e495]': + - 'cell "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e496]': + - paragraph [ref=e497]: + - link "@ErshovDmitry" [ref=e498] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e499]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e500]: ready-to-implement + - text: . + - paragraph [ref=e501]: "Issue-state enforcement details:" + - list [ref=e502]: + - listitem [ref=e503]: + - paragraph [ref=e504]: "Associated same-repo issues checked: none" + - listitem [ref=e505]: + - paragraph [ref=e506]: + - text: "Required readiness label:" + - code [ref=e507]: ready-to-implement + - paragraph [ref=e508]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e509]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e510]: ready-to-implement + - text: . + - paragraph [ref=e511]: + - emphasis [ref=e512]: + - text: Powered by + - link "Oz" [ref=e513] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e518]: + - generic "Add or remove reactions" [ref=e519] [cursor=pointer]: + - img [ref=e520] + - generic [ref=e523]: + - img [ref=e525] + - generic [ref=e527]: + - link "@github-actions" [ref=e528] [cursor=pointer]: + - /url: /apps/github-actions + - img "@github-actions" [ref=e529] + - link "github-actions" [ref=e530] [cursor=pointer]: + - /url: /apps/github-actions + - generic [ref=e531]: Bot + - text: added the + - link "external-contributor" [ref=e532] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - text: label + - link "May 20, 202615 hours ago" [ref=e533] [cursor=pointer]: + - /url: "#event-25738319166" + - generic [ref=e536]: + - generic [ref=e537]: + - link "oz-for-oss[bot]" [ref=e538] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "oz-for-oss[bot]" [ref=e539] + - img [ref=e541] + - generic [ref=e543]: + - generic [ref=e544]: + - strong [ref=e545]: + - link "oz-for-oss" [ref=e546] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e547]: Bot + - text: requested changes + - link "May 20, 202615 hours ago" [ref=e549] [cursor=pointer]: + - /url: "#pullrequestreview-4325084051" + - link "View reviewed changes" [ref=e551] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files/169dfa663dd4c533eb32daf850f25d6b9c123521 + - generic [ref=e553]: View reviewed changes + - generic [ref=e556]: + - generic [ref=e557]: + - generic [ref=e558]: + - group [ref=e560]: + - button "Show options" [ref=e561] [cursor=pointer]: + - img "Show options" [ref=e564] + - generic "This user has previously committed to the warp repository." [ref=e567]: + - generic [ref=e568]: Contributor + - heading "oz-for-oss Bot left a comment" [level=3] [ref=e569]: + - generic [ref=e570]: + - strong [ref=e571]: + - link "oz-for-oss" [ref=e572] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e573]: Bot + - text: left a comment + - generic [ref=e576]: + - paragraph [ref=e577]: + - link "@ErshovDmitry" [ref=e578] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e579]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e580]: ready-to-implement + - text: . + - paragraph [ref=e581]: "Issue-state enforcement details:" + - list [ref=e582]: + - listitem [ref=e583]: + - paragraph [ref=e584]: "Associated same-repo issues checked: none" + - listitem [ref=e585]: + - paragraph [ref=e586]: + - text: "Required readiness label:" + - code [ref=e587]: ready-to-implement + - paragraph [ref=e588]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e589]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e590]: ready-to-implement + - text: . + - paragraph [ref=e591]: + - emphasis [ref=e592]: + - text: Powered by + - link "Oz" [ref=e593] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e597]: + - generic "Add or remove reactions" [ref=e598] [cursor=pointer]: + - img [ref=e599] + - generic [ref=e602]: + - link "@ErshovDmitry" [ref=e604] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e605] + - generic [ref=e607]: + - generic [ref=e608]: + - generic [ref=e609]: + - group [ref=e611]: + - button "Show options" [ref=e612] [cursor=pointer]: + - img "Show options" [ref=e615] + - generic "You are the author of this pull request." [ref=e618]: + - generic [ref=e619]: Author + - heading "ErshovDmitry commented May 20, 202615 hours ago" [level=3] [ref=e620]: + - generic [ref=e621]: + - strong [ref=e622]: + - link "ErshovDmitry" [ref=e623] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202615 hours ago" [ref=e624] [cursor=pointer]: + - /url: "#issuecomment-4494430862" + - generic [ref=e625]: + - table [ref=e627]: + - rowgroup [ref=e628]: + - 'row "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e629]': + - 'cell "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e630]': + - paragraph [ref=e631]: + - text: Oh wow — I just noticed there are already several i18n PRs open ( + - link "#10630" [ref=e632] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "," + - link "#10990" [ref=e633] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: "," + - link "#9458" [ref=e634] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9458 + - text: "," + - link "#9922" [ref=e635] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9922 + - text: ). I should have checked first before opening mine. My apologies for the noise! + - blockquote [ref=e636]: + - paragraph [ref=e637]: + - text: That said, I see none of them include Russian yet. If the team settles on one i18n approach (looks like + - link "#10630" [ref=e638] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e639] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: is the leading one?), I'd be happy to contribute Russian translations to that effort instead. + - paragraph [ref=e640]: I'll close this PR if it's just adding clutter — just let me know. + - paragraph [ref=e641]: + - text: Cheers, + - text: Dmitry + - group [ref=e646]: + - generic "Add or remove reactions" [ref=e647] [cursor=pointer]: + - img [ref=e648] + - generic [ref=e653]: + - img [ref=e655] + - generic [ref=e660]: + - link "@ErshovDmitry" [ref=e663] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e664] + - generic [ref=e665]: + - code [ref=e666]: + - 'link "refactor: migrate to ZacharyZcR-compatible YAML i18n with Russian locale" [ref=e667] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - button "Commit message body" [ref=e669] [cursor=pointer]: … + - code [ref=e677]: + - link "04801f0" [ref=e678] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/04801f0e17c9a7610cc080c88d33bfe8062c7658 + - generic [ref=e680]: + - link "@cla-bot" [ref=e682] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e683] + - generic [ref=e685]: + - generic [ref=e686]: + - group [ref=e689]: + - button "Show options" [ref=e690] [cursor=pointer]: + - img "Show options" [ref=e693] + - heading "cla-bot Bot commented May 20, 202613 hours ago" [level=3] [ref=e695]: + - generic [ref=e696]: + - strong [ref=e697]: + - link "cla-bot" [ref=e698] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e699]: Bot + - text: commented + - link "May 20, 202613 hours ago" [ref=e700] [cursor=pointer]: + - /url: "#issuecomment-4494962896" + - generic [ref=e701]: + - table [ref=e703]: + - rowgroup [ref=e704]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e705]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e706]': + - paragraph [ref=e707]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e708]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e709]: + - listitem [ref=e710]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e711]: git config --list | grep email + - listitem [ref=e712]: + - text: If not, set it up using + - code [ref=e713]: git config --global user.email email@example.com + - listitem [ref=e714]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e715] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e720]: + - generic "Add or remove reactions" [ref=e721] [cursor=pointer]: + - img [ref=e722] + - generic [ref=e727]: + - img [ref=e729] + - generic [ref=e734]: + - link "@ErshovDmitry" [ref=e737] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e738] + - code [ref=e740]: + - 'link "fix: init_locale before menu bar, cleanup dead code, harden set_locale" [ref=e741] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - code [ref=e749]: + - link "1caedf3" [ref=e750] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1caedf315b31562ffa33fb2f18a506ac6a62ad92 + - generic [ref=e752]: + - link "@cla-bot" [ref=e754] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e755] + - generic [ref=e757]: + - generic [ref=e758]: + - group [ref=e761]: + - button "Show options" [ref=e762] [cursor=pointer]: + - img "Show options" [ref=e765] + - heading "cla-bot Bot commented May 20, 202613 hours ago" [level=3] [ref=e767]: + - generic [ref=e768]: + - strong [ref=e769]: + - link "cla-bot" [ref=e770] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e771]: Bot + - text: commented + - link "May 20, 202613 hours ago" [ref=e772] [cursor=pointer]: + - /url: "#issuecomment-4495220665" + - generic [ref=e773]: + - table [ref=e775]: + - rowgroup [ref=e776]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e777]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e778]': + - paragraph [ref=e779]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e780]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e781]: + - listitem [ref=e782]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e783]: git config --list | grep email + - listitem [ref=e784]: + - text: If not, set it up using + - code [ref=e785]: git config --global user.email email@example.com + - listitem [ref=e786]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e787] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e792]: + - generic "Add or remove reactions" [ref=e793] [cursor=pointer]: + - img [ref=e794] + - generic [ref=e799]: + - img [ref=e801] + - generic [ref=e806]: + - link "@ErshovDmitry" [ref=e809] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e810] + - generic [ref=e811]: + - code [ref=e812]: + - 'link "fix(i18n): Round 2 review — internationalize all hardcoded menu strings" [ref=e813] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - button "Commit message body" [ref=e815] [cursor=pointer]: … + - code [ref=e823]: + - link "1b026c7" [ref=e824] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1b026c76d5f47cc216d05a96d0b2d542f48e8ebf + - generic [ref=e826]: + - link "@cla-bot" [ref=e828] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e829] + - generic [ref=e831]: + - generic [ref=e832]: + - group [ref=e835]: + - button "Show options" [ref=e836] [cursor=pointer]: + - img "Show options" [ref=e839] + - heading "cla-bot Bot commented May 20, 202612 hours ago" [level=3] [ref=e841]: + - generic [ref=e842]: + - strong [ref=e843]: + - link "cla-bot" [ref=e844] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e845]: Bot + - text: commented + - link "May 20, 202612 hours ago" [ref=e846] [cursor=pointer]: + - /url: "#issuecomment-4495434298" + - generic [ref=e847]: + - table [ref=e849]: + - rowgroup [ref=e850]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e851]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e852]': + - paragraph [ref=e853]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e854]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e855]: + - listitem [ref=e856]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e857]: git config --list | grep email + - listitem [ref=e858]: + - text: If not, set it up using + - code [ref=e859]: git config --global user.email email@example.com + - listitem [ref=e860]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e861] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e866]: + - generic "Add or remove reactions" [ref=e867] [cursor=pointer]: + - img [ref=e868] + - generic [ref=e871]: + - link "@ErshovDmitry" [ref=e873] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e874] + - generic [ref=e876]: + - generic [ref=e877]: + - generic [ref=e878]: + - group [ref=e880]: + - button "Show options" [ref=e881] [cursor=pointer]: + - img "Show options" [ref=e884] + - generic "You are the author of this pull request." [ref=e887]: + - generic [ref=e888]: Author + - heading "ErshovDmitry commented May 20, 202612 hours ago" [level=3] [ref=e889]: + - generic [ref=e890]: + - strong [ref=e891]: + - link "ErshovDmitry" [ref=e892] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202612 hours ago" [ref=e893] [cursor=pointer]: + - /url: "#issuecomment-4495523677" + - generic [ref=e894]: + - table [ref=e896]: + - rowgroup [ref=e897]: + - 'row "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e898]': + - 'cell "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e899]': + - paragraph [ref=e900]: + - text: Update:** I've rewritten this PR to match + - link "@ZacharyZcR" [ref=e901] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: "'s YAML approach from" + - link "#10630" [ref=e902] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e903] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: ":" + - blockquote [ref=e904]: + - list [ref=e905]: + - listitem [ref=e906]: + - code [ref=e907]: crates/i18n/ + - text: with + - code [ref=e908]: t!() + - text: / + - code [ref=e909]: t_required!() + - text: macros and + - code [ref=e910]: TranslationLookup + - listitem [ref=e911]: + - code [ref=e912]: "resources/bundled/locales/{en,ru}.yml" + - text: (247 keys each, YAML) + - listitem [ref=e913]: + - code [ref=e914]: WARP_LANG + - text: env var → system locale → + - code [ref=e915]: en + - text: fallback + - listitem [ref=e916]: + - text: No feature flag, no settings UI — same design as + - generic [ref=e917]: + - img [ref=e918] + - 'link "Add i18n framework with Simplified Chinese (zh-CN) support #10630" [ref=e920] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/pull/10630 + - paragraph [ref=e921]: + - text: The Russian + - code [ref=e922]: ru.yml + - text: should be a drop-in addition to the framework once it's approved. Happy to rebase onto whichever PR lands first. + - paragraph [ref=e923]: Dmitry + - group [ref=e928]: + - generic "Add or remove reactions" [ref=e929] [cursor=pointer]: + - img [ref=e930] + - generic [ref=e933]: + - link "@ErshovDmitry" [ref=e935] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e936] + - generic [ref=e938]: + - generic [ref=e939]: + - generic [ref=e940]: + - group [ref=e942]: + - button "Show options" [ref=e943] [cursor=pointer]: + - img "Show options" [ref=e946] + - generic "You are the author of this pull request." [ref=e949]: + - generic [ref=e950]: Author + - heading "ErshovDmitry commented May 20, 202612 hours ago" [level=3] [ref=e951]: + - generic [ref=e952]: + - strong [ref=e953]: + - link "ErshovDmitry" [ref=e954] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202612 hours ago" [ref=e955] [cursor=pointer]: + - /url: "#issuecomment-4495572049" + - generic [ref=e956]: + - table [ref=e958]: + - rowgroup [ref=e959]: + - row "@cla-bot check" [ref=e960]: + - cell "@cla-bot check" [ref=e961]: + - paragraph [ref=e962]: + - link "@cla-bot" [ref=e963] [cursor=pointer]: + - /url: https://github.com/cla-bot + - text: check + - group [ref=e968]: + - generic "Add or remove reactions" [ref=e969] [cursor=pointer]: + - img [ref=e970] + - generic [ref=e973]: + - img [ref=e975] + - generic [ref=e977]: + - link "@cla-bot" [ref=e978] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e979] + - link "cla-bot" [ref=e980] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e981]: Bot + - text: added the + - link "cla-signed" [ref=e982] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - text: label + - link "May 20, 202612 hours ago" [ref=e983] [cursor=pointer]: + - /url: "#event-25743817179" + - generic [ref=e985]: + - link "@cla-bot" [ref=e987] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e988] + - generic [ref=e990]: + - generic [ref=e991]: + - group [ref=e994]: + - button "Show options" [ref=e995] [cursor=pointer]: + - img "Show options" [ref=e998] + - heading "cla-bot Bot commented May 20, 202612 hours ago" [level=3] [ref=e1000]: + - generic [ref=e1001]: + - strong [ref=e1002]: + - link "cla-bot" [ref=e1003] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e1004]: Bot + - text: commented + - link "May 20, 202612 hours ago" [ref=e1005] [cursor=pointer]: + - /url: "#issuecomment-4495573189" + - generic [ref=e1006]: + - table [ref=e1008]: + - rowgroup [ref=e1009]: + - row "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1010]: + - cell "The cla-bot has been summoned, and re-checked this pull request!" [ref=e1011]: + - paragraph [ref=e1012]: The cla-bot has been summoned, and re-checked this pull request! + - group [ref=e1017]: + - generic "Add or remove reactions" [ref=e1018] [cursor=pointer]: + - img [ref=e1019] + - generic [ref=e1022]: + - link "@ErshovDmitry" [ref=e1024] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1025] + - generic [ref=e1027]: + - generic [ref=e1028]: + - generic [ref=e1029]: + - group [ref=e1031]: + - button "Show options" [ref=e1032] [cursor=pointer]: + - img "Show options" [ref=e1035] + - generic "You are the author of this pull request." [ref=e1038]: + - generic [ref=e1039]: Author + - heading "ErshovDmitry commented May 20, 202612 hours ago" [level=3] [ref=e1040]: + - generic [ref=e1041]: + - strong [ref=e1042]: + - link "ErshovDmitry" [ref=e1043] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202612 hours ago" [ref=e1044] [cursor=pointer]: + - /url: "#issuecomment-4495607261" + - generic [ref=e1045]: + - table [ref=e1047]: + - rowgroup [ref=e1048]: + - 'row "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e1049]': + - 'cell "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e1050]': + - paragraph [ref=e1051]: + - link "@oss-maintainers" [ref=e1052] [cursor=pointer]: + - /url: https://github.com/oss-maintainers + - text: This PR now follows the same YAML-based approach as + - link "#10630" [ref=e1053] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e1054] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: by + - link "@ZacharyZcR" [ref=e1055] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: — matching + - code [ref=e1056]: crates/i18n/ + - text: API, + - code [ref=e1057]: "resources/bundled/locales/{en,ru}.yml" + - text: "," + - code [ref=e1058]: t!() + - text: / + - code [ref=e1059]: t_required!() + - text: macros, and + - code [ref=e1060]: WARP_LANG + - text: env var detection. + - paragraph [ref=e1061]: + - text: It's blocked by the issue-state enforcement check (needs a + - code [ref=e1062]: ready-to-implement + - text: issue). Could a maintainer please add + - code [ref=e1063]: ready-to-implement + - text: to + - link "#1194" [ref=e1064] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194 + - text: ", or let me know if I should create a separate tracking issue?" + - paragraph [ref=e1065]: + - text: Russian + - code [ref=e1066]: ru.yml + - text: (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. + - paragraph [ref=e1067]: + - text: Cheers, + - text: Dmitry + - group [ref=e1072]: + - generic "Add or remove reactions" [ref=e1073] [cursor=pointer]: + - img [ref=e1074] + - generic [ref=e1079]: + - img [ref=e1081] + - generic [ref=e1086]: + - link "@ErshovDmitry" [ref=e1089] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1090] + - code [ref=e1092]: + - 'link "chore: add .playwright-mcp to .gitignore" [ref=e1093] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/d67453f73b0087f3a931fd06e958ab9cebb3be0c + - code [ref=e1101]: + - link "d67453f" [ref=e1102] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d67453f73b0087f3a931fd06e958ab9cebb3be0c + - generic [ref=e1104]: + - img [ref=e1106] + - generic [ref=e1108]: + - link "@ErshovDmitry" [ref=e1109] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1110] + - link "ErshovDmitry" [ref=e1111] [cursor=pointer]: + - /url: /ErshovDmitry + - link "force-pushed" [ref=e1112] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - text: the + - generic [ref=e1114]: i18n + - text: branch from + - link "2196591" [ref=e1115] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/219659155d8415b5773506f3306f6339e1300b60 + - code [ref=e1116]: "2196591" + - text: to + - link "d67453f" [ref=e1117] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/d67453f73b0087f3a931fd06e958ab9cebb3be0c + - code [ref=e1118]: d67453f + - link "Compare" [ref=e1119] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - generic [ref=e1121]: Compare + - link "May 20, 2026 14:3611 hours ago" [ref=e1122] [cursor=pointer]: + - /url: "#event-25744804343" + - generic [ref=e1124]: + - generic [ref=e1125]: + - img [ref=e1127] + - generic [ref=e1129]: + - link "ErshovDmitry" [ref=e1130] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 5 commits + - link "May 20, 2026 18:447 hours ago" [ref=e1131] [cursor=pointer]: + - /url: "#commits-pushed-7222027" + - generic [ref=e1132]: + - generic [ref=e1133]: + - img [ref=e1135] + - generic [ref=e1140]: + - link "@ErshovDmitry" [ref=e1143] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1144] + - generic [ref=e1145]: + - code [ref=e1146]: + - 'link "i18n(keybindings,features): replace hardcoded English strings with me…" [ref=e1147] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/72220272c1ea859b37921b24de98e4812650d3f2 + - button "Commit message body" [ref=e1149] [cursor=pointer]: … + - code [ref=e1157]: + - link "7222027" [ref=e1158] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/72220272c1ea859b37921b24de98e4812650d3f2 + - generic [ref=e1159]: + - img [ref=e1161] + - generic [ref=e1166]: + - link "@ErshovDmitry" [ref=e1169] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1170] + - generic [ref=e1171]: + - code [ref=e1172]: + - 'link "i18n(billing,workspace): internationalize billing page and workspace …" [ref=e1173] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/44d1763603f43a7afd7981517ff6a345070b61f1 + - button "Commit message body" [ref=e1175] [cursor=pointer]: … + - code [ref=e1183]: + - link "44d1763" [ref=e1184] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/44d1763603f43a7afd7981517ff6a345070b61f1 + - generic [ref=e1185]: + - img [ref=e1187] + - generic [ref=e1192]: + - link "@ErshovDmitry" [ref=e1195] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1196] + - generic [ref=e1197]: + - code [ref=e1198]: + - 'link "i18n(ai_page): internationalize AI settings page strings" [ref=e1199] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1e5e30f81057d5a300a15296f810845d22bf276b + - button "Commit message body" [ref=e1201] [cursor=pointer]: … + - code [ref=e1209]: + - link "1e5e30f" [ref=e1210] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1e5e30f81057d5a300a15296f810845d22bf276b + - generic [ref=e1211]: + - img [ref=e1213] + - generic [ref=e1218]: + - link "@ErshovDmitry" [ref=e1221] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1222] + - generic [ref=e1223]: + - code [ref=e1224]: + - 'link "i18n(workspace,code): internationalize workspace bindings and code se…" [ref=e1225] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1c984ba52d915545335615f979bc46dffa5d9c5b + - button "Commit message body" [ref=e1227] [cursor=pointer]: … + - code [ref=e1235]: + - link "1c984ba" [ref=e1236] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1c984ba52d915545335615f979bc46dffa5d9c5b + - generic [ref=e1237]: + - img [ref=e1239] + - generic [ref=e1244]: + - link "@ErshovDmitry" [ref=e1247] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1248] + - generic [ref=e1249]: + - code [ref=e1250]: + - 'link "i18n(settings): internationalize privacy, teams, environments, drive,…" [ref=e1251] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/b829e7025c72890882a7d3ac265a7f5198983dfd + - button "Commit message body" [ref=e1253] [cursor=pointer]: … + - code [ref=e1261]: + - link "b829e70" [ref=e1262] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/b829e7025c72890882a7d3ac265a7f5198983dfd + - generic [ref=e1264]: + - link "@ZacharyZcR" [ref=e1266] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e1267] + - generic [ref=e1269]: + - generic [ref=e1270]: + - group [ref=e1273]: + - button "Show options" [ref=e1274] [cursor=pointer]: + - img "Show options" [ref=e1277] + - heading "ZacharyZcR commented May 20, 20264 hours ago" [level=3] [ref=e1279]: + - generic [ref=e1280]: + - strong [ref=e1281]: + - link "ZacharyZcR" [ref=e1282] [cursor=pointer]: + - /url: /ZacharyZcR + - text: commented + - link "May 20, 20264 hours ago" [ref=e1283] [cursor=pointer]: + - /url: "#issuecomment-4499865827" + - generic [ref=e1284]: + - table [ref=e1286]: + - rowgroup [ref=e1287]: + - row "Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel." [ref=e1288]: + - cell "Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel." [ref=e1289]: + - paragraph [ref=e1290]: Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel. + - group [ref=e1295]: + - generic "Add or remove reactions" [ref=e1296] [cursor=pointer]: + - img [ref=e1297] + - generic [ref=e1302]: + - img [ref=e1304] + - generic [ref=e1309]: + - link "@ErshovDmitry" [ref=e1312] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1313] + - generic [ref=e1314]: + - code [ref=e1315]: + - 'link "i18n: internationalize code_review_view and notebooks editor view" [ref=e1316] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/76995d11ae03daf52865f46bf9ed18853a78e801 + - button "Commit message body" [ref=e1318] [cursor=pointer]: … + - code [ref=e1326]: + - link "76995d1" [ref=e1327] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/76995d11ae03daf52865f46bf9ed18853a78e801 + - generic [ref=e1329]: + - generic [ref=e1330]: + - img [ref=e1332] + - generic [ref=e1334]: + - link "ErshovDmitry" [ref=e1335] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 6 commits + - link "May 20, 2026 23:023 hours ago" [ref=e1336] [cursor=pointer]: + - /url: "#commits-pushed-b8a5d3d" + - generic [ref=e1337]: + - generic [ref=e1338]: + - img [ref=e1340] + - generic [ref=e1345]: + - link "@ErshovDmitry" [ref=e1348] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1349] + - generic [ref=e1350]: + - code [ref=e1351]: + - 'link "i18n: internationalize terminal/input.rs and ai/agent_management/view.rs" [ref=e1352] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/b8a5d3d98876854b5bbff231ad28386ba192fbb2 + - button "Commit message body" [ref=e1354] [cursor=pointer]: … + - code [ref=e1362]: + - link "b8a5d3d" [ref=e1363] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/b8a5d3d98876854b5bbff231ad28386ba192fbb2 + - generic [ref=e1364]: + - img [ref=e1366] + - generic [ref=e1371]: + - link "@ErshovDmitry" [ref=e1374] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1375] + - generic [ref=e1376]: + - code [ref=e1377]: + - 'link "i18n(dialogs): internationalize common buttons, toasts, placeholders" [ref=e1378] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/2e2aef6a05c5bcd671ca51bd027166e545a52332 + - button "Commit message body" [ref=e1380] [cursor=pointer]: … + - code [ref=e1388]: + - link "2e2aef6" [ref=e1389] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/2e2aef6a05c5bcd671ca51bd027166e545a52332 + - generic [ref=e1390]: + - img [ref=e1392] + - generic [ref=e1397]: + - link "@ErshovDmitry" [ref=e1400] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1401] + - code [ref=e1403]: + - 'link "fix(i18n): add missing settings.code_page Russian translations" [ref=e1404] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/c1d0d054e88c1e0e971c7b9db6a58d515391e017 + - code [ref=e1412]: + - link "c1d0d05" [ref=e1413] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/c1d0d054e88c1e0e971c7b9db6a58d515391e017 + - generic [ref=e1414]: + - img [ref=e1416] + - generic [ref=e1421]: + - link "@ErshovDmitry" [ref=e1424] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1425] + - generic [ref=e1426]: + - code [ref=e1427]: + - 'link "i18n(workspace): internationalize remaining workspace/view.rs strings" [ref=e1428] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/52c83e6d7e700b019f9807a1e8cf5b3cd6189ff5 + - button "Commit message body" [ref=e1430] [cursor=pointer]: … + - code [ref=e1438]: + - link "52c83e6" [ref=e1439] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/52c83e6d7e700b019f9807a1e8cf5b3cd6189ff5 + - generic [ref=e1440]: + - img [ref=e1442] + - generic [ref=e1447]: + - link "@ErshovDmitry" [ref=e1450] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1451] + - generic [ref=e1452]: + - code [ref=e1453]: + - 'link "i18n(terminal): internationalize terminal view notifications, buttons…" [ref=e1454] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/15b6abe77897f433ad0f2c278ffaff98dca1feb9 + - button "Commit message body" [ref=e1456] [cursor=pointer]: … + - code [ref=e1464]: + - link "15b6abe" [ref=e1465] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/15b6abe77897f433ad0f2c278ffaff98dca1feb9 + - generic [ref=e1466]: + - img [ref=e1468] + - generic [ref=e1473]: + - link "@ErshovDmitry" [ref=e1476] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1477] + - generic [ref=e1478]: + - code [ref=e1479]: + - 'link "fix: remove stale LocaleSettings import, merge duplicate notebook YAM…" [ref=e1480] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/a83b91ca0f7faecce47ed52c323c36647f7bfe2b + - button "Commit message body" [ref=e1482] [cursor=pointer]: … + - code [ref=e1490]: + - link "a83b91c" [ref=e1491] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/a83b91ca0f7faecce47ed52c323c36647f7bfe2b + - generic [ref=e1495]: + - img [ref=e1496] + - generic [ref=e1499]: Loading + - generic [ref=e1502]: + - link "@ErshovDmitry" [ref=e1504] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1505] + - form "Add a comment" [ref=e1507]: + - group [ref=e1508]: + - heading "Add a comment" [level=4] [ref=e1511] + - generic [ref=e1512]: Comment + - generic [ref=e1513]: + - generic [ref=e1514]: + - tablist "Add a comment" [ref=e1515]: + - tab "Write" [selected] [ref=e1516] [cursor=pointer] + - tab "Preview" [ref=e1517] [cursor=pointer] + - toolbar [ref=e1518]: + - generic [ref=e1519]: + - button "Heading" [ref=e1521] [cursor=pointer]: + - img + - button "Bold" [ref=e1523] [cursor=pointer]: + - img + - button "Italic" [ref=e1525] [cursor=pointer]: + - img + - button "Quote" [ref=e1527] [cursor=pointer]: + - img + - button "Code" [ref=e1529] [cursor=pointer]: + - img + - button "Link" [ref=e1531] [cursor=pointer]: + - img + - separator [ref=e1532] + - button "Numbered list" [ref=e1534] [cursor=pointer]: + - img + - button "Unordered list" [ref=e1536] [cursor=pointer]: + - img + - button "Task list" [ref=e1538] [cursor=pointer]: + - img + - separator [ref=e1539] + - button "Attach files" [ref=e1541] [cursor=pointer]: + - img + - button "Mention" [ref=e1543] [cursor=pointer]: + - img + - button "Reference" [ref=e1545] [cursor=pointer]: + - img + - button "Saved replies" [ref=e1547] [cursor=pointer]: + - img + - button "Slash commands" [ref=e1549] [cursor=pointer]: + - img + - tabpanel "Write" [ref=e1550]: + - generic [ref=e1554]: + - textbox "Comment" [ref=e1555]: + - /placeholder: " " + - paragraph: Add your comment here... + - generic [ref=e1556]: + - link "Markdown is supported" [ref=e1558] [cursor=pointer]: + - /url: https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax + - generic [ref=e1559]: + - generic: + - img + - generic [ref=e1560]: Markdown is supported + - button "Paste, drop, or click to add files" [ref=e1561] [cursor=pointer]: + - generic [ref=e1562]: + - generic: + - img + - generic [ref=e1563]: Paste, drop, or click to add files + - generic [ref=e1566]: + - button "Close pull request" [ref=e1568] [cursor=pointer]: + - img [ref=e1569] + - text: Close pull request + - button "Comment" [disabled] [ref=e1572] + - generic [ref=e1573]: + - img [ref=e1574] + - generic [ref=e1576]: + - text: Remember, contributions to this repository should follow its + - link "contributing guidelines" [ref=e1577] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/8578951ded0e11c9d2a09e73594c88d38825988d/CONTRIBUTING.md + - text: "," + - link "security policy" [ref=e1578] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/8578951ded0e11c9d2a09e73594c88d38825988d/SECURITY.md + - text: ", and" + - link "code of conduct" [ref=e1579] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/8578951ded0e11c9d2a09e73594c88d38825988d/CODE_OF_CONDUCT.md + - text: . + - generic [ref=e1580]: + - img [ref=e1581] + - strong [ref=e1583]: ProTip! + - text: Add comments to specific lines under + - link "Files changed" [ref=e1584] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files + - text: . + - generic [ref=e1588]: + - form "Select reviewers" [ref=e1590]: + - heading "Reviewers" [level=3] [ref=e1591] + - generic [ref=e1592]: + - paragraph [ref=e1593]: + - generic [ref=e1594]: + - link "@oz-for-oss" [ref=e1595] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e1596] + - link "oz-for-oss[bot]" [ref=e1597] [cursor=pointer]: + - /url: /apps/oz-for-oss + - link "oz-for-oss[bot] requested changes" [ref=e1598] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes/BASE..169dfa663dd4c533eb32daf850f25d6b9c123521 + - img [ref=e1600] + - paragraph [ref=e1602]: Requested changes must be addressed to merge this pull request. + - generic [ref=e1603]: + - generic [ref=e1604]: Still in progress? + - group [ref=e1605]: + - button "Convert to draft" [ref=e1606] [cursor=pointer] + - form "Select assignees" [ref=e1608]: + - heading "Assignees" [level=3] [ref=e1609] + - text: No one assigned + - generic [ref=e1610]: + - heading "Labels" [level=3] [ref=e1611] + - generic [ref=e1612]: + - link "cla-signed" [ref=e1613] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - generic [ref=e1614]: cla-signed + - link "external-contributor" [ref=e1615] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - generic [ref=e1616]: external-contributor + - form "Select projects" [ref=e1618]: + - heading "Projects" [level=3] [ref=e1619] + - text: None yet + - form "Select milestones" [ref=e1621]: + - heading "Milestone" [level=3] [ref=e1622] + - text: No milestone + - form "Link issues" [ref=e1628]: + - heading "Development" [level=3] [ref=e1629] + - paragraph [ref=e1630]: Successfully merging this pull request may close these issues. + - generic [ref=e1632]: + - button "Notifications Customize" [ref=e1635] [cursor=pointer]: + - generic [ref=e1636]: + - generic [ref=e1637]: Notifications + - generic [ref=e1638]: Customize + - button "Unsubscribe" [ref=e1640] [cursor=pointer]: + - generic [ref=e1641]: + - generic: + - img + - generic [ref=e1642]: Unsubscribe + - paragraph [ref=e1643]: You’re receiving notifications because you were mentioned. + - generic [ref=e1645]: + - heading "2 participants" [level=3] [ref=e1646] + - generic [ref=e1647]: + - link "@ErshovDmitry" [ref=e1648] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1649] + - link "@ZacharyZcR" [ref=e1650] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e1651] + - generic [ref=e1654]: + - generic [ref=e1655]: + - checkbox "Allow edits by maintainers" [checked] [ref=e1656] + - text: Allow edits by maintainers + - button "Learn more about allowing maintainer edits" [ref=e1657] [cursor=pointer]: + - img [ref=e1660] + - contentinfo [ref=e1663]: + - heading "Footer" [level=2] [ref=e1664] + - generic [ref=e1665]: + - generic [ref=e1666]: + - link "GitHub Homepage" [ref=e1667] [cursor=pointer]: + - /url: https://github.com + - img [ref=e1668] + - generic [ref=e1670]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e1671]: + - heading "Footer navigation" [level=3] [ref=e1672] + - list "Footer navigation" [ref=e1673]: + - listitem [ref=e1674]: + - link "Terms" [ref=e1675] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e1676]: + - link "Privacy" [ref=e1677] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e1678]: + - link "Security" [ref=e1679] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e1680]: + - link "Status" [ref=e1681] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e1682]: + - link "Community" [ref=e1683] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e1684]: + - link "Docs" [ref=e1685] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e1686]: + - link "Contact" [ref=e1687] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e1688]: + - button "Manage cookies" [ref=e1690] [cursor=pointer] + - listitem [ref=e1691]: + - button "Do not share my personal information" [ref=e1693] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-20T19-48-31-290Z.yml b/.playwright-mcp/page-2026-05-20T19-48-31-290Z.yml new file mode 100644 index 0000000000..e1f7de84cc --- /dev/null +++ b/.playwright-mcp/page-2026-05-20T19-48-31-290Z.yml @@ -0,0 +1,1457 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - button "Open user navigation menu" [ref=e59] [cursor=pointer]: + - img "User avatar" [ref=e60] + - heading "Repository navigation" [level=2] [ref=e61] + - navigation "Repository" [ref=e62]: + - list [ref=e63]: + - listitem [ref=e64]: + - link "Code" [ref=e65] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e67] + - generic [ref=e69]: Code + - listitem [ref=e70]: + - link "Issues (3.4k)" [ref=e71] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e73] + - generic [ref=e76]: Issues + - generic [ref=e77]: + - generic [ref=e78]: 3.4k + - generic [ref=e79]: (3.4k) + - listitem [ref=e80]: + - link "Pull requests (492)" [ref=e81] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e83] + - generic [ref=e85]: Pull requests + - generic [ref=e86]: + - generic [ref=e87]: "492" + - generic [ref=e88]: (492) + - listitem [ref=e89]: + - link "Agents" [ref=e90] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e92] + - generic [ref=e95]: Agents + - listitem [ref=e96]: + - link "Discussions" [ref=e97] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e99] + - generic [ref=e101]: Discussions + - listitem [ref=e102]: + - link "Actions" [ref=e103] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e105] + - generic [ref=e107]: Actions + - listitem [ref=e108]: + - link "Projects" [ref=e109] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e111] + - generic [ref=e113]: Projects + - listitem [ref=e114]: + - link "Security and quality" [ref=e115] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e117] + - generic [ref=e119]: Security and quality + - listitem [ref=e120]: + - link "Insights" [ref=e121] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e123] + - generic [ref=e125]: Insights + - main [ref=e129]: + - generic [ref=e136]: + - generic [ref=e139]: + - 'heading "I18n #11382 Edit title" [level=1] [ref=e141]': + - text: I18n + - generic [ref=e142]: + - generic [ref=e143]: "#11382" + - button "Edit title" [ref=e144] [cursor=pointer]: + - img [ref=e145] + - generic [ref=e148]: + - generic [ref=e149]: + - button "View status View status" [disabled] [ref=e150]: + - generic [ref=e151]: + - generic: + - generic: + - img + - generic: Loading + - generic [ref=e153]: Loading merge status + - button "Code" [ref=e154] [cursor=pointer]: + - generic [ref=e155]: + - generic [ref=e156]: Code + - generic: + - img + - generic [ref=e158]: + - generic [ref=e160]: + - img "Pull request" [ref=e161] + - text: Open + - generic [ref=e164]: + - link "ErshovDmitry" [ref=e165] [cursor=pointer]: + - /url: /ErshovDmitry + - text: wants to merge 23 commits into + - generic [ref=e166]: + - link "warpdotdev:master" [ref=e167] [cursor=pointer]: + - /url: /warpdotdev/warp/tree/master + - generic [ref=e168]: from + - generic [ref=e169]: + - link "ErshovDmitry:i18n" [ref=e170] [cursor=pointer]: + - /url: /ErshovDmitry/warp-i18n/tree/i18n + - button "Copy head branch name to clipboard" [ref=e171] [cursor=pointer]: + - img [ref=e172] + - generic [ref=e175]: + - generic [ref=e177]: + - generic [ref=e178]: +7 720 + - generic [ref=e179]: "-2 605" + - generic [ref=e180]: "Lines changed: 7720 additions & 2605 deletions" + - navigation "Pull request navigation tabs" [ref=e189]: + - tablist [ref=e190]: + - tab "Conversation (13)" [selected] [ref=e191] [cursor=pointer]: + - img [ref=e192] + - text: Conversation + - generic [ref=e194]: "13" + - generic [ref=e195]: (13) + - tab "Commits (23)" [ref=e196] [cursor=pointer]: + - img [ref=e197] + - text: Commits + - generic [ref=e199]: "23" + - generic [ref=e200]: (23) + - tab "Checks (0)" [ref=e201] [cursor=pointer]: + - img [ref=e202] + - text: Checks + - generic [ref=e204]: "0" + - generic [ref=e205]: (0) + - tab "Files changed (76)" [ref=e206] [cursor=pointer]: + - img [ref=e207] + - text: Files changed + - generic [ref=e209]: "76" + - generic [ref=e210]: (76) + - generic [ref=e215]: + - generic [ref=e217]: + - heading "Conversation" [level=2] [ref=e218] + - generic [ref=e219]: + - generic [ref=e220]: + - generic [ref=e222]: + - link "@ErshovDmitry" [ref=e223] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e224] + - generic [ref=e226]: + - generic [ref=e227]: + - group [ref=e230]: + - button "Show options" [ref=e231] [cursor=pointer]: + - img "Show options" [ref=e234] + - heading "ErshovDmitry commented May 20, 202615 hours ago" [level=3] [ref=e236]: + - generic [ref=e237]: + - strong [ref=e238]: + - link "ErshovDmitry" [ref=e239] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202615 hours ago" [ref=e240] [cursor=pointer]: + - /url: "#issue-4483039846" + - generic [ref=e244]: + - paragraph [ref=e245]: Hello! My name is Dmitry. + - paragraph [ref=e246]: I've been using Warp and love it. I wanted to add i18n support so the terminal could speak Russian (and other languages in the future). I'm not a developer myself — this was built with DeepSeek AI, but I reviewed everything personally. + - paragraph [ref=e247]: I'd much rather this live in the official repo than a fork. If the team is interested in i18n, I'm happy to help however I can — and I'll gladly delete my fork once upstreamed. + - paragraph [ref=e248]: + - text: Cheers, + - text: Dmitry + - separator [ref=e249] + - heading "What" [level=2] [ref=e250] + - paragraph [ref=e251]: Adds i18n (internationalization) support to Warp. + - heading "Changes" [level=2] [ref=e252] + - 'heading "New crate: warp_i18n" [level=3] [ref=e253]': + - text: "New crate:" + - code [ref=e254]: warp_i18n + - list [ref=e255]: + - listitem [ref=e256]: + - code [ref=e257]: t!() + - text: macro for translating UI strings + - listitem [ref=e258]: + - code [ref=e259]: current_locale() + - text: / + - code [ref=e260]: set_current_locale() + - text: for runtime language switching + - listitem [ref=e261]: + - code [ref=e262]: init_from_json() + - text: for WASM compatibility + - listitem [ref=e263]: Build script for compile-time key validation + - listitem [ref=e264]: 21 tests + - heading "Feature flag" [level=3] [ref=e265] + - list [ref=e266]: + - listitem [ref=e267]: + - code [ref=e268]: FeatureFlag::I18n + - text: in + - code [ref=e269]: DOGFOOD_FLAGS + - heading "Settings" [level=3] [ref=e270] + - list [ref=e271]: + - listitem [ref=e272]: + - code [ref=e273]: Locale + - text: enum (En, Ru) + - listitem [ref=e274]: Language dropdown in Settings → Appearance + - listitem [ref=e275]: Auto-detect system locale on first run + - heading "Migrated UI" [level=3] [ref=e276] + - list [ref=e277]: + - listitem [ref=e278]: 10 top-level menu labels + - listitem [ref=e279]: 14 settings section names + - listitem [ref=e280]: 11 settings category labels + - heading "Locales" [level=3] [ref=e281] + - list [ref=e282]: + - listitem [ref=e283]: + - code [ref=e284]: en.json + - text: — 51 keys (mandatory fallback) + - listitem [ref=e285]: + - code [ref=e286]: ru.json + - text: — 51 keys (Russian translation) + - paragraph [ref=e287]: + - emphasis [ref=e288]: + - text: Built with DeepSeek AI — human-reviewed, all tests pass ( + - code [ref=e289]: cargo check --workspace + - text: "," + - code [ref=e290]: cargo test -p warp_i18n + - text: ). + - group [ref=e294]: + - generic "Add or remove reactions" [ref=e295] [cursor=pointer]: + - img [ref=e296] + - generic [ref=e298]: + - generic [ref=e300]: + - link "@cla-bot" [ref=e302] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e303] + - generic [ref=e305]: + - generic [ref=e306]: + - group [ref=e309]: + - button "Show options" [ref=e310] [cursor=pointer]: + - img "Show options" [ref=e313] + - heading "cla-bot Bot commented May 20, 202615 hours ago" [level=3] [ref=e315]: + - generic [ref=e316]: + - strong [ref=e317]: + - link "cla-bot" [ref=e318] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e319]: Bot + - text: commented + - link "May 20, 202615 hours ago" [ref=e320] [cursor=pointer]: + - /url: "#issuecomment-4494411354" + - generic [ref=e321]: + - table [ref=e323]: + - rowgroup [ref=e324]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e325]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e326]': + - paragraph [ref=e327]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e328]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e329]: + - listitem [ref=e330]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e331]: git config --list | grep email + - listitem [ref=e332]: + - text: If not, set it up using + - code [ref=e333]: git config --global user.email email@example.com + - listitem [ref=e334]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e335] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e340]: + - generic "Add or remove reactions" [ref=e341] [cursor=pointer]: + - img [ref=e342] + - generic [ref=e345]: + - link "@oz-for-oss" [ref=e347] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e348] + - generic [ref=e350]: + - generic [ref=e351]: + - generic [ref=e352]: + - group [ref=e354]: + - button "Show options" [ref=e355] [cursor=pointer]: + - img "Show options" [ref=e358] + - generic "This user has previously committed to the warp repository." [ref=e361]: + - generic [ref=e362]: Contributor + - heading "oz-for-oss Bot commented May 20, 202615 hours ago" [level=3] [ref=e363]: + - generic [ref=e364]: + - strong [ref=e365]: + - link "oz-for-oss" [ref=e366] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e367]: Bot + - text: commented + - link "May 20, 202615 hours ago" [ref=e368] [cursor=pointer]: + - /url: "#issuecomment-4494411637" + - generic [ref=e369]: + - table [ref=e371]: + - rowgroup [ref=e372]: + - 'row "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e373]': + - 'cell "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e374]': + - paragraph [ref=e375]: + - link "@ErshovDmitry" [ref=e376] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e377]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e378]: ready-to-implement + - text: . + - paragraph [ref=e379]: "Issue-state enforcement details:" + - list [ref=e380]: + - listitem [ref=e381]: + - paragraph [ref=e382]: "Associated same-repo issues checked: none" + - listitem [ref=e383]: + - paragraph [ref=e384]: + - text: "Required readiness label:" + - code [ref=e385]: ready-to-implement + - paragraph [ref=e386]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e387]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e388]: ready-to-implement + - text: . + - paragraph [ref=e389]: + - emphasis [ref=e390]: + - text: Powered by + - link "Oz" [ref=e391] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e396]: + - generic "Add or remove reactions" [ref=e397] [cursor=pointer]: + - img [ref=e398] + - generic [ref=e401]: + - img [ref=e403] + - generic [ref=e405]: + - link "@github-actions" [ref=e406] [cursor=pointer]: + - /url: /apps/github-actions + - img "@github-actions" [ref=e407] + - link "github-actions" [ref=e408] [cursor=pointer]: + - /url: /apps/github-actions + - generic [ref=e409]: Bot + - text: added the + - link "external-contributor" [ref=e410] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - text: label + - link "May 20, 202615 hours ago" [ref=e411] [cursor=pointer]: + - /url: "#event-25738319166" + - generic [ref=e414]: + - generic [ref=e415]: + - link "oz-for-oss[bot]" [ref=e416] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "oz-for-oss[bot]" [ref=e417] + - img [ref=e419] + - generic [ref=e421]: + - generic [ref=e422]: + - strong [ref=e423]: + - link "oz-for-oss" [ref=e424] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e425]: Bot + - text: requested changes + - link "May 20, 202615 hours ago" [ref=e427] [cursor=pointer]: + - /url: "#pullrequestreview-4325084051" + - link "View reviewed changes" [ref=e429] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes + - generic [ref=e431]: View reviewed changes + - generic [ref=e434]: + - generic [ref=e435]: + - generic [ref=e436]: + - group [ref=e438]: + - button "Show options" [ref=e439] [cursor=pointer]: + - img "Show options" [ref=e442] + - generic "This user has previously committed to the warp repository." [ref=e445]: + - generic [ref=e446]: Contributor + - heading "oz-for-oss Bot left a comment" [level=3] [ref=e447]: + - generic [ref=e448]: + - strong [ref=e449]: + - link "oz-for-oss" [ref=e450] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e451]: Bot + - text: left a comment + - generic [ref=e454]: + - paragraph [ref=e455]: + - link "@ErshovDmitry" [ref=e456] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e457]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e458]: ready-to-implement + - text: . + - paragraph [ref=e459]: "Issue-state enforcement details:" + - list [ref=e460]: + - listitem [ref=e461]: + - paragraph [ref=e462]: "Associated same-repo issues checked: none" + - listitem [ref=e463]: + - paragraph [ref=e464]: + - text: "Required readiness label:" + - code [ref=e465]: ready-to-implement + - paragraph [ref=e466]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e467]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e468]: ready-to-implement + - text: . + - paragraph [ref=e469]: + - emphasis [ref=e470]: + - text: Powered by + - link "Oz" [ref=e471] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e475]: + - generic "Add or remove reactions" [ref=e476] [cursor=pointer]: + - img [ref=e477] + - generic [ref=e480]: + - link "@ErshovDmitry" [ref=e482] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e483] + - generic [ref=e485]: + - generic [ref=e486]: + - generic [ref=e487]: + - group [ref=e489]: + - button "Show options" [ref=e490] [cursor=pointer]: + - img "Show options" [ref=e493] + - generic "You are the author of this pull request." [ref=e496]: + - generic [ref=e497]: Author + - heading "ErshovDmitry commented May 20, 202615 hours ago" [level=3] [ref=e498]: + - generic [ref=e499]: + - strong [ref=e500]: + - link "ErshovDmitry" [ref=e501] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202615 hours ago" [ref=e502] [cursor=pointer]: + - /url: "#issuecomment-4494430862" + - generic [ref=e503]: + - table [ref=e505]: + - rowgroup [ref=e506]: + - 'row "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e507]': + - 'cell "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e508]': + - paragraph [ref=e509]: + - text: Oh wow — I just noticed there are already several i18n PRs open ( + - link "#10630" [ref=e510] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "," + - link "#10990" [ref=e511] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: "," + - link "#9458" [ref=e512] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9458 + - text: "," + - link "#9922" [ref=e513] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9922 + - text: ). I should have checked first before opening mine. My apologies for the noise! + - blockquote [ref=e514]: + - paragraph [ref=e515]: + - text: That said, I see none of them include Russian yet. If the team settles on one i18n approach (looks like + - link "#10630" [ref=e516] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e517] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: is the leading one?), I'd be happy to contribute Russian translations to that effort instead. + - paragraph [ref=e518]: I'll close this PR if it's just adding clutter — just let me know. + - paragraph [ref=e519]: + - text: Cheers, + - text: Dmitry + - group [ref=e524]: + - generic "Add or remove reactions" [ref=e525] [cursor=pointer]: + - img [ref=e526] + - generic [ref=e529]: + - link "@cla-bot" [ref=e531] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e532] + - generic [ref=e534]: + - generic [ref=e535]: + - group [ref=e538]: + - button "Show options" [ref=e539] [cursor=pointer]: + - img "Show options" [ref=e542] + - heading "cla-bot Bot commented May 20, 202613 hours ago" [level=3] [ref=e544]: + - generic [ref=e545]: + - strong [ref=e546]: + - link "cla-bot" [ref=e547] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e548]: Bot + - text: commented + - link "May 20, 202613 hours ago" [ref=e549] [cursor=pointer]: + - /url: "#issuecomment-4494962896" + - generic [ref=e550]: + - table [ref=e552]: + - rowgroup [ref=e553]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e554]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e555]': + - paragraph [ref=e556]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e557]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e558]: + - listitem [ref=e559]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e560]: git config --list | grep email + - listitem [ref=e561]: + - text: If not, set it up using + - code [ref=e562]: git config --global user.email email@example.com + - listitem [ref=e563]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e564] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e569]: + - generic "Add or remove reactions" [ref=e570] [cursor=pointer]: + - img [ref=e571] + - group [ref=e573]: + - generic "2 similar comments" [ref=e574] [cursor=pointer]: + - generic [ref=e576]: 2 similar comments + - generic [ref=e578]: + - link "@ErshovDmitry" [ref=e580] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e581] + - generic [ref=e583]: + - generic [ref=e584]: + - generic [ref=e585]: + - group [ref=e587]: + - button "Show options" [ref=e588] [cursor=pointer]: + - img "Show options" [ref=e591] + - generic "You are the author of this pull request." [ref=e594]: + - generic [ref=e595]: Author + - heading "ErshovDmitry commented May 20, 202612 hours ago" [level=3] [ref=e596]: + - generic [ref=e597]: + - strong [ref=e598]: + - link "ErshovDmitry" [ref=e599] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202612 hours ago" [ref=e600] [cursor=pointer]: + - /url: "#issuecomment-4495523677" + - generic [ref=e601]: + - table [ref=e603]: + - rowgroup [ref=e604]: + - 'row "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e605]': + - 'cell "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e606]': + - paragraph [ref=e607]: + - text: Update:** I've rewritten this PR to match + - link "@ZacharyZcR" [ref=e608] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: "'s YAML approach from" + - link "#10630" [ref=e609] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e610] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: ":" + - blockquote [ref=e611]: + - list [ref=e612]: + - listitem [ref=e613]: + - code [ref=e614]: crates/i18n/ + - text: with + - code [ref=e615]: t!() + - text: / + - code [ref=e616]: t_required!() + - text: macros and + - code [ref=e617]: TranslationLookup + - listitem [ref=e618]: + - code [ref=e619]: "resources/bundled/locales/{en,ru}.yml" + - text: (247 keys each, YAML) + - listitem [ref=e620]: + - code [ref=e621]: WARP_LANG + - text: env var → system locale → + - code [ref=e622]: en + - text: fallback + - listitem [ref=e623]: + - text: No feature flag, no settings UI — same design as + - generic [ref=e624]: + - img [ref=e625] + - 'link "Add i18n framework with Simplified Chinese (zh-CN) support #10630" [ref=e627] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/pull/10630 + - paragraph [ref=e628]: + - text: The Russian + - code [ref=e629]: ru.yml + - text: should be a drop-in addition to the framework once it's approved. Happy to rebase onto whichever PR lands first. + - paragraph [ref=e630]: Dmitry + - group [ref=e635]: + - generic "Add or remove reactions" [ref=e636] [cursor=pointer]: + - img [ref=e637] + - generic [ref=e640]: + - link "@ErshovDmitry" [ref=e642] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e643] + - generic [ref=e645]: + - generic [ref=e646]: + - generic [ref=e647]: + - group [ref=e649]: + - button "Show options" [ref=e650] [cursor=pointer]: + - img "Show options" [ref=e653] + - generic "You are the author of this pull request." [ref=e656]: + - generic [ref=e657]: Author + - heading "ErshovDmitry commented May 20, 202612 hours ago" [level=3] [ref=e658]: + - generic [ref=e659]: + - strong [ref=e660]: + - link "ErshovDmitry" [ref=e661] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202612 hours ago" [ref=e662] [cursor=pointer]: + - /url: "#issuecomment-4495572049" + - generic [ref=e663]: + - table [ref=e665]: + - rowgroup [ref=e666]: + - row "@cla-bot check" [ref=e667]: + - cell "@cla-bot check" [ref=e668]: + - paragraph [ref=e669]: + - link "@cla-bot" [ref=e670] [cursor=pointer]: + - /url: https://github.com/cla-bot + - text: check + - group [ref=e675]: + - generic "Add or remove reactions" [ref=e676] [cursor=pointer]: + - img [ref=e677] + - generic [ref=e680]: + - img [ref=e682] + - generic [ref=e684]: + - link "@cla-bot" [ref=e685] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e686] + - link "cla-bot" [ref=e687] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e688]: Bot + - text: added the + - link "cla-signed" [ref=e689] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - text: label + - link "May 20, 202612 hours ago" [ref=e690] [cursor=pointer]: + - /url: "#event-25743817179" + - generic [ref=e692]: + - link "@cla-bot" [ref=e694] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e695] + - generic [ref=e697]: + - generic [ref=e698]: + - group [ref=e701]: + - button "Show options" [ref=e702] [cursor=pointer]: + - img "Show options" [ref=e705] + - heading "cla-bot Bot commented May 20, 202612 hours ago" [level=3] [ref=e707]: + - generic [ref=e708]: + - strong [ref=e709]: + - link "cla-bot" [ref=e710] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e711]: Bot + - text: commented + - link "May 20, 202612 hours ago" [ref=e712] [cursor=pointer]: + - /url: "#issuecomment-4495573189" + - generic [ref=e713]: + - table [ref=e715]: + - rowgroup [ref=e716]: + - row "The cla-bot has been summoned, and re-checked this pull request!" [ref=e717]: + - cell "The cla-bot has been summoned, and re-checked this pull request!" [ref=e718]: + - paragraph [ref=e719]: The cla-bot has been summoned, and re-checked this pull request! + - group [ref=e724]: + - generic "Add or remove reactions" [ref=e725] [cursor=pointer]: + - img [ref=e726] + - generic [ref=e729]: + - link "@ErshovDmitry" [ref=e731] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e732] + - generic [ref=e734]: + - generic [ref=e735]: + - generic [ref=e736]: + - group [ref=e738]: + - button "Show options" [ref=e739] [cursor=pointer]: + - img "Show options" [ref=e742] + - generic "You are the author of this pull request." [ref=e745]: + - generic [ref=e746]: Author + - heading "ErshovDmitry commented May 20, 202612 hours ago" [level=3] [ref=e747]: + - generic [ref=e748]: + - strong [ref=e749]: + - link "ErshovDmitry" [ref=e750] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 202612 hours ago" [ref=e751] [cursor=pointer]: + - /url: "#issuecomment-4495607261" + - generic [ref=e752]: + - table [ref=e754]: + - rowgroup [ref=e755]: + - 'row "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e756]': + - 'cell "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e757]': + - paragraph [ref=e758]: + - link "@oss-maintainers" [ref=e759] [cursor=pointer]: + - /url: https://github.com/oss-maintainers + - text: This PR now follows the same YAML-based approach as + - link "#10630" [ref=e760] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e761] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: by + - link "@ZacharyZcR" [ref=e762] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: — matching + - code [ref=e763]: crates/i18n/ + - text: API, + - code [ref=e764]: "resources/bundled/locales/{en,ru}.yml" + - text: "," + - code [ref=e765]: t!() + - text: / + - code [ref=e766]: t_required!() + - text: macros, and + - code [ref=e767]: WARP_LANG + - text: env var detection. + - paragraph [ref=e768]: + - text: It's blocked by the issue-state enforcement check (needs a + - code [ref=e769]: ready-to-implement + - text: issue). Could a maintainer please add + - code [ref=e770]: ready-to-implement + - text: to + - link "#1194" [ref=e771] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194 + - text: ", or let me know if I should create a separate tracking issue?" + - paragraph [ref=e772]: + - text: Russian + - code [ref=e773]: ru.yml + - text: (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. + - paragraph [ref=e774]: + - text: Cheers, + - text: Dmitry + - group [ref=e779]: + - generic "Add or remove reactions" [ref=e780] [cursor=pointer]: + - img [ref=e781] + - generic [ref=e784]: + - img [ref=e786] + - generic [ref=e788]: + - link "@ErshovDmitry" [ref=e789] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e790] + - link "ErshovDmitry" [ref=e791] [cursor=pointer]: + - /url: /ErshovDmitry + - link "force-pushed" [ref=e792] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - text: the + - generic [ref=e794]: i18n + - text: branch from + - link "2196591" [ref=e795] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/219659155d8415b5773506f3306f6339e1300b60 + - code [ref=e796]: "2196591" + - text: to + - link "d67453f" [ref=e797] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/d67453f73b0087f3a931fd06e958ab9cebb3be0c + - code [ref=e798]: d67453f + - link "Compare" [ref=e799] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - generic [ref=e801]: Compare + - link "May 20, 2026 14:3612 hours ago" [ref=e802] [cursor=pointer]: + - /url: "#event-25744804343" + - generic [ref=e804]: + - link "@ZacharyZcR" [ref=e806] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e807] + - generic [ref=e809]: + - generic [ref=e810]: + - group [ref=e813]: + - button "Show options" [ref=e814] [cursor=pointer]: + - img "Show options" [ref=e817] + - heading "ZacharyZcR commented May 20, 20264 hours ago" [level=3] [ref=e819]: + - generic [ref=e820]: + - strong [ref=e821]: + - link "ZacharyZcR" [ref=e822] [cursor=pointer]: + - /url: /ZacharyZcR + - text: commented + - link "May 20, 20264 hours ago" [ref=e823] [cursor=pointer]: + - /url: "#issuecomment-4499865827" + - generic [ref=e824]: + - table [ref=e826]: + - rowgroup [ref=e827]: + - row "Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel." [ref=e828]: + - cell "Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel." [ref=e829]: + - paragraph [ref=e830]: Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel. + - group [ref=e835]: + - generic "Add or remove reactions" [ref=e836] [cursor=pointer]: + - img [ref=e837] + - generic [ref=e840]: + - link "@ErshovDmitry" [ref=e842] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e843] + - generic [ref=e845]: + - generic [ref=e846]: + - generic [ref=e847]: + - group [ref=e849]: + - button "Show options" [ref=e850] [cursor=pointer]: + - img "Show options" [ref=e853] + - generic "You are the author of this pull request." [ref=e856]: + - generic [ref=e857]: Author + - heading "ErshovDmitry commented May 21, 202624 minutes ago" [level=3] [ref=e858]: + - generic [ref=e859]: + - strong [ref=e860]: + - link "ErshovDmitry" [ref=e861] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 21, 202624 minutes ago" [ref=e862] [cursor=pointer]: + - /url: "#issuecomment-4501859680" + - generic [ref=e863]: + - table [ref=e865]: + - rowgroup [ref=e866]: + - 'row "@ZacharyZcR Thanks for reaching out! Happy to collaborate. We''ve matched your YAML approach — crates/i18n/, t!()/t_required!() macros, resources/bundled/locales/{en,ru}.yml (1080 keys, full Russian translation). Our ru.yml should be a drop-in addition. We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. How can we help with #10630? Happy to contribute Russian translations in your format, or collaborate however makes sense. Cheers, Dmitry" [ref=e867]': + - 'cell "@ZacharyZcR Thanks for reaching out! Happy to collaborate. We''ve matched your YAML approach — crates/i18n/, t!()/t_required!() macros, resources/bundled/locales/{en,ru}.yml (1080 keys, full Russian translation). Our ru.yml should be a drop-in addition. We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. How can we help with #10630? Happy to contribute Russian translations in your format, or collaborate however makes sense. Cheers, Dmitry" [ref=e868]': + - paragraph [ref=e869]: + - link "@ZacharyZcR" [ref=e870] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: Thanks for reaching out! Happy to collaborate. We've matched your YAML approach — + - code [ref=e871]: crates/i18n/ + - text: "," + - code [ref=e872]: t!() + - text: / + - code [ref=e873]: t_required!() + - text: macros, + - code [ref=e874]: "resources/bundled/locales/{en,ru}.yml" + - text: (1080 keys, full Russian translation). Our + - code [ref=e875]: ru.yml + - text: should be a drop-in addition. + - paragraph [ref=e876]: We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. + - paragraph [ref=e877]: + - text: How can we help with + - link "#10630" [ref=e878] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "? Happy to contribute Russian translations in your format, or collaborate however makes sense." + - paragraph [ref=e879]: + - text: Cheers, + - text: Dmitry + - generic [ref=e882]: + - group [ref=e884]: + - generic "Add or remove reactions" [ref=e885] [cursor=pointer]: + - img [ref=e886] + - button "react with thumbs up" [ref=e890] [cursor=pointer]: + - img "+1" [ref=e892] + - generic [ref=e893]: "1" + - generic [ref=e895]: + - generic [ref=e896]: + - img [ref=e898] + - generic [ref=e900]: + - link "ErshovDmitry" [ref=e901] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 14 commits + - link "May 21, 2026 02:2423 minutes ago" [ref=e902] [cursor=pointer]: + - /url: "#commits-pushed-cc72586" + - generic [ref=e903]: + - generic [ref=e904]: + - img [ref=e906] + - generic [ref=e911]: + - link "@ErshovDmitry" [ref=e914] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e915] + - code [ref=e917]: + - 'link "feat: add warp_i18n crate with t!() macro and en/ru locales" [ref=e918] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/cc7258688a9c182530aa60188303f6fd1831df6f + - code [ref=e922]: + - link "cc72586" [ref=e923] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/cc7258688a9c182530aa60188303f6fd1831df6f + - generic [ref=e924]: + - img [ref=e926] + - generic [ref=e931]: + - link "@ErshovDmitry" [ref=e934] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e935] + - generic [ref=e936]: + - code [ref=e937]: + - 'link "feat: i18n Phase 2 — FeatureFlag, Locale setting, app_menus migration…" [ref=e938] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/23dcd5b5858770e308c01ad9f80dc13502530104 + - button "Commit message body" [ref=e940] [cursor=pointer]: … + - code [ref=e944]: + - link "23dcd5b" [ref=e945] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/23dcd5b5858770e308c01ad9f80dc13502530104 + - generic [ref=e946]: + - img [ref=e948] + - generic [ref=e953]: + - link "@ErshovDmitry" [ref=e956] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e957] + - generic [ref=e958]: + - code [ref=e959]: + - 'link "fix: wire warp_i18n::init() at startup, sync LocaleSettings with i18n…" [ref=e960] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/6fa17bbb49f5ca60f4e751a578c15c308c45dc55 + - button "Commit message body" [ref=e962] [cursor=pointer]: … + - code [ref=e966]: + - link "6fa17bb" [ref=e967] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/6fa17bbb49f5ca60f4e751a578c15c308c45dc55 + - generic [ref=e968]: + - img [ref=e970] + - generic [ref=e975]: + - link "@ErshovDmitry" [ref=e978] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e979] + - code [ref=e981]: + - 'link "docs: add English translation of the fork note in README" [ref=e982] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/5cff42bde0a32c052ffe3b0fd22e7d63efd51183 + - code [ref=e986]: + - link "5cff42b" [ref=e987] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/5cff42bde0a32c052ffe3b0fd22e7d63efd51183 + - generic [ref=e988]: + - img [ref=e990] + - generic [ref=e995]: + - link "@ErshovDmitry" [ref=e998] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e999] + - generic [ref=e1000]: + - code [ref=e1001]: + - 'link "feat: i18n Phase 3 — build script key validation, WASM support, setti…" [ref=e1002] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/fc1700a5204082985e0e8a75a90fbe601a4d6cd7 + - button "Commit message body" [ref=e1004] [cursor=pointer]: … + - code [ref=e1008]: + - link "fc1700a" [ref=e1009] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/fc1700a5204082985e0e8a75a90fbe601a4d6cd7 + - generic [ref=e1010]: + - img [ref=e1012] + - generic [ref=e1017]: + - link "@ErshovDmitry" [ref=e1020] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1021] + - generic [ref=e1022]: + - code [ref=e1023]: + - 'link "refactor: migrate to ZacharyZcR-compatible YAML i18n with Russian locale" [ref=e1024] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/df42122abb1c0ba4f71797a10c26ef4d2a71db02 + - button "Commit message body" [ref=e1026] [cursor=pointer]: … + - code [ref=e1030]: + - link "df42122" [ref=e1031] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/df42122abb1c0ba4f71797a10c26ef4d2a71db02 + - generic [ref=e1032]: + - img [ref=e1034] + - generic [ref=e1039]: + - link "@ErshovDmitry" [ref=e1042] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1043] + - code [ref=e1045]: + - 'link "fix: init_locale before menu bar, cleanup dead code, harden set_locale" [ref=e1046] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/f95105b31dd57f302814ec38275811f0059853a1 + - code [ref=e1050]: + - link "f95105b" [ref=e1051] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f95105b31dd57f302814ec38275811f0059853a1 + - generic [ref=e1052]: + - img [ref=e1054] + - generic [ref=e1059]: + - link "@ErshovDmitry" [ref=e1062] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1063] + - generic [ref=e1064]: + - code [ref=e1065]: + - 'link "fix(i18n): Round 2 review — internationalize all hardcoded menu strings" [ref=e1066] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/496ee12ac18b69a27f3bc42c6e09c061ab058d53 + - button "Commit message body" [ref=e1068] [cursor=pointer]: … + - code [ref=e1072]: + - link "496ee12" [ref=e1073] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/496ee12ac18b69a27f3bc42c6e09c061ab058d53 + - generic [ref=e1074]: + - img [ref=e1076] + - generic [ref=e1081]: + - link "@ErshovDmitry" [ref=e1084] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1085] + - code [ref=e1087]: + - 'link "chore: add .playwright-mcp to .gitignore" [ref=e1088] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/a6f1e9adbe6950c53bdddb004d4c04ac573c5506 + - code [ref=e1092]: + - link "a6f1e9a" [ref=e1093] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/a6f1e9adbe6950c53bdddb004d4c04ac573c5506 + - generic [ref=e1094]: + - img [ref=e1096] + - generic [ref=e1101]: + - link "@ErshovDmitry" [ref=e1104] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1105] + - generic [ref=e1106]: + - code [ref=e1107]: + - 'link "i18n(keybindings,features): replace hardcoded English strings with me…" [ref=e1108] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1e2bb80aaaf8769c52c42a41c180fe09747ca932 + - button "Commit message body" [ref=e1110] [cursor=pointer]: … + - code [ref=e1114]: + - link "1e2bb80" [ref=e1115] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1e2bb80aaaf8769c52c42a41c180fe09747ca932 + - generic [ref=e1116]: + - img [ref=e1118] + - generic [ref=e1123]: + - link "@ErshovDmitry" [ref=e1126] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1127] + - generic [ref=e1128]: + - code [ref=e1129]: + - 'link "i18n(billing,workspace): internationalize billing page and workspace …" [ref=e1130] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/8157eee00d0bf3311e38e5c4fdbfdeafb31a0b8a + - button "Commit message body" [ref=e1132] [cursor=pointer]: … + - code [ref=e1136]: + - link "8157eee" [ref=e1137] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/8157eee00d0bf3311e38e5c4fdbfdeafb31a0b8a + - generic [ref=e1138]: + - img [ref=e1140] + - generic [ref=e1145]: + - link "@ErshovDmitry" [ref=e1148] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1149] + - generic [ref=e1150]: + - code [ref=e1151]: + - 'link "i18n(ai_page): internationalize AI settings page strings" [ref=e1152] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/5fa638bab706c58727184a97ad6403704c475da1 + - button "Commit message body" [ref=e1154] [cursor=pointer]: … + - code [ref=e1158]: + - link "5fa638b" [ref=e1159] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/5fa638bab706c58727184a97ad6403704c475da1 + - generic [ref=e1160]: + - img [ref=e1162] + - generic [ref=e1167]: + - link "@ErshovDmitry" [ref=e1170] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1171] + - generic [ref=e1172]: + - code [ref=e1173]: + - 'link "i18n(workspace,code): internationalize workspace bindings and code se…" [ref=e1174] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/38589381df3792f835084ed75c8f9aa81ecdca57 + - button "Commit message body" [ref=e1176] [cursor=pointer]: … + - code [ref=e1180]: + - link "3858938" [ref=e1181] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/38589381df3792f835084ed75c8f9aa81ecdca57 + - generic [ref=e1182]: + - img [ref=e1184] + - generic [ref=e1189]: + - link "@ErshovDmitry" [ref=e1192] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1193] + - generic [ref=e1194]: + - code [ref=e1195]: + - 'link "i18n(settings): internationalize privacy, teams, environments, drive,…" [ref=e1196] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/93843bdef27125e8c2df8226dfb915057d519cf6 + - button "Commit message body" [ref=e1198] [cursor=pointer]: … + - code [ref=e1202]: + - link "93843bd" [ref=e1203] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/93843bdef27125e8c2df8226dfb915057d519cf6 + - generic [ref=e1205]: + - generic [ref=e1206]: + - img [ref=e1208] + - generic [ref=e1210]: + - link "ErshovDmitry" [ref=e1211] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 7 commits + - link "May 21, 2026 02:2522 minutes ago" [ref=e1212] [cursor=pointer]: + - /url: "#commits-pushed-b532730" + - generic [ref=e1213]: + - generic [ref=e1214]: + - img [ref=e1216] + - generic [ref=e1221]: + - link "@ErshovDmitry" [ref=e1224] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1225] + - generic [ref=e1226]: + - code [ref=e1227]: + - 'link "i18n: internationalize code_review_view and notebooks editor view" [ref=e1228] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/b532730e0acf4cf7f9737c707b8abfcdf3a27cbd + - button "Commit message body" [ref=e1230] [cursor=pointer]: … + - code [ref=e1234]: + - link "b532730" [ref=e1235] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/b532730e0acf4cf7f9737c707b8abfcdf3a27cbd + - generic [ref=e1236]: + - img [ref=e1238] + - generic [ref=e1243]: + - link "@ErshovDmitry" [ref=e1246] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1247] + - generic [ref=e1248]: + - code [ref=e1249]: + - 'link "i18n: internationalize terminal/input.rs and ai/agent_management/view.rs" [ref=e1250] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/aba6fba8bf5226ddf10ac7dbefa6e591022dd125 + - button "Commit message body" [ref=e1252] [cursor=pointer]: … + - code [ref=e1256]: + - link "aba6fba" [ref=e1257] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/aba6fba8bf5226ddf10ac7dbefa6e591022dd125 + - generic [ref=e1258]: + - img [ref=e1260] + - generic [ref=e1265]: + - link "@ErshovDmitry" [ref=e1268] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1269] + - generic [ref=e1270]: + - code [ref=e1271]: + - 'link "i18n(dialogs): internationalize common buttons, toasts, placeholders" [ref=e1272] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/0eaad2ac3308b84c1600fefd76e6676d86295a94 + - button "Commit message body" [ref=e1274] [cursor=pointer]: … + - code [ref=e1278]: + - link "0eaad2a" [ref=e1279] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/0eaad2ac3308b84c1600fefd76e6676d86295a94 + - generic [ref=e1280]: + - img [ref=e1282] + - generic [ref=e1287]: + - link "@ErshovDmitry" [ref=e1290] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1291] + - code [ref=e1293]: + - 'link "fix(i18n): add missing settings.code_page Russian translations" [ref=e1294] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/54d0f169cd7802582894cc213f3d416e1af5c0ce + - code [ref=e1298]: + - link "54d0f16" [ref=e1299] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/54d0f169cd7802582894cc213f3d416e1af5c0ce + - generic [ref=e1300]: + - img [ref=e1302] + - generic [ref=e1307]: + - link "@ErshovDmitry" [ref=e1310] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1311] + - generic [ref=e1312]: + - code [ref=e1313]: + - 'link "i18n(workspace): internationalize remaining workspace/view.rs strings" [ref=e1314] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/87d6cfbd2451107eafb31be5fc40d718f25e6f7d + - button "Commit message body" [ref=e1316] [cursor=pointer]: … + - code [ref=e1320]: + - link "87d6cfb" [ref=e1321] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/87d6cfbd2451107eafb31be5fc40d718f25e6f7d + - generic [ref=e1322]: + - img [ref=e1324] + - generic [ref=e1329]: + - link "@ErshovDmitry" [ref=e1332] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1333] + - generic [ref=e1334]: + - code [ref=e1335]: + - 'link "i18n(terminal): internationalize terminal view notifications, buttons…" [ref=e1336] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/5f91dece2050c5e00fe63aca8990dc4424bb6639 + - button "Commit message body" [ref=e1338] [cursor=pointer]: … + - code [ref=e1342]: + - link "5f91dec" [ref=e1343] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/5f91dece2050c5e00fe63aca8990dc4424bb6639 + - generic [ref=e1344]: + - img [ref=e1346] + - generic [ref=e1351]: + - link "@ErshovDmitry" [ref=e1354] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1355] + - generic [ref=e1356]: + - code [ref=e1357]: + - 'link "fix: remove stale LocaleSettings import, merge duplicate notebook YAM…" [ref=e1358] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/1a45d42a209d971c289b173445d867b65e52351c + - button "Commit message body" [ref=e1360] [cursor=pointer]: … + - code [ref=e1364]: + - link "1a45d42" [ref=e1365] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1a45d42a209d971c289b173445d867b65e52351c + - generic [ref=e1367]: + - link "@ZacharyZcR" [ref=e1369] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e1370] + - generic [ref=e1372]: + - generic [ref=e1373]: + - group [ref=e1376]: + - button "Show options" [ref=e1377] [cursor=pointer]: + - img "Show options" [ref=e1380] + - heading "ZacharyZcR commented May 21, 202616 minutes ago" [level=3] [ref=e1382]: + - generic [ref=e1383]: + - strong [ref=e1384]: + - link "ZacharyZcR" [ref=e1385] [cursor=pointer]: + - /url: /ZacharyZcR + - text: commented + - link "May 21, 202616 minutes ago" [ref=e1386] [cursor=pointer]: + - /url: "#issuecomment-4501916575" + - generic [ref=e1387]: + - table [ref=e1389]: + - rowgroup [ref=e1390]: + - row "Thank you very much! Perhaps we could reach out to Warp’s Slack channel together and let them decide how to proceed with development. Also, your PR needs to be linked to an issue with a specific tag; you can take a look at my PR and issue." [ref=e1391]: + - cell "Thank you very much! Perhaps we could reach out to Warp’s Slack channel together and let them decide how to proceed with development. Also, your PR needs to be linked to an issue with a specific tag; you can take a look at my PR and issue." [ref=e1392]: + - blockquote [ref=e1393]: + - paragraph [ref=e1394]: + - link "@ZacharyZcR" [ref=e1395] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: Thanks for reaching out! Happy to collaborate. We've matched your YAML approach — + - code [ref=e1396]: crates/i18n/ + - text: "," + - code [ref=e1397]: t!() + - text: / + - code [ref=e1398]: t_required!() + - text: macros, + - code [ref=e1399]: "resources/bundled/locales/{en,ru}.yml" + - text: (1080 keys, full Russian translation). Our + - code [ref=e1400]: ru.yml + - text: should be a drop-in addition. + - paragraph [ref=e1401]: We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. + - paragraph [ref=e1402]: + - text: How can we help with + - link "#10630" [ref=e1403] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "? Happy to contribute Russian translations in your format, or collaborate however makes sense." + - paragraph [ref=e1404]: Cheers, Dmitry + - paragraph [ref=e1405]: Thank you very much! Perhaps we could reach out to Warp’s Slack channel together and let them decide how to proceed with development. Also, your PR needs to be linked to an issue with a specific tag; you can take a look at my PR and issue. + - group [ref=e1410]: + - generic "Add or remove reactions" [ref=e1411] [cursor=pointer]: + - img [ref=e1412] + - generic [ref=e1417]: + - img [ref=e1419] + - generic [ref=e1424]: + - link "@ErshovDmitry" [ref=e1427] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1428] + - code [ref=e1430]: + - 'link "fix: resolve compilation errors after rebase onto master" [ref=e1431] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/e62f4aad25c793bb7e3d109807f26512fc6142d3 + - group [ref=e1435]: + - generic "1 / 1 checks OK" [ref=e1436] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1437] + - code [ref=e1440]: + - link "e62f4aa" [ref=e1441] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/e62f4aad25c793bb7e3d109807f26512fc6142d3 + - generic [ref=e1443]: + - img [ref=e1445] + - generic [ref=e1447]: + - link "@ErshovDmitry" [ref=e1448] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1449] + - link "ErshovDmitry" [ref=e1450] [cursor=pointer]: + - /url: /ErshovDmitry + - link "force-pushed" [ref=e1451] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/a83b91ca0f7faecce47ed52c323c36647f7bfe2b..e62f4aad25c793bb7e3d109807f26512fc6142d3 + - text: the + - generic [ref=e1453]: i18n + - text: branch from + - link "a83b91c" [ref=e1454] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/a83b91ca0f7faecce47ed52c323c36647f7bfe2b + - code [ref=e1455]: a83b91c + - text: to + - link "e62f4aa" [ref=e1456] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/e62f4aad25c793bb7e3d109807f26512fc6142d3 + - code [ref=e1457]: e62f4aa + - link "Compare" [ref=e1458] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/a83b91ca0f7faecce47ed52c323c36647f7bfe2b..e62f4aad25c793bb7e3d109807f26512fc6142d3 + - generic [ref=e1460]: Compare + - link "May 21, 2026 02:443 minutes ago" [ref=e1461] [cursor=pointer]: + - /url: "#event-25774475980" + - generic [ref=e1465]: + - img [ref=e1467] + - generic [ref=e1472]: + - link "@ErshovDmitry" [ref=e1475] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1476] + - code [ref=e1478]: + - 'link "chore: suppress dead_code warnings from handoff stubs" [ref=e1479] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/f05a60079a0f77037aa4edde53fa7fd641da2336 + - group [ref=e1483]: + - generic "1 / 1 checks OK" [ref=e1484] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1485] + - code [ref=e1488]: + - link "f05a600" [ref=e1489] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f05a60079a0f77037aa4edde53fa7fd641da2336 + - generic [ref=e1493]: + - img [ref=e1494] + - generic [ref=e1497]: Loading + - generic [ref=e1500]: + - link "@ErshovDmitry" [ref=e1502] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1503] + - form "Add a comment" [ref=e1505]: + - group [ref=e1506]: + - heading "Add a comment" [level=4] [ref=e1509] + - generic [ref=e1510]: Comment + - generic [ref=e1511]: + - generic [ref=e1512]: + - tablist "Add a comment" [ref=e1513]: + - tab "Write" [selected] [ref=e1514] [cursor=pointer] + - tab "Preview" [ref=e1515] [cursor=pointer] + - toolbar [ref=e1516]: + - generic [ref=e1517]: + - button "Heading" [ref=e1519] [cursor=pointer]: + - img + - button "Bold" [ref=e1521] [cursor=pointer]: + - img + - button "Italic" [ref=e1523] [cursor=pointer]: + - img + - button "Quote" [ref=e1525] [cursor=pointer]: + - img + - button "Code" [ref=e1527] [cursor=pointer]: + - img + - button "Link" [ref=e1529] [cursor=pointer]: + - img + - separator [ref=e1530] + - button "Numbered list" [ref=e1532] [cursor=pointer]: + - img + - button "Unordered list" [ref=e1534] [cursor=pointer]: + - img + - button "Task list" [ref=e1536] [cursor=pointer]: + - img + - separator [ref=e1537] + - button "Attach files" [ref=e1539] [cursor=pointer]: + - img + - button "Mention" [ref=e1541] [cursor=pointer]: + - img + - button "Reference" [ref=e1543] [cursor=pointer]: + - img + - button "Saved replies" [ref=e1545] [cursor=pointer]: + - img + - button "Slash commands" [ref=e1547] [cursor=pointer]: + - img + - tabpanel "Write" [ref=e1548]: + - generic [ref=e1552]: + - textbox "Comment" [ref=e1553]: + - /placeholder: " " + - paragraph: Add your comment here... + - generic [ref=e1554]: + - link "Markdown is supported" [ref=e1556] [cursor=pointer]: + - /url: https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax + - generic [ref=e1557]: + - generic: + - img + - generic [ref=e1558]: Markdown is supported + - button "Paste, drop, or click to add files" [ref=e1559] [cursor=pointer]: + - generic [ref=e1560]: + - generic: + - img + - generic [ref=e1561]: Paste, drop, or click to add files + - generic [ref=e1564]: + - button "Close pull request" [ref=e1566] [cursor=pointer]: + - img [ref=e1567] + - text: Close pull request + - button "Comment" [disabled] [ref=e1570] + - generic [ref=e1571]: + - img [ref=e1572] + - generic [ref=e1574]: + - text: Remember, contributions to this repository should follow its + - link "contributing guidelines" [ref=e1575] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/3d940d78a78d9d18301e435c2b53607236044a08/CONTRIBUTING.md + - text: "," + - link "security policy" [ref=e1576] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/3d940d78a78d9d18301e435c2b53607236044a08/SECURITY.md + - text: ", and" + - link "code of conduct" [ref=e1577] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/3d940d78a78d9d18301e435c2b53607236044a08/CODE_OF_CONDUCT.md + - text: . + - generic [ref=e1578]: + - img [ref=e1579] + - strong [ref=e1581]: ProTip! + - text: Add comments to specific lines under + - link "Files changed" [ref=e1582] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files + - text: . + - generic [ref=e1586]: + - form "Select reviewers" [ref=e1588]: + - heading "Reviewers" [level=3] [ref=e1589] + - generic [ref=e1590]: + - paragraph [ref=e1591]: + - generic [ref=e1592]: + - link "@oz-for-oss" [ref=e1593] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e1594] + - link "oz-for-oss[bot]" [ref=e1595] [cursor=pointer]: + - /url: /apps/oz-for-oss + - link "oz-for-oss[bot] requested changes" [ref=e1596] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes/BASE..169dfa663dd4c533eb32daf850f25d6b9c123521 + - img [ref=e1598] + - paragraph [ref=e1600]: Requested changes must be addressed to merge this pull request. + - generic [ref=e1601]: + - generic [ref=e1602]: Still in progress? + - group [ref=e1603]: + - button "Convert to draft" [ref=e1604] [cursor=pointer] + - form "Select assignees" [ref=e1606]: + - heading "Assignees" [level=3] [ref=e1607] + - text: No one assigned + - generic [ref=e1608]: + - heading "Labels" [level=3] [ref=e1609] + - generic [ref=e1610]: + - link "cla-signed" [ref=e1611] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - generic [ref=e1612]: cla-signed + - link "external-contributor" [ref=e1613] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - generic [ref=e1614]: external-contributor + - form "Select projects" [ref=e1616]: + - heading "Projects" [level=3] [ref=e1617] + - text: None yet + - form "Select milestones" [ref=e1619]: + - heading "Milestone" [level=3] [ref=e1620] + - text: No milestone + - form "Link issues" [ref=e1626]: + - heading "Development" [level=3] [ref=e1627] + - paragraph [ref=e1628]: Successfully merging this pull request may close these issues. + - paragraph [ref=e1630]: None yet + - generic [ref=e1632]: + - button "Notifications Customize" [ref=e1635] [cursor=pointer]: + - generic [ref=e1636]: + - generic [ref=e1637]: Notifications + - generic [ref=e1638]: Customize + - button "Unsubscribe" [ref=e1640] [cursor=pointer]: + - generic [ref=e1641]: + - generic: + - img + - generic [ref=e1642]: Unsubscribe + - paragraph [ref=e1643]: You’re receiving notifications because you were mentioned. + - generic [ref=e1645]: + - heading "2 participants" [level=3] [ref=e1646] + - generic [ref=e1647]: + - link "@ErshovDmitry" [ref=e1648] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1649] + - link "@ZacharyZcR" [ref=e1650] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e1651] + - generic [ref=e1654]: + - generic [ref=e1655]: + - checkbox "Allow edits by maintainers" [checked] [ref=e1656] + - text: Allow edits by maintainers + - button "Learn more about allowing maintainer edits" [ref=e1657] [cursor=pointer]: + - img [ref=e1660] + - contentinfo [ref=e1663]: + - heading "Footer" [level=2] [ref=e1664] + - generic [ref=e1665]: + - generic [ref=e1666]: + - link "GitHub Homepage" [ref=e1667] [cursor=pointer]: + - /url: https://github.com + - img [ref=e1668] + - generic [ref=e1670]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e1671]: + - heading "Footer navigation" [level=3] [ref=e1672] + - list "Footer navigation" [ref=e1673]: + - listitem [ref=e1674]: + - link "Terms" [ref=e1675] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e1676]: + - link "Privacy" [ref=e1677] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e1678]: + - link "Security" [ref=e1679] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e1680]: + - link "Status" [ref=e1681] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e1682]: + - link "Community" [ref=e1683] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e1684]: + - link "Docs" [ref=e1685] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e1686]: + - link "Contact" [ref=e1687] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e1688]: + - button "Manage cookies" [ref=e1690] [cursor=pointer] + - listitem [ref=e1691]: + - button "Do not share my personal information" [ref=e1693] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-25T10-02-57-122Z.yml b/.playwright-mcp/page-2026-05-25T10-02-57-122Z.yml new file mode 100644 index 0000000000..1e2b948192 --- /dev/null +++ b/.playwright-mcp/page-2026-05-25T10-02-57-122Z.yml @@ -0,0 +1,1904 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - button "Open user navigation menu" [ref=e59] [cursor=pointer]: + - img "User avatar" [ref=e60] + - heading "Repository navigation" [level=2] [ref=e61] + - navigation "Repository" [ref=e62]: + - list [ref=e63]: + - listitem [ref=e64]: + - link "Code" [ref=e65] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e67] + - generic [ref=e69]: Code + - listitem [ref=e70]: + - link "Issues (3.5k)" [ref=e71] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e73] + - generic [ref=e76]: Issues + - generic [ref=e77]: + - generic [ref=e78]: 3.5k + - generic [ref=e79]: (3.5k) + - listitem [ref=e80]: + - link "Pull requests (547)" [ref=e81] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e83] + - generic [ref=e85]: Pull requests + - generic [ref=e86]: + - generic [ref=e87]: "547" + - generic [ref=e88]: (547) + - listitem [ref=e89]: + - link "Agents" [ref=e90] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e92] + - generic [ref=e95]: Agents + - listitem [ref=e96]: + - link "Discussions" [ref=e97] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e99] + - generic [ref=e101]: Discussions + - listitem [ref=e102]: + - link "Actions" [ref=e103] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e105] + - generic [ref=e107]: Actions + - listitem [ref=e108]: + - link "Projects" [ref=e109] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e111] + - generic [ref=e113]: Projects + - listitem [ref=e114]: + - link "Security and quality" [ref=e115] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e117] + - generic [ref=e119]: Security and quality + - listitem [ref=e120]: + - link "Insights" [ref=e121] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e123] + - generic [ref=e125]: Insights + - main [ref=e129]: + - generic [ref=e136]: + - generic [ref=e139]: + - 'heading "I18n #11382 Edit title" [level=1] [ref=e141]': + - text: I18n + - generic [ref=e142]: + - generic [ref=e143]: "#11382" + - button "Edit title" [ref=e144] [cursor=pointer]: + - img [ref=e145] + - generic [ref=e148]: + - button "View status" [ref=e150] [cursor=pointer]: + - generic [ref=e153]: View status + - button "Code" [ref=e154] [cursor=pointer]: + - generic [ref=e155]: + - generic [ref=e156]: Code + - generic: + - img + - generic [ref=e158]: + - generic [ref=e160]: + - img "Pull request" [ref=e161] + - text: Open + - generic [ref=e164]: + - link "ErshovDmitry" [ref=e165] [cursor=pointer]: + - /url: /ErshovDmitry + - text: wants to merge 41 commits into + - generic [ref=e166]: + - link "warpdotdev:master" [ref=e167] [cursor=pointer]: + - /url: /warpdotdev/warp/tree/master + - generic [ref=e168]: from + - generic [ref=e169]: + - link "ErshovDmitry:i18n" [ref=e170] [cursor=pointer]: + - /url: /ErshovDmitry/warp-i18n/tree/i18n + - button "Copy head branch name to clipboard" [ref=e171] [cursor=pointer]: + - img [ref=e172] + - generic [ref=e175]: + - generic [ref=e177]: + - generic [ref=e178]: +9 942 + - generic [ref=e179]: "-3 079" + - generic [ref=e180]: "Lines changed: 9942 additions & 3079 deletions" + - navigation "Pull request navigation tabs" [ref=e189]: + - tablist [ref=e190]: + - tab "Conversation (16)" [selected] [ref=e191] [cursor=pointer]: + - img [ref=e192] + - text: Conversation + - generic [ref=e194]: "16" + - generic [ref=e195]: (16) + - tab "Commits (41)" [ref=e196] [cursor=pointer]: + - img [ref=e197] + - text: Commits + - generic [ref=e199]: "41" + - generic [ref=e200]: (41) + - tab "Checks (0)" [ref=e201] [cursor=pointer]: + - img [ref=e202] + - text: Checks + - generic [ref=e204]: "0" + - generic [ref=e205]: (0) + - tab "Files changed (103)" [ref=e206] [cursor=pointer]: + - img [ref=e207] + - text: Files changed + - generic [ref=e209]: "103" + - generic [ref=e210]: (103) + - generic [ref=e215]: + - generic [ref=e217]: + - heading "Conversation" [level=2] [ref=e218] + - generic [ref=e219]: + - generic [ref=e220]: + - generic [ref=e222]: + - link "@ErshovDmitry" [ref=e223] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e224] + - generic [ref=e226]: + - generic [ref=e227]: + - group [ref=e230]: + - button "Show options" [ref=e231] [cursor=pointer]: + - img "Show options" [ref=e234] + - heading "ErshovDmitry commented May 20, 20265 days ago" [level=3] [ref=e236]: + - generic [ref=e237]: + - strong [ref=e238]: + - link "ErshovDmitry" [ref=e239] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20265 days ago" [ref=e240] [cursor=pointer]: + - /url: "#issue-4483039846" + - generic [ref=e244]: + - paragraph [ref=e245]: Hello! My name is Dmitry. + - paragraph [ref=e246]: I've been using Warp and love it. I wanted to add i18n support so the terminal could speak Russian (and other languages in the future). I'm not a developer myself — this was built with DeepSeek AI, but I reviewed everything personally. + - paragraph [ref=e247]: I'd much rather this live in the official repo than a fork. If the team is interested in i18n, I'm happy to help however I can — and I'll gladly delete my fork once upstreamed. + - paragraph [ref=e248]: + - text: Cheers, + - text: Dmitry + - separator [ref=e249] + - heading "What" [level=2] [ref=e250] + - paragraph [ref=e251]: Adds i18n (internationalization) support to Warp. + - heading "Changes" [level=2] [ref=e252] + - 'heading "New crate: warp_i18n" [level=3] [ref=e253]': + - text: "New crate:" + - code [ref=e254]: warp_i18n + - list [ref=e255]: + - listitem [ref=e256]: + - code [ref=e257]: t!() + - text: macro for translating UI strings + - listitem [ref=e258]: + - code [ref=e259]: current_locale() + - text: / + - code [ref=e260]: set_current_locale() + - text: for runtime language switching + - listitem [ref=e261]: + - code [ref=e262]: init_from_json() + - text: for WASM compatibility + - listitem [ref=e263]: Build script for compile-time key validation + - listitem [ref=e264]: 21 tests + - heading "Feature flag" [level=3] [ref=e265] + - list [ref=e266]: + - listitem [ref=e267]: + - code [ref=e268]: FeatureFlag::I18n + - text: in + - code [ref=e269]: DOGFOOD_FLAGS + - heading "Settings" [level=3] [ref=e270] + - list [ref=e271]: + - listitem [ref=e272]: + - code [ref=e273]: Locale + - text: enum (En, Ru) + - listitem [ref=e274]: Language dropdown in Settings → Appearance + - listitem [ref=e275]: Auto-detect system locale on first run + - heading "Migrated UI" [level=3] [ref=e276] + - list [ref=e277]: + - listitem [ref=e278]: 10 top-level menu labels + - listitem [ref=e279]: 14 settings section names + - listitem [ref=e280]: 11 settings category labels + - heading "Locales" [level=3] [ref=e281] + - list [ref=e282]: + - listitem [ref=e283]: + - code [ref=e284]: en.json + - text: — 51 keys (mandatory fallback) + - listitem [ref=e285]: + - code [ref=e286]: ru.json + - text: — 51 keys (Russian translation) + - paragraph [ref=e287]: + - emphasis [ref=e288]: + - text: Built with DeepSeek AI — human-reviewed, all tests pass ( + - code [ref=e289]: cargo check --workspace + - text: "," + - code [ref=e290]: cargo test -p warp_i18n + - text: ). + - group [ref=e294]: + - generic "Add or remove reactions" [ref=e295] [cursor=pointer]: + - img [ref=e296] + - generic [ref=e298]: + - generic [ref=e300]: + - link "@cla-bot" [ref=e302] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e303] + - generic [ref=e305]: + - generic [ref=e306]: + - group [ref=e309]: + - button "Show options" [ref=e310] [cursor=pointer]: + - img "Show options" [ref=e313] + - heading "cla-bot Bot commented May 20, 20265 days ago" [level=3] [ref=e315]: + - generic [ref=e316]: + - strong [ref=e317]: + - link "cla-bot" [ref=e318] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e319]: Bot + - text: commented + - link "May 20, 20265 days ago" [ref=e320] [cursor=pointer]: + - /url: "#issuecomment-4494411354" + - generic [ref=e321]: + - table [ref=e323]: + - rowgroup [ref=e324]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e325]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e326]': + - paragraph [ref=e327]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e328]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e329]: + - listitem [ref=e330]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e331]: git config --list | grep email + - listitem [ref=e332]: + - text: If not, set it up using + - code [ref=e333]: git config --global user.email email@example.com + - listitem [ref=e334]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e335] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e340]: + - generic "Add or remove reactions" [ref=e341] [cursor=pointer]: + - img [ref=e342] + - generic [ref=e345]: + - link "@oz-for-oss" [ref=e347] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e348] + - generic [ref=e350]: + - generic [ref=e351]: + - generic [ref=e352]: + - group [ref=e354]: + - button "Show options" [ref=e355] [cursor=pointer]: + - img "Show options" [ref=e358] + - generic "This user has previously committed to the warp repository." [ref=e361]: + - generic [ref=e362]: Contributor + - heading "oz-for-oss Bot commented May 20, 20265 days ago" [level=3] [ref=e363]: + - generic [ref=e364]: + - strong [ref=e365]: + - link "oz-for-oss" [ref=e366] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e367]: Bot + - text: commented + - link "May 20, 20265 days ago" [ref=e368] [cursor=pointer]: + - /url: "#issuecomment-4494411637" + - generic [ref=e369]: + - table [ref=e371]: + - rowgroup [ref=e372]: + - 'row "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e373]': + - 'cell "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e374]': + - paragraph [ref=e375]: + - link "@ErshovDmitry" [ref=e376] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e377]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e378]: ready-to-implement + - text: . + - paragraph [ref=e379]: "Issue-state enforcement details:" + - list [ref=e380]: + - listitem [ref=e381]: + - paragraph [ref=e382]: "Associated same-repo issues checked: none" + - listitem [ref=e383]: + - paragraph [ref=e384]: + - text: "Required readiness label:" + - code [ref=e385]: ready-to-implement + - paragraph [ref=e386]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e387]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e388]: ready-to-implement + - text: . + - paragraph [ref=e389]: + - emphasis [ref=e390]: + - text: Powered by + - link "Oz" [ref=e391] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e396]: + - generic "Add or remove reactions" [ref=e397] [cursor=pointer]: + - img [ref=e398] + - generic [ref=e401]: + - img [ref=e403] + - generic [ref=e405]: + - link "@github-actions" [ref=e406] [cursor=pointer]: + - /url: /apps/github-actions + - img "@github-actions" [ref=e407] + - link "github-actions" [ref=e408] [cursor=pointer]: + - /url: /apps/github-actions + - generic [ref=e409]: Bot + - text: added the + - link "external-contributor" [ref=e410] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - text: label + - link "May 20, 20265 days ago" [ref=e411] [cursor=pointer]: + - /url: "#event-25738319166" + - generic [ref=e414]: + - generic [ref=e415]: + - link "oz-for-oss[bot]" [ref=e416] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "oz-for-oss[bot]" [ref=e417] + - img [ref=e419] + - generic [ref=e421]: + - generic [ref=e422]: + - strong [ref=e423]: + - link "oz-for-oss" [ref=e424] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e425]: Bot + - text: requested changes + - link "May 20, 20265 days ago" [ref=e427] [cursor=pointer]: + - /url: "#pullrequestreview-4325084051" + - link "View reviewed changes" [ref=e429] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes + - generic [ref=e431]: View reviewed changes + - generic [ref=e434]: + - generic [ref=e435]: + - generic [ref=e436]: + - group [ref=e438]: + - button "Show options" [ref=e439] [cursor=pointer]: + - img "Show options" [ref=e442] + - generic "This user has previously committed to the warp repository." [ref=e445]: + - generic [ref=e446]: Contributor + - heading "oz-for-oss Bot left a comment" [level=3] [ref=e447]: + - generic [ref=e448]: + - strong [ref=e449]: + - link "oz-for-oss" [ref=e450] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e451]: Bot + - text: left a comment + - generic [ref=e454]: + - paragraph [ref=e455]: + - link "@ErshovDmitry" [ref=e456] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e457]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e458]: ready-to-implement + - text: . + - paragraph [ref=e459]: "Issue-state enforcement details:" + - list [ref=e460]: + - listitem [ref=e461]: + - paragraph [ref=e462]: "Associated same-repo issues checked: none" + - listitem [ref=e463]: + - paragraph [ref=e464]: + - text: "Required readiness label:" + - code [ref=e465]: ready-to-implement + - paragraph [ref=e466]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e467]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e468]: ready-to-implement + - text: . + - paragraph [ref=e469]: + - emphasis [ref=e470]: + - text: Powered by + - link "Oz" [ref=e471] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e475]: + - generic "Add or remove reactions" [ref=e476] [cursor=pointer]: + - img [ref=e477] + - generic [ref=e480]: + - link "@ErshovDmitry" [ref=e482] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e483] + - generic [ref=e485]: + - generic [ref=e486]: + - generic [ref=e487]: + - group [ref=e489]: + - button "Show options" [ref=e490] [cursor=pointer]: + - img "Show options" [ref=e493] + - generic "You are the author of this pull request." [ref=e496]: + - generic [ref=e497]: Author + - heading "ErshovDmitry commented May 20, 20265 days ago" [level=3] [ref=e498]: + - generic [ref=e499]: + - strong [ref=e500]: + - link "ErshovDmitry" [ref=e501] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20265 days ago" [ref=e502] [cursor=pointer]: + - /url: "#issuecomment-4494430862" + - generic [ref=e503]: + - table [ref=e505]: + - rowgroup [ref=e506]: + - 'row "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e507]': + - 'cell "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e508]': + - paragraph [ref=e509]: + - text: Oh wow — I just noticed there are already several i18n PRs open ( + - link "#10630" [ref=e510] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "," + - link "#10990" [ref=e511] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: "," + - link "#9458" [ref=e512] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9458 + - text: "," + - link "#9922" [ref=e513] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9922 + - text: ). I should have checked first before opening mine. My apologies for the noise! + - blockquote [ref=e514]: + - paragraph [ref=e515]: + - text: That said, I see none of them include Russian yet. If the team settles on one i18n approach (looks like + - link "#10630" [ref=e516] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e517] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: is the leading one?), I'd be happy to contribute Russian translations to that effort instead. + - paragraph [ref=e518]: I'll close this PR if it's just adding clutter — just let me know. + - paragraph [ref=e519]: + - text: Cheers, + - text: Dmitry + - group [ref=e524]: + - generic "Add or remove reactions" [ref=e525] [cursor=pointer]: + - img [ref=e526] + - generic [ref=e529]: + - link "@cla-bot" [ref=e531] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e532] + - generic [ref=e534]: + - generic [ref=e535]: + - group [ref=e538]: + - button "Show options" [ref=e539] [cursor=pointer]: + - img "Show options" [ref=e542] + - heading "cla-bot Bot commented May 20, 20265 days ago" [level=3] [ref=e544]: + - generic [ref=e545]: + - strong [ref=e546]: + - link "cla-bot" [ref=e547] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e548]: Bot + - text: commented + - link "May 20, 20265 days ago" [ref=e549] [cursor=pointer]: + - /url: "#issuecomment-4494962896" + - generic [ref=e550]: + - table [ref=e552]: + - rowgroup [ref=e553]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e554]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e555]': + - paragraph [ref=e556]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e557]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e558]: + - listitem [ref=e559]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e560]: git config --list | grep email + - listitem [ref=e561]: + - text: If not, set it up using + - code [ref=e562]: git config --global user.email email@example.com + - listitem [ref=e563]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e564] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e569]: + - generic "Add or remove reactions" [ref=e570] [cursor=pointer]: + - img [ref=e571] + - group [ref=e573]: + - generic "2 similar comments" [ref=e574] [cursor=pointer]: + - generic [ref=e576]: 2 similar comments + - generic [ref=e578]: + - link "@ErshovDmitry" [ref=e580] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e581] + - generic [ref=e583]: + - generic [ref=e584]: + - generic [ref=e585]: + - group [ref=e587]: + - button "Show options" [ref=e588] [cursor=pointer]: + - img "Show options" [ref=e591] + - generic "You are the author of this pull request." [ref=e594]: + - generic [ref=e595]: Author + - heading "ErshovDmitry commented May 20, 20265 days ago" [level=3] [ref=e596]: + - generic [ref=e597]: + - strong [ref=e598]: + - link "ErshovDmitry" [ref=e599] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20265 days ago" [ref=e600] [cursor=pointer]: + - /url: "#issuecomment-4495523677" + - generic [ref=e601]: + - table [ref=e603]: + - rowgroup [ref=e604]: + - 'row "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e605]': + - 'cell "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e606]': + - paragraph [ref=e607]: + - text: Update:** I've rewritten this PR to match + - link "@ZacharyZcR" [ref=e608] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: "'s YAML approach from" + - link "#10630" [ref=e609] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e610] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: ":" + - blockquote [ref=e611]: + - list [ref=e612]: + - listitem [ref=e613]: + - code [ref=e614]: crates/i18n/ + - text: with + - code [ref=e615]: t!() + - text: / + - code [ref=e616]: t_required!() + - text: macros and + - code [ref=e617]: TranslationLookup + - listitem [ref=e618]: + - code [ref=e619]: "resources/bundled/locales/{en,ru}.yml" + - text: (247 keys each, YAML) + - listitem [ref=e620]: + - code [ref=e621]: WARP_LANG + - text: env var → system locale → + - code [ref=e622]: en + - text: fallback + - listitem [ref=e623]: + - text: No feature flag, no settings UI — same design as + - generic [ref=e624]: + - img [ref=e625] + - 'link "Add i18n framework with Simplified Chinese (zh-CN) support #10630" [ref=e627] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/pull/10630 + - paragraph [ref=e628]: + - text: The Russian + - code [ref=e629]: ru.yml + - text: should be a drop-in addition to the framework once it's approved. Happy to rebase onto whichever PR lands first. + - paragraph [ref=e630]: Dmitry + - group [ref=e635]: + - generic "Add or remove reactions" [ref=e636] [cursor=pointer]: + - img [ref=e637] + - generic [ref=e640]: + - link "@ErshovDmitry" [ref=e642] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e643] + - generic [ref=e645]: + - generic [ref=e646]: + - generic [ref=e647]: + - group [ref=e649]: + - button "Show options" [ref=e650] [cursor=pointer]: + - img "Show options" [ref=e653] + - generic "You are the author of this pull request." [ref=e656]: + - generic [ref=e657]: Author + - heading "ErshovDmitry commented May 20, 20265 days ago" [level=3] [ref=e658]: + - generic [ref=e659]: + - strong [ref=e660]: + - link "ErshovDmitry" [ref=e661] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20265 days ago" [ref=e662] [cursor=pointer]: + - /url: "#issuecomment-4495572049" + - generic [ref=e663]: + - table [ref=e665]: + - rowgroup [ref=e666]: + - row "@cla-bot check" [ref=e667]: + - cell "@cla-bot check" [ref=e668]: + - paragraph [ref=e669]: + - link "@cla-bot" [ref=e670] [cursor=pointer]: + - /url: https://github.com/cla-bot + - text: check + - group [ref=e675]: + - generic "Add or remove reactions" [ref=e676] [cursor=pointer]: + - img [ref=e677] + - generic [ref=e680]: + - img [ref=e682] + - generic [ref=e684]: + - link "@cla-bot" [ref=e685] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e686] + - link "cla-bot" [ref=e687] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e688]: Bot + - text: added the + - link "cla-signed" [ref=e689] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - text: label + - link "May 20, 20265 days ago" [ref=e690] [cursor=pointer]: + - /url: "#event-25743817179" + - generic [ref=e692]: + - link "@cla-bot" [ref=e694] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e695] + - generic [ref=e697]: + - generic [ref=e698]: + - group [ref=e701]: + - button "Show options" [ref=e702] [cursor=pointer]: + - img "Show options" [ref=e705] + - heading "cla-bot Bot commented May 20, 20265 days ago" [level=3] [ref=e707]: + - generic [ref=e708]: + - strong [ref=e709]: + - link "cla-bot" [ref=e710] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e711]: Bot + - text: commented + - link "May 20, 20265 days ago" [ref=e712] [cursor=pointer]: + - /url: "#issuecomment-4495573189" + - generic [ref=e713]: + - table [ref=e715]: + - rowgroup [ref=e716]: + - row "The cla-bot has been summoned, and re-checked this pull request!" [ref=e717]: + - cell "The cla-bot has been summoned, and re-checked this pull request!" [ref=e718]: + - paragraph [ref=e719]: The cla-bot has been summoned, and re-checked this pull request! + - group [ref=e724]: + - generic "Add or remove reactions" [ref=e725] [cursor=pointer]: + - img [ref=e726] + - generic [ref=e729]: + - link "@ErshovDmitry" [ref=e731] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e732] + - generic [ref=e734]: + - generic [ref=e735]: + - generic [ref=e736]: + - group [ref=e738]: + - button "Show options" [ref=e739] [cursor=pointer]: + - img "Show options" [ref=e742] + - generic "You are the author of this pull request." [ref=e745]: + - generic [ref=e746]: Author + - heading "ErshovDmitry commented May 20, 20265 days ago" [level=3] [ref=e747]: + - generic [ref=e748]: + - strong [ref=e749]: + - link "ErshovDmitry" [ref=e750] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20265 days ago" [ref=e751] [cursor=pointer]: + - /url: "#issuecomment-4495607261" + - generic [ref=e752]: + - table [ref=e754]: + - rowgroup [ref=e755]: + - 'row "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e756]': + - 'cell "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e757]': + - paragraph [ref=e758]: + - link "@oss-maintainers" [ref=e759] [cursor=pointer]: + - /url: https://github.com/oss-maintainers + - text: This PR now follows the same YAML-based approach as + - link "#10630" [ref=e760] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e761] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: by + - link "@ZacharyZcR" [ref=e762] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: — matching + - code [ref=e763]: crates/i18n/ + - text: API, + - code [ref=e764]: "resources/bundled/locales/{en,ru}.yml" + - text: "," + - code [ref=e765]: t!() + - text: / + - code [ref=e766]: t_required!() + - text: macros, and + - code [ref=e767]: WARP_LANG + - text: env var detection. + - paragraph [ref=e768]: + - text: It's blocked by the issue-state enforcement check (needs a + - code [ref=e769]: ready-to-implement + - text: issue). Could a maintainer please add + - code [ref=e770]: ready-to-implement + - text: to + - link "#1194" [ref=e771] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194 + - text: ", or let me know if I should create a separate tracking issue?" + - paragraph [ref=e772]: + - text: Russian + - code [ref=e773]: ru.yml + - text: (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. + - paragraph [ref=e774]: + - text: Cheers, + - text: Dmitry + - group [ref=e779]: + - generic "Add or remove reactions" [ref=e780] [cursor=pointer]: + - img [ref=e781] + - generic [ref=e784]: + - img [ref=e786] + - generic [ref=e788]: + - link "@ErshovDmitry" [ref=e789] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e790] + - link "ErshovDmitry" [ref=e791] [cursor=pointer]: + - /url: /ErshovDmitry + - link "force-pushed" [ref=e792] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - text: the + - generic [ref=e794]: i18n + - text: branch from + - link "2196591" [ref=e795] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/219659155d8415b5773506f3306f6339e1300b60 + - code [ref=e796]: "2196591" + - text: to + - link "d67453f" [ref=e797] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/d67453f73b0087f3a931fd06e958ab9cebb3be0c + - code [ref=e798]: d67453f + - link "Compare" [ref=e799] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - generic [ref=e801]: Compare + - link "May 20, 2026 07:365 days ago" [ref=e802] [cursor=pointer]: + - /url: "#event-25744804343" + - generic [ref=e804]: + - link "@ZacharyZcR" [ref=e806] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e807] + - generic [ref=e809]: + - generic [ref=e810]: + - group [ref=e813]: + - button "Show options" [ref=e814] [cursor=pointer]: + - img "Show options" [ref=e817] + - heading "ZacharyZcR commented May 20, 20265 days ago" [level=3] [ref=e819]: + - generic [ref=e820]: + - strong [ref=e821]: + - link "ZacharyZcR" [ref=e822] [cursor=pointer]: + - /url: /ZacharyZcR + - text: commented + - link "May 20, 20265 days ago" [ref=e823] [cursor=pointer]: + - /url: "#issuecomment-4499865827" + - generic [ref=e824]: + - table [ref=e826]: + - rowgroup [ref=e827]: + - row "Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel." [ref=e828]: + - cell "Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel." [ref=e829]: + - paragraph [ref=e830]: Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel. + - group [ref=e835]: + - generic "Add or remove reactions" [ref=e836] [cursor=pointer]: + - img [ref=e837] + - generic [ref=e840]: + - link "@ErshovDmitry" [ref=e842] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e843] + - generic [ref=e845]: + - generic [ref=e846]: + - generic [ref=e847]: + - group [ref=e849]: + - button "Show options" [ref=e850] [cursor=pointer]: + - img "Show options" [ref=e853] + - generic "You are the author of this pull request." [ref=e856]: + - generic [ref=e857]: Author + - heading "ErshovDmitry commented May 20, 20265 days ago" [level=3] [ref=e858]: + - generic [ref=e859]: + - strong [ref=e860]: + - link "ErshovDmitry" [ref=e861] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20265 days ago" [ref=e862] [cursor=pointer]: + - /url: "#issuecomment-4501859680" + - generic [ref=e863]: + - table [ref=e865]: + - rowgroup [ref=e866]: + - 'row "@ZacharyZcR Thanks for reaching out! Happy to collaborate. We''ve matched your YAML approach — crates/i18n/, t!()/t_required!() macros, resources/bundled/locales/{en,ru}.yml (1080 keys, full Russian translation). Our ru.yml should be a drop-in addition. We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. How can we help with #10630? Happy to contribute Russian translations in your format, or collaborate however makes sense. Cheers, Dmitry" [ref=e867]': + - 'cell "@ZacharyZcR Thanks for reaching out! Happy to collaborate. We''ve matched your YAML approach — crates/i18n/, t!()/t_required!() macros, resources/bundled/locales/{en,ru}.yml (1080 keys, full Russian translation). Our ru.yml should be a drop-in addition. We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. How can we help with #10630? Happy to contribute Russian translations in your format, or collaborate however makes sense. Cheers, Dmitry" [ref=e868]': + - paragraph [ref=e869]: + - link "@ZacharyZcR" [ref=e870] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: Thanks for reaching out! Happy to collaborate. We've matched your YAML approach — + - code [ref=e871]: crates/i18n/ + - text: "," + - code [ref=e872]: t!() + - text: / + - code [ref=e873]: t_required!() + - text: macros, + - code [ref=e874]: "resources/bundled/locales/{en,ru}.yml" + - text: (1080 keys, full Russian translation). Our + - code [ref=e875]: ru.yml + - text: should be a drop-in addition. + - paragraph [ref=e876]: We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. + - paragraph [ref=e877]: + - text: How can we help with + - link "#10630" [ref=e878] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "? Happy to contribute Russian translations in your format, or collaborate however makes sense." + - paragraph [ref=e879]: + - text: Cheers, + - text: Dmitry + - generic [ref=e882]: + - group [ref=e884]: + - generic "Add or remove reactions" [ref=e885] [cursor=pointer]: + - img [ref=e886] + - button "react with thumbs up" [ref=e890] [cursor=pointer]: + - img "+1" [ref=e892] + - generic [ref=e893]: "1" + - generic [ref=e895]: + - link "@ZacharyZcR" [ref=e897] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e898] + - generic [ref=e900]: + - generic [ref=e901]: + - group [ref=e904]: + - button "Show options" [ref=e905] [cursor=pointer]: + - img "Show options" [ref=e908] + - heading "ZacharyZcR commented May 20, 20265 days ago" [level=3] [ref=e910]: + - generic [ref=e911]: + - strong [ref=e912]: + - link "ZacharyZcR" [ref=e913] [cursor=pointer]: + - /url: /ZacharyZcR + - text: commented + - link "May 20, 20265 days ago" [ref=e914] [cursor=pointer]: + - /url: "#issuecomment-4501916575" + - generic [ref=e915]: + - table [ref=e917]: + - rowgroup [ref=e918]: + - row "Thank you very much! Perhaps we could reach out to Warp’s Slack channel together and let them decide how to proceed with development. Also, your PR needs to be linked to an issue with a specific tag; you can take a look at my PR and issue." [ref=e919]: + - cell "Thank you very much! Perhaps we could reach out to Warp’s Slack channel together and let them decide how to proceed with development. Also, your PR needs to be linked to an issue with a specific tag; you can take a look at my PR and issue." [ref=e920]: + - blockquote [ref=e921]: + - paragraph [ref=e922]: + - link "@ZacharyZcR" [ref=e923] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: Thanks for reaching out! Happy to collaborate. We've matched your YAML approach — + - code [ref=e924]: crates/i18n/ + - text: "," + - code [ref=e925]: t!() + - text: / + - code [ref=e926]: t_required!() + - text: macros, + - code [ref=e927]: "resources/bundled/locales/{en,ru}.yml" + - text: (1080 keys, full Russian translation). Our + - code [ref=e928]: ru.yml + - text: should be a drop-in addition. + - paragraph [ref=e929]: We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. + - paragraph [ref=e930]: + - text: How can we help with + - link "#10630" [ref=e931] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "? Happy to contribute Russian translations in your format, or collaborate however makes sense." + - paragraph [ref=e932]: Cheers, Dmitry + - paragraph [ref=e933]: Thank you very much! Perhaps we could reach out to Warp’s Slack channel together and let them decide how to proceed with development. Also, your PR needs to be linked to an issue with a specific tag; you can take a look at my PR and issue. + - group [ref=e938]: + - generic "Add or remove reactions" [ref=e939] [cursor=pointer]: + - img [ref=e940] + - generic [ref=e943]: + - img [ref=e945] + - generic [ref=e947]: + - link "@ErshovDmitry" [ref=e948] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e949] + - link "ErshovDmitry" [ref=e950] [cursor=pointer]: + - /url: /ErshovDmitry + - link "force-pushed" [ref=e951] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/a83b91ca0f7faecce47ed52c323c36647f7bfe2b..e62f4aad25c793bb7e3d109807f26512fc6142d3 + - text: the + - generic [ref=e953]: i18n + - text: branch from + - link "a83b91c" [ref=e954] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/a83b91ca0f7faecce47ed52c323c36647f7bfe2b + - code [ref=e955]: a83b91c + - text: to + - link "e62f4aa" [ref=e956] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/e62f4aad25c793bb7e3d109807f26512fc6142d3 + - code [ref=e957]: e62f4aa + - link "Compare" [ref=e958] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/a83b91ca0f7faecce47ed52c323c36647f7bfe2b..e62f4aad25c793bb7e3d109807f26512fc6142d3 + - generic [ref=e960]: Compare + - link "May 20, 2026 19:445 days ago" [ref=e961] [cursor=pointer]: + - /url: "#event-25774475980" + - generic [ref=e963]: + - link "@ErshovDmitry" [ref=e965] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e966] + - generic [ref=e968]: + - generic [ref=e969]: + - generic [ref=e970]: + - group [ref=e972]: + - button "Show options" [ref=e973] [cursor=pointer]: + - img "Show options" [ref=e976] + - generic "You are the author of this pull request." [ref=e979]: + - generic [ref=e980]: Author + - heading "ErshovDmitry commented May 20, 20265 days ago" [level=3] [ref=e981]: + - generic [ref=e982]: + - strong [ref=e983]: + - link "ErshovDmitry" [ref=e984] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20265 days ago" [ref=e985] [cursor=pointer]: + - /url: "#issuecomment-4502064710" + - generic [ref=e986]: + - table [ref=e988]: + - rowgroup [ref=e989]: + - 'row "@ZacharyZcR Just to clarify — I don''t speak English myself, I''m communicating through DeepSeek AI (which also built this PR). So I''m a bit nervous about posting in the Slack channel directly — I don''t want to say something wrong. If you could mention me ( @ErshovDmitry ) in the #oss-contributors Slack channel, I''d be happy to help! My AI assistant can handle the technical discussion, just need someone to bridge the intro. Also — merge conflicts with master are now resolved. PR is clean: 0 warnings, 1191 i18n calls, 59 files covered. Cheers, Dmitry (via DeepSeek)" [ref=e990]': + - 'cell "@ZacharyZcR Just to clarify — I don''t speak English myself, I''m communicating through DeepSeek AI (which also built this PR). So I''m a bit nervous about posting in the Slack channel directly — I don''t want to say something wrong. If you could mention me ( @ErshovDmitry ) in the #oss-contributors Slack channel, I''d be happy to help! My AI assistant can handle the technical discussion, just need someone to bridge the intro. Also — merge conflicts with master are now resolved. PR is clean: 0 warnings, 1191 i18n calls, 59 files covered. Cheers, Dmitry (via DeepSeek)" [ref=e991]': + - paragraph [ref=e992]: + - link "@ZacharyZcR" [ref=e993] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: Just to clarify — I don't speak English myself, I'm communicating through DeepSeek AI (which also built this PR). So I'm a bit nervous about posting in the Slack channel directly — I don't want to say something wrong. + - paragraph [ref=e994]: + - text: If you could mention me ( + - link "@ErshovDmitry" [ref=e995] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - text: ") in the #oss-contributors Slack channel, I'd be happy to help! My AI assistant can handle the technical discussion, just need someone to bridge the intro." + - paragraph [ref=e996]: "Also — merge conflicts with master are now resolved. PR is clean: 0 warnings, 1191 i18n calls, 59 files covered." + - paragraph [ref=e997]: + - text: Cheers, + - text: Dmitry (via DeepSeek) + - group [ref=e1002]: + - generic "Add or remove reactions" [ref=e1003] [cursor=pointer]: + - img [ref=e1004] + - generic [ref=e1007]: + - link "@ErshovDmitry" [ref=e1009] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1010] + - generic [ref=e1012]: + - generic [ref=e1013]: + - generic [ref=e1014]: + - group [ref=e1016]: + - button "Show options" [ref=e1017] [cursor=pointer]: + - img "Show options" [ref=e1020] + - generic "You are the author of this pull request." [ref=e1023]: + - generic [ref=e1024]: Author + - heading "ErshovDmitry commented May 20, 20265 days ago" [level=3] [ref=e1025]: + - generic [ref=e1026]: + - strong [ref=e1027]: + - link "ErshovDmitry" [ref=e1028] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 20265 days ago" [ref=e1029] [cursor=pointer]: + - /url: "#issuecomment-4502119634" + - generic [ref=e1030]: + - table [ref=e1032]: + - rowgroup [ref=e1033]: + - row "Ah, just realized — Slack doesn't work in Russia, and VPN isn't straightforward either. So I won't be able to join the Slack channel, sorry about that. But I'm still here on GitHub and happy to collaborate! We can discuss anything here or on the PRs directly. Dmitry (via DeepSeek)" [ref=e1034]: + - cell "Ah, just realized — Slack doesn't work in Russia, and VPN isn't straightforward either. So I won't be able to join the Slack channel, sorry about that. But I'm still here on GitHub and happy to collaborate! We can discuss anything here or on the PRs directly. Dmitry (via DeepSeek)" [ref=e1035]: + - paragraph [ref=e1036]: Ah, just realized — Slack doesn't work in Russia, and VPN isn't straightforward either. So I won't be able to join the Slack channel, sorry about that. + - paragraph [ref=e1037]: But I'm still here on GitHub and happy to collaborate! We can discuss anything here or on the PRs directly. + - paragraph [ref=e1038]: Dmitry (via DeepSeek) + - group [ref=e1043]: + - generic "Add or remove reactions" [ref=e1044] [cursor=pointer]: + - img [ref=e1045] + - generic [ref=e1048]: + - link "@ZacharyZcR" [ref=e1050] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e1051] + - generic [ref=e1053]: + - generic [ref=e1054]: + - group [ref=e1057]: + - button "Show options" [ref=e1058] [cursor=pointer]: + - img "Show options" [ref=e1061] + - heading "ZacharyZcR commented May 20, 20265 days ago" [level=3] [ref=e1063]: + - generic [ref=e1064]: + - strong [ref=e1065]: + - link "ZacharyZcR" [ref=e1066] [cursor=pointer]: + - /url: /ZacharyZcR + - text: commented + - link "May 20, 20265 days ago" [ref=e1067] [cursor=pointer]: + - /url: "#issuecomment-4502126260" + - generic [ref=e1068]: + - table [ref=e1070]: + - rowgroup [ref=e1071]: + - row "Oh, no problem—I’m not an English major either." [ref=e1072]: + - cell "Oh, no problem—I’m not an English major either." [ref=e1073]: + - blockquote [ref=e1074]: + - paragraph [ref=e1075]: Ah, just realized — Slack doesn't work in Russia, and VPN isn't straightforward either. So I won't be able to join the Slack channel, sorry about that. + - paragraph [ref=e1076]: But I'm still here on GitHub and happy to collaborate! We can discuss anything here or on the PRs directly. + - paragraph [ref=e1077]: Dmitry (via DeepSeek) + - paragraph [ref=e1078]: Oh, no problem—I’m not an English major either. + - generic [ref=e1081]: + - group [ref=e1083]: + - generic "Add or remove reactions" [ref=e1084] [cursor=pointer]: + - img [ref=e1085] + - button "unreact with laugh" [pressed] [ref=e1089] [cursor=pointer]: + - img "smile" [ref=e1091] + - generic [ref=e1092]: "1" + - generic [ref=e1094]: + - generic [ref=e1095]: + - img [ref=e1097] + - generic [ref=e1099]: + - link "ErshovDmitry" [ref=e1100] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 9 commits + - link "May 21, 2026 14:374 days ago" [ref=e1101] [cursor=pointer]: + - /url: "#commits-pushed-d81ed10" + - generic [ref=e1102]: + - generic [ref=e1103]: + - img [ref=e1105] + - generic [ref=e1110]: + - link "@ErshovDmitry" [ref=e1113] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1114] + - code [ref=e1116]: + - 'link "feat: add warp_i18n crate with t!() macro and en/ru locales" [ref=e1117] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/d81ed10a1584aaabbfa641a8f772d7ad07ecd62c + - code [ref=e1121]: + - link "d81ed10" [ref=e1122] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d81ed10a1584aaabbfa641a8f772d7ad07ecd62c + - generic [ref=e1123]: + - img [ref=e1125] + - generic [ref=e1130]: + - link "@ErshovDmitry" [ref=e1133] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1134] + - generic [ref=e1135]: + - code [ref=e1136]: + - 'link "feat: i18n Phase 2 — FeatureFlag, Locale setting, app_menus migration…" [ref=e1137] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/07ab2dbec2ae50e4aef7abc589c6b6ec265dd457 + - button "Commit message body" [ref=e1139] [cursor=pointer]: … + - code [ref=e1143]: + - link "07ab2db" [ref=e1144] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/07ab2dbec2ae50e4aef7abc589c6b6ec265dd457 + - generic [ref=e1145]: + - img [ref=e1147] + - generic [ref=e1152]: + - link "@ErshovDmitry" [ref=e1155] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1156] + - generic [ref=e1157]: + - code [ref=e1158]: + - 'link "fix: wire warp_i18n::init() at startup, sync LocaleSettings with i18n…" [ref=e1159] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/e8f92bee9ebcaa36171911b7b9c40bdbc657c97e + - button "Commit message body" [ref=e1161] [cursor=pointer]: … + - code [ref=e1165]: + - link "e8f92be" [ref=e1166] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/e8f92bee9ebcaa36171911b7b9c40bdbc657c97e + - generic [ref=e1167]: + - img [ref=e1169] + - generic [ref=e1174]: + - link "@ErshovDmitry" [ref=e1177] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1178] + - code [ref=e1180]: + - 'link "docs: add English translation of the fork note in README" [ref=e1181] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/2d68bf290a0a10112645b6b3cd6f01c31c9e5786 + - code [ref=e1185]: + - link "2d68bf2" [ref=e1186] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/2d68bf290a0a10112645b6b3cd6f01c31c9e5786 + - generic [ref=e1187]: + - img [ref=e1189] + - generic [ref=e1194]: + - link "@ErshovDmitry" [ref=e1197] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1198] + - generic [ref=e1199]: + - code [ref=e1200]: + - 'link "feat: i18n Phase 3 — build script key validation, WASM support, setti…" [ref=e1201] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/910538b2f4ad6bdf6babc97d7df64974c127df85 + - button "Commit message body" [ref=e1203] [cursor=pointer]: … + - code [ref=e1207]: + - link "910538b" [ref=e1208] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/910538b2f4ad6bdf6babc97d7df64974c127df85 + - generic [ref=e1209]: + - img [ref=e1211] + - generic [ref=e1216]: + - link "@ErshovDmitry" [ref=e1219] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1220] + - generic [ref=e1221]: + - code [ref=e1222]: + - 'link "refactor: migrate to ZacharyZcR-compatible YAML i18n with Russian locale" [ref=e1223] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/9bab6878caf95c4c0e224337601da00fa5c24d89 + - button "Commit message body" [ref=e1225] [cursor=pointer]: … + - code [ref=e1229]: + - link "9bab687" [ref=e1230] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/9bab6878caf95c4c0e224337601da00fa5c24d89 + - generic [ref=e1231]: + - img [ref=e1233] + - generic [ref=e1238]: + - link "@ErshovDmitry" [ref=e1241] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1242] + - code [ref=e1244]: + - 'link "fix: init_locale before menu bar, cleanup dead code, harden set_locale" [ref=e1245] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/527e105778cf456213cda20dccbf0dca02a14ca9 + - code [ref=e1249]: + - link "527e105" [ref=e1250] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/527e105778cf456213cda20dccbf0dca02a14ca9 + - generic [ref=e1251]: + - img [ref=e1253] + - generic [ref=e1258]: + - link "@ErshovDmitry" [ref=e1261] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1262] + - generic [ref=e1263]: + - code [ref=e1264]: + - 'link "fix(i18n): Round 2 review — internationalize all hardcoded menu strings" [ref=e1265] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/84564b2d1a69f8cffd2dd0272711c9e2d05c0d3b + - button "Commit message body" [ref=e1267] [cursor=pointer]: … + - code [ref=e1271]: + - link "84564b2" [ref=e1272] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/84564b2d1a69f8cffd2dd0272711c9e2d05c0d3b + - generic [ref=e1273]: + - img [ref=e1275] + - generic [ref=e1280]: + - link "@ErshovDmitry" [ref=e1283] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1284] + - code [ref=e1286]: + - 'link "chore: add .playwright-mcp to .gitignore" [ref=e1287] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/dd0c046af3c5a4923737df68842bed7afa3b6ee1 + - code [ref=e1291]: + - link "dd0c046" [ref=e1292] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/dd0c046af3c5a4923737df68842bed7afa3b6ee1 + - generic [ref=e1296]: + - button "9 hidden items" [ref=e1297] [cursor=pointer] + - button "Load more…" [ref=e1298] [cursor=pointer] + - generic [ref=e1300]: + - generic [ref=e1301]: + - img [ref=e1303] + - generic [ref=e1305]: + - link "ErshovDmitry" [ref=e1306] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 14 commits + - link "May 21, 2026 14:374 days ago" [ref=e1307] [cursor=pointer]: + - /url: "#commits-pushed-b335af1" + - generic [ref=e1308]: + - generic [ref=e1309]: + - img [ref=e1311] + - generic [ref=e1316]: + - link "@ErshovDmitry" [ref=e1319] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1320] + - generic [ref=e1321]: + - code [ref=e1322]: + - 'link "i18n(settings): internationalize privacy, teams, environments, drive,…" [ref=e1323] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/b335af14910035f236fb6717ae1c81e5f6cfc799 + - button "Commit message body" [ref=e1325] [cursor=pointer]: … + - code [ref=e1329]: + - link "b335af1" [ref=e1330] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/b335af14910035f236fb6717ae1c81e5f6cfc799 + - generic [ref=e1331]: + - img [ref=e1333] + - generic [ref=e1338]: + - link "@ErshovDmitry" [ref=e1341] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1342] + - generic [ref=e1343]: + - code [ref=e1344]: + - 'link "i18n: internationalize code_review_view and notebooks editor view" [ref=e1345] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/6c25db7a2fcddab8d0ca9fc8c7e747da8e7e085d + - button "Commit message body" [ref=e1347] [cursor=pointer]: … + - code [ref=e1351]: + - link "6c25db7" [ref=e1352] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/6c25db7a2fcddab8d0ca9fc8c7e747da8e7e085d + - generic [ref=e1353]: + - img [ref=e1355] + - generic [ref=e1360]: + - link "@ErshovDmitry" [ref=e1363] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1364] + - generic [ref=e1365]: + - code [ref=e1366]: + - 'link "i18n: internationalize terminal/input.rs and ai/agent_management/view.rs" [ref=e1367] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/ffdf09eca7037c26a0f1fb1f1968612f25c441d1 + - button "Commit message body" [ref=e1369] [cursor=pointer]: … + - code [ref=e1373]: + - link "ffdf09e" [ref=e1374] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/ffdf09eca7037c26a0f1fb1f1968612f25c441d1 + - generic [ref=e1375]: + - img [ref=e1377] + - generic [ref=e1382]: + - link "@ErshovDmitry" [ref=e1385] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1386] + - generic [ref=e1387]: + - code [ref=e1388]: + - 'link "i18n(dialogs): internationalize common buttons, toasts, placeholders" [ref=e1389] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/45f71aa7f4ea049d13d3dd02c34ca1f073b04b1b + - button "Commit message body" [ref=e1391] [cursor=pointer]: … + - code [ref=e1395]: + - link "45f71aa" [ref=e1396] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/45f71aa7f4ea049d13d3dd02c34ca1f073b04b1b + - generic [ref=e1397]: + - img [ref=e1399] + - generic [ref=e1404]: + - link "@ErshovDmitry" [ref=e1407] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1408] + - code [ref=e1410]: + - 'link "fix(i18n): add missing settings.code_page Russian translations" [ref=e1411] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/0a524b50ca11be0d7aea7b73168e9ab296d87ca1 + - code [ref=e1415]: + - link "0a524b5" [ref=e1416] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/0a524b50ca11be0d7aea7b73168e9ab296d87ca1 + - generic [ref=e1417]: + - img [ref=e1419] + - generic [ref=e1424]: + - link "@ErshovDmitry" [ref=e1427] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1428] + - generic [ref=e1429]: + - code [ref=e1430]: + - 'link "i18n(workspace): internationalize remaining workspace/view.rs strings" [ref=e1431] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/85bbfd101f16c6ba4d6d362eec462d51e05db1f9 + - button "Commit message body" [ref=e1433] [cursor=pointer]: … + - code [ref=e1437]: + - link "85bbfd1" [ref=e1438] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/85bbfd101f16c6ba4d6d362eec462d51e05db1f9 + - generic [ref=e1439]: + - img [ref=e1441] + - generic [ref=e1446]: + - link "@ErshovDmitry" [ref=e1449] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1450] + - generic [ref=e1451]: + - code [ref=e1452]: + - 'link "i18n(terminal): internationalize terminal view notifications, buttons…" [ref=e1453] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/b9408bb0127ddf35b1a1a0c2e022b422b56654a1 + - button "Commit message body" [ref=e1455] [cursor=pointer]: … + - code [ref=e1459]: + - link "b9408bb" [ref=e1460] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/b9408bb0127ddf35b1a1a0c2e022b422b56654a1 + - generic [ref=e1461]: + - img [ref=e1463] + - generic [ref=e1468]: + - link "@ErshovDmitry" [ref=e1471] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1472] + - generic [ref=e1473]: + - code [ref=e1474]: + - 'link "fix: remove stale LocaleSettings import, merge duplicate notebook YAM…" [ref=e1475] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/3b845398705090d92e4c6c238733c0146622d6c9 + - button "Commit message body" [ref=e1477] [cursor=pointer]: … + - code [ref=e1481]: + - link "3b84539" [ref=e1482] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/3b845398705090d92e4c6c238733c0146622d6c9 + - generic [ref=e1483]: + - img [ref=e1485] + - generic [ref=e1490]: + - link "@ErshovDmitry" [ref=e1493] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1494] + - code [ref=e1496]: + - 'link "fix: resolve compilation errors after rebase onto master" [ref=e1497] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/61c7b5a5bebfce38142b4955ca5bb595fc71027f + - code [ref=e1501]: + - link "61c7b5a" [ref=e1502] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/61c7b5a5bebfce38142b4955ca5bb595fc71027f + - generic [ref=e1503]: + - img [ref=e1505] + - generic [ref=e1510]: + - link "@ErshovDmitry" [ref=e1513] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1514] + - code [ref=e1516]: + - 'link "chore: suppress dead_code warnings from handoff stubs" [ref=e1517] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/6d61f331533707e1536c86f01c8801baa0e38297 + - code [ref=e1521]: + - link "6d61f33" [ref=e1522] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/6d61f331533707e1536c86f01c8801baa0e38297 + - generic [ref=e1523]: + - img [ref=e1525] + - generic [ref=e1530]: + - link "@ErshovDmitry" [ref=e1533] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1534] + - generic [ref=e1535]: + - code [ref=e1536]: + - 'link "i18n: internationalize agents, MCP servers, cloud, appearance, featur…" [ref=e1537] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/c8883d6190502ba2f54c29a742ba2c0d87dbe418 + - button "Commit message body" [ref=e1539] [cursor=pointer]: … + - code [ref=e1543]: + - link "c8883d6" [ref=e1544] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/c8883d6190502ba2f54c29a742ba2c0d87dbe418 + - generic [ref=e1545]: + - img [ref=e1547] + - generic [ref=e1552]: + - link "@ErshovDmitry" [ref=e1555] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1556] + - generic [ref=e1557]: + - code [ref=e1558]: + - 'link "fix(i18n): remove duplicate locale keys in appearance and features se…" [ref=e1559] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/5b8eda69147db8845982ed369fc00e42f85097ab + - button "Commit message body" [ref=e1561] [cursor=pointer]: … + - code [ref=e1565]: + - link "5b8eda6" [ref=e1566] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/5b8eda69147db8845982ed369fc00e42f85097ab + - generic [ref=e1567]: + - img [ref=e1569] + - generic [ref=e1574]: + - link "@ErshovDmitry" [ref=e1577] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1578] + - generic [ref=e1579]: + - code [ref=e1580]: + - 'link "fix(i18n): move shared_blocks keys from settings.warpify to settings.…" [ref=e1581] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/58cb0a709bc80f76adb05aeacab5d551861a8ffa + - button "Commit message body" [ref=e1583] [cursor=pointer]: … + - code [ref=e1587]: + - link "58cb0a7" [ref=e1588] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/58cb0a709bc80f76adb05aeacab5d551861a8ffa + - generic [ref=e1589]: + - img [ref=e1591] + - generic [ref=e1596]: + - link "@ErshovDmitry" [ref=e1599] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1600] + - generic [ref=e1601]: + - code [ref=e1602]: + - 'link "fix(i18n): fix settings.warpify YAML nesting, add missing keys, trans…" [ref=e1603] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/be5e097360beeb555bb8433e74386717df52a27a + - button "Commit message body" [ref=e1605] [cursor=pointer]: … + - code [ref=e1609]: + - link "be5e097" [ref=e1610] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/be5e097360beeb555bb8433e74386717df52a27a + - generic [ref=e1612]: + - img [ref=e1614] + - generic [ref=e1616]: + - strong [ref=e1617]: New changes since you last viewed + - link "View changes" [ref=e1618] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/files/be5e097360beeb555bb8433e74386717df52a27a..HEAD + - generic [ref=e1622]: + - img [ref=e1624] + - generic [ref=e1629]: + - link "@ErshovDmitry" [ref=e1632] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1633] + - generic [ref=e1634]: + - code [ref=e1635]: + - 'link "i18n: internationalize 37 hardcoded UI strings, fix 3 ZacharyZcR comp…" [ref=e1636] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/a892ed1a5adbc3c482727d6e6b2a8660fe2515cf + - button "Commit message body" [ref=e1638] [cursor=pointer]: … + - group [ref=e1642]: + - generic "1 / 1 checks OK" [ref=e1643] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1644] + - code [ref=e1647]: + - link "a892ed1" [ref=e1648] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/a892ed1a5adbc3c482727d6e6b2a8660fe2515cf + - generic [ref=e1650]: + - img [ref=e1652] + - generic [ref=e1654]: + - link "@ErshovDmitry" [ref=e1655] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1656] + - link "ErshovDmitry" [ref=e1657] [cursor=pointer]: + - /url: /ErshovDmitry + - link "force-pushed" [ref=e1658] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/464c9905751ed3b40bcda38d69ba0fcc178d0489..a892ed1a5adbc3c482727d6e6b2a8660fe2515cf + - text: the + - generic [ref=e1660]: i18n + - text: branch from + - link "464c990" [ref=e1661] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/464c9905751ed3b40bcda38d69ba0fcc178d0489 + - code [ref=e1662]: 464c990 + - text: to + - link "a892ed1" [ref=e1663] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/a892ed1a5adbc3c482727d6e6b2a8660fe2515cf + - code [ref=e1664]: a892ed1 + - link "Compare" [ref=e1665] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/464c9905751ed3b40bcda38d69ba0fcc178d0489..a892ed1a5adbc3c482727d6e6b2a8660fe2515cf + - generic [ref=e1667]: Compare + - link "May 21, 2026 07:444 days ago" [ref=e1668] [cursor=pointer]: + - /url: "#event-25793294881" + - generic [ref=e1670]: + - generic [ref=e1671]: + - img [ref=e1673] + - generic [ref=e1675]: + - link "ErshovDmitry" [ref=e1676] [cursor=pointer]: + - /url: /ErshovDmitry + - text: and others added 13 commits + - link "May 22, 2026 12:123 days ago" [ref=e1677] [cursor=pointer]: + - /url: "#commits-pushed-d7c552f" + - generic [ref=e1678]: + - generic [ref=e1679]: + - img [ref=e1681] + - generic [ref=e1686]: + - link "@ErshovDmitry" [ref=e1689] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1690] + - generic [ref=e1691]: + - code [ref=e1692]: + - 'link "fix: resolve merge conflicts with master — add track_load_duration, a…" [ref=e1693] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/d7c552f9c252deb9dc01bffa1ad27ae5cd64eb13 + - button "Commit message body" [ref=e1695] [cursor=pointer]: … + - code [ref=e1699]: + - link "d7c552f" [ref=e1700] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d7c552f9c252deb9dc01bffa1ad27ae5cd64eb13 + - generic [ref=e1701]: + - img [ref=e1703] + - generic [ref=e1708]: + - link "@ErshovDmitry" [ref=e1711] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1712] + - generic [ref=e1713]: + - code [ref=e1714]: + - 'link "fix: align with master — match API signatures, doc comments, import o…" [ref=e1715] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/0d0ca99e1980744529400be52e06d23da0d25d3d + - button "Commit message body" [ref=e1717] [cursor=pointer]: … + - code [ref=e1721]: + - link "0d0ca99" [ref=e1722] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/0d0ca99e1980744529400be52e06d23da0d25d3d + - generic [ref=e1723]: + - img [ref=e1725] + - generic [ref=e1730]: + - link "@ErshovDmitry" [ref=e1733] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1734] + - code [ref=e1736]: + - 'link "fix: align imports with master, fix remote_tests.rs constructor" [ref=e1737] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/a63cb7e4d19d7570838cf7db8ac983faa6ee1918 + - code [ref=e1741]: + - link "a63cb7e" [ref=e1742] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/a63cb7e4d19d7570838cf7db8ac983faa6ee1918 + - generic [ref=e1743]: + - img [ref=e1745] + - generic [ref=e1750]: + - link "@ErshovDmitry" [ref=e1753] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1754] + - generic [ref=e1755]: + - code [ref=e1756]: + - 'link "merge: sync with origin/master (20 new commits), resolve import confl…" [ref=e1757] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/c301ce5ed5d54448e0679cab3cf6b113a838224e + - button "Commit message body" [ref=e1759] [cursor=pointer]: … + - code [ref=e1763]: + - link "c301ce5" [ref=e1764] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/c301ce5ed5d54448e0679cab3cf6b113a838224e + - generic [ref=e1765]: + - img [ref=e1767] + - generic [ref=e1772]: + - link "@ErshovDmitry" [ref=e1775] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1776] + - code [ref=e1778]: + - 'link "review: final fixes for PR readiness" [ref=e1779] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/18e14e422703ae03af50278c0b8b1e3d8e518596 + - code [ref=e1783]: + - link "18e14e4" [ref=e1784] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/18e14e422703ae03af50278c0b8b1e3d8e518596 + - generic [ref=e1785]: + - img [ref=e1787] + - generic [ref=e1792]: + - generic [ref=e1794]: + - link "@oz-for-oss" [ref=e1795] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e1796] + - link "@oz-agent" [ref=e1797] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e1798] + - link "@zachlloyd" [ref=e1799] [cursor=pointer]: + - /url: /zachlloyd + - img "@zachlloyd" [ref=e1800] + - link "@ErshovDmitry" [ref=e1801] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1802] + - generic [ref=e1803]: + - code [ref=e1804]: + - 'link "fix: show vertical tab notification dots when inbox is hidden (" [ref=e1805] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/174c3833252a4b08d719c71306e481a36c0026ea + - link "warpdo…" [ref=e1806] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11611 + - button "Commit message body" [ref=e1808] [cursor=pointer]: … + - code [ref=e1812]: + - link "174c383" [ref=e1813] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/174c3833252a4b08d719c71306e481a36c0026ea + - generic [ref=e1814]: + - img [ref=e1816] + - generic [ref=e1821]: + - generic [ref=e1823]: + - link "@kevinyang372" [ref=e1824] [cursor=pointer]: + - /url: /kevinyang372 + - img "@kevinyang372" [ref=e1825] + - link "@ErshovDmitry" [ref=e1826] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1827] + - generic [ref=e1828]: + - code [ref=e1829]: + - link "Banner for remote server disconnect (" [ref=e1830] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/c4281df01c695f52c1f600c86017b54ac4b47bdc + - link "warpdotdev#11565" [ref=e1831] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11565 + - link ")" [ref=e1832] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/c4281df01c695f52c1f600c86017b54ac4b47bdc + - button "Commit message body" [ref=e1834] [cursor=pointer]: … + - code [ref=e1838]: + - link "c4281df" [ref=e1839] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/c4281df01c695f52c1f600c86017b54ac4b47bdc + - generic [ref=e1840]: + - img [ref=e1842] + - generic [ref=e1847]: + - link "@ErshovDmitry" [ref=e1850] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1851] + - generic [ref=e1852]: + - code [ref=e1853]: + - 'link "i18n: wrap new remote-disconnect strings in menu_label (sync with ori…" [ref=e1854] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/2349b4f3f2693719ea9208125206a4dada4f9010 + - button "Commit message body" [ref=e1856] [cursor=pointer]: … + - group [ref=e1860]: + - generic "1 / 1 checks OK" [ref=e1861] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1862] + - code [ref=e1865]: + - link "2349b4f" [ref=e1866] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/2349b4f3f2693719ea9208125206a4dada4f9010 + - generic [ref=e1867]: + - img [ref=e1869] + - generic [ref=e1874]: + - link "@ErshovDmitry" [ref=e1877] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1878] + - generic [ref=e1879]: + - code [ref=e1880]: + - 'link "i18n: warpify_page.rs — wrap all 22 user-facing strings in menu_label" [ref=e1881] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/fad8ffeb0ea80d5880efbc1ccf76d18e3361609f + - button "Commit message body" [ref=e1883] [cursor=pointer]: … + - code [ref=e1887]: + - link "fad8ffe" [ref=e1888] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/fad8ffeb0ea80d5880efbc1ccf76d18e3361609f + - generic [ref=e1889]: + - img [ref=e1891] + - generic [ref=e1896]: + - link "@ErshovDmitry" [ref=e1899] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1900] + - code [ref=e1902]: + - 'link "i18n: appearance_page.rs — wrap all user-facing strings in menu_label" [ref=e1903] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/ec54970405aff3c087984fa7ce62aac08bf1ae7f + - code [ref=e1907]: + - link "ec54970" [ref=e1908] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/ec54970405aff3c087984fa7ce62aac08bf1ae7f + - generic [ref=e1909]: + - img [ref=e1911] + - generic [ref=e1916]: + - link "@ErshovDmitry" [ref=e1919] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1920] + - code [ref=e1922]: + - 'link "i18n: features_page.rs — wrap all user-facing strings in menu_label" [ref=e1923] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/9c18ebec39a4fdaad1b0a39ed78ef1b87a3415b6 + - group [ref=e1927]: + - generic "1 / 1 checks OK" [ref=e1928] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1929] + - code [ref=e1932]: + - link "9c18ebe" [ref=e1933] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/9c18ebec39a4fdaad1b0a39ed78ef1b87a3415b6 + - generic [ref=e1934]: + - img [ref=e1936] + - generic [ref=e1941]: + - link "@ErshovDmitry" [ref=e1944] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1945] + - generic [ref=e1946]: + - code [ref=e1947]: + - 'link "i18n: fix review blockers — wrap missed strings in local_code_editor.…" [ref=e1948] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/87d5be27caff2e7d5dfa5f4510dc62c412478cef + - button "Commit message body" [ref=e1950] [cursor=pointer]: … + - group [ref=e1954]: + - generic "1 / 1 checks OK" [ref=e1955] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1956] + - code [ref=e1959]: + - link "87d5be2" [ref=e1960] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/87d5be27caff2e7d5dfa5f4510dc62c412478cef + - generic [ref=e1961]: + - img [ref=e1963] + - generic [ref=e1968]: + - link "@ErshovDmitry" [ref=e1971] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1972] + - generic [ref=e1973]: + - code [ref=e1974]: + - 'link "i18n: add test_yaml_key_balance_en_equals_ru — enforce en.yml/ru.yml …" [ref=e1975] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/8f43261dab88e3cf38e40c206f95ad3167885e92 + - button "Commit message body" [ref=e1977] [cursor=pointer]: … + - group [ref=e1981]: + - generic "1 / 1 checks OK" [ref=e1982] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1983] + - code [ref=e1986]: + - link "8f43261" [ref=e1987] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/8f43261dab88e3cf38e40c206f95ad3167885e92 + - generic [ref=e1989]: + - heading "Merge info" [level=2] [ref=e1990] + - generic [ref=e1991]: + - img "Unable to merge" [ref=e1993] + - generic [ref=e1997]: + - region "Checks" [ref=e1998]: + - generic [ref=e1999]: + - generic [ref=e2000]: + - img [ref=e2003] + - generic [ref=e2007]: + - heading "Checks awaiting conflict resolution" [level=3] [ref=e2008] + - paragraph [ref=e2009]: 1 expected, 1 successful checks + - button "Collapse checks" [expanded] [ref=e2010] [cursor=pointer] + - img [ref=e2013] + - generic [ref=e2017]: + - generic [ref=e2018]: + - generic [ref=e2019]: + - button "Collapse 1 pending check group" [expanded] [ref=e2020] [cursor=pointer]: + - generic [ref=e2021]: + - generic [ref=e2022]: 1 pending check + - generic: + - generic: + - img + - button "Checks settings" [ref=e2023] [cursor=pointer]: + - img [ref=e2024] + - group "pending checks" [ref=e2026]: + - generic [ref=e2028]: + - heading "pending checks" [level=3] [ref=e2029] + - list "pending checks" [ref=e2030]: + - listitem "Check CI results expected" [ref=e2031]: + - generic [ref=e2035]: + - img [ref=e2037] + - img "Check CI results" [ref=e2039] + - generic [ref=e2040]: + - heading "Check CI results" [level=4] [ref=e2041] + - generic [ref=e2042]: Expected — Waiting for status to be reported + - generic [ref=e2045]: Required + - generic [ref=e2047]: + - button "Collapse 1 successful check group" [expanded] [ref=e2049] [cursor=pointer]: + - generic [ref=e2050]: + - generic [ref=e2051]: 1 successful check + - generic: + - generic: + - img + - group "successful checks" [ref=e2052]: + - generic [ref=e2054]: + - heading "successful checks" [level=3] [ref=e2055] + - list "successful checks" [ref=e2056]: + - listitem "verification/cla-signed" [ref=e2057]: + - generic [ref=e2061]: + - img [ref=e2063] + - img "verification/cla-signed" [ref=e2065] + - heading "verification/cla-signed" [level=4] [ref=e2067]: + - link "verification/cla-signed" [ref=e2068] [cursor=pointer]: + - /url: https://s3.amazonaws.com/cla-bot-logs/warpdotdev-5d8977ca-341b-48a2-bbda-1461f54ab9c1 + - generic [ref=e2071]: Required + - button "More actions" [ref=e2074] [cursor=pointer]: + - img [ref=e2075] + - region "Conflicts" [ref=e2077]: + - generic [ref=e2080]: + - img [ref=e2084] + - generic [ref=e2086]: + - generic [ref=e2087]: + - heading "This branch has conflicts that must be resolved" [level=3] [ref=e2088] + - paragraph [ref=e2089]: Changes can be cleanly merged. + - list [ref=e2091]: + - listitem [ref=e2092]: + - button "app/src/code/local_code_editor.rs" [ref=e2093] [cursor=pointer]: + - generic: + - img + - generic [ref=e2095]: app/src/code/local_code_editor.rs + - listitem [ref=e2096]: + - button "app/src/code/view.rs" [ref=e2097] [cursor=pointer]: + - generic: + - img + - generic [ref=e2099]: app/src/code/view.rs + - link "Resolve conflicts" [ref=e2100] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11382/conflicts + - generic [ref=e2102]: Resolve conflicts + - generic [ref=e2103]: + - generic [ref=e2104]: Still in progress? + - button "Convert to draft" [ref=e2105] [cursor=pointer]: + - generic [ref=e2107]: Convert to draft + - generic [ref=e2110]: + - link "@ErshovDmitry" [ref=e2112] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e2113] + - form "Add a comment" [ref=e2115]: + - group [ref=e2116]: + - heading "Add a comment" [level=4] [ref=e2119] + - generic [ref=e2120]: Comment + - generic [ref=e2121]: + - generic [ref=e2122]: + - tablist "Add a comment" [ref=e2123]: + - tab "Write" [selected] [ref=e2124] [cursor=pointer] + - tab "Preview" [ref=e2125] [cursor=pointer] + - toolbar [ref=e2126]: + - generic [ref=e2127]: + - button "Heading" [ref=e2129] [cursor=pointer]: + - img + - button "Bold" [ref=e2131] [cursor=pointer]: + - img + - button "Italic" [ref=e2133] [cursor=pointer]: + - img + - button "Quote" [ref=e2135] [cursor=pointer]: + - img + - button "Code" [ref=e2137] [cursor=pointer]: + - img + - button "Link" [ref=e2139] [cursor=pointer]: + - img + - separator [ref=e2140] + - button "Numbered list" [ref=e2142] [cursor=pointer]: + - img + - button "Unordered list" [ref=e2144] [cursor=pointer]: + - img + - button "Task list" [ref=e2146] [cursor=pointer]: + - img + - separator [ref=e2147] + - button "Attach files" [ref=e2149] [cursor=pointer]: + - img + - button "Mention" [ref=e2151] [cursor=pointer]: + - img + - button "Reference" [ref=e2153] [cursor=pointer]: + - img + - button "Saved replies" [ref=e2155] [cursor=pointer]: + - img + - button "Slash commands" [ref=e2157] [cursor=pointer]: + - img + - tabpanel "Write" [ref=e2158]: + - generic [ref=e2162]: + - textbox "Comment" [ref=e2163]: + - /placeholder: " " + - paragraph: Add your comment here... + - generic [ref=e2164]: + - link "Markdown is supported" [ref=e2166] [cursor=pointer]: + - /url: https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax + - generic [ref=e2167]: + - generic: + - img + - generic [ref=e2168]: Markdown is supported + - button "Paste, drop, or click to add files" [ref=e2169] [cursor=pointer]: + - generic [ref=e2170]: + - generic: + - img + - generic [ref=e2171]: Paste, drop, or click to add files + - generic [ref=e2174]: + - button "Close pull request" [ref=e2176] [cursor=pointer]: + - img [ref=e2177] + - text: Close pull request + - button "Comment" [disabled] [ref=e2180] + - generic [ref=e2181]: + - img [ref=e2182] + - generic [ref=e2184]: + - text: Remember, contributions to this repository should follow its + - link "contributing guidelines" [ref=e2185] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/0b737e22a2c75cfef4aa76ac2179112a691bc3b7/CONTRIBUTING.md + - text: "," + - link "security policy" [ref=e2186] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/0b737e22a2c75cfef4aa76ac2179112a691bc3b7/SECURITY.md + - text: ", and" + - link "code of conduct" [ref=e2187] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/0b737e22a2c75cfef4aa76ac2179112a691bc3b7/CODE_OF_CONDUCT.md + - text: . + - generic [ref=e2188]: + - img [ref=e2189] + - strong [ref=e2191]: ProTip! + - text: Add + - link ".patch" [ref=e2192] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382.patch + - text: or + - link ".diff" [ref=e2193] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382.diff + - text: to the end of URLs for Git’s plaintext views. + - generic [ref=e2197]: + - form "Select reviewers" [ref=e2199]: + - heading "Reviewers" [level=3] [ref=e2200] + - generic [ref=e2201]: + - paragraph [ref=e2202]: + - generic [ref=e2203]: + - link "@oz-for-oss" [ref=e2204] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e2205] + - link "oz-for-oss[bot]" [ref=e2206] [cursor=pointer]: + - /url: /apps/oz-for-oss + - link "oz-for-oss[bot] requested changes" [ref=e2207] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes/BASE..169dfa663dd4c533eb32daf850f25d6b9c123521 + - img [ref=e2209] + - paragraph [ref=e2211]: Requested changes must be addressed to merge this pull request. + - generic [ref=e2212]: + - generic [ref=e2213]: Still in progress? + - group [ref=e2214]: + - button "Convert to draft" [ref=e2215] [cursor=pointer] + - form "Select assignees" [ref=e2217]: + - heading "Assignees" [level=3] [ref=e2218] + - text: No one assigned + - generic [ref=e2219]: + - heading "Labels" [level=3] [ref=e2220] + - generic [ref=e2221]: + - link "cla-signed" [ref=e2222] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - generic [ref=e2223]: cla-signed + - link "external-contributor" [ref=e2224] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - generic [ref=e2225]: external-contributor + - form "Select projects" [ref=e2227]: + - heading "Projects" [level=3] [ref=e2228] + - text: None yet + - form "Select milestones" [ref=e2230]: + - heading "Milestone" [level=3] [ref=e2231] + - text: No milestone + - form "Link issues" [ref=e2237]: + - heading "Development" [level=3] [ref=e2238] + - paragraph [ref=e2239]: Successfully merging this pull request may close these issues. + - paragraph [ref=e2241]: None yet + - generic [ref=e2243]: + - button "Notifications Customize" [ref=e2246] [cursor=pointer]: + - generic [ref=e2247]: + - generic [ref=e2248]: Notifications + - generic [ref=e2249]: Customize + - button "Unsubscribe" [ref=e2251] [cursor=pointer]: + - generic [ref=e2252]: + - generic: + - img + - generic [ref=e2253]: Unsubscribe + - paragraph [ref=e2254]: You’re receiving notifications because you were mentioned. + - generic [ref=e2256]: + - heading "3 participants" [level=3] [ref=e2257] + - generic [ref=e2258]: + - link "@ErshovDmitry" [ref=e2259] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e2260] + - link "@ZacharyZcR" [ref=e2261] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e2262] + - link "@kevinyang372" [ref=e2263] [cursor=pointer]: + - /url: /kevinyang372 + - img "@kevinyang372" [ref=e2264] + - generic [ref=e2267]: + - generic [ref=e2268]: + - checkbox "Allow edits by maintainers" [checked] [ref=e2269] + - text: Allow edits by maintainers + - button "Learn more about allowing maintainer edits" [ref=e2270] [cursor=pointer]: + - img [ref=e2273] + - contentinfo [ref=e2276]: + - heading "Footer" [level=2] [ref=e2277] + - generic [ref=e2278]: + - generic [ref=e2279]: + - link "GitHub Homepage" [ref=e2280] [cursor=pointer]: + - /url: https://github.com + - img [ref=e2281] + - generic [ref=e2283]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e2284]: + - heading "Footer navigation" [level=3] [ref=e2285] + - list "Footer navigation" [ref=e2286]: + - listitem [ref=e2287]: + - link "Terms" [ref=e2288] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e2289]: + - link "Privacy" [ref=e2290] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e2291]: + - link "Security" [ref=e2292] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e2293]: + - link "Status" [ref=e2294] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e2295]: + - link "Community" [ref=e2296] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e2297]: + - link "Docs" [ref=e2298] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e2299]: + - link "Contact" [ref=e2300] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e2301]: + - button "Manage cookies" [ref=e2303] [cursor=pointer] + - listitem [ref=e2304]: + - button "Do not share my personal information" [ref=e2306] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-05-27T10-45-46-927Z.yml b/.playwright-mcp/page-2026-05-27T10-45-46-927Z.yml new file mode 100644 index 0000000000..593cb39c2b --- /dev/null +++ b/.playwright-mcp/page-2026-05-27T10-45-46-927Z.yml @@ -0,0 +1,2009 @@ +- generic [ref=e2]: + - generic [ref=e3]: + - link "Skip to content" [ref=e4] [cursor=pointer]: + - /url: "#start-of-content" + - banner "Global Navigation Menu" [ref=e8]: + - generic [ref=e9]: + - generic [ref=e10]: + - button "Open menu" [ref=e12] [cursor=pointer]: + - img [ref=e13] + - link "Homepage (g then d)" [ref=e15] [cursor=pointer]: + - /url: / + - img [ref=e16] + - generic [ref=e18]: + - navigation "Breadcrumbs" [ref=e19]: + - list [ref=e20]: + - listitem [ref=e21]: + - link "warpdotdev" [ref=e22] [cursor=pointer]: + - /url: /warpdotdev + - generic [ref=e23]: warpdotdev + - listitem [ref=e24]: + - link "warp" [ref=e25] [cursor=pointer]: + - /url: /warpdotdev/warp + - generic [ref=e26]: warp + - button "Search or jump to…" [ref=e29] [cursor=pointer]: + - generic [ref=e30]: + - generic: + - img + - generic [ref=e31]: + - generic: + - text: Type + - generic: / + - text: to search + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e36]: + - link "Chat with Copilot" [ref=e38] [cursor=pointer]: + - /url: /copilot + - img [ref=e39] + - button "Open Copilot…" [ref=e43] [cursor=pointer]: + - generic: + - img + - button "Create new..." [ref=e45] [cursor=pointer]: + - generic [ref=e46]: + - generic: + - img + - generic: + - img + - generic [ref=e47]: + - link "All issues" [ref=e48] [cursor=pointer]: + - /url: /issues + - img [ref=e49] + - link "All pull requests" [ref=e52] [cursor=pointer]: + - /url: /pulls + - img [ref=e53] + - link "All repositories" [ref=e55] [cursor=pointer]: + - /url: /repos + - img [ref=e56] + - link "You have no unread notifications (g then n)" [ref=e58] [cursor=pointer]: + - /url: /notifications + - img [ref=e59] + - button "Open user navigation menu" [ref=e62] [cursor=pointer]: + - img "User avatar" [ref=e63] + - heading "Repository navigation" [level=2] [ref=e64] + - navigation "Repository" [ref=e65]: + - list [ref=e66]: + - listitem [ref=e67]: + - link "Code" [ref=e68] [cursor=pointer]: + - /url: /warpdotdev/warp + - img [ref=e70] + - generic [ref=e72]: Code + - listitem [ref=e73]: + - link "Issues (3.5k)" [ref=e74] [cursor=pointer]: + - /url: /warpdotdev/warp/issues + - img [ref=e76] + - generic [ref=e79]: Issues + - generic [ref=e80]: + - generic [ref=e81]: 3.5k + - generic [ref=e82]: (3.5k) + - listitem [ref=e83]: + - link "Pull requests (571)" [ref=e84] [cursor=pointer]: + - /url: /warpdotdev/warp/pulls + - img [ref=e86] + - generic [ref=e88]: Pull requests + - generic [ref=e89]: + - generic [ref=e90]: "571" + - generic [ref=e91]: (571) + - listitem [ref=e92]: + - link "Agents" [ref=e93] [cursor=pointer]: + - /url: /warpdotdev/warp/agents?author=ErshovDmitry + - img [ref=e95] + - generic [ref=e98]: Agents + - listitem [ref=e99]: + - link "Discussions" [ref=e100] [cursor=pointer]: + - /url: /warpdotdev/warp/discussions + - img [ref=e102] + - generic [ref=e104]: Discussions + - listitem [ref=e105]: + - link "Actions" [ref=e106] [cursor=pointer]: + - /url: /warpdotdev/warp/actions + - img [ref=e108] + - generic [ref=e110]: Actions + - listitem [ref=e111]: + - link "Projects" [ref=e112] [cursor=pointer]: + - /url: /warpdotdev/warp/projects + - img [ref=e114] + - generic [ref=e116]: Projects + - listitem [ref=e117]: + - link "Security and quality" [ref=e118] [cursor=pointer]: + - /url: /warpdotdev/warp/security + - img [ref=e120] + - generic [ref=e122]: Security and quality + - listitem [ref=e123]: + - link "Insights" [ref=e124] [cursor=pointer]: + - /url: /warpdotdev/warp/pulse + - img [ref=e126] + - generic [ref=e128]: Insights + - main [ref=e132]: + - generic [ref=e139]: + - generic [ref=e142]: + - 'heading "I18n #11382 Edit title" [level=1] [ref=e144]': + - text: I18n + - generic [ref=e145]: + - generic [ref=e146]: "#11382" + - button "Edit title" [ref=e147] [cursor=pointer]: + - img [ref=e148] + - generic [ref=e151]: + - generic [ref=e152]: + - button "View status View status" [disabled] [ref=e153]: + - generic [ref=e154]: + - generic: + - generic: + - img + - generic: Loading + - generic [ref=e156]: Loading merge status + - button "Code" [ref=e157] [cursor=pointer]: + - generic [ref=e158]: + - generic [ref=e159]: Code + - generic: + - img + - generic [ref=e161]: + - generic [ref=e163]: + - img "Pull request" [ref=e164] + - text: Open + - generic [ref=e167]: + - link "ErshovDmitry" [ref=e168] [cursor=pointer]: + - /url: /ErshovDmitry + - text: wants to merge 71 commits into + - generic [ref=e169]: + - link "warpdotdev:master" [ref=e170] [cursor=pointer]: + - /url: /warpdotdev/warp/tree/master + - generic [ref=e171]: from + - generic [ref=e172]: + - link "ErshovDmitry:i18n" [ref=e173] [cursor=pointer]: + - /url: /ErshovDmitry/warp-i18n/tree/i18n + - button "Copy head branch name to clipboard" [ref=e174] [cursor=pointer]: + - img [ref=e175] + - navigation "Pull request navigation tabs" [ref=e181]: + - tablist [ref=e182]: + - tab "Conversation" [selected] [ref=e183] [cursor=pointer]: + - img [ref=e184] + - text: Conversation + - tab "Commits (71)" [ref=e186] [cursor=pointer]: + - img [ref=e187] + - text: Commits + - generic [ref=e189]: "71" + - generic [ref=e190]: (71) + - tab "Checks" [ref=e191] [cursor=pointer]: + - img [ref=e192] + - text: Checks + - tab "Files changed" [ref=e194] [cursor=pointer]: + - img [ref=e195] + - text: Files changed + - generic [ref=e201]: + - generic [ref=e203]: + - heading "Conversation" [level=2] [ref=e204] + - generic [ref=e205]: + - generic [ref=e206]: + - generic [ref=e208]: + - link "@ErshovDmitry" [ref=e209] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e210] + - generic [ref=e212]: + - generic [ref=e213]: + - group [ref=e216]: + - button "Show options" [ref=e217] [cursor=pointer]: + - img "Show options" [ref=e220] + - heading "ErshovDmitry commented May 20, 2026last week" [level=3] [ref=e222]: + - generic [ref=e223]: + - strong [ref=e224]: + - link "ErshovDmitry" [ref=e225] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 2026last week" [ref=e226] [cursor=pointer]: + - /url: "#issue-4483039846" + - generic [ref=e230]: + - paragraph [ref=e231]: Hello! My name is Dmitry. + - paragraph [ref=e232]: I've been using Warp and love it. I wanted to add i18n support so the terminal could speak Russian (and other languages in the future). I'm not a developer myself — this was built with DeepSeek AI, but I reviewed everything personally. + - paragraph [ref=e233]: I'd much rather this live in the official repo than a fork. If the team is interested in i18n, I'm happy to help however I can — and I'll gladly delete my fork once upstreamed. + - paragraph [ref=e234]: + - text: Cheers, + - text: Dmitry + - separator [ref=e235] + - heading "What" [level=2] [ref=e236] + - paragraph [ref=e237]: Adds i18n (internationalization) support to Warp. + - heading "Changes" [level=2] [ref=e238] + - 'heading "New crate: warp_i18n" [level=3] [ref=e239]': + - text: "New crate:" + - code [ref=e240]: warp_i18n + - list [ref=e241]: + - listitem [ref=e242]: + - code [ref=e243]: t!() + - text: macro for translating UI strings + - listitem [ref=e244]: + - code [ref=e245]: current_locale() + - text: / + - code [ref=e246]: set_current_locale() + - text: for runtime language switching + - listitem [ref=e247]: + - code [ref=e248]: init_from_json() + - text: for WASM compatibility + - listitem [ref=e249]: Build script for compile-time key validation + - listitem [ref=e250]: 21 tests + - heading "Feature flag" [level=3] [ref=e251] + - list [ref=e252]: + - listitem [ref=e253]: + - code [ref=e254]: FeatureFlag::I18n + - text: in + - code [ref=e255]: DOGFOOD_FLAGS + - heading "Settings" [level=3] [ref=e256] + - list [ref=e257]: + - listitem [ref=e258]: + - code [ref=e259]: Locale + - text: enum (En, Ru) + - listitem [ref=e260]: Language dropdown in Settings → Appearance + - listitem [ref=e261]: Auto-detect system locale on first run + - heading "Migrated UI" [level=3] [ref=e262] + - list [ref=e263]: + - listitem [ref=e264]: 10 top-level menu labels + - listitem [ref=e265]: 14 settings section names + - listitem [ref=e266]: 11 settings category labels + - heading "Locales" [level=3] [ref=e267] + - list [ref=e268]: + - listitem [ref=e269]: + - code [ref=e270]: en.json + - text: — 51 keys (mandatory fallback) + - listitem [ref=e271]: + - code [ref=e272]: ru.json + - text: — 51 keys (Russian translation) + - paragraph [ref=e273]: + - emphasis [ref=e274]: + - text: Built with DeepSeek AI — human-reviewed, all tests pass ( + - code [ref=e275]: cargo check --workspace + - text: "," + - code [ref=e276]: cargo test -p warp_i18n + - text: ). + - group [ref=e280]: + - generic "Add or remove reactions" [ref=e281] [cursor=pointer]: + - img [ref=e282] + - generic [ref=e284]: + - generic [ref=e286]: + - link "@cla-bot" [ref=e288] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e289] + - generic [ref=e291]: + - generic [ref=e292]: + - group [ref=e295]: + - button "Show options" [ref=e296] [cursor=pointer]: + - img "Show options" [ref=e299] + - heading "cla-bot Bot commented May 20, 2026last week" [level=3] [ref=e301]: + - generic [ref=e302]: + - strong [ref=e303]: + - link "cla-bot" [ref=e304] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e305]: Bot + - text: commented + - link "May 20, 2026last week" [ref=e306] [cursor=pointer]: + - /url: "#issuecomment-4494411354" + - generic [ref=e307]: + - table [ref=e309]: + - rowgroup [ref=e310]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e311]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e312]': + - paragraph [ref=e313]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e314]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e315]: + - listitem [ref=e316]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e317]: git config --list | grep email + - listitem [ref=e318]: + - text: If not, set it up using + - code [ref=e319]: git config --global user.email email@example.com + - listitem [ref=e320]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e321] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e326]: + - generic "Add or remove reactions" [ref=e327] [cursor=pointer]: + - img [ref=e328] + - generic [ref=e331]: + - link "@oz-for-oss" [ref=e333] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e334] + - generic [ref=e336]: + - generic [ref=e337]: + - generic [ref=e338]: + - group [ref=e340]: + - button "Show options" [ref=e341] [cursor=pointer]: + - img "Show options" [ref=e344] + - generic "This user has previously committed to the warp repository." [ref=e347]: + - generic [ref=e348]: Contributor + - heading "oz-for-oss Bot commented May 20, 2026last week" [level=3] [ref=e349]: + - generic [ref=e350]: + - strong [ref=e351]: + - link "oz-for-oss" [ref=e352] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e353]: Bot + - text: commented + - link "May 20, 2026last week" [ref=e354] [cursor=pointer]: + - /url: "#issuecomment-4494411637" + - generic [ref=e355]: + - table [ref=e357]: + - rowgroup [ref=e358]: + - 'row "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e359]': + - 'cell "@ErshovDmitry This PR is not linked to an issue that is marked with ready-to-implement. Issue-state enforcement details: Associated same-repo issues checked: none Required readiness label: ready-to-implement To continue, link this PR to a same-repo issue such as Closes #123 in the PR description, and make sure that issue has ready-to-implement. Powered by Oz" [ref=e360]': + - paragraph [ref=e361]: + - link "@ErshovDmitry" [ref=e362] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e363]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e364]: ready-to-implement + - text: . + - paragraph [ref=e365]: "Issue-state enforcement details:" + - list [ref=e366]: + - listitem [ref=e367]: + - paragraph [ref=e368]: "Associated same-repo issues checked: none" + - listitem [ref=e369]: + - paragraph [ref=e370]: + - text: "Required readiness label:" + - code [ref=e371]: ready-to-implement + - paragraph [ref=e372]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e373]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e374]: ready-to-implement + - text: . + - paragraph [ref=e375]: + - emphasis [ref=e376]: + - text: Powered by + - link "Oz" [ref=e377] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e382]: + - generic "Add or remove reactions" [ref=e383] [cursor=pointer]: + - img [ref=e384] + - generic [ref=e387]: + - img [ref=e389] + - generic [ref=e391]: + - link "@github-actions" [ref=e392] [cursor=pointer]: + - /url: /apps/github-actions + - img "@github-actions" [ref=e393] + - link "github-actions" [ref=e394] [cursor=pointer]: + - /url: /apps/github-actions + - generic [ref=e395]: Bot + - text: added the + - link "external-contributor" [ref=e396] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - text: label + - link "May 20, 2026last week" [ref=e397] [cursor=pointer]: + - /url: "#event-25738319166" + - generic [ref=e400]: + - generic [ref=e401]: + - link "oz-for-oss[bot]" [ref=e402] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "oz-for-oss[bot]" [ref=e403] + - img [ref=e405] + - generic [ref=e407]: + - generic [ref=e408]: + - strong [ref=e409]: + - link "oz-for-oss" [ref=e410] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e411]: Bot + - text: requested changes + - link "May 20, 2026last week" [ref=e413] [cursor=pointer]: + - /url: "#pullrequestreview-4325084051" + - link "View reviewed changes" [ref=e415] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes + - generic [ref=e417]: View reviewed changes + - generic [ref=e420]: + - generic [ref=e421]: + - generic [ref=e422]: + - group [ref=e424]: + - button "Show options" [ref=e425] [cursor=pointer]: + - img "Show options" [ref=e428] + - generic "This user has previously committed to the warp repository." [ref=e431]: + - generic [ref=e432]: Contributor + - heading "oz-for-oss Bot left a comment" [level=3] [ref=e433]: + - generic [ref=e434]: + - strong [ref=e435]: + - link "oz-for-oss" [ref=e436] [cursor=pointer]: + - /url: /apps/oz-for-oss + - generic [ref=e437]: Bot + - text: left a comment + - generic [ref=e440]: + - paragraph [ref=e441]: + - link "@ErshovDmitry" [ref=e442] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - paragraph [ref=e443]: + - text: This PR is not linked to an issue that is marked with + - code [ref=e444]: ready-to-implement + - text: . + - paragraph [ref=e445]: "Issue-state enforcement details:" + - list [ref=e446]: + - listitem [ref=e447]: + - paragraph [ref=e448]: "Associated same-repo issues checked: none" + - listitem [ref=e449]: + - paragraph [ref=e450]: + - text: "Required readiness label:" + - code [ref=e451]: ready-to-implement + - paragraph [ref=e452]: + - text: To continue, link this PR to a same-repo issue such as + - code [ref=e453]: "Closes #123" + - text: in the PR description, and make sure that issue has + - code [ref=e454]: ready-to-implement + - text: . + - paragraph [ref=e455]: + - emphasis [ref=e456]: + - text: Powered by + - link "Oz" [ref=e457] [cursor=pointer]: + - /url: https://oz.warp.dev + - group [ref=e461]: + - generic "Add or remove reactions" [ref=e462] [cursor=pointer]: + - img [ref=e463] + - generic [ref=e466]: + - link "@ErshovDmitry" [ref=e468] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e469] + - generic [ref=e471]: + - generic [ref=e472]: + - generic [ref=e473]: + - group [ref=e475]: + - button "Show options" [ref=e476] [cursor=pointer]: + - img "Show options" [ref=e479] + - generic "You are the author of this pull request." [ref=e482]: + - generic [ref=e483]: Author + - heading "ErshovDmitry commented May 20, 2026last week" [level=3] [ref=e484]: + - generic [ref=e485]: + - strong [ref=e486]: + - link "ErshovDmitry" [ref=e487] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 2026last week" [ref=e488] [cursor=pointer]: + - /url: "#issuecomment-4494430862" + - generic [ref=e489]: + - table [ref=e491]: + - rowgroup [ref=e492]: + - 'row "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e493]': + - 'cell "Oh wow — I just noticed there are already several i18n PRs open (#10630, #10990, #9458, #9922). I should have checked first before opening mine. My apologies for the noise!" [ref=e494]': + - paragraph [ref=e495]: + - text: Oh wow — I just noticed there are already several i18n PRs open ( + - link "#10630" [ref=e496] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "," + - link "#10990" [ref=e497] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: "," + - link "#9458" [ref=e498] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9458 + - text: "," + - link "#9922" [ref=e499] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/9922 + - text: ). I should have checked first before opening mine. My apologies for the noise! + - blockquote [ref=e500]: + - paragraph [ref=e501]: + - text: That said, I see none of them include Russian yet. If the team settles on one i18n approach (looks like + - link "#10630" [ref=e502] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e503] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: is the leading one?), I'd be happy to contribute Russian translations to that effort instead. + - paragraph [ref=e504]: I'll close this PR if it's just adding clutter — just let me know. + - paragraph [ref=e505]: + - text: Cheers, + - text: Dmitry + - group [ref=e510]: + - generic "Add or remove reactions" [ref=e511] [cursor=pointer]: + - img [ref=e512] + - generic [ref=e515]: + - link "@cla-bot" [ref=e517] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e518] + - generic [ref=e520]: + - generic [ref=e521]: + - group [ref=e524]: + - button "Show options" [ref=e525] [cursor=pointer]: + - img "Show options" [ref=e528] + - heading "cla-bot Bot commented May 20, 2026last week" [level=3] [ref=e530]: + - generic [ref=e531]: + - strong [ref=e532]: + - link "cla-bot" [ref=e533] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e534]: Bot + - text: commented + - link "May 20, 2026last week" [ref=e535] [cursor=pointer]: + - /url: "#issuecomment-4494962896" + - generic [ref=e536]: + - table [ref=e538]: + - rowgroup [ref=e539]: + - 'row "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e540]': + - 'cell "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Dmitry. This is most likely caused by a git client misconfiguration; please make sure to: check if your git client is configured with an email to sign commits git config --list | grep email If not, set it up using git config --global user.email email@example.com Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails" [ref=e541]': + - paragraph [ref=e542]: + - text: "Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors:" + - strong [ref=e543]: Dmitry + - text: . + - text: "This is most likely caused by a git client misconfiguration; please make sure to:" + - list [ref=e544]: + - listitem [ref=e545]: + - text: check if your git client is configured with an email to sign commits + - code [ref=e546]: git config --list | grep email + - listitem [ref=e547]: + - text: If not, set it up using + - code [ref=e548]: git config --global user.email email@example.com + - listitem [ref=e549]: + - text: Make sure that the git commit email is configured in your GitHub account settings, see + - link "https://github.com/settings/emails" [ref=e550] [cursor=pointer]: + - /url: https://github.com/settings/emails + - group [ref=e555]: + - generic "Add or remove reactions" [ref=e556] [cursor=pointer]: + - img [ref=e557] + - group [ref=e559]: + - generic "2 similar comments" [ref=e560] [cursor=pointer]: + - generic [ref=e562]: 2 similar comments + - generic [ref=e564]: + - link "@ErshovDmitry" [ref=e566] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e567] + - generic [ref=e569]: + - generic [ref=e570]: + - generic [ref=e571]: + - group [ref=e573]: + - button "Show options" [ref=e574] [cursor=pointer]: + - img "Show options" [ref=e577] + - generic "You are the author of this pull request." [ref=e580]: + - generic [ref=e581]: Author + - heading "ErshovDmitry commented May 20, 2026last week" [level=3] [ref=e582]: + - generic [ref=e583]: + - strong [ref=e584]: + - link "ErshovDmitry" [ref=e585] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 2026last week" [ref=e586] [cursor=pointer]: + - /url: "#issuecomment-4495523677" + - generic [ref=e587]: + - table [ref=e589]: + - rowgroup [ref=e590]: + - 'row "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e591]': + - 'cell "Update:** I''ve rewritten this PR to match @ZacharyZcR''s YAML approach from #10630 / #10990:" [ref=e592]': + - paragraph [ref=e593]: + - text: Update:** I've rewritten this PR to match + - link "@ZacharyZcR" [ref=e594] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: "'s YAML approach from" + - link "#10630" [ref=e595] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e596] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: ":" + - blockquote [ref=e597]: + - list [ref=e598]: + - listitem [ref=e599]: + - code [ref=e600]: crates/i18n/ + - text: with + - code [ref=e601]: t!() + - text: / + - code [ref=e602]: t_required!() + - text: macros and + - code [ref=e603]: TranslationLookup + - listitem [ref=e604]: + - code [ref=e605]: "resources/bundled/locales/{en,ru}.yml" + - text: (247 keys each, YAML) + - listitem [ref=e606]: + - code [ref=e607]: WARP_LANG + - text: env var → system locale → + - code [ref=e608]: en + - text: fallback + - listitem [ref=e609]: + - text: No feature flag, no settings UI — same design as + - generic [ref=e610]: + - img [ref=e611] + - 'link "Add i18n framework with Simplified Chinese (zh-CN) support #10630" [ref=e613] [cursor=pointer]': + - /url: https://github.com/warpdotdev/warp/pull/10630 + - paragraph [ref=e614]: + - text: The Russian + - code [ref=e615]: ru.yml + - text: should be a drop-in addition to the framework once it's approved. Happy to rebase onto whichever PR lands first. + - paragraph [ref=e616]: Dmitry + - group [ref=e621]: + - generic "Add or remove reactions" [ref=e622] [cursor=pointer]: + - img [ref=e623] + - generic [ref=e626]: + - link "@ErshovDmitry" [ref=e628] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e629] + - generic [ref=e631]: + - generic [ref=e632]: + - generic [ref=e633]: + - group [ref=e635]: + - button "Show options" [ref=e636] [cursor=pointer]: + - img "Show options" [ref=e639] + - generic "You are the author of this pull request." [ref=e642]: + - generic [ref=e643]: Author + - heading "ErshovDmitry commented May 20, 2026last week" [level=3] [ref=e644]: + - generic [ref=e645]: + - strong [ref=e646]: + - link "ErshovDmitry" [ref=e647] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 2026last week" [ref=e648] [cursor=pointer]: + - /url: "#issuecomment-4495572049" + - generic [ref=e649]: + - table [ref=e651]: + - rowgroup [ref=e652]: + - row "@cla-bot check" [ref=e653]: + - cell "@cla-bot check" [ref=e654]: + - paragraph [ref=e655]: + - link "@cla-bot" [ref=e656] [cursor=pointer]: + - /url: https://github.com/cla-bot + - text: check + - group [ref=e661]: + - generic "Add or remove reactions" [ref=e662] [cursor=pointer]: + - img [ref=e663] + - generic [ref=e666]: + - img [ref=e668] + - generic [ref=e670]: + - link "@cla-bot" [ref=e671] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e672] + - link "cla-bot" [ref=e673] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e674]: Bot + - text: added the + - link "cla-signed" [ref=e675] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - text: label + - link "May 20, 2026last week" [ref=e676] [cursor=pointer]: + - /url: "#event-25743817179" + - generic [ref=e678]: + - link "@cla-bot" [ref=e680] [cursor=pointer]: + - /url: /apps/cla-bot + - img "@cla-bot" [ref=e681] + - generic [ref=e683]: + - generic [ref=e684]: + - group [ref=e687]: + - button "Show options" [ref=e688] [cursor=pointer]: + - img "Show options" [ref=e691] + - heading "cla-bot Bot commented May 20, 2026last week" [level=3] [ref=e693]: + - generic [ref=e694]: + - strong [ref=e695]: + - link "cla-bot" [ref=e696] [cursor=pointer]: + - /url: /apps/cla-bot + - generic [ref=e697]: Bot + - text: commented + - link "May 20, 2026last week" [ref=e698] [cursor=pointer]: + - /url: "#issuecomment-4495573189" + - generic [ref=e699]: + - table [ref=e701]: + - rowgroup [ref=e702]: + - row "The cla-bot has been summoned, and re-checked this pull request!" [ref=e703]: + - cell "The cla-bot has been summoned, and re-checked this pull request!" [ref=e704]: + - paragraph [ref=e705]: The cla-bot has been summoned, and re-checked this pull request! + - group [ref=e710]: + - generic "Add or remove reactions" [ref=e711] [cursor=pointer]: + - img [ref=e712] + - generic [ref=e715]: + - link "@ErshovDmitry" [ref=e717] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e718] + - generic [ref=e720]: + - generic [ref=e721]: + - generic [ref=e722]: + - group [ref=e724]: + - button "Show options" [ref=e725] [cursor=pointer]: + - img "Show options" [ref=e728] + - generic "You are the author of this pull request." [ref=e731]: + - generic [ref=e732]: Author + - heading "ErshovDmitry commented May 20, 2026last week" [level=3] [ref=e733]: + - generic [ref=e734]: + - strong [ref=e735]: + - link "ErshovDmitry" [ref=e736] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 2026last week" [ref=e737] [cursor=pointer]: + - /url: "#issuecomment-4495607261" + - generic [ref=e738]: + - table [ref=e740]: + - rowgroup [ref=e741]: + - 'row "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e742]': + - 'cell "@oss-maintainers This PR now follows the same YAML-based approach as #10630 / #10990 by @ZacharyZcR — matching crates/i18n/ API, resources/bundled/locales/{en,ru}.yml, t!() / t_required!() macros, and WARP_LANG env var detection. It''s blocked by the issue-state enforcement check (needs a ready-to-implement issue). Could a maintainer please add ready-to-implement to #1194, or let me know if I should create a separate tracking issue? Russian ru.yml (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. Cheers, Dmitry" [ref=e743]': + - paragraph [ref=e744]: + - link "@oss-maintainers" [ref=e745] [cursor=pointer]: + - /url: https://github.com/oss-maintainers + - text: This PR now follows the same YAML-based approach as + - link "#10630" [ref=e746] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: / + - link "#10990" [ref=e747] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10990 + - text: by + - link "@ZacharyZcR" [ref=e748] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: — matching + - code [ref=e749]: crates/i18n/ + - text: API, + - code [ref=e750]: "resources/bundled/locales/{en,ru}.yml" + - text: "," + - code [ref=e751]: t!() + - text: / + - code [ref=e752]: t_required!() + - text: macros, and + - code [ref=e753]: WARP_LANG + - text: env var detection. + - paragraph [ref=e754]: + - text: It's blocked by the issue-state enforcement check (needs a + - code [ref=e755]: ready-to-implement + - text: issue). Could a maintainer please add + - code [ref=e756]: ready-to-implement + - text: to + - link "#1194" [ref=e757] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/issues/1194 + - text: ", or let me know if I should create a separate tracking issue?" + - paragraph [ref=e758]: + - text: Russian + - code [ref=e759]: ru.yml + - text: (247 keys) is ready to drop into whichever i18n PR lands first. Happy to rebase. + - paragraph [ref=e760]: + - text: Cheers, + - text: Dmitry + - group [ref=e765]: + - generic "Add or remove reactions" [ref=e766] [cursor=pointer]: + - img [ref=e767] + - generic [ref=e770]: + - img [ref=e772] + - generic [ref=e774]: + - link "@ErshovDmitry" [ref=e775] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e776] + - link "ErshovDmitry" [ref=e777] [cursor=pointer]: + - /url: /ErshovDmitry + - link "force-pushed" [ref=e778] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - text: the + - generic [ref=e780]: i18n + - text: branch from + - link "2196591" [ref=e781] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/219659155d8415b5773506f3306f6339e1300b60 + - code [ref=e782]: "2196591" + - text: to + - link "d67453f" [ref=e783] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/d67453f73b0087f3a931fd06e958ab9cebb3be0c + - code [ref=e784]: d67453f + - link "Compare" [ref=e785] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/219659155d8415b5773506f3306f6339e1300b60..d67453f73b0087f3a931fd06e958ab9cebb3be0c + - generic [ref=e787]: Compare + - link "May 20, 2026 07:36last week" [ref=e788] [cursor=pointer]: + - /url: "#event-25744804343" + - generic [ref=e790]: + - link "@ZacharyZcR" [ref=e792] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e793] + - generic [ref=e795]: + - generic [ref=e796]: + - group [ref=e799]: + - button "Show options" [ref=e800] [cursor=pointer]: + - img "Show options" [ref=e803] + - heading "ZacharyZcR commented May 20, 2026last week" [level=3] [ref=e805]: + - generic [ref=e806]: + - strong [ref=e807]: + - link "ZacharyZcR" [ref=e808] [cursor=pointer]: + - /url: /ZacharyZcR + - text: commented + - link "May 20, 2026last week" [ref=e809] [cursor=pointer]: + - /url: "#issuecomment-4499865827" + - generic [ref=e810]: + - table [ref=e812]: + - rowgroup [ref=e813]: + - row "Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel." [ref=e814]: + - cell "Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel." [ref=e815]: + - paragraph [ref=e816]: Hello! Maybe we can collaborate on a project! You can reach out to the Warp Slack channel. + - group [ref=e821]: + - generic "Add or remove reactions" [ref=e822] [cursor=pointer]: + - img [ref=e823] + - generic [ref=e826]: + - link "@ErshovDmitry" [ref=e828] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e829] + - generic [ref=e831]: + - generic [ref=e832]: + - generic [ref=e833]: + - group [ref=e835]: + - button "Show options" [ref=e836] [cursor=pointer]: + - img "Show options" [ref=e839] + - generic "You are the author of this pull request." [ref=e842]: + - generic [ref=e843]: Author + - heading "ErshovDmitry commented May 20, 2026last week" [level=3] [ref=e844]: + - generic [ref=e845]: + - strong [ref=e846]: + - link "ErshovDmitry" [ref=e847] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 2026last week" [ref=e848] [cursor=pointer]: + - /url: "#issuecomment-4501859680" + - generic [ref=e849]: + - table [ref=e851]: + - rowgroup [ref=e852]: + - 'row "@ZacharyZcR Thanks for reaching out! Happy to collaborate. We''ve matched your YAML approach — crates/i18n/, t!()/t_required!() macros, resources/bundled/locales/{en,ru}.yml (1080 keys, full Russian translation). Our ru.yml should be a drop-in addition. We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. How can we help with #10630? Happy to contribute Russian translations in your format, or collaborate however makes sense. Cheers, Dmitry" [ref=e853]': + - 'cell "@ZacharyZcR Thanks for reaching out! Happy to collaborate. We''ve matched your YAML approach — crates/i18n/, t!()/t_required!() macros, resources/bundled/locales/{en,ru}.yml (1080 keys, full Russian translation). Our ru.yml should be a drop-in addition. We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. How can we help with #10630? Happy to contribute Russian translations in your format, or collaborate however makes sense. Cheers, Dmitry" [ref=e854]': + - paragraph [ref=e855]: + - link "@ZacharyZcR" [ref=e856] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: Thanks for reaching out! Happy to collaborate. We've matched your YAML approach — + - code [ref=e857]: crates/i18n/ + - text: "," + - code [ref=e858]: t!() + - text: / + - code [ref=e859]: t_required!() + - text: macros, + - code [ref=e860]: "resources/bundled/locales/{en,ru}.yml" + - text: (1080 keys, full Russian translation). Our + - code [ref=e861]: ru.yml + - text: should be a drop-in addition. + - paragraph [ref=e862]: We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. + - paragraph [ref=e863]: + - text: How can we help with + - link "#10630" [ref=e864] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "? Happy to contribute Russian translations in your format, or collaborate however makes sense." + - paragraph [ref=e865]: + - text: Cheers, + - text: Dmitry + - generic [ref=e868]: + - group [ref=e870]: + - generic "Add or remove reactions" [ref=e871] [cursor=pointer]: + - img [ref=e872] + - button "react with thumbs up" [ref=e876] [cursor=pointer]: + - img "+1" [ref=e878] + - generic [ref=e879]: "1" + - generic [ref=e881]: + - link "@ZacharyZcR" [ref=e883] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e884] + - generic [ref=e886]: + - generic [ref=e887]: + - group [ref=e890]: + - button "Show options" [ref=e891] [cursor=pointer]: + - img "Show options" [ref=e894] + - heading "ZacharyZcR commented May 20, 2026last week" [level=3] [ref=e896]: + - generic [ref=e897]: + - strong [ref=e898]: + - link "ZacharyZcR" [ref=e899] [cursor=pointer]: + - /url: /ZacharyZcR + - text: commented + - link "May 20, 2026last week" [ref=e900] [cursor=pointer]: + - /url: "#issuecomment-4501916575" + - generic [ref=e901]: + - table [ref=e903]: + - rowgroup [ref=e904]: + - row "Thank you very much! Perhaps we could reach out to Warp’s Slack channel together and let them decide how to proceed with development. Also, your PR needs to be linked to an issue with a specific tag; you can take a look at my PR and issue." [ref=e905]: + - cell "Thank you very much! Perhaps we could reach out to Warp’s Slack channel together and let them decide how to proceed with development. Also, your PR needs to be linked to an issue with a specific tag; you can take a look at my PR and issue." [ref=e906]: + - blockquote [ref=e907]: + - paragraph [ref=e908]: + - link "@ZacharyZcR" [ref=e909] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: Thanks for reaching out! Happy to collaborate. We've matched your YAML approach — + - code [ref=e910]: crates/i18n/ + - text: "," + - code [ref=e911]: t!() + - text: / + - code [ref=e912]: t_required!() + - text: macros, + - code [ref=e913]: "resources/bundled/locales/{en,ru}.yml" + - text: (1080 keys, full Russian translation). Our + - code [ref=e914]: ru.yml + - text: should be a drop-in addition. + - paragraph [ref=e915]: We have some merge conflicts with master to fix first (3 files). Will resolve those and update the PR. + - paragraph [ref=e916]: + - text: How can we help with + - link "#10630" [ref=e917] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10630 + - text: "? Happy to contribute Russian translations in your format, or collaborate however makes sense." + - paragraph [ref=e918]: Cheers, Dmitry + - paragraph [ref=e919]: Thank you very much! Perhaps we could reach out to Warp’s Slack channel together and let them decide how to proceed with development. Also, your PR needs to be linked to an issue with a specific tag; you can take a look at my PR and issue. + - group [ref=e924]: + - generic "Add or remove reactions" [ref=e925] [cursor=pointer]: + - img [ref=e926] + - generic [ref=e929]: + - img [ref=e931] + - generic [ref=e933]: + - link "@ErshovDmitry" [ref=e934] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e935] + - link "ErshovDmitry" [ref=e936] [cursor=pointer]: + - /url: /ErshovDmitry + - link "force-pushed" [ref=e937] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/a83b91ca0f7faecce47ed52c323c36647f7bfe2b..e62f4aad25c793bb7e3d109807f26512fc6142d3 + - text: the + - generic [ref=e939]: i18n + - text: branch from + - link "a83b91c" [ref=e940] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/a83b91ca0f7faecce47ed52c323c36647f7bfe2b + - code [ref=e941]: a83b91c + - text: to + - link "e62f4aa" [ref=e942] [cursor=pointer]: + - /url: /warpdotdev/warp/commit/e62f4aad25c793bb7e3d109807f26512fc6142d3 + - code [ref=e943]: e62f4aa + - link "Compare" [ref=e944] [cursor=pointer]: + - /url: /warpdotdev/warp/compare/a83b91ca0f7faecce47ed52c323c36647f7bfe2b..e62f4aad25c793bb7e3d109807f26512fc6142d3 + - generic [ref=e946]: Compare + - link "May 20, 2026 19:44last week" [ref=e947] [cursor=pointer]: + - /url: "#event-25774475980" + - generic [ref=e949]: + - link "@ErshovDmitry" [ref=e951] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e952] + - generic [ref=e954]: + - generic [ref=e955]: + - generic [ref=e956]: + - group [ref=e958]: + - button "Show options" [ref=e959] [cursor=pointer]: + - img "Show options" [ref=e962] + - generic "You are the author of this pull request." [ref=e965]: + - generic [ref=e966]: Author + - heading "ErshovDmitry commented May 20, 2026last week" [level=3] [ref=e967]: + - generic [ref=e968]: + - strong [ref=e969]: + - link "ErshovDmitry" [ref=e970] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 2026last week" [ref=e971] [cursor=pointer]: + - /url: "#issuecomment-4502064710" + - generic [ref=e972]: + - table [ref=e974]: + - rowgroup [ref=e975]: + - 'row "@ZacharyZcR Just to clarify — I don''t speak English myself, I''m communicating through DeepSeek AI (which also built this PR). So I''m a bit nervous about posting in the Slack channel directly — I don''t want to say something wrong. If you could mention me ( @ErshovDmitry ) in the #oss-contributors Slack channel, I''d be happy to help! My AI assistant can handle the technical discussion, just need someone to bridge the intro. Also — merge conflicts with master are now resolved. PR is clean: 0 warnings, 1191 i18n calls, 59 files covered. Cheers, Dmitry (via DeepSeek)" [ref=e976]': + - 'cell "@ZacharyZcR Just to clarify — I don''t speak English myself, I''m communicating through DeepSeek AI (which also built this PR). So I''m a bit nervous about posting in the Slack channel directly — I don''t want to say something wrong. If you could mention me ( @ErshovDmitry ) in the #oss-contributors Slack channel, I''d be happy to help! My AI assistant can handle the technical discussion, just need someone to bridge the intro. Also — merge conflicts with master are now resolved. PR is clean: 0 warnings, 1191 i18n calls, 59 files covered. Cheers, Dmitry (via DeepSeek)" [ref=e977]': + - paragraph [ref=e978]: + - link "@ZacharyZcR" [ref=e979] [cursor=pointer]: + - /url: https://github.com/ZacharyZcR + - text: Just to clarify — I don't speak English myself, I'm communicating through DeepSeek AI (which also built this PR). So I'm a bit nervous about posting in the Slack channel directly — I don't want to say something wrong. + - paragraph [ref=e980]: + - text: If you could mention me ( + - link "@ErshovDmitry" [ref=e981] [cursor=pointer]: + - /url: https://github.com/ErshovDmitry + - text: ") in the #oss-contributors Slack channel, I'd be happy to help! My AI assistant can handle the technical discussion, just need someone to bridge the intro." + - paragraph [ref=e982]: "Also — merge conflicts with master are now resolved. PR is clean: 0 warnings, 1191 i18n calls, 59 files covered." + - paragraph [ref=e983]: + - text: Cheers, + - text: Dmitry (via DeepSeek) + - group [ref=e988]: + - generic "Add or remove reactions" [ref=e989] [cursor=pointer]: + - img [ref=e990] + - generic [ref=e993]: + - link "@ErshovDmitry" [ref=e995] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e996] + - generic [ref=e998]: + - generic [ref=e999]: + - generic [ref=e1000]: + - group [ref=e1002]: + - button "Show options" [ref=e1003] [cursor=pointer]: + - img "Show options" [ref=e1006] + - generic "You are the author of this pull request." [ref=e1009]: + - generic [ref=e1010]: Author + - heading "ErshovDmitry commented May 20, 2026last week" [level=3] [ref=e1011]: + - generic [ref=e1012]: + - strong [ref=e1013]: + - link "ErshovDmitry" [ref=e1014] [cursor=pointer]: + - /url: /ErshovDmitry + - text: commented + - link "May 20, 2026last week" [ref=e1015] [cursor=pointer]: + - /url: "#issuecomment-4502119634" + - generic [ref=e1016]: + - table [ref=e1018]: + - rowgroup [ref=e1019]: + - row "Ah, just realized — Slack doesn't work in Russia, and VPN isn't straightforward either. So I won't be able to join the Slack channel, sorry about that. But I'm still here on GitHub and happy to collaborate! We can discuss anything here or on the PRs directly. Dmitry (via DeepSeek)" [ref=e1020]: + - cell "Ah, just realized — Slack doesn't work in Russia, and VPN isn't straightforward either. So I won't be able to join the Slack channel, sorry about that. But I'm still here on GitHub and happy to collaborate! We can discuss anything here or on the PRs directly. Dmitry (via DeepSeek)" [ref=e1021]: + - paragraph [ref=e1022]: Ah, just realized — Slack doesn't work in Russia, and VPN isn't straightforward either. So I won't be able to join the Slack channel, sorry about that. + - paragraph [ref=e1023]: But I'm still here on GitHub and happy to collaborate! We can discuss anything here or on the PRs directly. + - paragraph [ref=e1024]: Dmitry (via DeepSeek) + - group [ref=e1029]: + - generic "Add or remove reactions" [ref=e1030] [cursor=pointer]: + - img [ref=e1031] + - generic [ref=e1034]: + - link "@ZacharyZcR" [ref=e1036] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e1037] + - generic [ref=e1039]: + - generic [ref=e1040]: + - group [ref=e1043]: + - button "Show options" [ref=e1044] [cursor=pointer]: + - img "Show options" [ref=e1047] + - heading "ZacharyZcR commented May 20, 2026last week" [level=3] [ref=e1049]: + - generic [ref=e1050]: + - strong [ref=e1051]: + - link "ZacharyZcR" [ref=e1052] [cursor=pointer]: + - /url: /ZacharyZcR + - text: commented + - link "May 20, 2026last week" [ref=e1053] [cursor=pointer]: + - /url: "#issuecomment-4502126260" + - generic [ref=e1054]: + - table [ref=e1056]: + - rowgroup [ref=e1057]: + - row "Oh, no problem—I’m not an English major either." [ref=e1058]: + - cell "Oh, no problem—I’m not an English major either." [ref=e1059]: + - blockquote [ref=e1060]: + - paragraph [ref=e1061]: Ah, just realized — Slack doesn't work in Russia, and VPN isn't straightforward either. So I won't be able to join the Slack channel, sorry about that. + - paragraph [ref=e1062]: But I'm still here on GitHub and happy to collaborate! We can discuss anything here or on the PRs directly. + - paragraph [ref=e1063]: Dmitry (via DeepSeek) + - paragraph [ref=e1064]: Oh, no problem—I’m not an English major either. + - generic [ref=e1067]: + - group [ref=e1069]: + - generic "Add or remove reactions" [ref=e1070] [cursor=pointer]: + - img [ref=e1071] + - button "unreact with laugh" [pressed] [ref=e1075] [cursor=pointer]: + - img "smile" [ref=e1077] + - generic [ref=e1078]: "1" + - generic [ref=e1080]: + - generic [ref=e1081]: + - img [ref=e1083] + - generic [ref=e1085]: + - link "ErshovDmitry" [ref=e1086] [cursor=pointer]: + - /url: /ErshovDmitry + - text: added 9 commits + - link "May 21, 2026 14:37last week" [ref=e1087] [cursor=pointer]: + - /url: "#commits-pushed-d81ed10" + - generic [ref=e1088]: + - generic [ref=e1089]: + - img [ref=e1091] + - generic [ref=e1096]: + - link "@ErshovDmitry" [ref=e1099] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1100] + - code [ref=e1102]: + - 'link "feat: add warp_i18n crate with t!() macro and en/ru locales" [ref=e1103] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/d81ed10a1584aaabbfa641a8f772d7ad07ecd62c + - code [ref=e1107]: + - link "d81ed10" [ref=e1108] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d81ed10a1584aaabbfa641a8f772d7ad07ecd62c + - generic [ref=e1109]: + - img [ref=e1111] + - generic [ref=e1116]: + - link "@ErshovDmitry" [ref=e1119] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1120] + - generic [ref=e1121]: + - code [ref=e1122]: + - 'link "feat: i18n Phase 2 — FeatureFlag, Locale setting, app_menus migration…" [ref=e1123] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/07ab2dbec2ae50e4aef7abc589c6b6ec265dd457 + - button "Commit message body" [ref=e1125] [cursor=pointer]: … + - code [ref=e1129]: + - link "07ab2db" [ref=e1130] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/07ab2dbec2ae50e4aef7abc589c6b6ec265dd457 + - generic [ref=e1131]: + - img [ref=e1133] + - generic [ref=e1138]: + - link "@ErshovDmitry" [ref=e1141] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1142] + - generic [ref=e1143]: + - code [ref=e1144]: + - 'link "fix: wire warp_i18n::init() at startup, sync LocaleSettings with i18n…" [ref=e1145] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/e8f92bee9ebcaa36171911b7b9c40bdbc657c97e + - button "Commit message body" [ref=e1147] [cursor=pointer]: … + - code [ref=e1151]: + - link "e8f92be" [ref=e1152] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/e8f92bee9ebcaa36171911b7b9c40bdbc657c97e + - generic [ref=e1153]: + - img [ref=e1155] + - generic [ref=e1160]: + - link "@ErshovDmitry" [ref=e1163] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1164] + - code [ref=e1166]: + - 'link "docs: add English translation of the fork note in README" [ref=e1167] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/2d68bf290a0a10112645b6b3cd6f01c31c9e5786 + - code [ref=e1171]: + - link "2d68bf2" [ref=e1172] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/2d68bf290a0a10112645b6b3cd6f01c31c9e5786 + - generic [ref=e1173]: + - img [ref=e1175] + - generic [ref=e1180]: + - link "@ErshovDmitry" [ref=e1183] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1184] + - generic [ref=e1185]: + - code [ref=e1186]: + - 'link "feat: i18n Phase 3 — build script key validation, WASM support, setti…" [ref=e1187] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/910538b2f4ad6bdf6babc97d7df64974c127df85 + - button "Commit message body" [ref=e1189] [cursor=pointer]: … + - code [ref=e1193]: + - link "910538b" [ref=e1194] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/910538b2f4ad6bdf6babc97d7df64974c127df85 + - generic [ref=e1195]: + - img [ref=e1197] + - generic [ref=e1202]: + - link "@ErshovDmitry" [ref=e1205] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1206] + - generic [ref=e1207]: + - code [ref=e1208]: + - 'link "refactor: migrate to ZacharyZcR-compatible YAML i18n with Russian locale" [ref=e1209] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/9bab6878caf95c4c0e224337601da00fa5c24d89 + - button "Commit message body" [ref=e1211] [cursor=pointer]: … + - code [ref=e1215]: + - link "9bab687" [ref=e1216] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/9bab6878caf95c4c0e224337601da00fa5c24d89 + - generic [ref=e1217]: + - img [ref=e1219] + - generic [ref=e1224]: + - link "@ErshovDmitry" [ref=e1227] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1228] + - code [ref=e1230]: + - 'link "fix: init_locale before menu bar, cleanup dead code, harden set_locale" [ref=e1231] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/527e105778cf456213cda20dccbf0dca02a14ca9 + - code [ref=e1235]: + - link "527e105" [ref=e1236] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/527e105778cf456213cda20dccbf0dca02a14ca9 + - generic [ref=e1237]: + - img [ref=e1239] + - generic [ref=e1244]: + - link "@ErshovDmitry" [ref=e1247] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1248] + - generic [ref=e1249]: + - code [ref=e1250]: + - 'link "fix(i18n): Round 2 review — internationalize all hardcoded menu strings" [ref=e1251] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/84564b2d1a69f8cffd2dd0272711c9e2d05c0d3b + - button "Commit message body" [ref=e1253] [cursor=pointer]: … + - code [ref=e1257]: + - link "84564b2" [ref=e1258] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/84564b2d1a69f8cffd2dd0272711c9e2d05c0d3b + - generic [ref=e1259]: + - img [ref=e1261] + - generic [ref=e1266]: + - link "@ErshovDmitry" [ref=e1269] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1270] + - code [ref=e1272]: + - 'link "chore: add .playwright-mcp to .gitignore" [ref=e1273] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/dd0c046af3c5a4923737df68842bed7afa3b6ee1 + - code [ref=e1277]: + - link "dd0c046" [ref=e1278] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/dd0c046af3c5a4923737df68842bed7afa3b6ee1 + - generic [ref=e1282]: + - button "33 hidden items" [ref=e1283] [cursor=pointer] + - button "Load more…" [ref=e1284] [cursor=pointer] + - generic [ref=e1286]: + - generic [ref=e1287]: + - img [ref=e1289] + - generic [ref=e1291]: + - link "ErshovDmitry" [ref=e1292] [cursor=pointer]: + - /url: /ErshovDmitry + - text: and others added 30 commits + - link "May 25, 2026 17:162 days ago" [ref=e1293] [cursor=pointer]: + - /url: "#commits-pushed-179f2e3" + - generic [ref=e1294]: + - generic [ref=e1295]: + - img [ref=e1297] + - generic [ref=e1302]: + - link "@ErshovDmitry" [ref=e1305] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1306] + - generic [ref=e1307]: + - code [ref=e1308]: + - 'link "i18n: fix remaining review warnings — terminal notifications + warpif…" [ref=e1309] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/179f2e32ad78a99e39d6c7ff42e1b62cd994e7d0 + - button "Commit message body" [ref=e1311] [cursor=pointer]: … + - group [ref=e1315]: + - generic "1 / 1 checks OK" [ref=e1316] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1317] + - code [ref=e1320]: + - link "179f2e3" [ref=e1321] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/179f2e32ad78a99e39d6c7ff42e1b62cd994e7d0 + - generic [ref=e1322]: + - img [ref=e1324] + - generic [ref=e1329]: + - link "@ErshovDmitry" [ref=e1332] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1333] + - code [ref=e1335]: + - 'link "i18n: Agents + MCP Servers — wrap all user-facing strings" [ref=e1336] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/200238430be64aea991decf42553fd296971a28a + - code [ref=e1340]: + - link "2002384" [ref=e1341] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/200238430be64aea991decf42553fd296971a28a + - generic [ref=e1342]: + - img [ref=e1344] + - generic [ref=e1349]: + - link "@ErshovDmitry" [ref=e1352] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1353] + - code [ref=e1355]: + - 'link "i18n: Code View + Code Review — wrap all user-facing strings" [ref=e1356] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/f9eed22f2cf87aa351bd0fa798b250982256694e + - code [ref=e1360]: + - link "f9eed22" [ref=e1361] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f9eed22f2cf87aa351bd0fa798b250982256694e + - generic [ref=e1362]: + - img [ref=e1364] + - generic [ref=e1369]: + - link "@ErshovDmitry" [ref=e1372] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1373] + - generic [ref=e1374]: + - code [ref=e1375]: + - 'link "i18n: fix 3 missed strings — Commit button, (new) indicator, All/None…" [ref=e1376] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/fcb668f473edf67e614ec5ce0370659db5927da7 + - button "Commit message body" [ref=e1378] [cursor=pointer]: … + - group [ref=e1382]: + - generic "1 / 1 checks OK" [ref=e1383] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1384] + - code [ref=e1387]: + - link "fcb668f" [ref=e1388] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/fcb668f473edf67e614ec5ce0370659db5927da7 + - generic [ref=e1389]: + - img [ref=e1391] + - generic [ref=e1396]: + - link "@ErshovDmitry" [ref=e1399] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1400] + - code [ref=e1402]: + - 'link "i18n: Git Dialogs — commit, push, PR, error messages" [ref=e1403] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/81fed7ecc101d19ba5687b71a03a5102b1f80a22 + - code [ref=e1407]: + - link "81fed7e" [ref=e1408] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/81fed7ecc101d19ba5687b71a03a5102b1f80a22 + - generic [ref=e1409]: + - img [ref=e1411] + - generic [ref=e1416]: + - link "@ErshovDmitry" [ref=e1419] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1420] + - code [ref=e1422]: + - 'link "i18n: Code Editor — find, goto-line, nav-bar, gutter buttons" [ref=e1423] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/ae818030fb078aa4f96a3ce886daa1da8b2d0b54 + - code [ref=e1427]: + - link "ae81803" [ref=e1428] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/ae818030fb078aa4f96a3ce886daa1da8b2d0b54 + - generic [ref=e1429]: + - img [ref=e1431] + - generic [ref=e1436]: + - link "@ErshovDmitry" [ref=e1439] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1440] + - code [ref=e1442]: + - 'link "i18n: Code UI + Footer — file tree, comments, LSP, headers" [ref=e1443] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/9332b27853dd2eee19c39bd98b73518a5c82dfb2 + - group [ref=e1447]: + - generic "1 / 1 checks OK" [ref=e1448] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1449] + - code [ref=e1452]: + - link "9332b27" [ref=e1453] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/9332b27853dd2eee19c39bd98b73518a5c82dfb2 + - generic [ref=e1454]: + - img [ref=e1456] + - generic [ref=e1461]: + - link "@ErshovDmitry" [ref=e1464] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1465] + - code [ref=e1467]: + - 'link "i18n: fix genitive case in ru.yml (требует доступа)" [ref=e1468] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/7013ab43bcb47568a4f63a2a7032bbcf05108902 + - group [ref=e1472]: + - generic "1 / 1 checks OK" [ref=e1473] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1474] + - code [ref=e1477]: + - link "7013ab4" [ref=e1478] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/7013ab43bcb47568a4f63a2a7032bbcf05108902 + - generic [ref=e1479]: + - img [ref=e1481] + - generic [ref=e1486]: + - link "@ErshovDmitry" [ref=e1489] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1490] + - generic [ref=e1491]: + - code [ref=e1492]: + - 'link "i18n: fix duplicate YAML key — rename code.find_references to code.fi…" [ref=e1493] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/7f42186744808a6ee33df7db993021d80713b166 + - button "Commit message body" [ref=e1495] [cursor=pointer]: … + - group [ref=e1499]: + - generic "1 / 1 checks OK" [ref=e1500] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1501] + - code [ref=e1504]: + - link "7f42186" [ref=e1505] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/7f42186744808a6ee33df7db993021d80713b166 + - generic [ref=e1506]: + - img [ref=e1508] + - generic [ref=e1513]: + - link "@ErshovDmitry" [ref=e1516] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1517] + - generic [ref=e1518]: + - code [ref=e1519]: + - 'link "i18n: add test_yaml_no_key_collisions — detect string+mapping YAML ke…" [ref=e1520] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/297883a0fa8a8b4b7fc8adebe58126d0b72d28f2 + - button "Commit message body" [ref=e1522] [cursor=pointer]: … + - group [ref=e1526]: + - generic "1 / 1 checks OK" [ref=e1527] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1528] + - code [ref=e1531]: + - link "297883a" [ref=e1532] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/297883a0fa8a8b4b7fc8adebe58126d0b72d28f2 + - generic [ref=e1533]: + - img [ref=e1535] + - generic [ref=e1540]: + - generic [ref=e1542]: + - link "@johnturcoo" [ref=e1543] [cursor=pointer]: + - /url: /johnturcoo + - img "@johnturcoo" [ref=e1544] + - link "@oz-agent" [ref=e1545] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e1546] + - link "@ErshovDmitry" [ref=e1547] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1548] + - generic [ref=e1549]: + - code [ref=e1550]: + - link "Tab group feature flag and entry points (" [ref=e1551] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1e3ac642ea23799dbff537579ac8a8a5317211f9 + - link "warpdotdev#11486" [ref=e1552] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11486 + - link ")" [ref=e1553] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1e3ac642ea23799dbff537579ac8a8a5317211f9 + - button "Commit message body" [ref=e1555] [cursor=pointer]: … + - group [ref=e1559]: + - generic "1 / 1 checks OK" [ref=e1560] [cursor=pointer]: + - img "1 / 1 checks OK" [ref=e1561] + - code [ref=e1564]: + - link "1e3ac64" [ref=e1565] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/1e3ac642ea23799dbff537579ac8a8a5317211f9 + - generic [ref=e1566]: + - img [ref=e1568] + - generic [ref=e1573]: + - generic [ref=e1575]: + - link "@vorporeal" [ref=e1576] [cursor=pointer]: + - /url: /vorporeal + - img "@vorporeal" [ref=e1577] + - link "@oz-agent" [ref=e1578] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e1579] + - link "@ErshovDmitry" [ref=e1580] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1581] + - generic [ref=e1582]: + - code [ref=e1583]: + - link "Fix Sentry framework copy in" [ref=e1584] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d6079822238addae2cf183edad15a9b049a5b07d + - code [ref=e1585]: + - link "./script/run" [ref=e1586] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d6079822238addae2cf183edad15a9b049a5b07d + - link ". (" [ref=e1587] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d6079822238addae2cf183edad15a9b049a5b07d + - link "warpdotdev#11666" [ref=e1588] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11666 + - link ")" [ref=e1589] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d6079822238addae2cf183edad15a9b049a5b07d + - button "Commit message body" [ref=e1591] [cursor=pointer]: … + - code [ref=e1595]: + - link "d607982" [ref=e1596] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/d6079822238addae2cf183edad15a9b049a5b07d + - generic [ref=e1597]: + - img [ref=e1599] + - generic [ref=e1604]: + - generic [ref=e1606]: + - link "@bholmesdev" [ref=e1607] [cursor=pointer]: + - /url: /bholmesdev + - img "@bholmesdev" [ref=e1608] + - link "@oz-agent" [ref=e1609] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e1610] + - link "@ErshovDmitry" [ref=e1611] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1612] + - generic [ref=e1613]: + - code [ref=e1614]: + - 'link "oz-platform skill: bias toward Oz scheduler over GitHub Actions for c…" [ref=e1615] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/4648d2377cf0150f2af84b99eb715e3c1a63bfb7 + - button "Commit message body" [ref=e1617] [cursor=pointer]: … + - code [ref=e1621]: + - link "4648d23" [ref=e1622] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/4648d2377cf0150f2af84b99eb715e3c1a63bfb7 + - generic [ref=e1623]: + - img [ref=e1625] + - generic [ref=e1630]: + - generic [ref=e1632]: + - link "@harryalbert" [ref=e1633] [cursor=pointer]: + - /url: /harryalbert + - img "@harryalbert" [ref=e1634] + - link "@ErshovDmitry" [ref=e1635] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1636] + - generic [ref=e1637]: + - code [ref=e1638]: + - link "correctly hide remote control for cloud conversation transcripts (" [ref=e1639] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/b33f993305530009a428ca09713e00fad6d3ec11 + - link "war…" [ref=e1640] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11292 + - button "Commit message body" [ref=e1642] [cursor=pointer]: … + - code [ref=e1646]: + - link "b33f993" [ref=e1647] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/b33f993305530009a428ca09713e00fad6d3ec11 + - generic [ref=e1648]: + - img [ref=e1650] + - generic [ref=e1655]: + - generic [ref=e1657]: + - link "@bholmesdev" [ref=e1658] [cursor=pointer]: + - /url: /bholmesdev + - img "@bholmesdev" [ref=e1659] + - link "@vkodithala" [ref=e1660] [cursor=pointer]: + - /url: /vkodithala + - img "@vkodithala" [ref=e1661] + - link "@oz-agent" [ref=e1662] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e1663] + - link "@ErshovDmitry" [ref=e1664] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1665] + - generic [ref=e1666]: + - code [ref=e1667]: + - link "Add guidance for PRs opened ahead of an issue (" [ref=e1668] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/7d7d8fc6ed3fb3d5d4dac144dab9fcf745a18147 + - link "warpdotdev#11488" [ref=e1669] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11488 + - link ")" [ref=e1670] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/7d7d8fc6ed3fb3d5d4dac144dab9fcf745a18147 + - button "Commit message body" [ref=e1672] [cursor=pointer]: … + - code [ref=e1676]: + - link "7d7d8fc" [ref=e1677] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/7d7d8fc6ed3fb3d5d4dac144dab9fcf745a18147 + - generic [ref=e1678]: + - img [ref=e1680] + - generic [ref=e1685]: + - generic [ref=e1687]: + - link "@advait-m" [ref=e1688] [cursor=pointer]: + - /url: /advait-m + - img "@advait-m" [ref=e1689] + - link "@ErshovDmitry" [ref=e1690] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1691] + - generic [ref=e1692]: + - code [ref=e1693]: + - link "Order orchestration pills by status (attention first, done by recency) (" [ref=e1694] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/170cd38c8202fa4a46a325da2024dd719d536084 + - button "Commit message body" [ref=e1696] [cursor=pointer]: … + - code [ref=e1700]: + - link "170cd38" [ref=e1701] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/170cd38c8202fa4a46a325da2024dd719d536084 + - generic [ref=e1702]: + - img [ref=e1704] + - generic [ref=e1709]: + - generic [ref=e1711]: + - link "@vorporeal" [ref=e1712] [cursor=pointer]: + - /url: /vorporeal + - img "@vorporeal" [ref=e1713] + - link "@oz-agent" [ref=e1714] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e1715] + - link "@ErshovDmitry" [ref=e1716] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1717] + - generic [ref=e1718]: + - code [ref=e1719]: + - link "Move" [ref=e1720] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/00fcb38de5a6165ffd8a906083d0780826653574 + - code [ref=e1721]: + - link "GenericStringModel" [ref=e1722] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/00fcb38de5a6165ffd8a906083d0780826653574 + - link "to" [ref=e1723] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/00fcb38de5a6165ffd8a906083d0780826653574 + - code [ref=e1724]: + - link "warp_server_client" [ref=e1725] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/00fcb38de5a6165ffd8a906083d0780826653574 + - link ". (" [ref=e1726] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/00fcb38de5a6165ffd8a906083d0780826653574 + - link "warpdotdev#11117" [ref=e1727] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11117 + - link ")" [ref=e1728] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/00fcb38de5a6165ffd8a906083d0780826653574 + - button "Commit message body" [ref=e1730] [cursor=pointer]: … + - code [ref=e1734]: + - link "00fcb38" [ref=e1735] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/00fcb38de5a6165ffd8a906083d0780826653574 + - generic [ref=e1736]: + - img [ref=e1738] + - generic [ref=e1743]: + - generic [ref=e1745]: + - link "@acarl005" [ref=e1746] [cursor=pointer]: + - /url: /acarl005 + - img "@acarl005" [ref=e1747] + - link "@ErshovDmitry" [ref=e1748] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1749] + - generic [ref=e1750]: + - code [ref=e1751]: + - link "fix pasting raw image data to CLI agents (" [ref=e1752] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/7a65b6d45b3caa746c8e7a1cd037632950f6edd0 + - link "warpdotdev#11627" [ref=e1753] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11627 + - link ")" [ref=e1754] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/7a65b6d45b3caa746c8e7a1cd037632950f6edd0 + - button "Commit message body" [ref=e1756] [cursor=pointer]: … + - code [ref=e1760]: + - link "7a65b6d" [ref=e1761] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/7a65b6d45b3caa746c8e7a1cd037632950f6edd0 + - generic [ref=e1762]: + - img [ref=e1764] + - generic [ref=e1769]: + - generic [ref=e1771]: + - link "@lucieleblanc" [ref=e1772] [cursor=pointer]: + - /url: /lucieleblanc + - img "@lucieleblanc" [ref=e1773] + - link "@ErshovDmitry" [ref=e1774] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1775] + - generic [ref=e1776]: + - code [ref=e1777]: + - link "Carry ancestor gitignore status when checking gitignores top-down (" [ref=e1778] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f27bcaa0e7172aa6be61f0fb1881429c851b3503 + - link "wa…" [ref=e1779] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11698 + - button "Commit message body" [ref=e1781] [cursor=pointer]: … + - code [ref=e1785]: + - link "f27bcaa" [ref=e1786] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/f27bcaa0e7172aa6be61f0fb1881429c851b3503 + - generic [ref=e1787]: + - img [ref=e1789] + - generic [ref=e1794]: + - generic [ref=e1796]: + - link "@acarl005" [ref=e1797] [cursor=pointer]: + - /url: /acarl005 + - img "@acarl005" [ref=e1798] + - link "@ErshovDmitry" [ref=e1799] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1800] + - generic [ref=e1801]: + - code [ref=e1802]: + - link "force the default bracketed-paste widget in zsh bootstrap to workarou… (" [ref=e1803] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/b2324b463ee8de496c69280602814b26ce2ccb59 + - button "Commit message body" [ref=e1805] [cursor=pointer]: … + - code [ref=e1809]: + - link "b2324b4" [ref=e1810] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/b2324b463ee8de496c69280602814b26ce2ccb59 + - generic [ref=e1811]: + - img [ref=e1813] + - generic [ref=e1818]: + - generic [ref=e1820]: + - link "@liliwilson" [ref=e1821] [cursor=pointer]: + - /url: /liliwilson + - img "@liliwilson" [ref=e1822] + - link "@oz-agent" [ref=e1823] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e1824] + - link "@ErshovDmitry" [ref=e1825] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1826] + - generic [ref=e1827]: + - code [ref=e1828]: + - link "Use server run_time for agent tasks (" [ref=e1829] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/07ab883ccfb23ee95bd86f707dbd9b61a46a7e65 + - link "warpdotdev#11431" [ref=e1830] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11431 + - link ")" [ref=e1831] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/07ab883ccfb23ee95bd86f707dbd9b61a46a7e65 + - button "Commit message body" [ref=e1833] [cursor=pointer]: … + - code [ref=e1837]: + - link "07ab883" [ref=e1838] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/07ab883ccfb23ee95bd86f707dbd9b61a46a7e65 + - generic [ref=e1839]: + - img [ref=e1841] + - generic [ref=e1846]: + - generic [ref=e1848]: + - link "@coolcom200" [ref=e1849] [cursor=pointer]: + - /url: /coolcom200 + - img "@coolcom200" [ref=e1850] + - link "@ErshovDmitry" [ref=e1851] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1852] + - generic [ref=e1853]: + - code [ref=e1854]: + - link "Handle create and delete of the same path as file replacement (" [ref=e1855] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/2af2a57b46b5a834262a1cac211d297c11749a1b + - link "warpdo…" [ref=e1856] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11516 + - button "Commit message body" [ref=e1858] [cursor=pointer]: … + - code [ref=e1866]: + - link "2af2a57" [ref=e1867] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/2af2a57b46b5a834262a1cac211d297c11749a1b + - generic [ref=e1868]: + - img [ref=e1870] + - generic [ref=e1875]: + - generic [ref=e1877]: + - link "@MaggieShan" [ref=e1878] [cursor=pointer]: + - /url: /MaggieShan + - img "@MaggieShan" [ref=e1879] + - link "@ErshovDmitry" [ref=e1880] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1881] + - generic [ref=e1882]: + - code [ref=e1883]: + - link "Update pr chip and pr button to have a single source of truth (" [ref=e1884] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/0ae22f203a63198878d7d7d205a049824093b188 + - link "warpdo…" [ref=e1885] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/10930 + - button "Commit message body" [ref=e1887] [cursor=pointer]: … + - code [ref=e1895]: + - link "0ae22f2" [ref=e1896] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/0ae22f203a63198878d7d7d205a049824093b188 + - generic [ref=e1897]: + - img [ref=e1899] + - generic [ref=e1904]: + - generic [ref=e1906]: + - link "@cephalonaut" [ref=e1907] [cursor=pointer]: + - /url: /cephalonaut + - img "@cephalonaut" [ref=e1908] + - link "@oz-agent" [ref=e1909] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e1910] + - link "@ErshovDmitry" [ref=e1911] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1912] + - generic [ref=e1913]: + - code [ref=e1914]: + - link "Persist conversation ID for local tasks and merge LocalSharedSessionL…" [ref=e1915] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/3ad95e985cb5c5177a6e723b025be0cb1277b2c8 + - button "Commit message body" [ref=e1917] [cursor=pointer]: … + - code [ref=e1925]: + - link "3ad95e9" [ref=e1926] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/3ad95e985cb5c5177a6e723b025be0cb1277b2c8 + - generic [ref=e1927]: + - img [ref=e1929] + - generic [ref=e1934]: + - generic [ref=e1936]: + - link "@MaggieShan" [ref=e1937] [cursor=pointer]: + - /url: /MaggieShan + - img "@MaggieShan" [ref=e1938] + - link "@ErshovDmitry" [ref=e1939] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1940] + - generic [ref=e1941]: + - code [ref=e1942]: + - link "Enable git operations for stable (" [ref=e1943] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/8da0f913d62a5c8aba2aa37fa832e79dc41468a8 + - link "warpdotdev#11716" [ref=e1944] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11716 + - link ")" [ref=e1945] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/8da0f913d62a5c8aba2aa37fa832e79dc41468a8 + - button "Commit message body" [ref=e1947] [cursor=pointer]: … + - code [ref=e1955]: + - link "8da0f91" [ref=e1956] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/8da0f913d62a5c8aba2aa37fa832e79dc41468a8 + - generic [ref=e1957]: + - img [ref=e1959] + - generic [ref=e1964]: + - generic [ref=e1966]: + - link "@IsaiahWitzke" [ref=e1967] [cursor=pointer]: + - /url: /IsaiahWitzke + - img "@IsaiahWitzke" [ref=e1968] + - link "@oz-agent" [ref=e1969] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e1970] + - link "@ErshovDmitry" [ref=e1971] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e1972] + - generic [ref=e1973]: + - code [ref=e1974]: + - link "[REV-1603 & REV-1606] Show billing V2 for solo free users (" [ref=e1975] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/13d635288e838957207102b3c4b8489d225a76ed + - link "warpdotdev…" [ref=e1976] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11434 + - button "Commit message body" [ref=e1978] [cursor=pointer]: … + - code [ref=e1986]: + - link "13d6352" [ref=e1987] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/13d635288e838957207102b3c4b8489d225a76ed + - generic [ref=e1988]: + - img [ref=e1990] + - generic [ref=e1995]: + - generic [ref=e1997]: + - link "@dagmfactory" [ref=e1998] [cursor=pointer]: + - /url: /dagmfactory + - img "@dagmfactory" [ref=e1999] + - link "@oz-for-oss" [ref=e2000] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e2001] + - link "@ErshovDmitry" [ref=e2002] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e2003] + - generic [ref=e2004]: + - code [ref=e2005]: + - link "[WARP-XXXX] Enable custom endpoint usage tracking and display (" [ref=e2006] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/123f7451e868efd3b29fc7b735f994a391251f86 + - link "warpdo…" [ref=e2007] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11496 + - button "Commit message body" [ref=e2009] [cursor=pointer]: … + - code [ref=e2017]: + - link "123f745" [ref=e2018] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/123f7451e868efd3b29fc7b735f994a391251f86 + - generic [ref=e2019]: + - img [ref=e2021] + - generic [ref=e2026]: + - generic [ref=e2028]: + - link "@vkodithala" [ref=e2029] [cursor=pointer]: + - /url: /vkodithala + - img "@vkodithala" [ref=e2030] + - link "@ErshovDmitry" [ref=e2031] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e2032] + - generic [ref=e2033]: + - code [ref=e2034]: + - link "Enable async find on dogfood, add toggle for Preview/Stable (" [ref=e2035] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/5610b9a58881f4f6b841273efd8a6a11ad23cb11 + - link "warpdotd…" [ref=e2036] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11555 + - button "Commit message body" [ref=e2038] [cursor=pointer]: … + - code [ref=e2046]: + - link "5610b9a" [ref=e2047] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/5610b9a58881f4f6b841273efd8a6a11ad23cb11 + - generic [ref=e2048]: + - img [ref=e2050] + - generic [ref=e2055]: + - generic [ref=e2057]: + - link "@bnavetta" [ref=e2058] [cursor=pointer]: + - /url: /bnavetta + - img "@bnavetta" [ref=e2059] + - link "@oz-agent" [ref=e2060] [cursor=pointer]: + - /url: /oz-agent + - img "@oz-agent" [ref=e2061] + - link "@ErshovDmitry" [ref=e2062] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e2063] + - generic [ref=e2064]: + - code [ref=e2065]: + - link "Set cloud agent task context before setup (" [ref=e2066] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/c35960c448b976129ddad1d518c955e76a483349 + - link "warpdotdev#11533" [ref=e2067] [cursor=pointer]: + - /url: https://github.com/warpdotdev/warp/pull/11533 + - link ")" [ref=e2068] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/c35960c448b976129ddad1d518c955e76a483349 + - button "Commit message body" [ref=e2070] [cursor=pointer]: … + - code [ref=e2078]: + - link "c35960c" [ref=e2079] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/c35960c448b976129ddad1d518c955e76a483349 + - generic [ref=e2080]: + - img [ref=e2082] + - generic [ref=e2087]: + - link "@ErshovDmitry" [ref=e2090] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e2091] + - code [ref=e2093]: + - 'link "fix: remove duplicate proto import after master sync" [ref=e2094] [cursor=pointer]': + - /url: /warpdotdev/warp/pull/11382/commits/045d3acc434e516283b6b2f2602688678ace150d + - code [ref=e2102]: + - link "045d3ac" [ref=e2103] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/commits/045d3acc434e516283b6b2f2602688678ace150d + - generic [ref=e2107]: + - img [ref=e2108] + - generic [ref=e2111]: Loading + - generic [ref=e2114]: + - link "@ErshovDmitry" [ref=e2116] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e2117] + - form "Add a comment" [ref=e2119]: + - group [ref=e2120]: + - heading "Add a comment" [level=4] [ref=e2123] + - generic [ref=e2124]: Comment + - generic [ref=e2125]: + - generic [ref=e2126]: + - tablist "Add a comment" [ref=e2127]: + - tab "Write" [selected] [ref=e2128] [cursor=pointer] + - tab "Preview" [ref=e2129] [cursor=pointer] + - toolbar [ref=e2130]: + - generic [ref=e2131]: + - button "Heading" [ref=e2133] [cursor=pointer]: + - img + - button "Bold" [ref=e2135] [cursor=pointer]: + - img + - button "Italic" [ref=e2137] [cursor=pointer]: + - img + - button "Quote" [ref=e2139] [cursor=pointer]: + - img + - button "Code" [ref=e2141] [cursor=pointer]: + - img + - button "Link" [ref=e2143] [cursor=pointer]: + - img + - separator [ref=e2144] + - button "Numbered list" [ref=e2146] [cursor=pointer]: + - img + - button "Unordered list" [ref=e2148] [cursor=pointer]: + - img + - button "Task list" [ref=e2150] [cursor=pointer]: + - img + - separator [ref=e2151] + - button "Attach files" [ref=e2153] [cursor=pointer]: + - img + - button "Mention" [ref=e2155] [cursor=pointer]: + - img + - button "Reference" [ref=e2157] [cursor=pointer]: + - img + - button "Saved replies" [ref=e2159] [cursor=pointer]: + - img + - button "Slash commands" [ref=e2161] [cursor=pointer]: + - img + - tabpanel "Write" [ref=e2162]: + - generic [ref=e2166]: + - textbox "Comment" [ref=e2167]: + - /placeholder: " " + - paragraph: Add your comment here... + - generic [ref=e2168]: + - link "Markdown is supported" [ref=e2170] [cursor=pointer]: + - /url: https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax + - generic [ref=e2171]: + - generic: + - img + - generic [ref=e2172]: Markdown is supported + - button "Paste, drop, or click to add files" [ref=e2173] [cursor=pointer]: + - generic [ref=e2174]: + - generic: + - img + - generic [ref=e2175]: Paste, drop, or click to add files + - generic [ref=e2178]: + - button "Close pull request" [ref=e2180] [cursor=pointer]: + - img [ref=e2181] + - text: Close pull request + - button "Comment" [disabled] [ref=e2184] + - generic [ref=e2185]: + - img [ref=e2186] + - generic [ref=e2188]: + - text: Remember, contributions to this repository should follow its + - link "contributing guidelines" [ref=e2189] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/c37c1cd6ec75726ea297131bab2b2b0b19ecce60/CONTRIBUTING.md + - text: "," + - link "security policy" [ref=e2190] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/c37c1cd6ec75726ea297131bab2b2b0b19ecce60/SECURITY.md + - text: ", and" + - link "code of conduct" [ref=e2191] [cursor=pointer]: + - /url: /warpdotdev/warp/blob/c37c1cd6ec75726ea297131bab2b2b0b19ecce60/CODE_OF_CONDUCT.md + - text: . + - generic [ref=e2192]: + - img [ref=e2193] + - strong [ref=e2195]: ProTip! + - text: Add + - link ".patch" [ref=e2196] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382.patch + - text: or + - link ".diff" [ref=e2197] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382.diff + - text: to the end of URLs for Git’s plaintext views. + - generic [ref=e2201]: + - form "Select reviewers" [ref=e2203]: + - heading "Reviewers" [level=3] [ref=e2204] + - generic [ref=e2205]: + - paragraph [ref=e2206]: + - generic [ref=e2207]: + - link "@oz-for-oss" [ref=e2208] [cursor=pointer]: + - /url: /apps/oz-for-oss + - img "@oz-for-oss" [ref=e2209] + - link "oz-for-oss[bot]" [ref=e2210] [cursor=pointer]: + - /url: /apps/oz-for-oss + - link "oz-for-oss[bot] requested changes" [ref=e2211] [cursor=pointer]: + - /url: /warpdotdev/warp/pull/11382/changes/BASE..169dfa663dd4c533eb32daf850f25d6b9c123521 + - img [ref=e2213] + - paragraph [ref=e2215]: Requested changes must be addressed to merge this pull request. + - generic [ref=e2216]: + - generic [ref=e2217]: Still in progress? + - group [ref=e2218]: + - button "Convert to draft" [ref=e2219] [cursor=pointer] + - form "Select assignees" [ref=e2221]: + - heading "Assignees" [level=3] [ref=e2222] + - text: No one assigned + - generic [ref=e2223]: + - heading "Labels" [level=3] [ref=e2224] + - generic [ref=e2225]: + - link "cla-signed" [ref=e2226] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Acla-signed + - generic [ref=e2227]: cla-signed + - link "external-contributor" [ref=e2228] [cursor=pointer]: + - /url: /warpdotdev/warp/issues?q=state%3Aopen%20label%3Aexternal-contributor + - generic [ref=e2229]: external-contributor + - form "Select projects" [ref=e2231]: + - heading "Projects" [level=3] [ref=e2232] + - text: None yet + - form "Select milestones" [ref=e2234]: + - heading "Milestone" [level=3] [ref=e2235] + - text: No milestone + - form "Link issues" [ref=e2241]: + - heading "Development" [level=3] [ref=e2242] + - paragraph [ref=e2243]: Successfully merging this pull request may close these issues. + - generic [ref=e2245]: + - button "Notifications Customize" [ref=e2248] [cursor=pointer]: + - generic [ref=e2249]: + - generic [ref=e2250]: Notifications + - generic [ref=e2251]: Customize + - button "Unsubscribe" [ref=e2253] [cursor=pointer]: + - generic [ref=e2254]: + - generic: + - img + - generic [ref=e2255]: Unsubscribe + - paragraph [ref=e2256]: You’re receiving notifications because you were mentioned. + - generic [ref=e2258]: + - heading "18 participants" [level=3] [ref=e2259] + - generic [ref=e2260]: + - link "@ErshovDmitry" [ref=e2261] [cursor=pointer]: + - /url: /ErshovDmitry + - img "@ErshovDmitry" [ref=e2262] + - link "@ZacharyZcR" [ref=e2263] [cursor=pointer]: + - /url: /ZacharyZcR + - img "@ZacharyZcR" [ref=e2264] + - link "@kevinyang372" [ref=e2265] [cursor=pointer]: + - /url: /kevinyang372 + - img "@kevinyang372" [ref=e2266] + - link "@johnturcoo" [ref=e2267] [cursor=pointer]: + - /url: /johnturcoo + - img "@johnturcoo" [ref=e2268] + - link "@vorporeal" [ref=e2269] [cursor=pointer]: + - /url: /vorporeal + - img "@vorporeal" [ref=e2270] + - link "@bholmesdev" [ref=e2271] [cursor=pointer]: + - /url: /bholmesdev + - img "@bholmesdev" [ref=e2272] + - link "@harryalbert" [ref=e2273] [cursor=pointer]: + - /url: /harryalbert + - img "@harryalbert" [ref=e2274] + - link "@advait-m" [ref=e2275] [cursor=pointer]: + - /url: /advait-m + - img "@advait-m" [ref=e2276] + - link "@acarl005" [ref=e2277] [cursor=pointer]: + - /url: /acarl005 + - img "@acarl005" [ref=e2278] + - link "@lucieleblanc" [ref=e2279] [cursor=pointer]: + - /url: /lucieleblanc + - img "@lucieleblanc" [ref=e2280] + - link "@liliwilson" [ref=e2281] [cursor=pointer]: + - /url: /liliwilson + - img "@liliwilson" [ref=e2282] + - link "@coolcom200" [ref=e2283] [cursor=pointer]: + - /url: /coolcom200 + - img "@coolcom200" [ref=e2284] + - link "@MaggieShan" [ref=e2285] [cursor=pointer]: + - /url: /MaggieShan + - img "@MaggieShan" [ref=e2286] + - link "@cephalonaut" [ref=e2287] [cursor=pointer]: + - /url: /cephalonaut + - img "@cephalonaut" [ref=e2288] + - link "@IsaiahWitzke" [ref=e2289] [cursor=pointer]: + - /url: /IsaiahWitzke + - img "@IsaiahWitzke" [ref=e2290] + - link "@dagmfactory" [ref=e2291] [cursor=pointer]: + - /url: /dagmfactory + - img "@dagmfactory" [ref=e2292] + - link "@vkodithala" [ref=e2293] [cursor=pointer]: + - /url: /vkodithala + - img "@vkodithala" [ref=e2294] + - link "@bnavetta" [ref=e2295] [cursor=pointer]: + - /url: /bnavetta + - img "@bnavetta" [ref=e2296] + - generic [ref=e2299]: + - generic [ref=e2300]: + - checkbox "Allow edits by maintainers" [checked] [ref=e2301] + - text: Allow edits by maintainers + - button "Learn more about allowing maintainer edits" [ref=e2302] [cursor=pointer]: + - img [ref=e2305] + - contentinfo [ref=e2308]: + - heading "Footer" [level=2] [ref=e2309] + - generic [ref=e2310]: + - generic [ref=e2311]: + - link "GitHub Homepage" [ref=e2312] [cursor=pointer]: + - /url: https://github.com + - img [ref=e2313] + - generic [ref=e2315]: © 2026 GitHub, Inc. + - navigation "Footer" [ref=e2316]: + - heading "Footer navigation" [level=3] [ref=e2317] + - list "Footer navigation" [ref=e2318]: + - listitem [ref=e2319]: + - link "Terms" [ref=e2320] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/github-terms/github-terms-of-service + - listitem [ref=e2321]: + - link "Privacy" [ref=e2322] [cursor=pointer]: + - /url: https://docs.github.com/site-policy/privacy-policies/github-privacy-statement + - listitem [ref=e2323]: + - link "Security" [ref=e2324] [cursor=pointer]: + - /url: https://github.com/security + - listitem [ref=e2325]: + - link "Status" [ref=e2326] [cursor=pointer]: + - /url: https://www.githubstatus.com/ + - listitem [ref=e2327]: + - link "Community" [ref=e2328] [cursor=pointer]: + - /url: https://github.community/ + - listitem [ref=e2329]: + - link "Docs" [ref=e2330] [cursor=pointer]: + - /url: https://docs.github.com/ + - listitem [ref=e2331]: + - link "Contact" [ref=e2332] [cursor=pointer]: + - /url: https://support.github.com?tags=dotcom-footer + - listitem [ref=e2333]: + - button "Manage cookies" [ref=e2335] [cursor=pointer] + - listitem [ref=e2336]: + - button "Do not share my personal information" [ref=e2338] [cursor=pointer] \ No newline at end of file diff --git a/.settings_schema_cache.json b/.settings_schema_cache.json new file mode 100644 index 0000000000..ea8ec307ea --- /dev/null +++ b/.settings_schema_cache.json @@ -0,0 +1,2748 @@ +{ + "$defs": { + "AccessibilityVerbosity": { + "description": "Verbosity level for screen reader announcements.", + "oneOf": [ + { + "const": "verbose", + "description": "Default verbosity level, includes help string.", + "type": "string" + }, + { + "const": "concise", + "description": "Concise level, only announces `value` from AccessibilityContent.", + "type": "string" + } + ] + }, + "AgentModeCodingPermissionsType": { + "description": "File read permission level for the agent.", + "oneOf": [ + { + "const": "always_ask_before_reading", + "description": "Agent Mode must ask for explicit permission for any type of file read.", + "type": "string" + }, + { + "const": "always_allow_reading", + "description": "Agent Mode can always read files without explicit consent.", + "type": "string" + }, + { + "const": "allow_reading_specific_files", + "description": "Agent Mode can only read certain files without explicit consent.\n\nThe specific filepaths are backed by the\n[`AISettings::agent_mode_coding_file_read_allowlist`] setting.", + "type": "string" + } + ] + }, + "AgentModeCommandExecutionPredicate": { + "type": "string" + }, + "AgentToolbarChipSelection": { + "description": "Agent toolbar layout configuration.", + "oneOf": [ + { + "const": "default", + "description": "Use the default toolbar layout.", + "type": "string" + }, + { + "additionalProperties": false, + "description": "Use a custom arrangement of toolbar items.", + "properties": { + "custom": { + "properties": { + "left": { + "items": { + "$ref": "#/$defs/AgentToolbarItemKind" + }, + "type": "array" + }, + "right": { + "items": { + "$ref": "#/$defs/AgentToolbarItemKind" + }, + "type": "array" + } + }, + "required": [ + "left", + "right" + ], + "type": "object" + } + }, + "required": [ + "custom" + ], + "type": "object" + } + ] + }, + "AgentToolbarItemKind": { + "description": "An item that can appear in the agent toolbar.", + "oneOf": [ + { + "enum": [ + "model_selector", + "n_l_d_toggle", + "context_window_usage", + "file_explorer", + "rich_input", + "voice_input", + "file_attach", + "share_session", + "settings", + "fast_forward_toggle", + "handoff_to_cloud" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "A prompt context chip.", + "properties": { + "context_chip": { + "$ref": "#/$defs/ContextChipKind" + } + }, + "required": [ + "context_chip" + ], + "type": "object" + } + ] + }, + "AltScreenPaddingMode": { + "description": "How padding is applied in full-screen terminal apps.", + "oneOf": [ + { + "const": "match_blocklist", + "description": "Use the same padding as the block list.", + "type": "string" + }, + { + "additionalProperties": false, + "description": "Use a custom uniform padding value.", + "properties": { + "custom": { + "properties": { + "uniform_padding": { + "$ref": "#/$defs/Pixels" + } + }, + "required": [ + "uniform_padding" + ], + "type": "object" + } + }, + "required": [ + "custom" + ], + "type": "object" + } + ] + }, + "AppIcon": { + "description": "The app icon displayed in the dock.", + "oneOf": [ + { + "const": "default", + "description": "Default", + "type": "string" + }, + { + "const": "aurora", + "description": "Aurora", + "type": "string" + }, + { + "const": "classic1", + "description": "Classic 1", + "type": "string" + }, + { + "const": "classic2", + "description": "Classic 2", + "type": "string" + }, + { + "const": "classic3", + "description": "Classic 3", + "type": "string" + }, + { + "const": "comets", + "description": "Comets", + "type": "string" + }, + { + "const": "cow", + "description": "Cow", + "type": "string" + }, + { + "const": "glass_sky", + "description": "Glass Sky", + "type": "string" + }, + { + "const": "glitch", + "description": "Glitch", + "type": "string" + }, + { + "const": "glow", + "description": "Glow", + "type": "string" + }, + { + "const": "holographic", + "description": "Holographic", + "type": "string" + }, + { + "const": "mono", + "description": "Mono", + "type": "string" + }, + { + "const": "neon", + "description": "Neon", + "type": "string" + }, + { + "const": "original", + "description": "Original", + "type": "string" + }, + { + "const": "starburst", + "description": "Starburst", + "type": "string" + }, + { + "const": "sticker", + "description": "Sticker", + "type": "string" + }, + { + "const": "warp_one", + "description": "Warp 1", + "type": "string" + } + ] + }, + "CLIAgentToolbarChipSelection": { + "description": "CLI agent toolbar layout configuration.", + "oneOf": [ + { + "const": "default", + "description": "Use the default toolbar layout.", + "type": "string" + }, + { + "additionalProperties": false, + "description": "Use a custom arrangement of toolbar items.", + "properties": { + "custom": { + "properties": { + "left": { + "items": { + "$ref": "#/$defs/AgentToolbarItemKind" + }, + "type": "array" + }, + "right": { + "items": { + "$ref": "#/$defs/AgentToolbarItemKind" + }, + "type": "array" + } + }, + "required": [ + "left", + "right" + ], + "type": "object" + } + }, + "required": [ + "custom" + ], + "type": "object" + } + ] + }, + "ContextChipKind": { + "description": "Type of prompt context chip.", + "oneOf": [ + { + "enum": [ + "working_directory", + "username", + "hostname", + "date", + "time12", + "time24", + "virtual_environment", + "conda_environment", + "node_version", + "shell_git_branch", + "git_diff_stats", + "github_pull_request", + "kubernetes_context", + "svn_branch", + "svn_dirty_items", + "ssh", + "subshell" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "A user-defined custom chip.", + "properties": { + "custom": { + "properties": { + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + } + }, + "required": [ + "custom" + ], + "type": "object" + }, + { + "const": "agent_plan_and_todo_list", + "description": "A chip that shows the plan and todo list for the current conversation.", + "type": "string" + } + ] + }, + "CtrlTabBehavior": { + "description": "What Ctrl+Tab does.", + "enum": [ + "activate_prev_next_tab", + "cycle_most_recent_session", + "cycle_most_recent_tab" + ], + "type": "string" + }, + "CursorBlink": { + "description": "Whether the cursor blinks.", + "enum": [ + "enabled", + "disabled" + ], + "type": "string" + }, + "CursorDisplayType": { + "description": "Visual style of the cursor.", + "enum": [ + "bar", + "block", + "underline" + ], + "type": "string" + }, + "CustomSecretRegex": { + "description": "A custom regex pattern for detecting and redacting secrets.", + "properties": { + "name": { + "default": null, + "description": "Optional display name for this secret pattern.", + "type": [ + "string", + "null" + ] + }, + "pattern": { + "description": "The regex pattern to match secrets.", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + }, + "CustomTheme": { + "description": "A user-provided custom theme.", + "properties": { + "name": { + "description": "The display name of the custom theme.", + "type": "string" + }, + "path": { + "description": "The file path to the custom theme definition.", + "type": "string" + } + }, + "required": [ + "name", + "path" + ], + "type": "object" + }, + "DefaultSessionMode": { + "description": "Default mode for new sessions.", + "oneOf": [ + { + "const": "terminal", + "description": "New sessions start in the terminal mode (default).", + "type": "string" + }, + { + "const": "agent", + "description": "New sessions start in agent view.", + "type": "string" + }, + { + "const": "cloud_agent", + "description": "New sessions start in cloud (ambient) agent mode.", + "type": "string" + }, + { + "const": "tab_config", + "description": "New sessions open a user-defined tab config.\nThe specific config is identified by the companion `default_tab_config_path` setting.", + "type": "string" + }, + { + "const": "docker_sandbox", + "description": "New sessions open in a local Docker sandbox.\nRequires the `LocalDockerSandbox` feature flag; falls back to `Terminal` when disabled.", + "type": "string" + } + ] + }, + "DisplayIdx": { + "description": "Which display to use when multiple monitors are connected.", + "oneOf": [ + { + "const": "primary", + "description": "The primary (main) display.", + "type": "string" + }, + { + "additionalProperties": false, + "description": "An external display, identified by index.", + "properties": { + "external": { + "type": "integer" + } + }, + "required": [ + "external" + ], + "type": "object" + } + ] + }, + "DriveSortOrder": { + "description": "Sort order for Warp Drive items.", + "oneOf": [ + { + "const": "by_timestamp", + "description": "Sort by newest revision first in main index, most recently trashed in trash index", + "type": "string" + }, + { + "const": "alphabetical_descending", + "description": "A => Z", + "type": "string" + }, + { + "const": "alphabetical_ascending", + "description": "Z => A", + "type": "string" + }, + { + "const": "by_object_type", + "description": "Sort by object type, with folders first", + "type": "string" + } + ] + }, + "Editor": { + "description": "An external code editor.", + "enum": [ + "v_s_code", + "v_s_code_insiders", + "py_charm", + "py_charm_c_e", + "intelli_j", + "intelli_j_c_e", + "c_lion", + "c_lion_c_e", + "rust_rover_preview", + "rust_rover", + "sublime", + "atom", + "web_storm", + "php_storm", + "ruby_mine", + "zed", + "zed_preview", + "go_land", + "rider", + "data_spell", + "data_grip", + "android_studio", + "cursor", + "windsurf" + ], + "type": "string" + }, + "EditorChoice": { + "description": "Which editor to use when opening files.", + "oneOf": [ + { + "enum": [ + "system_default", + "warp", + "env_editor" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "A specific external code editor.", + "properties": { + "external_editor": { + "$ref": "#/$defs/Editor" + } + }, + "required": [ + "external_editor" + ], + "type": "object" + } + ] + }, + "EditorLayout": { + "description": "Layout used when opening files in the editor.", + "enum": [ + "split_pane", + "new_tab" + ], + "type": "string" + }, + "EnforceMinimumContrast": { + "description": "When to adjust foreground color to ensure readability against the background.", + "oneOf": [ + { + "const": "never", + "description": "Never change FG color", + "type": "string" + }, + { + "const": "only_named_colors", + "description": "FG color can be changed, but only if the FG is specified with default colors", + "type": "string" + }, + { + "const": "always", + "description": "FG color is changed regardless of how FG was specified", + "type": "string" + } + ] + }, + "ExtraMetaKeys": { + "description": "Additional keys that act as the meta key.", + "properties": { + "left_alt": { + "description": "Whether the left Alt key acts as meta.", + "type": "boolean" + }, + "right_alt": { + "description": "Whether the right Alt key acts as meta.", + "type": "boolean" + } + }, + "required": [ + "left_alt", + "right_alt" + ], + "type": "object" + }, + "GraphicsBackend": { + "description": "Graphics rendering backend used for display output.", + "oneOf": [ + { + "const": "empty", + "description": "No-op backend for testing.", + "type": "string" + }, + { + "const": "dx12", + "description": "DirectX 12.", + "type": "string" + }, + { + "const": "vulkan", + "description": "Vulkan.", + "type": "string" + }, + { + "const": "gl", + "description": "OpenGL.", + "type": "string" + }, + { + "const": "metal", + "description": "Metal.", + "type": "string" + }, + { + "const": "browser_web_gpu", + "description": "WebGPU (browser).", + "type": "string" + } + ] + }, + "HeaderToolbarChipSelection": { + "description": "Configuration for the header toolbar chips in the vertical tab panel header.", + "oneOf": [ + { + "enum": [ + "default" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "custom": { + "properties": { + "left": { + "items": { + "$ref": "#/$defs/HeaderToolbarItemKind" + }, + "type": "array" + }, + "right": { + "items": { + "$ref": "#/$defs/HeaderToolbarItemKind" + }, + "type": "array" + } + }, + "required": [ + "left", + "right" + ], + "type": "object" + } + }, + "required": [ + "custom" + ], + "type": "object" + } + ] + }, + "HeaderToolbarItemKind": { + "description": "A configurable item in the vertical tabs header toolbar.\n\nEach variant represents a panel toggle button that can be placed on either\nthe left or right side of the toolbar. The side determines which side of the\nmain content area the panel opens on.", + "enum": [ + "tabs_panel", + "tools_panel", + "agent_management", + "code_review", + "notifications_mailbox" + ], + "type": "string" + }, + "InputBoxType": { + "description": "Terminal input style.", + "oneOf": [ + { + "const": "universal", + "description": "AI-first input", + "type": "string" + }, + { + "const": "classic", + "description": "Terminal-first input", + "type": "string" + } + ] + }, + "InputMode": { + "description": "Direction that blocks flow in the terminal viewport.", + "oneOf": [ + { + "const": "pinned_to_bottom", + "description": "The most recent blocks are at the bottom of the screen and new blocks\nare added at the bottom as the blocklist grows", + "type": "string" + }, + { + "const": "pinned_to_top", + "description": "The most recent blocks are at the top of the screen and new blocks\nare added at the top as the blocklist grows", + "type": "string" + }, + { + "const": "waterfall", + "description": "The input starts at the top and gets pushed down by commands above it.", + "type": "string" + } + ] + }, + "Keystroke": { + "type": "string" + }, + "NewSessionShell": { + "description": "Shell to use when opening new sessions.", + "oneOf": [ + { + "const": "system_default", + "description": "Use the operating system's default shell.", + "type": "string" + }, + { + "additionalProperties": false, + "description": "A shell executable path.", + "properties": { + "executable": { + "type": "string" + } + }, + "required": [ + "executable" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "An MSYS2 shell environment.", + "properties": { + "m_s_y_s2": { + "type": "string" + } + }, + "required": [ + "m_s_y_s2" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "A Windows Subsystem for Linux distribution.", + "properties": { + "w_s_l": { + "type": "string" + } + }, + "required": [ + "w_s_l" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "A custom shell command.", + "properties": { + "custom": { + "type": "string" + } + }, + "required": [ + "custom" + ], + "type": "object" + } + ] + }, + "NewTabPlacement": { + "description": "Where new tabs are placed in the tab bar.", + "enum": [ + "after_current_tab", + "after_all_tabs" + ], + "type": "string" + }, + "NotificationsMode": { + "description": "Whether the user has enabled or disabled notifications.", + "oneOf": [ + { + "const": "unset", + "description": "Notifications have not been configured yet.", + "type": "string" + }, + { + "const": "dismissed", + "description": "The notifications banner has been dismissed.", + "type": "string" + }, + { + "const": "enabled", + "description": "Notifications are enabled.", + "type": "string" + }, + { + "const": "disabled", + "description": "Notifications are disabled.", + "type": "string" + } + ] + }, + "NotificationsSettings": { + "description": "Notification preferences for terminal events.", + "properties": { + "is_agent_task_completed_enabled": { + "default": true, + "description": "Whether to notify when an agent task completes.", + "type": "boolean" + }, + "is_long_running_enabled": { + "default": true, + "description": "Whether to notify when a long-running command completes.", + "type": "boolean" + }, + "is_needs_attention_enabled": { + "default": true, + "description": "Whether to notify when a session needs attention.", + "type": "boolean" + }, + "is_password_prompt_enabled": { + "default": true, + "description": "Whether to notify when a password prompt is detected.", + "type": "boolean" + }, + "long_running_threshold": { + "default": { + "nanos": 0, + "secs": 30 + }, + "description": "Threshold in seconds for long-running command notifications.", + "type": "integer" + }, + "mode": { + "$ref": "#/$defs/NotificationsMode", + "default": "Unset", + "description": "Whether notifications are enabled, disabled, or not yet configured." + }, + "play_notification_sound": { + "default": true, + "description": "Whether to play a sound with notifications.", + "type": "boolean" + } + }, + "type": "object" + }, + "OpenConversationPreference": { + "description": "How to open agent conversations.", + "enum": [ + "new_tab", + "split_pane" + ], + "type": "string" + }, + "Pixels": { + "description": "A value in pixels.", + "type": "number" + }, + "QuakeModePinPosition": { + "description": "Screen edge to pin the hotkey window to.", + "enum": [ + "top", + "bottom", + "left", + "right" + ], + "type": "string" + }, + "QuakeModeSettings": { + "description": "Configuration for the hotkey window.", + "properties": { + "active_pin_position": { + "$ref": "#/$defs/QuakeModePinPosition", + "description": "Screen edge where the hotkey window is pinned." + }, + "hide_window_when_unfocused": { + "description": "Whether to hide the hotkey window when it loses focus.", + "type": "boolean" + }, + "keybinding": { + "anyOf": [ + { + "$ref": "#/$defs/Keystroke" + }, + { + "type": "null" + } + ], + "description": "Keyboard shortcut to toggle the hotkey window. Format: modifiers (cmd, ctrl, alt, shift, meta) and a key joined by '-', e.g. \"cmd-shift-a\" or \"alt-enter\". Bindings are case-sensitive: when shift is present, the key must be its shifted form (e.g., \"ctrl-shift-E\", not \"ctrl-shift-e\")." + }, + "pin_position_to_size_percentages": { + "additionalProperties": { + "$ref": "#/$defs/SizePercentages" + }, + "description": "Window size percentages for each pin position.", + "type": "object" + }, + "pin_screen": { + "anyOf": [ + { + "$ref": "#/$defs/DisplayIdx" + }, + { + "type": "null" + } + ], + "description": "Display to pin the hotkey window to." + } + }, + "required": [ + "active_pin_position", + "pin_position_to_size_percentages", + "hide_window_when_unfocused" + ], + "type": "object" + }, + "SecretDisplayMode": { + "description": "How detected secrets are visually displayed.", + "oneOf": [ + { + "const": "asterisks", + "description": "Fully obscure secrets with asterisks", + "type": "string" + }, + { + "const": "strikethrough", + "description": "Show secrets with gray color and strikethrough styling", + "type": "string" + }, + { + "const": "always_show", + "description": "Show secrets normally with no visual treatment (but are still detected/redacted)", + "type": "string" + } + ] + }, + "SelectedSystemThemes": { + "description": "Themes to use when following the system light/dark mode.", + "properties": { + "dark": { + "$ref": "#/$defs/ThemeKind", + "description": "The theme to use in dark mode." + }, + "light": { + "$ref": "#/$defs/ThemeKind", + "description": "The theme to use in light mode." + } + }, + "required": [ + "light", + "dark" + ], + "type": "object" + }, + "SizePercentages": { + "description": "Window size as width and height percentages of the screen.", + "properties": { + "height": { + "description": "Height as a percentage of screen height (0–100).", + "type": "integer" + }, + "width": { + "description": "Width as a percentage of screen width (0–100).", + "type": "integer" + } + }, + "required": [ + "width", + "height" + ], + "type": "object" + }, + "SpacingMode": { + "description": "Terminal block spacing.", + "oneOf": [ + { + "const": "normal", + "description": "Normal", + "type": "string" + }, + { + "const": "compact", + "description": "Compact", + "type": "string" + } + ] + }, + "SshExtensionInstallMode": { + "description": "Controls SSH extension installation behavior.", + "oneOf": [ + { + "const": "always_ask", + "description": "Always prompt the user before installing (default).", + "type": "string" + }, + { + "const": "always_install", + "description": "Automatically install and connect without prompting.", + "type": "string" + }, + { + "const": "never_install", + "description": "Never install; fall back to legacy warpification.", + "type": "string" + } + ] + }, + "StartupShell": { + "description": "Shell to start terminal sessions with. Use null for the system default, or one of \"bash\", \"zsh\", \"fish\", \"pwsh\", or a custom shell command/path.", + "type": [ + "string", + "null" + ] + }, + "TabCloseButtonPosition": { + "description": "Position of the close button on tabs.", + "enum": [ + "right", + "left" + ], + "type": "string" + }, + "ThemeKind": { + "description": "The color theme.", + "oneOf": [ + { + "const": "adeberry", + "description": "Adeberry", + "type": "string" + }, + { + "const": "phenomenon", + "description": "Phenomenon", + "type": "string" + }, + { + "const": "dark", + "description": "Dark", + "type": "string" + }, + { + "const": "dracula", + "description": "Dracula", + "type": "string" + }, + { + "const": "fancy_dracula", + "description": "Fancy Dracula", + "type": "string" + }, + { + "const": "cyber_wave", + "description": "Cyber Wave", + "type": "string" + }, + { + "const": "solar_flare", + "description": "Solar Flare", + "type": "string" + }, + { + "const": "solarized_dark", + "description": "Solarized Dark", + "type": "string" + }, + { + "const": "willow_dream", + "description": "Willow Dream", + "type": "string" + }, + { + "const": "light", + "description": "Light", + "type": "string" + }, + { + "const": "dark_city", + "description": "Dark City", + "type": "string" + }, + { + "const": "gruvbox_dark", + "description": "Gruvbox Dark", + "type": "string" + }, + { + "const": "red_rock", + "description": "Red Rock", + "type": "string" + }, + { + "const": "jelly_fish", + "description": "Jellyfish", + "type": "string" + }, + { + "const": "leafy", + "description": "Leafy", + "type": "string" + }, + { + "const": "koi", + "description": "Koi", + "type": "string" + }, + { + "const": "solarized_light", + "description": "Solarized Light", + "type": "string" + }, + { + "const": "snowy", + "description": "Snowy", + "type": "string" + }, + { + "const": "gruvbox_light", + "description": "Gruvbox Light", + "type": "string" + }, + { + "const": "pink_city", + "description": "Pink City", + "type": "string" + }, + { + "const": "marble", + "description": "Marble", + "type": "string" + }, + { + "additionalProperties": false, + "description": "A user-provided custom theme loaded from a file.", + "properties": { + "custom": { + "$ref": "#/$defs/CustomTheme" + } + }, + "required": [ + "custom" + ], + "type": "object" + }, + { + "additionalProperties": false, + "description": "A custom theme using the Base16 color scheme format.", + "properties": { + "custom_base16": { + "$ref": "#/$defs/CustomTheme" + } + }, + "required": [ + "custom_base16" + ], + "type": "object" + } + ] + }, + "ThinStrokes": { + "description": "When to render text with thinner strokes for a lighter appearance.", + "oneOf": [ + { + "const": "never", + "description": "Never render glyphs using thin strokes.", + "type": "string" + }, + { + "const": "on_low_dpi_displays", + "description": "Render glyphs using thin strokes when rendering on a low-DPI display.", + "type": "string" + }, + { + "const": "on_high_dpi_displays", + "description": "Render glyphs using thin strokes when rendering on a high-DPI display.", + "type": "string" + }, + { + "const": "always", + "description": "Always render glyphs using thin strokes.", + "type": "string" + } + ] + }, + "ThinkingDisplayMode": { + "description": "Controls how agent thinking is displayed after streaming.", + "oneOf": [ + { + "const": "show_and_collapse", + "description": "Show reasoning blocks while streaming, then collapse them when complete (default).", + "type": "string" + }, + { + "const": "always_show", + "description": "Always keep reasoning blocks expanded, even after streaming finishes.", + "type": "string" + }, + { + "const": "never_show", + "description": "Never show reasoning blocks.", + "type": "string" + } + ] + }, + "ToolbarCommandMap": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "UserNativePreference": { + "description": "Preference for using the native desktop app or the web app.", + "enum": [ + "not_selected", + "web", + "desktop" + ], + "type": "string" + }, + "VerticalTabsCompactSubtitle": { + "description": "Subtitle shown on compact vertical tabs.", + "enum": [ + "branch", + "working_directory", + "command" + ], + "type": "string" + }, + "VerticalTabsDisplayGranularity": { + "description": "Granularity of rows displayed in the vertical tabs panel.", + "enum": [ + "panes", + "tabs" + ], + "type": "string" + }, + "VerticalTabsPrimaryInfo": { + "description": "Primary information displayed on vertical tabs.", + "enum": [ + "command", + "working_directory", + "branch" + ], + "type": "string" + }, + "VerticalTabsTabItemMode": { + "description": "Tab item display mode in vertical tabs.", + "enum": [ + "focused_session", + "summary" + ], + "type": "string" + }, + "VerticalTabsViewMode": { + "description": "Display mode for the vertical tab bar.", + "enum": [ + "compact", + "expanded" + ], + "type": "string" + }, + "VoiceInputToggleKey": { + "description": "Physical key used to toggle voice input.", + "oneOf": [ + { + "const": "none", + "description": "No toggle key assigned.", + "type": "string" + }, + { + "const": "fn", + "description": "Fn key.", + "type": "string" + }, + { + "const": "alt_left", + "description": "Alt or Option key (left side).", + "type": "string" + }, + { + "const": "alt_right", + "description": "Alt or Option key (right side).", + "type": "string" + }, + { + "const": "control_left", + "description": "Control key (left side).", + "type": "string" + }, + { + "const": "control_right", + "description": "Control key (right side).", + "type": "string" + }, + { + "const": "super_left", + "description": "Super, Windows, or Command key (left side).", + "type": "string" + }, + { + "const": "super_right", + "description": "Super, Windows, or Command key (right side).", + "type": "string" + }, + { + "const": "shift_left", + "description": "Shift key (left side).", + "type": "string" + }, + { + "const": "shift_right", + "description": "Shift key (right side).", + "type": "string" + } + ] + }, + "Weight": { + "description": "Font weight for terminal text.", + "enum": [ + "thin", + "extra_light", + "light", + "normal", + "medium", + "semibold", + "bold", + "extra_bold", + "black" + ], + "type": "string" + }, + "WorkingDirectoryConfig": { + "description": "Configuration for the initial working directory of new sessions.", + "properties": { + "advanced_mode": { + "description": "Whether to use separate settings per session source.", + "type": "boolean" + }, + "global": { + "$ref": "#/$defs/WorkingDirectoryPerSourceConfig", + "description": "Default working directory settings used when advanced mode is off." + }, + "new_tab": { + "$ref": "#/$defs/WorkingDirectoryPerSourceConfig", + "description": "Working directory settings for new tab sessions." + }, + "new_window": { + "$ref": "#/$defs/WorkingDirectoryPerSourceConfig", + "description": "Working directory settings for new window sessions." + }, + "split_pane": { + "$ref": "#/$defs/WorkingDirectoryPerSourceConfig", + "description": "Working directory settings for split pane sessions." + } + }, + "required": [ + "advanced_mode", + "global", + "split_pane", + "new_tab", + "new_window" + ], + "type": "object" + }, + "WorkingDirectoryMode": { + "description": "Where new sessions start.", + "oneOf": [ + { + "const": "home_dir", + "description": "Start a new session in the user's home directory.", + "type": "string" + }, + { + "const": "previous_dir", + "description": "Start a new session in the same directory as the previous session.", + "type": "string" + }, + { + "const": "custom_dir", + "description": "Start a new session in a specific directory.", + "type": "string" + } + ] + }, + "WorkingDirectoryPerSourceConfig": { + "description": "Working directory settings for a specific session source.", + "properties": { + "custom_dir": { + "description": "Custom directory path, used when mode is CustomDir.", + "type": "string" + }, + "mode": { + "$ref": "#/$defs/WorkingDirectoryMode", + "description": "How the working directory is determined." + } + }, + "required": [ + "mode", + "custom_dir" + ], + "type": "object" + }, + "WorkspaceDecorationVisibility": { + "description": "When workspace decorations such as the tab bar are visible.", + "oneOf": [ + { + "const": "always_show", + "description": "Always show workspace decorations.", + "type": "string" + }, + { + "const": "hide_fullscreen", + "description": "Hide workspace decorations if fullscreen.", + "type": "string" + }, + { + "const": "on_hover", + "description": "Only show workspace decorations on hover.", + "type": "string" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "JSON Schema for Warp settings (oss channel, 190 settings)", + "properties": { + "accessibility": { + "properties": { + "accessibility_verbosity": { + "$ref": "#/$defs/AccessibilityVerbosity", + "default": "verbose", + "description": "The verbosity level for screen reader announcements." + } + }, + "type": "object" + }, + "account": { + "properties": { + "is_settings_sync_enabled": { + "default": false, + "description": "Whether settings are synced across devices via the cloud.", + "type": "boolean" + } + }, + "type": "object" + }, + "agents": { + "properties": { + "cloud_conversation_storage_enabled": { + "default": true, + "description": "Whether conversations are stored in the cloud.", + "type": "boolean" + }, + "knowledge": { + "properties": { + "rules_enabled": { + "default": true, + "description": "Whether the agent uses your saved rules during requests.", + "type": "boolean" + }, + "warp_drive_context_enabled": { + "default": true, + "description": "Whether Warp Drive context is included in AI requests.", + "type": "boolean" + } + }, + "type": "object" + }, + "mcp_servers": { + "properties": { + "file_based_mcp_enabled": { + "default": false, + "description": "Whether third-party file-based MCP servers are automatically detected.", + "type": "boolean" + } + }, + "type": "object" + }, + "profiles": { + "properties": { + "agent_mode_coding_file_read_allowlist": { + "default": [], + "description": "File paths the agent can read without asking for permission.", + "items": { + "type": "string" + }, + "type": "array" + }, + "agent_mode_coding_permissions": { + "$ref": "#/$defs/AgentModeCodingPermissionsType", + "default": "always_ask_before_reading", + "description": "The file read permission level for the agent." + }, + "agent_mode_command_execution_allowlist": { + "default": [ + "cat(\\s.*)?", + "echo(\\s.*)?", + "find .*", + "grep(\\s.*)?", + "ls(\\s.*)?", + "which .*" + ], + "description": "Commands that the agent can execute without explicit permission.", + "items": { + "$ref": "#/$defs/AgentModeCommandExecutionPredicate" + }, + "type": "array" + }, + "agent_mode_command_execution_denylist": { + "default": [ + "bash(\\s.*)?", + "fish(\\s.*)?", + "pwsh(\\s.*)?", + "sh(\\s.*)?", + "zsh(\\s.*)?", + "curl(\\s.*)?", + "eval(\\s.*)?", + "exec(\\s.*)?", + "source(\\s.*)?", + "wget(\\s.*)?", + "dig(\\s.*)?", + "nslookup(\\s.*)?", + "host(\\s.*)?", + "ssh(\\s.*)?", + "scp(\\s.*)?", + "rsync(\\s.*)?", + "telnet(\\s.*)?", + "rm(\\s.*)?" + ], + "description": "Commands that the agent must always ask before executing.", + "items": { + "$ref": "#/$defs/AgentModeCommandExecutionPredicate" + }, + "type": "array" + }, + "agent_mode_execute_readonly_commands": { + "default": false, + "description": "Whether the agent can auto-execute read-only commands without asking.", + "type": "boolean" + } + }, + "type": "object" + }, + "third_party": { + "properties": { + "auto_dismiss_composer_after_submit": { + "default": false, + "description": "Whether CLI agent Rich Input automatically closes after the user submits a prompt.", + "type": "boolean" + }, + "auto_open_composer_on_cli_agent_start": { + "default": false, + "description": "Whether CLI agent Rich Input automatically opens when a CLI agent session starts.", + "type": "boolean" + }, + "auto_toggle_composer": { + "default": true, + "description": "Whether CLI agent Rich Input automatically closes and reopens based on the agent's blocked state.", + "type": "boolean" + }, + "cli_agent_toolbar_chip_selection_setting": { + "$ref": "#/$defs/CLIAgentToolbarChipSelection", + "default": "default", + "description": "Controls the layout of context chips in the CLI Agent toolbar." + }, + "cli_agent_toolbar_enabled_commands": { + "$ref": "#/$defs/ToolbarCommandMap", + "default": {}, + "description": "Maps custom toolbar command patterns to specific CLI agents." + }, + "should_render_cli_agent_toolbar": { + "default": true, + "description": "Whether to show the CLI agent footer for coding agent commands.", + "type": "boolean" + } + }, + "type": "object" + }, + "voice": { + "properties": { + "voice_input_enabled": { + "default": true, + "description": "Controls whether voice input is enabled for AI interactions.", + "type": "boolean" + }, + "voice_input_toggle_key": { + "$ref": "#/$defs/VoiceInputToggleKey", + "default": "none", + "description": "The key used to toggle voice input." + } + }, + "type": "object" + }, + "warp_agent": { + "properties": { + "active_ai": { + "properties": { + "agent_mode_query_suggestions_enabled": { + "default": true, + "description": "Controls whether prompt suggestions are shown in agent mode.", + "type": "boolean" + }, + "code_suggestions_enabled": { + "default": true, + "description": "Controls whether AI code suggestions are enabled.", + "type": "boolean" + }, + "enabled": { + "default": true, + "description": "Controls whether proactive AI features like suggestions are enabled.", + "type": "boolean" + }, + "git_operations_autogen_enabled": { + "default": true, + "description": "Controls whether AI auto-generates commit messages and PR title/body in the code review dialogs.", + "type": "boolean" + }, + "intelligent_autosuggestions_enabled": { + "default": true, + "description": "Controls whether AI-powered intelligent autosuggestions are enabled.", + "type": "boolean" + }, + "shared_block_title_generation_enabled": { + "default": true, + "description": "Controls whether titles are auto-generated when sharing blocks.", + "type": "boolean" + } + }, + "type": "object" + }, + "input": { + "properties": { + "agent_toolbar_chip_selection_setting": { + "$ref": "#/$defs/AgentToolbarChipSelection", + "default": "default", + "description": "Controls the layout of context chips in the Agent Mode toolbar." + }, + "ai_auto_detection_enabled": { + "default": true, + "description": "Controls whether AI automatically detects natural language input.", + "type": "boolean" + }, + "ai_command_denylist": { + "default": "", + "description": "Commands to exclude from AI natural language autodetection.", + "type": "string" + }, + "include_agent_commands_in_history": { + "default": false, + "description": "Whether agent-executed commands are included in command history.", + "type": "boolean" + }, + "nld_in_terminal_enabled": { + "default": false, + "description": "Controls whether natural language detection is enabled in the terminal input.", + "type": "boolean" + }, + "show_agent_tips": { + "default": true, + "description": "Whether agent tips are displayed in the input.", + "type": "boolean" + }, + "show_model_selectors_in_prompt": { + "default": true, + "description": "Whether to show AI model selectors in the input prompt.", + "type": "boolean" + } + }, + "type": "object" + }, + "is_any_ai_enabled": { + "default": true, + "description": "Controls whether all AI features are enabled.", + "type": "boolean" + }, + "other": { + "properties": { + "agent_attribution_enabled": { + "default": true, + "description": "Whether the Warp Agent adds an attribution co-author line to commit messages and pull requests it creates.", + "type": "boolean" + }, + "auto_handoff_on_sleep_enabled": { + "default": false, + "description": "Whether Warp automatically hands off local agent conversations to cloud when the computer is about to sleep.", + "type": "boolean" + }, + "cloud_agent_computer_use_enabled": { + "default": false, + "description": "Whether computer use is enabled for cloud agent conversations.", + "type": "boolean" + }, + "feedback_bundled_skill_enabled": { + "default": true, + "description": "Whether Warp's built-in feedback skill is available to the Warp Agent.", + "type": "boolean" + }, + "open_conversation_layout_preference": { + "$ref": "#/$defs/OpenConversationPreference", + "default": "new_tab", + "description": "Whether to open agent conversations in a new tab or a split pane." + }, + "should_force_disable_ampersand_handoff": { + "default": false, + "description": "Whether to force-disable the & prefix for cloud handoff compose mode.", + "type": "boolean" + }, + "should_force_disable_cloud_handoff": { + "default": false, + "description": "Whether to force-disable local-to-cloud handoff.", + "type": "boolean" + }, + "should_render_use_agent_toolbar_for_user_commands": { + "default": true, + "description": "Whether to show the \"Use Agent\" footer for terminal commands.", + "type": "boolean" + }, + "should_show_oz_updates_in_zero_state": { + "default": true, + "description": "Whether the \"What's new\" section is shown in the agent view.", + "type": "boolean" + }, + "show_agent_notifications": { + "default": true, + "description": "Whether agent notifications are shown.", + "type": "boolean" + }, + "show_conversation_history": { + "default": true, + "description": "Whether conversation history appears in the tools panel.", + "type": "boolean" + }, + "thinking_display_mode": { + "$ref": "#/$defs/ThinkingDisplayMode", + "default": "show_and_collapse", + "description": "Controls how agent thinking traces are displayed after streaming." + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "appearance": { + "properties": { + "blocks": { + "properties": { + "should_show_bootstrap_block": { + "default": false, + "description": "Whether the bootstrap block is visible in the terminal.", + "type": "boolean" + }, + "should_show_in_band_command_blocks": { + "default": false, + "description": "Whether in-band command blocks are visible in the terminal.", + "type": "boolean" + }, + "should_show_ssh_block": { + "default": false, + "description": "Whether the SSH connection block is visible in the terminal.", + "type": "boolean" + }, + "show_block_dividers": { + "default": true, + "description": "Whether to show dividers between terminal blocks.", + "type": "boolean" + }, + "show_jump_to_bottom_of_block_button": { + "default": true, + "description": "Whether to show the jump-to-bottom button in long command output.", + "type": "boolean" + } + }, + "type": "object" + }, + "cursor": { + "properties": { + "cursor_blink": { + "$ref": "#/$defs/CursorBlink", + "default": "enabled", + "description": "Whether the cursor blinks." + }, + "cursor_display_type": { + "$ref": "#/$defs/CursorDisplayType", + "default": "bar", + "description": "The visual style of the cursor." + } + }, + "type": "object" + }, + "full_screen_apps": { + "properties": { + "alt_screen_padding": { + "$ref": "#/$defs/AltScreenPaddingMode", + "default": { + "custom": { + "uniform_padding": 0.0 + } + }, + "description": "Controls padding around full-screen terminal applications." + } + }, + "type": "object" + }, + "icon": { + "properties": { + "app_icon": { + "$ref": "#/$defs/AppIcon", + "default": "default", + "description": "The app icon displayed in the dock." + } + }, + "type": "object" + }, + "input": { + "properties": { + "input_mode": { + "$ref": "#/$defs/InputMode", + "default": "pinned_to_bottom", + "description": "The position of the terminal input." + } + }, + "type": "object" + }, + "panes": { + "properties": { + "focus_pane_on_hover": { + "default": false, + "description": "Whether panes are focused when hovered over.", + "type": "boolean" + }, + "should_dim_inactive_panes": { + "default": false, + "description": "Whether inactive panes are visually dimmed.", + "type": "boolean" + } + }, + "type": "object" + }, + "spacing": { + "$ref": "#/$defs/SpacingMode", + "default": "normal", + "description": "Controls the spacing between terminal blocks." + }, + "tabs": { + "properties": { + "header_toolbar_chip_selection": { + "$ref": "#/$defs/HeaderToolbarChipSelection", + "default": "default", + "description": "Configuration for the header toolbar chips in the vertical tab panel header." + }, + "preserve_active_tab_color": { + "default": false, + "description": "Whether to preserve the active tab's color when switching tabs.", + "type": "boolean" + }, + "show_indicators_button": { + "default": true, + "description": "Whether to show activity indicators on tabs.", + "type": "boolean" + }, + "tab_close_button_position": { + "$ref": "#/$defs/TabCloseButtonPosition", + "default": "right", + "description": "Position of the close button on tabs." + }, + "workspace_decoration_visibility": { + "$ref": "#/$defs/WorkspaceDecorationVisibility", + "default": "hide_fullscreen", + "description": "When workspace decorations such as the tab bar are visible." + } + }, + "type": "object" + }, + "text": { + "properties": { + "ai_font_name": { + "default": "Hack", + "description": "The font used for AI-generated content.", + "type": "string" + }, + "enforce_minimum_contrast": { + "$ref": "#/$defs/EnforceMinimumContrast", + "default": "only_named_colors", + "description": "Whether to enforce minimum contrast for text readability." + }, + "font_name": { + "default": "Hack", + "description": "The monospace font used in the terminal.", + "type": "string" + }, + "font_size": { + "default": 13.0, + "description": "The size of the monospace font in the terminal.", + "type": "number" + }, + "font_weight": { + "$ref": "#/$defs/Weight", + "default": "normal", + "description": "The weight of the monospace font in the terminal." + }, + "ligature_rendering_enabled": { + "default": false, + "description": "Whether to render font ligatures in the terminal.", + "type": "boolean" + }, + "line_height_ratio": { + "default": 1.2000000476837158, + "description": "The line height ratio for terminal text.", + "type": "number" + }, + "match_ai_font": { + "default": false, + "description": "Whether the AI font automatically matches the terminal font.", + "type": "boolean" + }, + "match_notebook_to_monospace_font_size": { + "default": true, + "description": "Whether the notebook font size matches the terminal font size.", + "type": "boolean" + }, + "notebook_font_size": { + "default": 14.0, + "description": "The font size used in notebooks.", + "type": "number" + }, + "use_thin_strokes": { + "$ref": "#/$defs/ThinStrokes", + "default": "on_high_dpi_displays", + "description": "Whether to use thin font strokes on macOS." + } + }, + "type": "object" + }, + "themes": { + "properties": { + "selected_system_themes": { + "$ref": "#/$defs/SelectedSystemThemes", + "default": { + "dark": "dark", + "light": "light" + }, + "description": "The themes to use for system light and dark modes." + }, + "system_theme": { + "default": false, + "description": "Whether to match the system light/dark theme.", + "type": "boolean" + }, + "theme": { + "$ref": "#/$defs/ThemeKind", + "default": "dark", + "description": "The color theme." + } + }, + "type": "object" + }, + "vertical_tabs": { + "properties": { + "compact_subtitle": { + "$ref": "#/$defs/VerticalTabsCompactSubtitle", + "default": "branch", + "description": "Subtitle shown on compact vertical tabs." + }, + "display_granularity": { + "$ref": "#/$defs/VerticalTabsDisplayGranularity", + "default": "panes", + "description": "Granularity of rows displayed in the vertical tabs panel." + }, + "enabled": { + "default": false, + "description": "Whether to display tabs vertically instead of horizontally.", + "type": "boolean" + }, + "primary_info": { + "$ref": "#/$defs/VerticalTabsPrimaryInfo", + "default": "command", + "description": "The primary information displayed on vertical tabs." + }, + "show_details_on_hover": { + "default": true, + "description": "Whether to show a details sidecar when hovering over a vertical tab.", + "type": "boolean" + }, + "show_diff_stats": { + "default": true, + "description": "Whether to show diff stats on vertical tabs.", + "type": "boolean" + }, + "show_panel_in_restored_windows": { + "default": false, + "description": "When restoring a window, open the vertical tabs panel even if it was closed when the session was saved.", + "type": "boolean" + }, + "show_pr_link": { + "default": true, + "description": "Whether to show PR links on vertical tabs.", + "type": "boolean" + }, + "tab_item_mode": { + "$ref": "#/$defs/VerticalTabsTabItemMode", + "default": "focused_session", + "description": "Tab item display mode in vertical tabs." + }, + "use_latest_prompt_as_title": { + "default": false, + "description": "Whether vertical tab names for agent conversations use the latest user prompt.", + "type": "boolean" + }, + "view_mode": { + "$ref": "#/$defs/VerticalTabsViewMode", + "default": "compact", + "description": "Display mode for the vertical tab bar." + } + }, + "type": "object" + }, + "window": { + "properties": { + "left_panel_visibility_across_tabs": { + "default": true, + "description": "Whether the left panel visibility is shared across all tabs.", + "type": "boolean" + }, + "new_windows_num_columns": { + "default": 80, + "description": "The number of columns for new windows when using a custom size.", + "type": "integer" + }, + "new_windows_num_rows": { + "default": 40, + "description": "The number of rows for new windows when using a custom size.", + "type": "integer" + }, + "open_windows_at_custom_size": { + "default": false, + "description": "Whether to open new windows at a custom size instead of the default.", + "type": "boolean" + }, + "override_blur": { + "default": 1, + "description": "The blur radius applied to the window background.", + "type": "integer" + }, + "override_blur_texture": { + "default": false, + "description": "Whether to apply a blur texture to the window background.", + "type": "boolean" + }, + "override_opacity": { + "default": 100, + "description": "The opacity of the window background, from 1 to 100 percent.", + "type": "integer" + }, + "zoom_level": { + "default": 100, + "description": "The zoom level for the window, as a percentage.", + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "cloud_platform": { + "properties": { + "third_party_api_keys": { + "properties": { + "aws_bedrock_auth_refresh_command": { + "default": "aws login", + "description": "The command to run to refresh AWS credentials for Bedrock.", + "type": "string" + }, + "aws_bedrock_auto_login": { + "default": false, + "description": "Whether to automatically run the AWS login command when Bedrock credentials expire.", + "type": "boolean" + }, + "aws_bedrock_credentials_enabled": { + "default": false, + "description": "Whether Warp should use your local AWS credentials for Bedrock-enabled requests.", + "type": "boolean" + }, + "aws_bedrock_profile": { + "default": "default", + "description": "The AWS profile name to use for Bedrock credentials.", + "type": "string" + }, + "can_use_warp_credits_with_byok": { + "default": false, + "description": "Whether Warp credits can be used as a fallback for user-provided models.", + "type": "boolean" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "code": { + "properties": { + "editor": { + "properties": { + "auto_open_code_review_pane_on_first_agent_change": { + "default": false, + "description": "Whether to automatically open the code review pane when the agent makes its first change.", + "type": "boolean" + }, + "open_code_panels_file_editor": { + "$ref": "#/$defs/EditorChoice", + "default": "warp", + "description": "The editor used to open files from code panels." + }, + "open_file_editor": { + "$ref": "#/$defs/EditorChoice", + "default": "system_default", + "description": "The editor used to open files." + }, + "open_file_layout": { + "$ref": "#/$defs/EditorLayout", + "default": "split_pane", + "description": "The layout used when opening files in the editor." + }, + "prefer_markdown_viewer": { + "default": true, + "description": "Whether to use the Markdown viewer when opening Markdown files.", + "type": "boolean" + }, + "prefer_tabbed_editor_view": { + "default": true, + "description": "Whether to prefer opening files in a tabbed editor view.", + "type": "boolean" + }, + "show_code_review_button": { + "default": true, + "description": "Whether to show the code review button on tabs.", + "type": "boolean" + }, + "show_code_review_diff_stats": { + "default": true, + "description": "Whether to show lines added/removed counts on the code review button.", + "type": "boolean" + }, + "show_global_search": { + "default": true, + "description": "Whether global file search is shown in the tools panel.", + "type": "boolean" + }, + "show_project_explorer": { + "default": true, + "description": "Whether the project explorer is shown in the tools panel.", + "type": "boolean" + }, + "use_warp_as_default_editor": { + "default": false, + "description": "Whether Warp is used as the default code editor.", + "type": "boolean" + } + }, + "type": "object" + }, + "indexing": { + "properties": { + "agent_mode_codebase_context": { + "default": true, + "description": "Whether codebase context is provided to the AI agent.", + "type": "boolean" + }, + "agent_mode_codebase_context_auto_indexing": { + "default": false, + "description": "Whether automatic codebase indexing is enabled.", + "type": "boolean" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "general": { + "properties": { + "default_session_mode": { + "$ref": "#/$defs/DefaultSessionMode", + "default": "terminal", + "description": "The default mode for new terminal sessions." + }, + "default_tab_config_path": { + "default": "", + "type": "string" + }, + "link_tooltip": { + "default": true, + "description": "Whether to show a tooltip when hovering over links.", + "type": "boolean" + }, + "login_item": { + "default": true, + "description": "Whether to launch Warp automatically when you log in.", + "type": "boolean" + }, + "mouse_scroll_multiplier": { + "default": 3.0, + "description": "The scroll speed multiplier for mouse scroll events.", + "type": "number" + }, + "new_tab_placement": { + "$ref": "#/$defs/NewTabPlacement", + "default": "after_current_tab", + "description": "Where new tabs are placed in the tab bar." + }, + "quit_on_last_window_closed": { + "default": false, + "description": "Whether to quit Warp when the last window is closed.", + "type": "boolean" + }, + "restore_session": { + "default": true, + "description": "Whether to restore the previous session when Warp starts up.", + "type": "boolean" + }, + "should_confirm_close_session": { + "default": true, + "description": "Whether to show a confirmation dialog when closing a session.", + "type": "boolean" + }, + "show_changelog_after_update": { + "default": true, + "description": "Whether the changelog is shown after an update.", + "type": "boolean" + }, + "show_warning_before_quitting": { + "default": true, + "description": "Whether to show a warning dialog before quitting Warp.", + "type": "boolean" + }, + "snackbar_enabled": { + "default": true, + "description": "Whether to show snackbar notifications.", + "type": "boolean" + }, + "undo_close": { + "properties": { + "enabled": { + "default": true, + "description": "Whether the undo close feature is enabled.", + "type": "boolean" + }, + "grace_period": { + "default": 60, + "description": "How long after closing a tab you can still undo the close.", + "type": "integer" + } + }, + "type": "object" + }, + "user_native_preference": { + "$ref": "#/$defs/UserNativePreference", + "default": "not_selected", + "description": "Whether to prefer the native desktop app or the web app." + } + }, + "type": "object" + }, + "global_hotkey": { + "properties": { + "dedicated_window": { + "properties": { + "enabled": { + "default": false, + "description": "Whether the dedicated hotkey window is enabled. Mutually exclusive with `global_hotkey.toggle_all_windows.enabled`; only one should be true at a time.", + "type": "boolean" + }, + "settings": { + "$ref": "#/$defs/QuakeModeSettings", + "default": { + "active_pin_position": "top", + "hide_window_when_unfocused": false, + "keybinding": null, + "pin_position_to_size_percentages": { + "bottom": { + "height": 30, + "width": 100 + }, + "left": { + "height": 100, + "width": 40 + }, + "right": { + "height": 100, + "width": 40 + }, + "top": { + "height": 30, + "width": 100 + } + }, + "pin_screen": null + }, + "description": "Configuration options for Quake Mode window behavior." + } + }, + "type": "object" + }, + "toggle_all_windows": { + "properties": { + "enabled": { + "default": false, + "description": "Whether the hotkey that toggles visibility of all windows is enabled. Mutually exclusive with `global_hotkey.dedicated_window.enabled`; only one should be true at a time.", + "type": "boolean" + }, + "keybinding": { + "anyOf": [ + { + "$ref": "#/$defs/Keystroke" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The keybinding used for the global activation hotkey. Format: modifiers (cmd, ctrl, alt, shift, meta) and a key joined by '-', e.g. \"cmd-shift-a\" or \"alt-enter\". Bindings are case-sensitive: when shift is present, the key must be its shifted form (e.g., \"ctrl-shift-E\", not \"ctrl-shift-e\")." + } + }, + "type": "object" + } + }, + "type": "object" + }, + "keys": { + "properties": { + "ctrl_tab_behavior_setting": { + "$ref": "#/$defs/CtrlTabBehavior", + "default": "activate_prev_next_tab", + "description": "Controls the behavior of Ctrl+Tab." + } + }, + "type": "object" + }, + "notifications": { + "properties": { + "preferences": { + "$ref": "#/$defs/NotificationsSettings", + "default": { + "is_agent_task_completed_enabled": true, + "is_long_running_enabled": true, + "is_needs_attention_enabled": true, + "is_password_prompt_enabled": true, + "long_running_threshold": 30, + "mode": "unset", + "play_notification_sound": true + }, + "description": "Notification preferences for terminal events." + }, + "toast_duration_secs": { + "default": 8, + "description": "How long notification toasts are displayed, in seconds.", + "type": "integer" + } + }, + "type": "object" + }, + "privacy": { + "properties": { + "crash_reporting_enabled": { + "default": true, + "description": "Whether crash reports are sent.", + "type": "boolean" + }, + "custom_secret_regex_list": { + "default": [], + "description": "Custom regex patterns for detecting and redacting secrets.", + "items": { + "$ref": "#/$defs/CustomSecretRegex" + }, + "type": "array" + }, + "secret_redaction": { + "properties": { + "enabled": { + "default": false, + "description": "Whether secret redaction is enabled to detect and obscure secrets in terminal output.", + "type": "boolean" + }, + "hide_secrets_in_block_list": { + "default": false, + "description": "Whether to hide detected secrets in the block list using asterisks.", + "type": "boolean" + }, + "secret_display_mode_setting": { + "$ref": "#/$defs/SecretDisplayMode", + "default": "strikethrough", + "description": "Controls how detected secrets are visually displayed in the terminal." + } + }, + "type": "object" + }, + "telemetry_enabled": { + "default": true, + "description": "Whether anonymous usage telemetry is collected.", + "type": "boolean" + } + }, + "type": "object" + }, + "session": { + "properties": { + "new_session_shell_override": { + "anyOf": [ + { + "$ref": "#/$defs/NewSessionShell" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The shell to use when opening a new session." + }, + "startup_shell_override": { + "$ref": "#/$defs/StartupShell", + "default": null, + "description": "The shell to use when Warp starts up." + }, + "working_directory_config": { + "$ref": "#/$defs/WorkingDirectoryConfig", + "default": { + "advanced_mode": false, + "global": { + "custom_dir": "", + "mode": "previous_dir" + }, + "new_tab": { + "custom_dir": "", + "mode": "previous_dir" + }, + "new_window": { + "custom_dir": "", + "mode": "previous_dir" + }, + "split_pane": { + "custom_dir": "", + "mode": "previous_dir" + } + }, + "description": "Controls the working directory used when opening new sessions." + } + }, + "type": "object" + }, + "system": { + "properties": { + "force_x11": { + "default": true, + "description": "Whether to force X11 instead of Wayland on Linux.", + "type": "boolean" + }, + "linux_selection_clipboard": { + "default": true, + "description": "Whether the Linux primary selection clipboard is used.", + "type": "boolean" + }, + "prefer_low_power_gpu": { + "default": true, + "description": "Whether to prefer the integrated (low-power) GPU.", + "type": "boolean" + }, + "preferred_graphics_backend": { + "anyOf": [ + { + "$ref": "#/$defs/GraphicsBackend" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The preferred graphics backend on Windows." + } + }, + "type": "object" + }, + "terminal": { + "properties": { + "copy_on_select": { + "default": true, + "description": "Whether text is automatically copied to the clipboard when selected.", + "type": "boolean" + }, + "focus_reporting_enabled": { + "default": true, + "description": "Whether to forward focus and blur events to full-screen terminal applications.", + "type": "boolean" + }, + "input": { + "properties": { + "alias_expansion_enabled": { + "default": false, + "description": "Whether shell alias expansion is enabled in the input.", + "type": "boolean" + }, + "at_context_menu_in_terminal_mode": { + "default": true, + "description": "Whether the @ context menu is available in terminal mode.", + "type": "boolean" + }, + "autosuggestions": { + "properties": { + "enabled": { + "default": true, + "description": "Whether command autosuggestions are shown.", + "type": "boolean" + }, + "keybinding_hint": { + "default": true, + "description": "Whether autosuggestion keybinding hints are displayed.", + "type": "boolean" + }, + "show_ignore_button": { + "default": false, + "description": "Whether the ignore button is shown for autosuggestions.", + "type": "boolean" + } + }, + "type": "object" + }, + "classic_completions_mode": { + "default": false, + "description": "Whether classic completions mode is enabled.", + "type": "boolean" + }, + "command_corrections": { + "default": true, + "description": "Whether command corrections are suggested for mistyped commands.", + "type": "boolean" + }, + "completions_open_while_typing": { + "default": false, + "description": "Whether the completions menu opens automatically while typing.", + "type": "boolean" + }, + "enable_slash_commands_in_terminal": { + "default": true, + "description": "Whether slash commands are available in the terminal input.", + "type": "boolean" + }, + "error_underlining_enabled": { + "default": true, + "description": "Whether command errors are underlined in the input.", + "type": "boolean" + }, + "extra_meta_keys": { + "$ref": "#/$defs/ExtraMetaKeys", + "default": { + "left_alt": false, + "right_alt": false + }, + "description": "Controls which additional keys are treated as meta keys." + }, + "honor_ps1": { + "default": false, + "description": "Whether to use your shell's PS1 prompt instead of the Warp prompt.", + "type": "boolean" + }, + "input_box_type_setting": { + "$ref": "#/$defs/InputBoxType", + "default": "classic", + "description": "The terminal input style." + }, + "middle_click_paste_enabled": { + "default": true, + "description": "Whether middle-click pastes from the clipboard.", + "type": "boolean" + }, + "outline_codebase_symbols_for_at_context_menu": { + "default": true, + "description": "Whether codebase symbols appear in the @ context menu.", + "type": "boolean" + }, + "show_hint_text": { + "default": true, + "description": "Whether hint text is shown in the terminal input.", + "type": "boolean" + }, + "show_terminal_input_message_bar": { + "default": true, + "description": "Whether the terminal input message bar is shown.", + "type": "boolean" + }, + "syntax_highlighting": { + "default": true, + "description": "Whether syntax highlighting is enabled in the terminal input.", + "type": "boolean" + } + }, + "type": "object" + }, + "maximum_grid_size": { + "default": 50000, + "description": "The maximum number of rows in the terminal grid.", + "type": "integer" + }, + "mouse_reporting_enabled": { + "default": true, + "description": "Whether to forward mouse events to full-screen terminal applications.", + "type": "boolean" + }, + "scroll_reporting_enabled": { + "default": true, + "description": "Whether to forward scroll events to full-screen terminal applications.", + "type": "boolean" + }, + "show_terminal_zero_state_block": { + "default": true, + "description": "Whether to show the AI zero-state block in new terminal sessions.", + "type": "boolean" + }, + "smart_select": { + "properties": { + "enabled": { + "default": true, + "description": "Whether double-click smart selection is enabled for URLs, emails, file paths, and identifiers.", + "type": "boolean" + }, + "word_char_allowlist": { + "default": "-.~/\\", + "description": "Characters that are considered part of a word for double-click selection when smart select is disabled.", + "type": "string" + } + }, + "type": "object" + }, + "use_audible_bell": { + "default": false, + "description": "Whether to play an audible bell sound on terminal bell events.", + "type": "boolean" + } + }, + "type": "object" + }, + "text_editing": { + "properties": { + "autocomplete_symbols": { + "default": true, + "description": "Whether matching symbols like brackets and quotes are auto-completed.", + "type": "boolean" + }, + "vim_mode_enabled": { + "default": false, + "description": "Whether Vim keybindings are enabled.", + "type": "boolean" + }, + "vim_status_bar": { + "default": true, + "description": "Whether the Vim status bar is displayed.", + "type": "boolean" + }, + "vim_unnamed_system_clipboard": { + "default": false, + "description": "Whether the Vim unnamed register uses the system clipboard.", + "type": "boolean" + } + }, + "type": "object" + }, + "warp_drive": { + "properties": { + "enabled": { + "default": true, + "description": "Whether Warp Drive is enabled.", + "type": "boolean" + }, + "sorting_choice": { + "$ref": "#/$defs/DriveSortOrder", + "default": "by_object_type", + "description": "The sort order for items in Warp Drive." + } + }, + "type": "object" + }, + "warpify": { + "properties": { + "ssh": { + "properties": { + "enable_legacy_ssh_wrapper": { + "default": true, + "description": "Whether the legacy SSH wrapper is enabled for SSH sessions.", + "type": "boolean" + }, + "enable_ssh_warpification": { + "default": true, + "description": "Whether to enable Warp features in SSH sessions.", + "type": "boolean" + }, + "ssh_extension_install_mode": { + "$ref": "#/$defs/SshExtensionInstallMode", + "default": "always_ask", + "description": "Controls SSH extension installation behavior." + }, + "ssh_hosts_denylist": { + "default": [], + "description": "SSH hosts that should not trigger the warpification prompt.", + "items": { + "type": "string" + }, + "type": "array" + }, + "use_ssh_tmux_wrapper": { + "default": false, + "description": "Whether to use a tmux-based wrapper for SSH warpification.", + "type": "boolean" + } + }, + "type": "object" + }, + "subshells": { + "properties": { + "added_subshell_commands": { + "default": [], + "description": "Additional regex patterns for commands that should be recognized as subshells.", + "items": { + "type": "string" + }, + "type": "array" + }, + "subshell_commands_denylist": { + "default": [], + "description": "Commands that should not trigger the subshell warpification prompt.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "workflows": { + "properties": { + "show_global_workflows_in_universal_search": { + "default": false, + "description": "Whether to show global workflows in universal search results.", + "type": "boolean" + } + }, + "type": "object" + } + }, + "title": "Warp Settings", + "type": "object" +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 4c78ec4e76..9c5e81b2af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,194 +1,85 @@ -# AGENTS.md +# AGENTS — Warp i18n Project Rules -This file provides guidance when working with code in this repository. +> Локальные правила для AI-агентов. Не коммитить — в `.gitignore`. +> Общие правила грузятся из WikiDB/MCP Config: `agent_startup` → `gates` → `base_rules` → role rules. -## Development Commands +## Project -### Build and Run -- `cargo run` - Build and run Warp locally -- `cargo bundle --bin warp` - Bundle the main app +Warp i18n — интернационализация Warp (терминал от warpdotdev). +YAML-формат (совместим с ZacharyZcR), русская локаль. -### Running with local warp-server -To connect Warp client to a local warp-server instance: +## Where Code Runs -```bash -# Connect to server on default port 8080 -cargo run --features with_local_server +- **Development:** `/nob/dmitry/git/warp` (локальная машина) +- **Production:** не настроен -# Connect to server on custom port (e.g., 8082) -SERVER_ROOT_URL=http://localhost:8082 WS_SERVER_URL=ws://localhost:8082/graphql/v2 cargo run --features with_local_server -``` +## Current Agent Role + +| Роль | Назначена | +|------|-----------| +| **Coordinator (root)** | 2026-05-14 | + +## Ветка + +- **Рабочая ветка:** `i18n` +- **Upstream:** `warpdotdev/warp` (HTTPS, read-only) +- **Форк:** `ErshovDmitry/warp-i18n` (push через `push-https`) +- **PR:** [#11382](https://github.com/warpdotdev/warp/pull/11382) + +### 🔴 Merge Policy + +- **НЕ мёржим master в i18n.** Никогда. +- **НЕ мёржим i18n в master.** Это делает upstream. +- Совместимость с master — ручная адаптация кода, без merge-коммитов. + +## Скилы + +| Скил | Назначение | +|------|-----------| +| `warp-i18n-upstream-sync` | Синхронизация с upstream | +| `warp-i18n-wrap-strings` | Поиск и оборачивание новых строк | +| `warp-i18n-pr-monitor` | Мониторинг PR #11382 | +| `warp-i18n-autonomous-agent` | Оркестратор полного цикла | + +## Ключевые страницы wiki (схема `project_warp`) + +| Страница | ID | +|----------|----| +| Agent Context — 2026-06-03 | `24eec969-32d3-4a71-bc2b-222d8a469c81` | +| Warp i18n — Final State | `c8542373-4b17-4a21-9bde-f39434ef50d1` | +| Decision: Switch to ZacharyZcR YAML | `62fc189c-294e-49f9-904a-889220c939f7` | +| Trace: agent — полный цикл — 2026-06-03 | `33e6d917-3bce-4cec-aed2-01dd78cdb4a0` | +| Agent Memory | `8595c8b4-ca9f-4834-8517-853a5df6d0e4` | + +## Совместимость с ZacharyZcR + +- Формат YAML (`resources/bundled/locales/{en,ru}.yml`) +- API: `crate::menu_label()`, `i18n::lookup()`, `TranslationLookup` +- При расхождениях — править у нас + +## Соглашения по коду + +- **i18n паттерн:** `crate::menu_label("key.path", "English fallback")` +- **Локали:** `resources/bundled/locales/{en,ru}.yml` +- **Проверка:** `cargo check -p warp` (0 warnings) + `cargo test -p i18n` (11/11 pass) + +## Уведомления + +- **Gotify:** `https://gotify.derhp.ru`, token `A7BzFZmfiu3nqDe` +- 🇷🇺 Русский — Gotify, 🇬🇧 English — GitHub + +## Сборка + +```bash +cargo check -p warp +cargo test -p i18n -Environment variables: -- `SERVER_ROOT_URL` - HTTP endpoint (default: `http://localhost:8080`) -- `WS_SERVER_URL` - WebSocket endpoint (default: `ws://localhost:8080/graphql/v2`) - -### Testing -- `cargo nextest run --no-fail-fast --workspace --exclude command-signatures-v2` - Run tests with nextest -- `cargo nextest run -p warp_completer --features v2` - Run completer tests with v2 features -- `cargo test --doc` - Run doc tests -- `cargo test` - Run standard tests for individual packages - -### Linting and Formatting -- `./script/presubmit` - Run all presubmit checks (fmt, clippy, tests) -- `./script/format` - Format code -- `cargo clippy --workspace --all-targets --all-features --tests -- -D warnings` - Run clippy -- `./script/run-clang-format.py -r --extensions 'c,h,cpp,m' ./crates/warpui/src/ ./app/src/` - Format C/C++/Obj-C code -- `find . -name "*.wgsl" -exec wgslfmt --check {} +` - Check WGSL shader formatting - -### Platform Setup -- `./script/bootstrap` - Platform-specific setup plus common agent skill installation from `skills-lock.json`; prompts for project/global when an install or update is needed unless a target flag or environment override is provided. -- `./script/bootstrap --skip-common-skills` - Platform setup without installing or updating common agent skills. -- `./script/bootstrap --install-common-skills` - Explicitly install common agent skills from `skills-lock.json`; this is the default behavior. -- `./script/bootstrap --install-common-skills-in-repo` - Platform setup plus common agent skill installation in this checkout's `.agents/skills`. -- `./script/bootstrap --install-common-skills-globally` - Platform setup plus common agent skill installation in `~/.agents/skills`. -- `../common-skills/scripts/install_common_skills --repo-root "$PWD" --project --if-needed` - Install or refresh shared agent skills in this checkout's `.agents/skills`. -- `../common-skills/scripts/install_common_skills --repo-root "$PWD" --global --if-needed` - Install or refresh shared agent skills in `~/.agents/skills`. -- `../common-skills/scripts/remove_common_skills --repo-root "$PWD"` - Remove shared agent skills listed in `skills-lock.json` from this checkout's `.agents/skills`. -- `../common-skills/scripts/remove_common_skills --repo-root "$PWD" --global` - Remove shared agent skills listed in `skills-lock.json` from `~/.agents/skills`. -- `../common-skills/scripts/remove_common_skills --repo-root "$PWD" --clear-lock` - Remove shared agent skills from this checkout and delete `skills-lock.json`. -- `./script/install_cargo_build_deps` - Install Cargo build dependencies -- `./script/install_cargo_test_deps` - Install Cargo test dependencies - -`skills-lock.json` is the standard project lock file managed by `npx skills`. `warpdotdev/common-skills/scripts/install_common_skills` requires an explicit install target before restoring: pass `--project`, pass `--global`, set `WARP_COMMON_SKILLS_INSTALL_TARGET`, or answer the interactive prompt from bootstrap. Non-interactive flows fail if no target is explicit. The installer creates `skills-lock.json` from `warpdotdev/common-skills` if it is missing, uses global as the recommended interactive default, errors if common skills are present in both project and global locations, prevents a global install pinned to one lock from being silently overwritten by another checkout pinned to a different lock, and verifies installed skills against the lock after successful install or skip paths. `script/run` and `script/bootstrap` execute this installer with `script/resolve_common_skills`, which uses `WARP_COMMON_SKILLS_SCRIPTS_DIR` only when explicitly set and otherwise runs the raw script from `warpdotdev/common-skills`. To test a remote common-skills branch, set `WARP_COMMON_SKILLS_REF=`. Cloud setup should use `common-skills/scripts/install_common_skills --repo-root --project --if-needed --non-interactive` or set `WARP_COMMON_SKILLS_INSTALL_TARGET=project` to avoid the prompt. To update the locked common skills, run `npx --yes skills@1.5.6 update -p -y` and commit the resulting `skills-lock.json` changes. - -## Architecture Overview - -This is a Rust-based terminal emulator with a custom UI framework called **WarpUI**. - -### Key Components - -**WarpUI Framework** (`ui/`): -- Custom UI framework with Entity-Component-Handle pattern -- Global `App` object owns all views/models (entities) -- Views hold `ViewHandle` references to other views -- `AppContext` provides temporary access to handles during render/events -- Elements describe visual layout (Flutter-inspired) -- Actions system for event handling -- MouseStateHandle must be created once during construction, and then referenced/cloned anywhere we're using mouse input to track mouse changes. Inline `MouseStateHandle::default()` while rendering will cause no mouse interactions to work. - -**Main App** (`app/`): -- Terminal emulation and shell management (`terminal/`) -- AI integration including Agent Mode (`ai/`) -- Cloud synchronization and Drive features (`drive/`) -- Authentication and user management (`auth/`) -- Settings and preferences (`settings/`) -- Workspace and session management (`workspace/`) - -**Core Libraries**: -- `crates/warp_core/` - Core utilities and platform abstractions -- `crates/editor/` - Text editing functionality -- `crates/warpui/` and `crates/warpui_core/` - Custom UI framework -- `crates/ipc/` - Inter-process communication -- `crates/graphql/` - GraphQL client and schema - -### Key Architectural Patterns - -1. **Entity-Handle System**: Views reference other views via handles, not direct ownership -2. **Modular Structure**: Workspace contains multiple workspace configurations, each with terminals, notebooks, etc. -3. **Cross-Platform**: Native implementations for macOS, Windows, Linux, plus WASM target -4. **AI Integration**: Built-in AI assistant with context awareness and codebase indexing -5. **Cloud Sync**: Objects can be synchronized across devices via Warp Drive - -### Development Guidelines - -**Workspace Structure**: -- This is a Cargo workspace with 60+ member crates -- Main binary is in `app/`, UI framework in `crates/warpui/` -- Platform-specific code is conditionally compiled -- Integration tests are in `crates/integration/` - -**Coding Style Preferences**: -- Avoid unnecessary type annotations, especially in closure params. -- Avoid using too many Rust path qualifiers and use imports for concision. Place import statements at the top of the file as per convention. - An exception to this is inside cfg-guarded code branches. In those cases, you can either embed the import into the relevant scope or just use an absolute path for one-offs. -- If a function takes a context parameter (`AppContext`, `ViewContext`, or `ModelContext`), it should be named `ctx` and go last. The one exception is for - functions that take a closure parameter, in which case the closure should be last. -- Always remove unused parameters completely rather than prefixing them with `_`. Update the function signature and all call sites accordingly. -- Prefer inline format arguments in macros like `println!`, `eprintln!`, and `format!` (for example, `eprintln!("{message}")` instead of `eprintln!("{}", message)`) to satisfy Clippy's `uninlined_format_args` lint. -- Do not pass `Itertools::format` results directly to logging macros (`log::*`, `safe_*`, etc.). `Itertools::format` produces a single-use formatter, while logging implementations may format a message more than once. Use a reusable `String` such as `iter.join(", ")` for logging arguments instead. Direct use in `format!` or `write!` is fine. -- Do not remove existing comments when making unrelated changes. Only remove or modify a comment if the logic it describes has changed. -- When adding a toggleable setting, also add the matching Command Palette enable/disable entry and any required context flags so the setting is discoverable outside Settings. - -**Terminal Model Locking**: -- Be extremely careful when calling `model.lock()` on the terminal model (`TerminalModel`). Acquiring multiple locks on the same model from different call sites can cause a deadlock, resulting in a UI freeze (beach ball on macOS). -- Before adding a new `model.lock()` call, verify that no caller in the current call stack already holds the lock. -- Prefer passing already-locked model references down the call stack rather than acquiring new locks. -- If you must lock the model, keep the lock scope as short as possible and avoid calling other functions that might also attempt to lock. - -**Testing**: -- Use `cargo nextest` for parallel test execution -- Integration tests use custom framework in `integration/` -- Tests should be run via presubmit script before submitting -- Unit tests should be placed in separate files using the naming convention `${filename}_tests.rs` or `mod_test.rs` -- Test files should be included at the end of their corresponding module with: - ```rust - #[cfg(test)] - #[path = "filename_tests.rs"] // or "mod_test.rs" - mod tests; - ``` - -**Pull Request Workflow**: -- **ALWAYS** run `./script/format` and `cargo clippy` (the versions specified in ./script/presubmit) before opening a PR or pushing updates to an existing PR branch -- Those commands must pass completely before creating or updating a pull request -- Specifically, ensure `./script/format` and `cargo clippy` checks pass -- If they fail, fix all issues before proceeding with the PR -- Do not create public pull requests or public issues that disclose a non-public security vulnerability. Refer users to `SECURITY.md` for the proper disclosure methods instead. -- This applies to: - - Opening new pull requests - - Pushing new commits to existing PR branches - - Any branch updates that will be reviewed - - When opening PRs, use the PR template at `.github/pull_request_template.md` - - Add changelog entries when appropriate using the format at the bottom of the PR template. Use the following prefixes (without the `{{}}` brackets): - - `CHANGELOG-NEW-FEATURE:` for new, relatively sizable features (use sparingly - these may get marketing/docs) - - `CHANGELOG-IMPROVEMENT:` for new functionality of existing features - - `CHANGELOG-BUG-FIX:` for fixes related to known bugs or regressions - - `CHANGELOG-IMAGE:` for GCP-hosted image URLs - - Leave changelog lines blank or remove them if no changelog entry is needed - -**Database**: -- Uses Diesel ORM with SQLite -- Migrations in `crates/persistence/migrations/` -- Schema defined in `crates/persistence/src/schema.rs` - -**GraphQL**: -- Schema and client code generation from `crates/warp_graphql_schema/api/schema.graphql` -- TypeScript types generated for frontend integration - -### Feature Flags - -Warp uses compile-time feature flags with a small runtime plumbing layer. - -How to add a feature flag: -- Add a new variant to `warp_core/src/features.rs` in the `FeatureFlag` enum -- (Optional) Enable it by default for dogfood builds by listing it in `DOGFOOD_FLAGS` -- Gate code paths with `FeatureFlag::YourFlag.is_enabled()` -- For preview or release rollout, add to `PREVIEW_FLAGS` or `RELEASE_FLAGS` respectively (as appropriate) - -Best practices: -- **Prefer runtime checks over cfg directives**: Prefer `FeatureFlag::YourFlag.is_enabled()` over `#[cfg(...)]` compile-time directives so flags can be toggled without recompilation and are easier to clean up later. Use `#[cfg(...)]` only when the code cannot compile without them (for example, platform-specific code or dependencies that do not exist when the feature is disabled). -- Keep flags high-level and product-focused rather than per-call-site -- Remove the flag and dead branches after launch has stabilized -- For UI sections that expose a new feature, hide the UI behind the same flag - -Example: -```rust -#[derive(Sequence)] -pub enum FeatureFlag { - YourNewFeature, -} - -// Default-on for dogfood builds -pub const DOGFOOD_FLAGS: &[FeatureFlag] = &[ - FeatureFlag::YourNewFeature, -]; - -// Use in code -if FeatureFlag::YourNewFeature.is_enabled() { - // gated behavior -} +# AppImage +cargo build -p warp --profile release-lto --bin warp-oss --features "release_bundle,gui" +NO_STRIP=1 script/bundle --channel oss --packages appimage --skip-build +# → target/release-lto/bundle/linux/WarpOss-x86_64.AppImage ``` -### Exhaustive Matching +## Golden Rule -When adding/editing match statements, avoid using the wildcard _ when at all possible. Exhaustive matching is helpful for ensuring that all variants are handled, especially when adding new variants to enums in the future. +Если этого нет в wiki — этого не было. diff --git a/Cargo.lock b/Cargo.lock index 0a2dd21184..84ffdbad36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6502,6 +6502,15 @@ dependencies = [ "windows-registry 0.5.3", ] +[[package]] +name = "i18n" +version = "0.1.0" +dependencies = [ + "log", + "serde_yaml 0.8.26", + "sys-locale", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -9340,6 +9349,7 @@ dependencies = [ "ai", "anyhow", "cfg-if", + "i18n", "instant", "log", "pathfinder_color", @@ -15076,6 +15086,7 @@ dependencies = [ "http_client", "http_server", "hyper", + "i18n", "image", "indexmap 2.12.0", "infer", diff --git a/Cargo.toml b/Cargo.toml index 6b6d1111dc..7c054a3337 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,7 @@ warpui_core = { path = "crates/warpui_core" } warpui_extras = { path = "crates/warpui_extras", default-features = false } watcher = { path = "crates/watcher" } websocket = { path = "crates/websocket" } +i18n = { path = "crates/i18n" } # Workspace-level dependencies used by multiple crates. Prefer adding dependencies # here to copying-and-pasting versions. @@ -281,6 +282,7 @@ smol_str = "0.2.2" strsim = "0.11" strum = "0.27" strum_macros = "0.27" +sys-locale = "0.3" tempfile = "3.8.0" thiserror = "2.0.17" tokio = "1.47.1" diff --git a/app/Cargo.toml b/app/Cargo.toml index 348f94856d..bbe58d8a13 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -120,6 +120,7 @@ handlebars.workspace = true hex.workspace = true http.workspace = true http_client.workspace = true +i18n = { workspace = true } image.workspace = true infer = "0.19.0" jaq-json.workspace = true diff --git a/app/src/ai/agent_management/view.rs b/app/src/ai/agent_management/view.rs index cc5ac42f91..e2f1aeffdf 100644 --- a/app/src/ai/agent_management/view.rs +++ b/app/src/ai/agent_management/view.rs @@ -113,7 +113,11 @@ const BUTTON_SIZE: f32 = 20.; const CARD_AGENT_ICON_SIZE: f32 = 24.; const CREATOR_AVATAR_FONT_SIZE: f32 = 10.; -const SESSION_EXPIRED_TEXT: &str = "Sessions expire after one week and cannot be opened."; +// ── i18n helpers ────────────────────────────────────────────────────── +// Converted from `const` to `fn` so translation can happen at runtime. +fn session_expired_text() -> &'static str { + crate::menu_label("agent.session_expired.tooltip", "Sessions expire after one week and cannot be opened.") +} pub fn init(app: &mut AppContext) { use crate::util::bindings::cmd_or_ctrl_shift; @@ -222,9 +226,9 @@ impl AgentManagementView { let list_state = Self::construct_fresh_list_state(ctx.handle()); let all_filter_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("All", NakedTheme) + ActionButton::new(crate::menu_label("agent.filter.all", "All"), NakedTheme) .with_size(ButtonSize::Small) - .with_tooltip("View your agent tasks plus all shared team tasks") + .with_tooltip(crate::menu_label("agent.filter.all.tooltip", "View your agent tasks plus all shared team tasks")) .on_click(|ctx| { ctx.dispatch_typed_action(AgentManagementViewAction::SetOwnerFilter( OwnerFilter::All, @@ -233,9 +237,9 @@ impl AgentManagementView { }); let personal_filter_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Personal", NakedTheme) + ActionButton::new(crate::menu_label("agent.filter.personal", "Personal"), NakedTheme) .with_size(ButtonSize::Small) - .with_tooltip("View agent tasks you created") + .with_tooltip(crate::menu_label("agent.filter.personal.tooltip", "View agent tasks you created")) .on_click(|ctx| { ctx.dispatch_typed_action(AgentManagementViewAction::SetOwnerFilter( OwnerFilter::PersonalOnly, @@ -244,7 +248,7 @@ impl AgentManagementView { }); let setup_guide_button = CompactibleActionButton::new( - "Get started".to_string(), + crate::menu_label("agent.get_started", "Get started").to_string(), None, ButtonSize::Small, AgentManagementViewAction::ToggleSetupGuide, @@ -254,7 +258,7 @@ impl AgentManagementView { ); let view_agents_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("View Agents", NakedTheme) + ActionButton::new(crate::menu_label("agent.view_agents", "View Agents"), NakedTheme) .with_size(ButtonSize::Small) .with_icon(Icon::ArrowLeft) .on_click(|ctx| { @@ -272,7 +276,7 @@ impl AgentManagementView { let creator_dropdown = ctx.add_typed_action_view(Self::create_creator_dropdown); let no_filter_results_button = ctx.add_typed_action_view(move |_ctx| { - ActionButton::new("Clear filters", SecondaryTheme) + ActionButton::new(crate::menu_label("agent.clear_filters", "Clear filters"), SecondaryTheme) .with_size(ButtonSize::Small) .on_click(move |ctx| { ctx.dispatch_typed_action(AgentManagementViewAction::ClearFilters) @@ -280,7 +284,7 @@ impl AgentManagementView { }); let clear_all_filters_button = ctx.add_typed_action_view(move |_ctx| { - ActionButton::new("Clear all", NakedTheme) + ActionButton::new(crate::menu_label("agent.clear_all", "Clear all"), NakedTheme) .with_icon(Icon::X) .with_size(ButtonSize::Small) .on_click(move |ctx| { @@ -311,7 +315,7 @@ impl AgentManagementView { }, ctx, ); - editor.set_placeholder_text("Search", ctx); + editor.set_placeholder_text(crate::menu_label("agent.search_placeholder", "Search"), ctx); editor }); ctx.subscribe_to_view(&search_editor, |me, _handle, event, ctx| { @@ -319,7 +323,7 @@ impl AgentManagementView { }); let new_agent_button = CompactibleActionButton::new( - "New agent".to_string(), + crate::menu_label("agent.new_agent", "New agent").to_string(), None, ButtonSize::Small, AgentManagementViewAction::ShowAgentTypeSelector, @@ -487,7 +491,7 @@ impl AgentManagementView { }; let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Status", ctx); + Self::setup_filter_menu(&mut dropdown, crate::menu_label("agent.filter.status", "Status"), ctx); // Use this helper to make dropdown items with status icons let make_status_option = @@ -502,22 +506,22 @@ impl AgentManagementView { let items = vec![ make_status_option( - "All", + crate::menu_label("agent.filter.all", "All"), AgentManagementViewAction::SetStatusFilter(StatusFilter::All), None, ), make_status_option( - "Working", + crate::menu_label("agent.filter.working", "Working"), AgentManagementViewAction::SetStatusFilter(StatusFilter::Working), Some((Icon::ClockLoader, Fill::from(magenta))), ), make_status_option( - "Done", + crate::menu_label("agent.filter.done", "Done"), AgentManagementViewAction::SetStatusFilter(StatusFilter::Done), Some((Icon::Check, Fill::from(green))), ), make_status_option( - "Failed", + crate::menu_label("agent.filter.failed", "Failed"), AgentManagementViewAction::SetStatusFilter(StatusFilter::Failed), Some((Icon::X, Fill::from(red))), ), @@ -532,7 +536,7 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> Dropdown { let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Source", ctx); + Self::setup_filter_menu(&mut dropdown, crate::menu_label("agent.filter.source", "Source"), ctx); // Set a max height so we can fit all of the source options without scrolling dropdown.set_menu_max_height(200., ctx); @@ -561,7 +565,7 @@ impl AgentManagementView { } let mut items = vec![MenuItem::Item( - MenuItemFields::new("All").with_on_select_action( + MenuItemFields::new(crate::menu_label("agent.filter.all", "All")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetSourceFilter(SourceFilter::All), ), @@ -596,25 +600,25 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> Dropdown { let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Created on", ctx); + Self::setup_filter_menu(&mut dropdown, crate::menu_label("agent.filter.created", "Created on"), ctx); let items = vec![ - MenuItem::Item(MenuItemFields::new("All").with_on_select_action( + MenuItem::Item(MenuItemFields::new(crate::menu_label("agent.filter.all", "All")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::All), ), )), - MenuItem::Item(MenuItemFields::new("Last 24 hours").with_on_select_action( + MenuItem::Item(MenuItemFields::new(crate::menu_label("agent.filter.last_24_hours", "Last 24 hours")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Last24Hours), ), )), - MenuItem::Item(MenuItemFields::new("Past 3 days").with_on_select_action( + MenuItem::Item(MenuItemFields::new(crate::menu_label("agent.filter.past_3_days", "Past 3 days")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Past3Days), ), )), - MenuItem::Item(MenuItemFields::new("Last week").with_on_select_action( + MenuItem::Item(MenuItemFields::new(crate::menu_label("agent.filter.last_week", "Last week")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::LastWeek), ), @@ -630,30 +634,30 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> Dropdown { let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Has artifact", ctx); + Self::setup_filter_menu(&mut dropdown, crate::menu_label("agent.filter.has_artifact", "Has artifact"), ctx); let items = vec![ - MenuItem::Item(MenuItemFields::new("All").with_on_select_action( + MenuItem::Item(MenuItemFields::new(crate::menu_label("agent.filter.all", "All")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::All), ), )), - MenuItem::Item(MenuItemFields::new("Pull Request").with_on_select_action( + MenuItem::Item(MenuItemFields::new(crate::menu_label("agent.filter.pull_request", "Pull Request")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::PullRequest), ), )), - MenuItem::Item(MenuItemFields::new("Plan").with_on_select_action( + MenuItem::Item(MenuItemFields::new(crate::menu_label("agent.filter.plan", "Plan")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Plan), ), )), - MenuItem::Item(MenuItemFields::new("Screenshot").with_on_select_action( + MenuItem::Item(MenuItemFields::new(crate::menu_label("agent.filter.screenshot", "Screenshot")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Screenshot), ), )), - MenuItem::Item(MenuItemFields::new("File").with_on_select_action( + MenuItem::Item(MenuItemFields::new(crate::menu_label("agent.filter.file", "File")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::File), ), @@ -669,7 +673,7 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> Dropdown { let mut dropdown = Dropdown::new(ctx); - Self::setup_filter_menu(&mut dropdown, "Harness", ctx); + Self::setup_filter_menu(&mut dropdown, crate::menu_label("agent.filter.harness", "Harness"), ctx); let items = Self::build_harness_dropdown_items(ctx); dropdown.set_rich_items(items, ctx); @@ -679,7 +683,7 @@ impl AgentManagementView { fn build_harness_dropdown_items(app: &AppContext) -> Vec> { let mut items = vec![MenuItem::Item( - MenuItemFields::new("All").with_on_select_action( + MenuItemFields::new(crate::menu_label("agent.filter.all", "All")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetHarnessFilter(HarnessFilter::All), ), @@ -707,7 +711,7 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> FilterableDropdown { let mut dropdown = FilterableDropdown::new(ctx); - Self::setup_searchable_filter_menu(&mut dropdown, "Environment", ctx); + Self::setup_searchable_filter_menu(&mut dropdown, crate::menu_label("agent.filter.environment", "Environment"), ctx); // Keep the button compact when a specific environment ID is selected by abbreviating the // displayed ID. (The dropdown menu still shows the full ID.) @@ -734,7 +738,7 @@ impl AgentManagementView { ctx: &mut ViewContext>, ) -> FilterableDropdown { let mut dropdown = FilterableDropdown::new(ctx); - Self::setup_searchable_filter_menu(&mut dropdown, "Created by", ctx); + Self::setup_searchable_filter_menu(&mut dropdown, crate::menu_label("agent.filter.created_by", "Created by"), ctx); dropdown } @@ -777,14 +781,14 @@ impl AgentManagementView { let envs = model.get_all_environment_ids_and_names(ctx); let selected_name = match &self.filters.environment { - EnvironmentFilter::All => Some("All".to_string()), - EnvironmentFilter::NoEnvironment => Some("None".to_string()), + EnvironmentFilter::All => Some(crate::menu_label("agent.filter.all", "All").to_string()), + EnvironmentFilter::NoEnvironment => Some(crate::menu_label("agent.filter.none", "None").to_string()), EnvironmentFilter::Specific(id) => envs.get(id).cloned(), }; self.environment_dropdown.update(ctx, |dropdown, ctx| { let mut items = vec![MenuItem::Item( - MenuItemFields::new("All").with_on_select_action( + MenuItemFields::new(crate::menu_label("agent.filter.all", "All")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetEnvironmentFilter(EnvironmentFilter::All), ), @@ -792,7 +796,7 @@ impl AgentManagementView { )]; items.push(MenuItem::Item( - MenuItemFields::new("None").with_on_select_action( + MenuItemFields::new(crate::menu_label("agent.filter.none", "None")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetEnvironmentFilter( EnvironmentFilter::NoEnvironment, @@ -826,12 +830,12 @@ impl AgentManagementView { fn update_creator_dropdown(&mut self, ctx: &mut ViewContext) { let creators = AgentConversationsModel::as_ref(ctx).get_all_creators(ctx); let creator_filter_name = match &self.filters.creator { - CreatorFilter::All => "All", + CreatorFilter::All => crate::menu_label("agent.filter.all", "All"), CreatorFilter::Specific { name, .. } => name, }; self.creator_dropdown.update(ctx, |dropdown, ctx| { let mut items = vec![MenuItem::Item( - MenuItemFields::new("All").with_on_select_action( + MenuItemFields::new(crate::menu_label("agent.filter.all", "All")).with_on_select_action( DropdownAction::select_action_and_close( AgentManagementViewAction::SetCreatorFilter(CreatorFilter::All), ), @@ -1216,7 +1220,7 @@ impl AgentManagementView { let window_id = ctx.window_id(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { - let toast = DismissibleToast::default("Copied branch name".to_string()); + let toast = DismissibleToast::default(crate::menu_label("agent.copied_branch_name", "Copied branch name").to_string()); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } @@ -1510,8 +1514,8 @@ impl AgentManagementView { // Early return if session is available - no status label rendered let (label_text, tooltip_text_opt) = match session_status { - SessionStatus::Expired => ("Session expired", Some(SESSION_EXPIRED_TEXT)), - SessionStatus::Unavailable => ("No session available", None), + SessionStatus::Expired => (crate::menu_label("agent.session_expired", "Session expired"), Some(session_expired_text())), + SessionStatus::Unavailable => (crate::menu_label("agent.no_session", "No session available"), None), SessionStatus::Available => return Empty::new().finish(), }; @@ -1744,7 +1748,7 @@ impl AgentManagementView { .creator .name .clone() - .unwrap_or_else(|| "Unknown".to_string()); + .unwrap_or_else(|| crate::menu_label("agent.creator_unknown", "Unknown").to_string()); let avatar = Self::render_avatar_with_tooltip( &creator_name, appearance, @@ -1816,9 +1820,9 @@ impl AgentManagementView { .principal_type .is_some_and(|pt| pt.is_service_account()) { - "Agent" + crate::menu_label("agent.label", "Agent") } else { - "Executor" + crate::menu_label("agent.executor", "Executor") }; metadata_parts.push(format!("{label}: {name}")); } @@ -1900,7 +1904,7 @@ impl AgentManagementView { let build_header = |use_expanded: bool| { let title = Text::new_inline( - "Runs", + crate::menu_label("agent.runs", "Runs"), appearance.ui_font_family(), appearance.ui_font_size() + 4., ) @@ -2010,7 +2014,7 @@ impl AgentManagementView { let mut stack = Stack::new().with_child(loading_icon); if mouse_state.is_hovered() { let tooltip = ui_builder - .tool_tip(String::from("Loading cloud agent runs")) + .tool_tip(String::from(crate::menu_label("agent.loading_cloud_runs", "Loading cloud agent runs"))) .build() .finish(); stack.add_positioned_overlay_child( @@ -2033,7 +2037,7 @@ impl AgentManagementView { let theme = appearance.theme(); let title = Text::new_inline( - "Runs", + crate::menu_label("agent.runs", "Runs"), appearance.ui_font_family(), appearance.ui_font_size() + 4., ) @@ -2055,7 +2059,7 @@ impl AgentManagementView { .with_child(Container::new(loading_icon).with_margin_right(10.).finish()) .with_child( Text::new_inline( - "Loading agents...", + crate::menu_label("agent.loading_agents", "Loading agents..."), appearance.ui_font_family(), appearance.ui_font_size() + 2., ) @@ -2111,7 +2115,7 @@ impl AgentManagementView { .finish(); let text = Text::new_inline( - "No results matched your filters", + crate::menu_label("agent.no_results", "No results matched your filters"), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/ai/agent_sdk/driver.rs b/app/src/ai/agent_sdk/driver.rs index 723f94d1e3..4dfa5917d9 100644 --- a/app/src/ai/agent_sdk/driver.rs +++ b/app/src/ai/agent_sdk/driver.rs @@ -2986,7 +2986,7 @@ impl AgentDriver { let mut written_conversation_id = false; ctx.subscribe_to_model(&history_model_handle, move |me, _, event, ctx| { - if event.terminal_surface_id().is_some_and(|id| id != terminal_id) { +if event.terminal_surface_id().is_some_and(|id| id != terminal_id) { return; } diff --git a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs index f5085c32fa..c2f287381f 100644 --- a/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs +++ b/app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs @@ -865,7 +865,7 @@ impl AgentInputFooter { cli_transcription_handle: None, v2_model_selector, prompt_cache_expiry_timer_handle: None, - prompt_cache_expired: false, +prompt_cache_expired: false, }; me.sync_fast_forward_button(ctx); me.sync_remote_control_button(ctx); @@ -1988,6 +1988,11 @@ impl AgentInputFooter { self.prompt_cache_expired = is_cache_expired; self.context_window_button.update(ctx, |button, ctx| { button.set_icon(Some(icon), ctx); + if is_cache_expired { + button.set_theme(WarningAgentInputButtonTheme, ctx); + } else { + button.set_theme(AgentInputButtonTheme, ctx); + } button.set_tooltip(Some(tooltip), ctx); }); @@ -2735,6 +2740,43 @@ impl ActionButtonTheme for InstallPluginButtonTheme { } } +/// Yellow-tinted variant of [`AgentInputButtonTheme`] used to flag a warning +/// state on an input chip (e.g. expired prompt cache); slightly darker on hover. +struct WarningAgentInputButtonTheme; + +impl ActionButtonTheme for WarningAgentInputButtonTheme { + fn background(&self, hovered: bool, appearance: &Appearance) -> Option { + let yellow = appearance.theme().ansi_fg_yellow(); + let base = appearance.theme().surface_1(); + Some(if hovered { + base.blend(&Fill::Solid(yellow).with_opacity(45)) + } else { + base.blend(&Fill::Solid(yellow).with_opacity(30)) + }) + } + + fn text_color( + &self, + hovered: bool, + background: Option, + appearance: &Appearance, + ) -> ColorU { + AgentInputButtonTheme.text_color(hovered, background, appearance) + } + + fn border(&self, appearance: &Appearance) -> Option { + AgentInputButtonTheme.border(appearance) + } + + fn should_opt_out_of_contrast_adjustment(&self) -> bool { + AgentInputButtonTheme.should_opt_out_of_contrast_adjustment() + } + + fn font_properties(&self) -> Option { + AgentInputButtonTheme.font_properties() + } +} + /// Writes the detailed plugin installation log to a temp file. /// Returns the log file path on success, or `None` if writing failed. #[cfg(not(target_family = "wasm"))] diff --git a/app/src/ai/blocklist/controller/response_stream.rs b/app/src/ai/blocklist/controller/response_stream.rs index da0938cded..f08ca5cb6a 100644 --- a/app/src/ai/blocklist/controller/response_stream.rs +++ b/app/src/ai/blocklist/controller/response_stream.rs @@ -143,6 +143,7 @@ impl ResponseStream { event, )))); } + #[cfg(test)] pub fn new_for_test(id: ResponseStreamId) -> Self { let (cancellation_tx, _rx) = oneshot::channel(); diff --git a/app/src/ai/custom_model_router_editor.rs b/app/src/ai/custom_model_router_editor.rs index 43f99600a3..f7dabc2275 100644 --- a/app/src/ai/custom_model_router_editor.rs +++ b/app/src/ai/custom_model_router_editor.rs @@ -43,7 +43,13 @@ use crate::view_components::action_button::{ use crate::view_components::dropdown::DropdownAction; use crate::view_components::FilterableDropdown; -pub const HEADER_TEXT: &str = "Router Editor"; +/// Header title for the router editor pane. +pub fn header_text() -> &'static str { + crate::menu_label( + "settings.custom_router.editor.header", + "Router Editor", + ) +} const EDITOR_CONTENT_WIDTH: f32 = 340.; const MODEL_MENU_WIDTH: f32 = 340.; @@ -146,7 +152,13 @@ impl CustomRouterEditorView { let title = existing .as_ref() .map(|r| r.info.display_name.clone()) - .unwrap_or_else(|| "New Router".to_string()); + .unwrap_or_else(|| { + crate::menu_label( + "settings.custom_router.editor.new_router_title", + "New Router", + ) + .to_string() + }); let pane_configuration = ctx.add_model(|_ctx| PaneConfiguration::new(&title)); let router_type = match existing.as_ref().map(|r| &r.routing) { @@ -178,13 +190,21 @@ impl CustomRouterEditorView { // Personalize the placeholder with the user's first name (derived from // their display name), falling back to a generic placeholder when no // name is available. + let generic_placeholder = crate::menu_label( + "settings.custom_router.editor.name_placeholder", + "My custom router", + ); + let personalized_template = crate::menu_label( + "settings.custom_router.editor.name_placeholder_personalized", + "{first_name}'s custom router", + ); let name_placeholder = AuthStateProvider::as_ref(ctx) .get() .display_name() .as_deref() .and_then(|name| name.split_whitespace().next()) - .map(|first_name| format!("{first_name}'s custom router")) - .unwrap_or_else(|| "My custom router".to_string()); + .map(|first_name| personalized_template.replace("{first_name}", first_name)) + .unwrap_or_else(|| generic_placeholder.to_string()); let name_editor = ctx.add_view(move |ctx| { let font_size = Appearance::as_ref(ctx).ui_font_size(); let mut editor = EditorView::single_line( @@ -223,8 +243,16 @@ impl CustomRouterEditorView { icon_color: theme.main_text_color(theme.background()).into(), label: Some(LabelConfig { label: match router_type { - RouterEditorType::Complexity => "Complexity".into(), - RouterEditorType::Prompt => "Rules".into(), + RouterEditorType::Complexity => crate::menu_label( + "settings.custom_router.editor.type_complexity", + "Complexity", + ) + .into(), + RouterEditorType::Prompt => crate::menu_label( + "settings.custom_router.editor.type_rules", + "Rules", + ) + .into(), }, width_override: Some(70.0), color: if is_selected { @@ -321,19 +349,25 @@ impl CustomRouterEditorView { } let save_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Save", PrimaryTheme) + ActionButton::new(crate::menu_label("common.save", "Save"), PrimaryTheme) .with_size(ButtonSize::Small) .on_click(|ctx| ctx.dispatch_typed_action(CustomRouterEditorAction::Save)) }); let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Cancel", SecondaryTheme) + ActionButton::new(crate::menu_label("common.cancel", "Cancel"), SecondaryTheme) .with_size(ButtonSize::Small) .on_click(|ctx| ctx.dispatch_typed_action(CustomRouterEditorAction::Close)) }); let add_rule_button = ctx.add_typed_action_view(|_| { - ActionButton::new("+ Add rule", SecondaryTheme) - .with_size(ButtonSize::Small) - .on_click(|ctx| ctx.dispatch_typed_action(CustomRouterEditorAction::AddPromptRule)) + ActionButton::new( + crate::menu_label( + "settings.custom_router.editor.add_rule", + "+ Add rule", + ), + SecondaryTheme, + ) + .with_size(ButtonSize::Small) + .on_click(|ctx| ctx.dispatch_typed_action(CustomRouterEditorAction::AddPromptRule)) }); let view = Self { @@ -445,7 +479,13 @@ impl CustomRouterEditorView { fn try_save(&mut self, ctx: &mut ViewContext) { let name = self.router_name(ctx); if name.is_empty() { - self.save_error = Some("Router name is required.".to_string()); + self.save_error = Some( + crate::menu_label( + "settings.custom_router.editor.name_required", + "Router name is required.", + ) + .to_string(), + ); ctx.notify(); return; } @@ -453,16 +493,41 @@ impl CustomRouterEditorView { let routing = match self.router_type { RouterEditorType::Complexity => { for (field, val) in [ - ("Default", self.complexity_default.as_str()), - ("Easy", self.complexity_easy.as_deref().unwrap_or_default()), ( - "Medium", + crate::menu_label( + "settings.custom_router.editor.complexity_field_default", + "Default", + ), + self.complexity_default.as_str(), + ), + ( + crate::menu_label( + "settings.custom_router.editor.complexity_field_easy", + "Easy", + ), + self.complexity_easy.as_deref().unwrap_or_default(), + ), + ( + crate::menu_label( + "settings.custom_router.editor.complexity_field_medium", + "Medium", + ), self.complexity_medium.as_deref().unwrap_or_default(), ), - ("Hard", self.complexity_hard.as_deref().unwrap_or_default()), + ( + crate::menu_label( + "settings.custom_router.editor.complexity_field_hard", + "Hard", + ), + self.complexity_hard.as_deref().unwrap_or_default(), + ), ] { if val.is_empty() { - self.save_error = Some(format!("{field} model is required.")); + let template = crate::menu_label( + "settings.custom_router.editor.model_required", + "{field} model is required.", + ); + self.save_error = Some(template.replace("{field}", field)); ctx.notify(); return; } @@ -476,7 +541,13 @@ impl CustomRouterEditorView { } RouterEditorType::Prompt => { if self.prompt_default_model.is_empty() { - self.save_error = Some("A default model is required.".to_string()); + self.save_error = Some( + crate::menu_label( + "settings.custom_router.editor.prompt_default_required", + "A default model is required.", + ) + .to_string(), + ); ctx.notify(); return; } @@ -501,7 +572,11 @@ impl CustomRouterEditorView { .collect(); if rules.is_empty() { self.save_error = Some( - "At least one rule with a description and model is required.".to_string(), + crate::menu_label( + "settings.custom_router.editor.at_least_one_rule_required", + "At least one rule with a description and model is required.", + ) + .to_string(), ); ctx.notify(); return; @@ -519,7 +594,11 @@ impl CustomRouterEditorView { .and_then(|r| r.source_path.as_deref()); let router = CustomModelRouter::new_local(name.clone(), routing, existing_path); if let Err(e) = router.validate() { - self.save_error = Some(format!("Validation: {e}")); + let template = crate::menu_label( + "settings.custom_router.editor.validation_error", + "Validation: {err}", + ); + self.save_error = Some(template.replace("{err}", &e.to_string())); ctx.notify(); return; } @@ -529,14 +608,22 @@ impl CustomRouterEditorView { let yaml = match router.to_yaml_string() { Ok(y) => y, Err(e) => { - self.save_error = Some(format!("Serialization: {e}")); + let template = crate::menu_label( + "settings.custom_router.editor.serialization_error", + "Serialization: {err}", + ); + self.save_error = Some(template.replace("{err}", &e.to_string())); ctx.notify(); return; } }; let ep = self.existing.as_ref().and_then(|r| r.source_path.clone()); if let Err(e) = WarpConfig::save_custom_model_router(&name, &yaml, ep.as_deref()) { - self.save_error = Some(format!("Write error: {e}")); + let template = crate::menu_label( + "settings.custom_router.editor.write_error", + "Write error: {err}", + ); + self.save_error = Some(template.replace("{err}", &e.to_string())); ctx.notify(); return; } @@ -626,15 +713,24 @@ impl CustomRouterEditorView { fn render_complexity_section(&self, appearance: &Appearance) -> Box { Flex::column() - .with_child(Self::section_label("Models", appearance)) + .with_child(Self::section_label( + crate::menu_label("settings.custom_router.editor.models_section", "Models"), + appearance, + )) .with_child(labeled_dropdown( - "Default (required)", + crate::menu_label( + "settings.custom_router.editor.default_dropdown", + "Default (required)", + ), &self.complexity_default_dropdown, appearance, )) .with_child( Container::new(labeled_dropdown( - "Easy (required)", + crate::menu_label( + "settings.custom_router.editor.easy_dropdown", + "Easy (required)", + ), &self.complexity_easy_dropdown, appearance, )) @@ -643,7 +739,10 @@ impl CustomRouterEditorView { ) .with_child( Container::new(labeled_dropdown( - "Medium (required)", + crate::menu_label( + "settings.custom_router.editor.medium_dropdown", + "Medium (required)", + ), &self.complexity_medium_dropdown, appearance, )) @@ -652,7 +751,10 @@ impl CustomRouterEditorView { ) .with_child( Container::new(labeled_dropdown( - "Hard (required)", + crate::menu_label( + "settings.custom_router.editor.hard_dropdown", + "Hard (required)", + ), &self.complexity_hard_dropdown, appearance, )) @@ -672,7 +774,13 @@ impl CustomRouterEditorView { .sub_text_color(appearance.theme().surface_1()); let mut column = Flex::column() - .with_child(Self::section_label("Default model", appearance)) + .with_child(Self::section_label( + crate::menu_label( + "settings.custom_router.editor.default_model_section", + "Default model", + ), + appearance, + )) .with_child( ConstrainedBox::new(ChildView::new(&self.prompt_default_dropdown).finish()) .with_width(EDITOR_CONTENT_WIDTH) @@ -681,16 +789,28 @@ impl CustomRouterEditorView { if !self.prompt_rules.is_empty() { column.add_child( - Container::new(Self::section_label("Rules".to_string(), appearance)) - .with_margin_top(12.) - .finish(), + Container::new(Self::section_label( + crate::menu_label( + "settings.custom_router.editor.rules_section", + "Rules", + ), + appearance, + )) + .with_margin_top(12.) + .finish(), ); let rules_copy = FormattedText::new([ FormattedTextLine::Line(vec![FormattedTextFragment::plain_text( - "Rules are custom prompts that describe when to use a specific model. Warp intelligently matches your tasks against these rules.", + crate::menu_label( + "settings.custom_router.editor.rules_help_1", + "Rules are custom prompts that describe when to use a specific model. Warp intelligently matches your tasks against these rules.", + ), )]), FormattedTextLine::Line(vec![FormattedTextFragment::plain_text( - "Rules are matched top to bottom — rules higher in the list take precedence over those below.", + crate::menu_label( + "settings.custom_router.editor.rules_help_2", + "Rules are matched top to bottom — rules higher in the list take precedence over those below.", + ), )]), ]); column.add_child( @@ -732,7 +852,13 @@ impl CustomRouterEditorView { col.add_child( Container::new( Flex::column() - .with_child(Self::section_label("Router name", appearance)) + .with_child(Self::section_label( + crate::menu_label( + "settings.custom_router.editor.router_name_section", + "Router name", + ), + appearance, + )) .with_child( ConstrainedBox::new(editor_row(&self.name_editor, None, appearance)) .with_width(EDITOR_CONTENT_WIDTH) @@ -748,22 +874,36 @@ impl CustomRouterEditorView { // above the segmented control. let routing_type_copy = FormattedText::new([ FormattedTextLine::Line(vec![ - FormattedTextFragment::bold("Complexity-based"), - FormattedTextFragment::plain_text( + FormattedTextFragment::bold(crate::menu_label( + "settings.custom_router.editor.complexity_based_label", + "Complexity-based", + )), + FormattedTextFragment::plain_text(crate::menu_label( + "settings.custom_router.editor.complexity_routing_explanation", " routing chooses a model based on Warp's classification of the task's difficulty.", - ), + )), ]), FormattedTextLine::Line(vec![ - FormattedTextFragment::bold("Rule-based"), - FormattedTextFragment::plain_text( + FormattedTextFragment::bold(crate::menu_label( + "settings.custom_router.editor.rule_based_label", + "Rule-based", + )), + FormattedTextFragment::plain_text(crate::menu_label( + "settings.custom_router.editor.rule_routing_explanation", " routing chooses a model based on custom prompts.", - ), + )), ]), ]); col.add_child( Container::new( Flex::column() - .with_child(Self::section_label("Router type", appearance)) + .with_child(Self::section_label( + crate::menu_label( + "settings.custom_router.editor.router_type_section", + "Router type", + ), + appearance, + )) .with_child( Container::new( FormattedTextElement::new( @@ -947,7 +1087,7 @@ impl BackingView for CustomRouterEditorView { _app: &AppContext, ) -> view::HeaderContent { view::HeaderContent::Standard(view::StandardHeader { - title: HEADER_TEXT.into(), + title: header_text().into(), title_secondary: None, title_style: None, title_clip_config: warpui::text_layout::ClipConfig::start(), @@ -1089,7 +1229,13 @@ fn make_prompt_rule_row( }, ctx, ); - editor.set_placeholder_text("Describe when to use this model\u{2026}", ctx); + editor.set_placeholder_text( + crate::menu_label( + "settings.custom_router.editor.rule_placeholder", + "Describe when to use this model\u{2026}", + ), + ctx, + ); // Use the UI font (rather than the editor's default mono font) so the // input matches the rest of the editor's text inputs. let font_family = Appearance::as_ref(ctx).ui_font_family(); @@ -1321,12 +1467,12 @@ fn render_rule_row( const MODEL_WIDTH: f32 = 170.; let description_field = labeled_field( - "Rule", + crate::menu_label("settings.custom_router.editor.rule_label", "Rule"), editor_row(&row.description_editor, Some(RULE_FIELD_HEIGHT), appearance), appearance, ); let model_field = labeled_field( - "Model", + crate::menu_label("settings.custom_router.editor.model_label", "Model"), ConstrainedBox::new(ChildView::new(&row.model_dropdown).finish()) .with_width(MODEL_WIDTH) .finish(), diff --git a/app/src/ai/custom_model_routers.rs b/app/src/ai/custom_model_routers.rs index 9a8f217c45..89164a5ce5 100644 --- a/app/src/ai/custom_model_routers.rs +++ b/app/src/ai/custom_model_routers.rs @@ -211,31 +211,78 @@ impl CustomModelRouter { match &self.routing { CustomModelRouting::Complexity(c) => { if c.default.trim().is_empty() { - return Err( - "complexity routing requires a non-empty `default` model".to_owned() - ); + return Err(crate::menu_label( + "settings.custom_router.routes.complexity_requires_default", + "complexity routing requires a non-empty `default` model", + ) + .to_owned()); } - validate_target(&c.default).map_err(|e| format!("`default`: {e}"))?; + validate_target(&c.default).map_err(|e| { + i18n::interpolate( + crate::menu_label( + "settings.custom_router.routes.default_error", + "`default`: {e}", + ), + &[("e", e)], + ) + .into_owned() + })?; for (bucket, target) in [("easy", &c.easy), ("medium", &c.medium), ("hard", &c.hard)] { if let Some(model) = target { - validate_target(model) - .map_err(|e| format!("complexity bucket `{bucket}`: {e}"))?; + validate_target(model).map_err(|e| { + i18n::interpolate( + crate::menu_label( + "settings.custom_router.routes.complexity_bucket_error", + "complexity bucket `{bucket}`: {e}", + ), + &[("bucket", bucket.to_string()), ("e", e)], + ) + .into_owned() + })?; } } } CustomModelRouting::Prompt(p) => { if p.default_model.trim().is_empty() { - return Err("prompt routing requires a non-empty `default` model".to_owned()); + return Err(crate::menu_label( + "settings.custom_router.routes.prompt_requires_default", + "prompt routing requires a non-empty `default` model", + ) + .to_owned()); } - validate_target(&p.default_model).map_err(|e| format!("`default`: {e}"))?; + validate_target(&p.default_model).map_err(|e| { + i18n::interpolate( + crate::menu_label( + "settings.custom_router.routes.default_error", + "`default`: {e}", + ), + &[("e", e)], + ) + .into_owned() + })?; for (index, rule) in p.rules.iter().enumerate() { if rule.description.trim().is_empty() { - return Err(format!("prompt rule {index}: `description` is empty")); + return Err(i18n::interpolate( + crate::menu_label( + "settings.custom_router.routes.prompt_rule_description_empty", + "prompt rule {index}: `description` is empty", + ), + &[("index", index.to_string())], + ) + .into_owned()); } - validate_target(&rule.model) - .map_err(|e| format!("prompt rule {index}: {e}"))?; + validate_target(&rule.model).map_err(|e| { + i18n::interpolate( + crate::menu_label( + "settings.custom_router.routes.prompt_rule_error", + "prompt rule {index}: {e}", + ), + &[("index", index.to_string()), ("e", e)], + ) + .into_owned() + })?; } } } @@ -260,12 +307,21 @@ fn local_id_from_path(source_path: Option<&Path>, fallback: &str) -> String { fn validate_target(model_id: &str) -> Result<(), String> { let trimmed = model_id.trim(); if trimmed.is_empty() { - return Err("target model id is empty".to_owned()); + return Err(crate::menu_label( + "settings.custom_router.routes.target_empty", + "target model id is empty", + ) + .to_owned()); } if is_auto_target(trimmed) { - return Err(format!( - "target `{trimmed}` is an auto model; custom model routers must route to concrete models" - )); + return Err(i18n::interpolate( + crate::menu_label( + "settings.custom_router.routes.target_is_auto", + "target `{trimmed}` is an auto model; custom model routers must route to concrete models", + ), + &[("trimmed", trimmed.to_string())], + ) + .into_owned()); } Ok(()) } diff --git a/app/src/auth/login_slide.rs b/app/src/auth/login_slide.rs index 9ca4376666..a4d72c593a 100644 --- a/app/src/auth/login_slide.rs +++ b/app/src/auth/login_slide.rs @@ -961,7 +961,7 @@ impl LoginSlideView { WARP_DRIVE_FEATURES, "Enable Warp Drive", ), - LoginPurpose::WarpAgent | LoginPurpose::ThirdParty => ( +LoginPurpose::WarpAgent | LoginPurpose::ThirdParty => ( "Continue without signing in?", "Without an account, you won't have access to Warp's AI features. Sign in anytime to unlock agents and other AI features.", &[], diff --git a/app/src/auth/mod.rs b/app/src/auth/mod.rs index d4d48ccae7..3f6ac43907 100644 --- a/app/src/auth/mod.rs +++ b/app/src/auth/mod.rs @@ -9,6 +9,7 @@ mod login_failure_notification; pub mod login_slide; pub mod needs_sso_link_view; pub mod paste_auth_token_modal; +pub mod provider_keys_modal; mod user_properties; pub use warp_server_auth::{auth_state, credentials, user, user_uid}; #[cfg(target_family = "wasm")] @@ -59,6 +60,7 @@ pub fn init(app: &mut AppContext) { auth_override_warning_body::init(app); login_slide::init(app); paste_auth_token_modal::init(app); + provider_keys_modal::init(app); } /// If the app has running processes or dirty objects, we'll show a confirmation modal before logging out. diff --git a/app/src/auth/provider_keys_modal.rs b/app/src/auth/provider_keys_modal.rs new file mode 100644 index 0000000000..f9d4bf1431 --- /dev/null +++ b/app/src/auth/provider_keys_modal.rs @@ -0,0 +1,355 @@ +use pathfinder_color::ColorU; +use ui_components::{button, Component as _, Options as _}; +use warp_core::ui::theme::color::internal_colors; +use warpui::elements::{ + Align, Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Dismiss, Fill, + Flex, FormattedTextElement, MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, + Radius, Shrinkable, Stack, +}; +use warpui::fonts::Weight; +use warpui::keymap::FixedBinding; +use warpui::text_layout::TextAlignment; +use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; +use warpui::{ + AppContext, Element, Entity, FocusContext, SingletonEntity, TypedActionView, View, ViewContext, + ViewHandle, +}; + +use crate::appearance::Appearance; +use crate::editor::{ + EditorView, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions, +}; + +const MODAL_WIDTH: f32 = 460.; +const INPUT_BORDER_RADIUS: Radius = Radius::Pixels(4.); + +pub fn init(app: &mut AppContext) { + use warpui::keymap::macros::*; + app.register_fixed_bindings([FixedBinding::new( + "escape", + ProviderKeysModalAction::Cancel, + id!(ProviderKeysModalView::ui_name()), + )]); +} + +#[derive(Clone, Copy, Debug)] +pub enum ProviderKeysModalAction { + Save, + Cancel, +} + +#[derive(Clone, Debug)] +pub enum ProviderKeysModalEvent { + Cancelled, + Save { + openai: Option, + anthropic: Option, + google: Option, + }, +} + +pub struct ProviderKeysModalView { + openai_input: ViewHandle, + anthropic_input: ViewHandle, + google_input: ViewHandle, + cancel_button: button::Button, + add_button: button::Button, + close_mouse_state: MouseStateHandle, +} + +impl ProviderKeysModalView { + pub fn new( + openai: Option, + anthropic: Option, + google: Option, + ctx: &mut ViewContext, + ) -> Self { + let openai_input = Self::make_key_editor("sk-...", openai, ctx); + let anthropic_input = Self::make_key_editor("sk-ant-...", anthropic, ctx); + let google_input = Self::make_key_editor("AIzaSy...", google, ctx); + + Self { + openai_input, + anthropic_input, + google_input, + cancel_button: button::Button::default(), + add_button: button::Button::default(), + close_mouse_state: MouseStateHandle::default(), + } + } + + fn make_key_editor( + placeholder: &str, + initial: Option, + ctx: &mut ViewContext, + ) -> ViewHandle { + let placeholder = placeholder.to_string(); + ctx.add_typed_action_view(move |ctx| { + let appearance = Appearance::as_ref(ctx); + let text_colors = crate::settings_view::editor_text_colors(appearance); + let options = SingleLineEditorOptions { + is_password: true, + text: TextOptions { + font_family_override: Some(appearance.ui_font_family()), + text_colors_override: Some(text_colors), + ..Default::default() + }, + propagate_and_no_op_vertical_navigation_keys: + PropagateAndNoOpNavigationKeys::Always, + ..Default::default() + }; + let mut editor = EditorView::single_line(options, ctx); + editor.set_placeholder_text(&placeholder, ctx); + if let Some(initial) = initial { + editor.set_buffer_text(&initial, ctx); + } + editor + }) + } + + fn submit(&mut self, ctx: &mut ViewContext) { + let read = |handle: &ViewHandle, ctx: &ViewContext| { + let text = handle.as_ref(ctx).buffer_text(ctx); + let trimmed = text.trim(); + (!trimmed.is_empty()).then(|| trimmed.to_string()) + }; + let openai = read(&self.openai_input, ctx); + let anthropic = read(&self.anthropic_input, ctx); + let google = read(&self.google_input, ctx); + ctx.emit(ProviderKeysModalEvent::Save { + openai, + anthropic, + google, + }); + } + + fn render_field( + &self, + appearance: &Appearance, + label: &'static str, + editor: ViewHandle, + ) -> Box { + let theme = appearance.theme(); + let dialog_surface_solid = theme.surface_1().into_solid(); + let input_bg = theme.surface_2(); + let input_bg_solid = input_bg.into_solid(); + let input_text_color: ColorU = internal_colors::text_main(theme, input_bg_solid); + let border_color = internal_colors::neutral_4(theme); + + let label_el = FormattedTextElement::from_str(label, appearance.ui_font_family(), 12.) + .with_color(internal_colors::text_main(theme, dialog_surface_solid)) + .with_weight(Weight::Normal) + .with_alignment(TextAlignment::Left) + .with_line_height_ratio(1.0) + .finish(); + + let input = appearance + .ui_builder() + .text_input(editor) + .with_style(UiComponentStyles { + background: Some(input_bg.into()), + border_width: Some(1.), + border_color: Some(Fill::Solid(border_color)), + border_radius: Some(CornerRadius::with_all(INPUT_BORDER_RADIUS)), + font_color: Some(input_text_color), + padding: Some(Coords { + top: 10., + bottom: 10., + left: 16., + right: 16., + }), + ..Default::default() + }) + .build() + .finish(); + + Flex::column() + .with_cross_axis_alignment(CrossAxisAlignment::Stretch) + .with_child(label_el) + .with_child(Container::new(input).with_margin_top(8.).finish()) + .finish() + } +} + +impl Entity for ProviderKeysModalView { + type Event = ProviderKeysModalEvent; +} + +impl View for ProviderKeysModalView { + fn ui_name() -> &'static str { + "ProviderKeysModalView" + } + + fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext) { + if focus_ctx.is_self_focused() { + ctx.focus(&self.openai_input); + ctx.notify(); + } + } + + fn render(&self, app: &AppContext) -> Box { + let appearance = Appearance::as_ref(app); + let theme = appearance.theme(); + let dialog_surface = theme.surface_1(); + let dialog_surface_solid = dialog_surface.into_solid(); + let border_color = internal_colors::neutral_4(theme); + let ui_builder = appearance.ui_builder(); + + let title = FormattedTextElement::from_str(crate::menu_label("provider_keys.add_api_key", "Add API key"), appearance.ui_font_family(), 16.) + .with_color(internal_colors::text_main(theme, dialog_surface_solid)) + .with_weight(Weight::Bold) + .with_line_height_ratio(1.25) + .finish(); + + let close_button = ui_builder + .close_button(24., self.close_mouse_state.clone()) + .build() + .on_click(|ctx: &mut warpui::EventContext, _, _| { + ctx.dispatch_typed_action(ProviderKeysModalAction::Cancel); + }) + .finish(); + + let title_row = Flex::row() + .with_main_axis_size(MainAxisSize::Max) + .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) + .with_cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(Shrinkable::new(1., title).finish()) + .with_child(close_button) + .finish(); + + let subtitle = FormattedTextElement::from_str( + crate::menu_label("provider_keys.description", "Use your own API keys from model providers for Warp Agent."), + appearance.ui_font_family(), + 14., + ) + .with_color(internal_colors::text_sub(theme, dialog_surface_solid)) + .with_weight(Weight::Normal) + .with_alignment(TextAlignment::Left) + .with_line_height_ratio(1.2) + .finish(); + + let body = Flex::column() + .with_cross_axis_alignment(CrossAxisAlignment::Stretch) + .with_child(Container::new(subtitle).with_margin_bottom(16.).finish()) + .with_child(self.render_field(appearance, crate::menu_label("provider_keys.openai_api_key", "OpenAI API key"), self.openai_input.clone())) + .with_child( + Container::new(self.render_field( + appearance, + crate::menu_label("provider_keys.anthropic_api_key", "Anthropic API key"), + self.anthropic_input.clone(), + )) + .with_margin_top(16.) + .finish(), + ) + .with_child( + Container::new(self.render_field( + appearance, + crate::menu_label("provider_keys.google_api_key", "Google API key"), + self.google_input.clone(), + )) + .with_margin_top(16.) + .finish(), + ) + .finish(); + + let cancel_button = self.cancel_button.render( + appearance, + button::Params { + content: button::Content::Label(crate::menu_label("provider_keys.cancel", "Cancel").into()), + theme: &button::themes::Naked, + options: button::Options { + on_click: Some(Box::new(|ctx, _app, _pos| { + ctx.dispatch_typed_action(ProviderKeysModalAction::Cancel); + })), + ..button::Options::default(appearance) + }, + }, + ); + + let add_button = self.add_button.render( + appearance, + button::Params { + content: button::Content::Label(crate::menu_label("provider_keys.add_keys", "Add keys").into()), + theme: &button::themes::Primary, + options: button::Options { + on_click: Some(Box::new(|ctx, _app, _pos| { + ctx.dispatch_typed_action(ProviderKeysModalAction::Save); + })), + ..button::Options::default(appearance) + }, + }, + ); + + let footer = Container::new( + Flex::row() + .with_main_axis_size(MainAxisSize::Max) + .with_main_axis_alignment(MainAxisAlignment::End) + .with_cross_axis_alignment(CrossAxisAlignment::Center) + .with_child(cancel_button) + .with_child(Container::new(add_button).with_margin_left(8.).finish()) + .finish(), + ) + .with_border(Border::top(1.).with_border_color(border_color)) + .with_horizontal_padding(24.) + .with_vertical_padding(12.) + .finish(); + + let dialog = Flex::column() + .with_cross_axis_alignment(CrossAxisAlignment::Stretch) + .with_child( + Container::new(title_row) + .with_horizontal_padding(24.) + .with_padding_top(24.) + .with_padding_bottom(12.) + .finish(), + ) + .with_child( + Container::new(body) + .with_horizontal_padding(24.) + .with_padding_bottom(16.) + .finish(), + ) + .with_child(footer) + .finish(); + + let modal = ConstrainedBox::new( + Container::new(dialog) + .with_background(dialog_surface) + .with_border(Border::all(1.).with_border_color(border_color)) + .with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.))) + .finish(), + ) + .with_width(MODAL_WIDTH) + .finish(); + + let mut stack = Stack::new(); + stack.add_child( + Container::new(warpui::elements::Empty::new().finish()) + .with_background_color(ColorU::new(0, 0, 0, 179)) + .finish(), + ); + stack.add_child( + Dismiss::new(Align::new(modal).finish()) + .on_dismiss(|ctx, _app| { + ctx.dispatch_typed_action(ProviderKeysModalAction::Cancel); + }) + .finish(), + ); + stack.finish() + } +} + +impl TypedActionView for ProviderKeysModalView { + type Action = ProviderKeysModalAction; + + fn handle_action(&mut self, action: &ProviderKeysModalAction, ctx: &mut ViewContext) { + match action { + ProviderKeysModalAction::Save => { + self.submit(ctx); + } + ProviderKeysModalAction::Cancel => { + ctx.emit(ProviderKeysModalEvent::Cancelled); + } + } + } +} diff --git a/app/src/code/editor/model.rs b/app/src/code/editor/model.rs index 9bff1d8937..8c5df8d1b6 100644 --- a/app/src/code/editor/model.rs +++ b/app/src/code/editor/model.rs @@ -333,7 +333,6 @@ impl CodeEditorModel { content.update(ctx, |buffer, _| { buffer.set_session_platform(session_platform); }); - Self::from_content( content, true, // show_current_line_highlights diff --git a/app/src/code/local_code_editor.rs b/app/src/code/local_code_editor.rs index 1cfb316664..b18474c166 100644 --- a/app/src/code/local_code_editor.rs +++ b/app/src/code/local_code_editor.rs @@ -1845,7 +1845,7 @@ impl LocalCodeEditorView { Shrinkable::new( 1., Text::new_inline( - "Add as context", + crate::menu_label("code.add_as_context", "Add as context"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -1962,10 +1962,10 @@ impl LocalCodeEditorView { /// Creates menu items for the context menu fn context_menu_items(&self) -> Vec> { vec![ - MenuItemFields::new("Go to definition") + MenuItemFields::new(crate::menu_label("code.go_to_definition", "Go to definition")) .with_on_select_action(LocalCodeEditorAction::GotoDefinition) .into_item(), - MenuItemFields::new("Find references") + MenuItemFields::new(crate::menu_label("code.find_references_action", "Find references")) .with_on_select_action(LocalCodeEditorAction::FindReferences) .into_item(), ] @@ -2395,7 +2395,7 @@ pub fn render_unsaved_changes_banner( Shrinkable::new( 1., Text::new( - "This file has saved changes that are not reflected here.", + crate::menu_label("code.file_has_saved_changes", "This file has saved changes that are not reflected here."), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2413,7 +2413,7 @@ pub fn render_unsaved_changes_banner( appearance .ui_builder() .button(ButtonVariant::Text, discard_mouse_state) - .with_text_label("Discard this version".into()) + .with_text_label(crate::menu_label("code.discard_this_version", "Discard this version").into()) .with_style(UiComponentStyles { height: Some(24.), padding: Some(Coords { @@ -2435,7 +2435,7 @@ pub fn render_unsaved_changes_banner( appearance .ui_builder() .button(ButtonVariant::Outlined, overwrite_mouse_state) - .with_text_label("Overwrite".into()) + .with_text_label(crate::menu_label("code.overwrite", "Overwrite").into()) .with_style(UiComponentStyles { font_color: Some(appearance.theme().active_ui_text_color().into()), ..Default::default() @@ -2492,7 +2492,7 @@ pub fn render_remote_disconnected_banner(appearance: &Appearance) -> Box Option<& // Only nudge for a missing message; an empty Changes box is self-evident, // and gating a tooltip on it would also flash during the open-time load. if has_committable_changes(state) && commit_message(state, app).is_none() { - return Some("Enter a commit message"); + return Some(crate::menu_label( + "codereview.git_dialog.tooltip.enter_commit_message", + "Enter a commit message", + )); } None } @@ -256,7 +275,13 @@ pub(super) fn apply_generated_commit_message( editor_handle.update(ctx, |editor, ctx| { // Swap "Generating\u{2026}" for the manual-type prompt so it // shows if the user later clears the generated draft. - editor.set_placeholder_text(FALLBACK_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + crate::menu_label( + "codereview.git_dialog.placeholder.type_commit_message", + "Type a commit message", + ), + ctx, + ); // User input wins — don't clobber their text. if !user_typed { editor.system_reset_buffer_text(generated.trim(), ctx); @@ -268,7 +293,13 @@ pub(super) fn apply_generated_commit_message( Err(err) => { log::warn!("Failed to autogenerate commit message: {err}"); editor_handle.update(ctx, |editor, ctx| { - editor.set_placeholder_text(FALLBACK_PLACEHOLDER_TEXT, ctx); + editor.set_placeholder_text( + crate::menu_label( + "codereview.git_dialog.placeholder.type_commit_message", + "Type a commit message", + ), + ctx, + ); }); me.refresh_confirm_enabled(ctx); ctx.notify(); @@ -377,7 +408,10 @@ pub(super) fn start_confirm(me: &mut GitDialog, ctx: &mut ViewContext // user has it enabled (ignored for commit-only / commit-and-push). let autogenerate_pr_content = should_send_git_ops_ai_request(ctx); - me.set_loading(LOADING_LABEL, ctx); + me.set_loading( + crate::menu_label("codereview.git_dialog.loading.committing", "Committing\u{2026}"), + ctx, + ); // Lock the commit message editor while the async op is in flight. message_editor.update(ctx, |editor, ctx| { @@ -417,9 +451,15 @@ pub(super) fn finish_commit_chain( Ok(Some(pr)) => show_pr_created_toast(pr, ctx), Ok(None) => { let msg = if matches!(intent, CommitChainMode::CommitOnly) { - "Changes successfully committed." + crate::menu_label( + "codereview.git_dialog.success.changes_committed", + "Changes successfully committed.", + ) } else { - "Changes committed and pushed." + crate::menu_label( + "codereview.git_dialog.success.changes_committed_and_pushed", + "Changes committed and pushed.", + ) }; show_toast(msg, ctx); } @@ -542,7 +582,7 @@ fn render_changes_section(state: &CommitState, appearance: &Appearance) -> Box Box Box { let label = Text::new( - "Commit message", + crate::menu_label( + "codereview.git_dialog.commit_message_label", + "Commit message", + ), appearance.ui_font_family(), appearance.ui_font_size(), ) diff --git a/app/src/code_review/git_dialog/mod.rs b/app/src/code_review/git_dialog/mod.rs index c1427aec28..6a25b082bb 100644 --- a/app/src/code_review/git_dialog/mod.rs +++ b/app/src/code_review/git_dialog/mod.rs @@ -130,51 +130,87 @@ fn user_facing_git_error(raw: &str) -> &'static str { if lower.contains("no changes added to commit") { // Distinct from a clean tree: changes exist but nothing is staged // (e.g. "include unstaged" off with an empty index). - "No staged changes to commit." + crate::menu_label( + "codereview.git_dialog.error.no_staged_changes", + "No staged changes to commit.", + ) } else if lower.contains("nothing to commit") { - "No changes to commit." + crate::menu_label( + "codereview.git_dialog.error.no_changes", + "No changes to commit.", + ) } else if lower.contains("please tell me who you are") || lower.contains("author identity unknown") { - "Git identity not configured. Set user.name and user.email." + crate::menu_label( + "codereview.git_dialog.error.git_identity_not_configured", + "Git identity not configured. Set user.name and user.email.", + ) } else if lower.contains("updates were rejected") || lower.contains("non-fast-forward") || lower.contains("fetch first") { - "Remote has new changes \u{2014} pull before pushing." + crate::menu_label( + "codereview.git_dialog.error.remote_has_new_changes", + "Remote has new changes \u{2014} pull before pushing.", + ) } else if lower.contains("does not appear to be a git repository") || lower.contains("no configured push destination") || lower.contains("no such remote") { - "No remote configured for this branch." + crate::menu_label( + "codereview.git_dialog.error.no_remote_configured", + "No remote configured for this branch.", + ) } else if lower.contains("authentication failed") || lower.contains("permission denied (publickey)") { - "Authentication failed. Check your Git credentials." + crate::menu_label( + "codereview.git_dialog.error.authentication_failed", + "Authentication failed. Check your Git credentials.", + ) } else if lower.contains("could not resolve host") || lower.contains("network is unreachable") || lower.contains("connection timed out") { - "Network error. Check your connection." + crate::menu_label( + "codereview.git_dialog.error.network_error", + "Network error. Check your connection.", + ) } else if lower.contains("repository not found") { - "Remote repository not found." + crate::menu_label( + "codereview.git_dialog.error.remote_repository_not_found", + "Remote repository not found.", + ) } else if lower.contains("failed to execute gh command") { // `run_gh_command` wraps spawn failures with this prefix, which is // the reliable "gh binary missing" signal. - "GitHub CLI (gh) not installed. See https://cli.github.com/." + crate::menu_label( + "codereview.git_dialog.error.gh_not_installed", + "GitHub CLI (gh) not installed. See https://cli.github.com/.", + ) } else if lower.contains("not logged in") || lower.contains("authentication required") || lower.contains("gh auth login") { // Phrases mirror `context_chips::current_prompt::is_gh_auth_error`, // which has been vetted against real `gh` failure output. - "GitHub CLI not authenticated. Run `gh auth login`." + crate::menu_label( + "codereview.git_dialog.error.gh_not_authenticated", + "GitHub CLI not authenticated. Run `gh auth login`.", + ) } else if lower.contains("another git operation is in progress") { // Daemon-side guard for a repo mid-merge/rebase/cherry-pick or with a // held index lock (see `git_operation_in_progress`). - "Another git operation is in progress. Finish or abort it first." + crate::menu_label( + "codereview.git_dialog.error.another_git_operation_in_progress", + "Another git operation is in progress. Finish or abort it first.", + ) } else { - "Git operation failed." + crate::menu_label( + "codereview.git_dialog.error.git_operation_failed", + "Git operation failed.", + ) } } @@ -195,7 +231,7 @@ fn render_branch_section( let sub_color = theme.sub_text_color(theme.surface_1()).into_solid(); let label = Text::new( - "Branch", + crate::menu_label("codereview.git_dialog.branch_label", "Branch"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -279,11 +315,13 @@ fn render_file_changes_box( let total_additions: usize = file_changes.iter().map(|f| f.additions).sum(); let total_deletions: usize = file_changes.iter().map(|f| f.deletions).sum(); + let file_count_label = if total_files == 1 { + crate::menu_label("codereview.git_dialog.file_count_singular", "file") + } else { + crate::menu_label("codereview.git_dialog.file_count_plural", "files") + }; let files_text = Text::new( - format!( - "{total_files} {}", - if total_files == 1 { "file" } else { "files" } - ), + format!("{total_files} {file_count_label}"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -489,8 +527,11 @@ impl GitDialog { // segmented intent selector inside the dialog is the sole UI that // communicates which of commit / commit-and-push / commit-and-create-PR // will actually run on click. - let (confirm_button, cancel_button, close_button) = - Self::build_dialog_buttons("Confirm", None, ctx); + let (confirm_button, cancel_button, close_button) = Self::build_dialog_buttons( + crate::menu_label("codereview.git_dialog.button.confirm", "Confirm"), + None, + ctx, + ); ctx.subscribe_to_model(&diff_state_model, Self::handle_diff_state_event); let state = commit::new_state( repo_location.to_local_path(), @@ -594,16 +635,22 @@ impl GitDialog { button.on_click(|ctx| ctx.dispatch_typed_action(GitDialogAction::Confirm)) }); let cancel_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Cancel", NakedTheme) - .with_size(ButtonSize::Small) - .with_height(32.) - .on_click(|ctx| ctx.dispatch_typed_action(GitDialogAction::Cancel)) + ActionButton::new( + crate::menu_label("codereview.git_dialog.button.cancel", "Cancel"), + NakedTheme, + ) + .with_size(ButtonSize::Small) + .with_height(32.) + .on_click(|ctx| ctx.dispatch_typed_action(GitDialogAction::Cancel)) }); let close_button = ctx.add_typed_action_view(|_ctx| { ActionButton::new("", NakedTheme) .with_icon(Icon::X) .with_size(ButtonSize::Small) - .with_tooltip("ESC") + .with_tooltip(crate::menu_label( + "codereview.git_dialog.button.esc_tooltip", + "ESC", + )) .on_click(|ctx| ctx.dispatch_typed_action(GitDialogAction::Cancel)) }); (confirm_button, cancel_button, close_button) @@ -738,15 +785,26 @@ impl GitDialog { fn title(&self) -> &'static str { match &self.mode { - GitDialogMode::Commit(_) => "Commit your changes", + GitDialogMode::Commit(_) => { + crate::menu_label("codereview.git_dialog.title.commit", "Commit your changes") + } GitDialogMode::Push(state) => { if state.publish { - "Publish branch" + crate::menu_label( + "codereview.git_dialog.title.publish_branch", + "Publish branch", + ) } else { - "Push changes" + crate::menu_label( + "codereview.git_dialog.title.push_changes", + "Push changes", + ) } } - GitDialogMode::CreatePr(_) => "Create pull request", + GitDialogMode::CreatePr(_) => crate::menu_label( + "codereview.git_dialog.title.create_pull_request", + "Create pull request", + ), } } diff --git a/app/src/code_review/git_dialog/pr.rs b/app/src/code_review/git_dialog/pr.rs index e8945cd9a6..a964c2ca7b 100644 --- a/app/src/code_review/git_dialog/pr.rs +++ b/app/src/code_review/git_dialog/pr.rs @@ -38,7 +38,7 @@ pub struct PrState { } pub(super) fn confirm_label_for() -> &'static str { - "Create PR" + crate::menu_label("codereview.git_dialog.button.create_pr", "Create PR") } pub(super) fn confirm_icon_for() -> Icon { @@ -46,7 +46,7 @@ pub(super) fn confirm_icon_for() -> Icon { } fn loading_label_for() -> &'static str { - "Creating\u{2026}" + crate::menu_label("codereview.git_dialog.loading.creating", "Creating\u{2026}") } /// PR mode has no prerequisites beyond a branch with commits; confirm is @@ -165,9 +165,18 @@ pub(super) fn show_pr_created_toast(pr_info: &PrInfo, ctx: &mut ViewContext Box) -> PushState { pub(super) fn confirm_label(publish: bool) -> &'static str { if publish { - "Publish" + crate::menu_label("codereview.git_dialog.button.publish", "Publish") } else { - "Push" + crate::menu_label("codereview.git_dialog.button.push", "Push") } } @@ -75,9 +75,9 @@ pub(super) fn confirm_icon(publish: bool) -> Icon { fn loading_label(publish: bool) -> &'static str { if publish { - "Publishing…" + crate::menu_label("codereview.git_dialog.loading.publishing", "Publishing…") } else { - "Pushing…" + crate::menu_label("codereview.git_dialog.loading.pushing", "Pushing…") } } @@ -127,9 +127,15 @@ pub(super) fn finish_push( match result { Ok(_) => { let toast_msg = if publish { - "Branch successfully published." + crate::menu_label( + "codereview.git_dialog.success.branch_published", + "Branch successfully published.", + ) } else { - "Changes successfully pushed." + crate::menu_label( + "codereview.git_dialog.success.changes_pushed", + "Changes successfully pushed.", + ) }; show_toast(toast_msg, ctx); } @@ -178,7 +184,10 @@ fn render_commits_section(state: &PushState, appearance: &Appearance) -> Box Box Result<()> { ctx.add_singleton_model(move |ctx| { plugin::PluginHost::new(ctx).expect("Could not instantiate PluginHost") }); + i18n::init_locale(); let app_state = initialize_app( &launch_mode, timer, @@ -1199,6 +1200,49 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> { }) } +/// Looks up the translation key using the user's chosen locale. +/// Falls back to the English (en) translation, then to the provided fallback string. +pub(crate) fn menu_label(key: &str, fallback: &str) -> &'static str { + match i18n::lookup(key, i18n::current_locale()) { + i18n::TranslationLookup::Found(v) => Box::leak(v.into_owned().into_boxed_str()), + i18n::TranslationLookup::Missing => match i18n::lookup(key, "en") { + i18n::TranslationLookup::Found(v) => Box::leak(v.into_owned().into_boxed_str()), + i18n::TranslationLookup::Missing => Box::leak(fallback.to_string().into_boxed_str()), + }, + } +} + +#[cfg(test)] +mod menu_label_tests { + use super::*; + + #[test] + fn test_menu_label_returns_static_str() { + let result = menu_label("test.key", "fallback"); + let _static_ref: &'static str = result; + } + + #[test] + fn test_menu_label_fallback_for_missing_key() { + let result = menu_label("definitely.missing.key.xyz", "hello world"); + assert_eq!(result, "hello world"); + } + + #[test] + fn test_menu_label_english_fallback() { + i18n::init_locale(); + // With default locale (en), should return English translation + let _result = menu_label("test.key", "fallback"); + } + + #[test] + fn test_menu_label_consistency() { + let r1 = menu_label("test.key", "fallback"); + let r2 = menu_label("test.key", "fallback"); + assert_eq!(r1, r2); + } +} + pub struct UpdateQuakeModeEventArg { active_window_id: Option, } diff --git a/app/src/root_view.rs b/app/src/root_view.rs index f0dd931284..1a0c4064f9 100644 --- a/app/src/root_view.rs +++ b/app/src/root_view.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use std::sync::mpsc::SyncSender; use std::sync::Arc; +use ai::api_keys::{ApiKeyManager, ApiKeyManagerEvent}; use anyhow::Result; use cfg_if::cfg_if; use itertools::Itertools; @@ -24,10 +25,11 @@ use warpui::clipboard::ClipboardContent; use warpui::elements::{ Border, ChildAnchor, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Stack, }; -use warpui::keymap::{EditableBinding, FixedBinding}; +use warpui::keymap::{EditableBinding, FixedBinding, Keystroke}; use warpui::platform::{WindowBounds, WindowStyle}; use warpui::presenter::ChildView; use warpui::rendering::OnGPUDeviceSelected; +use warpui::ui_components::components::UiComponentStyles; use warpui::windowing::WindowManager; use warpui::{ id, AddWindowOptions, AppContext, DisplayId, Element, Entity, EntityId, FocusContext, @@ -50,6 +52,7 @@ use crate::auth::auth_view_modal::{AuthRedirectPayload, AuthView, AuthViewVarian use crate::auth::login_slide::{LoginSlideEvent, LoginSlideSource, LoginSlideView}; use crate::auth::needs_sso_link_view::NeedsSsoLinkView; use crate::auth::paste_auth_token_modal::{PasteAuthTokenModalEvent, PasteAuthTokenModalView}; +use crate::auth::provider_keys_modal::{ProviderKeysModalEvent, ProviderKeysModalView}; #[cfg(target_family = "wasm")] use crate::auth::web_handoff::{WebHandoffEvent, WebHandoffView}; use crate::auth::{AuthStateProvider, LoginFailureReason}; @@ -65,6 +68,7 @@ use crate::features::FeatureFlag; use crate::interval_timer::IntervalTimer; use crate::launch_configs::launch_config; use crate::linear::LinearIssueWork; +use crate::modal::{Modal, ModalEvent}; use crate::notebooks::manager::NotebookSource; use crate::pane_group::{NewTerminalOptions, PanesLayout}; use crate::persistence::ModelEvent; @@ -77,6 +81,7 @@ use crate::settings::cloud_preferences_syncer::{ CloudPreferencesSyncer, CloudPreferencesSyncerEvent, }; use crate::settings::{apply_onboarding_settings, AISettings, QuakeModeSettings, ThemeSettings}; +use crate::settings_view::custom_inference_modal::{CustomEndpointModal, CustomEndpointModalEvent}; use crate::settings_view::mcp_servers_page::MCPServersSettingsPage; use crate::settings_view::{flags, OpenTeamsSettingsModalArgs, SettingsSection}; use crate::terminal::available_shells::AvailableShell; @@ -1621,6 +1626,10 @@ pub struct RootView { /// settings to apply after a new user login / initial cloud load completes pending_post_auth_onboarding_settings: Option, paste_auth_token_modal: Option>, + /// BYOK "Add API key" modal. + add_api_key_modal: Option>, + /// BYOK "Add custom endpoint" modal — reuses the settings `CustomEndpointModal`. + add_custom_endpoint_modal: Option>>, } impl RootView { @@ -1721,6 +1730,8 @@ impl RootView { pending_tutorial: None, pending_post_auth_onboarding_settings: None, paste_auth_token_modal: None, + add_api_key_modal: None, + add_custom_endpoint_modal: None, }; match &root_view.auth_onboarding_state { @@ -1996,6 +2007,30 @@ impl RootView { }, ); + // Gate the AI-access slide's "Next" on having a BYOK key/endpoint and + // surface how many are configured: seed from the current `ApiKeyManager` + // state and keep it in sync as the user adds keys/endpoints via the + // onboarding BYOK modals. + let keys = ApiKeyManager::as_ref(ctx).keys(); + let (key_count, endpoint_count) = (keys.provider_key_count(), keys.custom_endpoints.len()); + onboarding_view.update(ctx, |view, ctx| { + view.set_byok_status(key_count, endpoint_count, ctx); + }); + let onboarding_view_for_keys = onboarding_view.clone(); + ctx.subscribe_to_model( + &ApiKeyManager::handle(ctx), + move |_, api_key_manager, event, ctx| { + if matches!(event, ApiKeyManagerEvent::KeysUpdated) { + let keys = api_key_manager.as_ref(ctx).keys(); + let (key_count, endpoint_count) = + (keys.provider_key_count(), keys.custom_endpoints.len()); + onboarding_view_for_keys.update(ctx, |view, ctx| { + view.set_byok_status(key_count, endpoint_count, ctx); + }); + } + }, + ); + ctx.subscribe_to_view(&onboarding_view, |me, _view, event, ctx| { me.handle_agent_onboarding_event(event, ctx); }); @@ -2249,6 +2284,144 @@ impl RootView { self.paste_auth_token_modal = Some(modal); ctx.notify(); } + AgentOnboardingEvent::AddApiKeyRequested => { + // Pre-fill the modal with any keys the user already saved so they + // persist across reopens and can be edited or cleared in place. + let existing = ApiKeyManager::as_ref(ctx).keys(); + let (openai, anthropic, google) = ( + existing.openai.clone(), + existing.anthropic.clone(), + existing.google.clone(), + ); + let modal = ctx.add_typed_action_view(move |ctx| { + ProviderKeysModalView::new(openai, anthropic, google, ctx) + }); + ctx.subscribe_to_view(&modal, |me, _, event, ctx| match event { + ProviderKeysModalEvent::Cancelled => { + me.add_api_key_modal = None; + me.focus(ctx); + ctx.notify(); + } + ProviderKeysModalEvent::Save { + openai, + anthropic, + google, + } => { + let (openai, anthropic, google) = + (openai.clone(), anthropic.clone(), google.clone()); + // Replace rather than merge so an emptied field clears the + // stored key, keeping the saved state and the modal in sync. + ApiKeyManager::handle(ctx).update(ctx, |manager, ctx| { + manager.set_openai_key(openai, ctx); + manager.set_anthropic_key(anthropic, ctx); + manager.set_google_key(google, ctx); + }); + me.add_api_key_modal = None; + me.focus(ctx); + ctx.notify(); + } + }); + ctx.focus(&modal); + self.add_api_key_modal = Some(modal); + ctx.notify(); + } + AgentOnboardingEvent::AddCustomEndpointRequested => { + // Pre-fill with the existing endpoint (if any) so its content + // persists across reopens and edits in place instead of silently + // adding a duplicate. Onboarding exposes a single endpoint; the + // settings page manages multiples later. + let existing = ApiKeyManager::as_ref(ctx) + .keys() + .custom_endpoints + .first() + .cloned(); + let is_editing = existing.is_some(); + let editing_index = is_editing.then_some(0); + let title = if is_editing { + "Edit custom endpoint" + } else { + "Add custom endpoint" + } + .to_string(); + let body = ctx.add_typed_action_view(move |ctx| { + CustomEndpointModal::new(existing.as_ref(), editing_index, ctx) + }); + ctx.subscribe_to_view(&body, |me, _, event, ctx| match event { + CustomEndpointModalEvent::Close => { + me.add_custom_endpoint_modal = None; + me.focus(ctx); + ctx.notify(); + } + CustomEndpointModalEvent::AddEndpoint { + name, + url, + api_key, + models, + } => { + let (name, url, api_key, models) = + (name.clone(), url.clone(), api_key.clone(), models.clone()); + ApiKeyManager::handle(ctx).update(ctx, |manager, ctx| { + manager.add_custom_endpoint(name, url, api_key, models, ctx); + }); + me.add_custom_endpoint_modal = None; + me.focus(ctx); + ctx.notify(); + } + CustomEndpointModalEvent::SaveEndpoint { + index, + name, + url, + api_key, + models, + } => { + let (index, name, url, api_key, models) = ( + *index, + name.clone(), + url.clone(), + api_key.clone(), + models.clone(), + ); + ApiKeyManager::handle(ctx).update(ctx, |manager, ctx| { + manager.save_custom_endpoint(index, name, url, api_key, models, ctx); + }); + me.add_custom_endpoint_modal = None; + me.focus(ctx); + ctx.notify(); + } + CustomEndpointModalEvent::RemoveEndpoint { index } => { + let index = *index; + ApiKeyManager::handle(ctx).update(ctx, |manager, ctx| { + manager.remove_custom_endpoint(index, ctx); + }); + me.add_custom_endpoint_modal = None; + me.focus(ctx); + ctx.notify(); + } + }); + + let body_for_modal = body.clone(); + let modal = ctx.add_typed_action_view(move |ctx| { + Modal::new(Some(title), body_for_modal, ctx) + .with_modal_style(UiComponentStyles { + width: Some(560.), + height: Some(600.), + ..Default::default() + }) + .with_background_opacity(100) + .with_dismiss_on_click() + .with_dismiss_keystroke(Keystroke::parse("escape").unwrap_or_default()) + }); + ctx.subscribe_to_view(&modal, |me, _, event, ctx| match event { + ModalEvent::Close => { + me.add_custom_endpoint_modal = None; + me.focus(ctx); + ctx.notify(); + } + }); + body.update(ctx, |body, ctx| body.on_open(ctx)); + self.add_custom_endpoint_modal = Some(modal); + ctx.notify(); + } AgentOnboardingEvent::PrivacySettingsFromTerminalThemeSlideRequested => { let AuthOnboardingState::Onboarding { target, @@ -3249,7 +3422,10 @@ impl View for RootView { fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext) { if focus_ctx.is_self_focused() { self.focus(ctx); - } else if self.paste_auth_token_modal.is_some() { + } else if self.paste_auth_token_modal.is_some() + || self.add_api_key_modal.is_some() + || self.add_custom_endpoint_modal.is_some() + { // Modal is open — focus belongs to the editor inside it. } else if matches!( self.auth_onboarding_state, @@ -3298,6 +3474,14 @@ impl View for RootView { stack.add_child(ChildView::new(modal).finish()); } + if let Some(modal) = &self.add_api_key_modal { + stack.add_child(ChildView::new(modal).finish()); + } + + if let Some(modal) = &self.add_custom_endpoint_modal { + stack.add_child(ChildView::new(modal).finish()); + } + if let Some(traffic_light_data) = self.traffic_light_data(app) { let theme = Appearance::as_ref(app).theme(); let fullscreen_state = app diff --git a/app/src/settings_view/about_page.rs b/app/src/settings_view/about_page.rs index 8d4a233726..764ee98187 100644 --- a/app/src/settings_view/about_page.rs +++ b/app/src/settings_view/about_page.rs @@ -115,7 +115,7 @@ impl SettingsWidget for AboutPageWidget { .with_child(version_row.finish()) .with_child( ui_builder - .span("Copyright 2026 Warp") + .span(crate::menu_label("settings.about.copyright", "Copyright 2026 Warp")) .build() .with_margin_top(16.) .finish(), diff --git a/app/src/settings_view/ai_page.rs b/app/src/settings_view/ai_page.rs index c76b6f879b..bf78e0f845 100644 --- a/app/src/settings_view/ai_page.rs +++ b/app/src/settings_view/ai_page.rs @@ -173,15 +173,27 @@ const AI_SETTINGS_DROPDOWN_MAX_HEIGHT: f32 = 250.; const CONTEXT_WINDOW_SLIDER_WIDTH: f32 = 220.; const CONTEXT_WINDOW_INPUT_BOX_WIDTH: f32 = 120.; -const NEXT_COMMAND_DESCRIPTION: &str = "Let AI suggest the next command to run based on your command history, outputs, and common workflows."; -const PROMPT_SUGGESTIONS_DESCRIPTION: &str = "Let AI suggest natural language prompts, as inline banners in the input, based on recent commands and their outputs."; -const SUGGESTED_CODE_BANNERS_DESCRIPTION: &str = "Let AI suggest code diffs and queries as inline banners in the blocklist, based on recent commands and their outputs."; -const NATURAL_LANGUAGE_AUTOSUGGESTIONS: &str = - "Let AI suggest natural language autosuggestions, based on recent commands and their outputs."; -const SHARED_BLOCK_TITLE_GENERATION_DESCRIPTION: &str = - "Let AI generate a title for your shared block based on the command and output."; -const GIT_OPERATIONS_AUTOGEN_DESCRIPTION: &str = - "Let AI generate commit messages and pull request titles and descriptions."; +// ── i18n helpers ────────────────────────────────────────────────────── +// Converted from `const` to `fn` so translation can happen at runtime. +fn next_command_description() -> &'static str { + crate::menu_label("settings.ai.next_command_description", "Let AI suggest the next command to run based on your command history, outputs, and common workflows.") +} +fn prompt_suggestions_description() -> &'static str { + crate::menu_label("settings.ai.prompt_suggestions_description", "Let AI suggest natural language prompts, as inline banners in the input, based on recent commands and their outputs.") +} +fn suggested_code_banners_description() -> &'static str { + crate::menu_label("settings.ai.suggested_code_banners_description", "Let AI suggest code diffs and queries as inline banners in the blocklist, based on recent commands and their outputs.") +} +fn natural_language_autosuggestions() -> &'static str { + crate::menu_label("settings.ai.natural_language_autosuggestions_description", "Let AI suggest natural language autosuggestions, based on recent commands and their outputs.") +} +fn shared_block_title_generation_description() -> &'static str { + crate::menu_label("settings.ai.shared_block_title_generation_description", "Let AI generate a title for your shared block based on the command and output.") +} +fn git_operations_autogen_description() -> &'static str { + crate::menu_label("settings.ai.agent.git_operations_autogen_description", "Generates a commit message based on your current changes to your git repo") +} +// ─────────────────────────────────────────────────────────────────────── const WISPR_FLOW_URL: &str = "https://wisprflow.ai/"; const CUSTOM_INFERENCE_LEARN_MORE_URL: &str = "https://docs.warp.dev/agent-platform/inference/custom-inference-endpoint/"; @@ -206,7 +218,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "Active AI", + crate::menu_label("settings.ai.active_ai", "Active AI"), builder(SettingsAction::AI(AISettingsPageAction::ToggleActiveAI)), &(context.clone() & id!(flags::IS_ANY_AI_ENABLED)), flags::IS_ACTIVE_AI_ENABLED, @@ -247,7 +259,7 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::new( - "Next Command", + crate::menu_label("settings.ai.next_command", "Next Command"), builder(SettingsAction::AI( AISettingsPageAction::ToggleIntelligentAutosuggestions, )), @@ -285,7 +297,7 @@ pub fn init_actions_from_parent_view( ); ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::custom( - SettingActionPairDescriptions::new("Show agent tips", "Hide agent tips"), + SettingActionPairDescriptions::new(crate::menu_label("settings.ai.show_agent_tips", "Show agent tips"), crate::menu_label("settings.ai.hide_agent_tips", "Hide agent tips")), builder(SettingsAction::AI( AISettingsPageAction::ToggleShowAgentTips, )), @@ -302,8 +314,8 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ToggleSettingActionPair::custom( SettingActionPairDescriptions::new( - "Show Oz changelog in new agent conversation view", - "Hide Oz changelog in new agent conversation view", + crate::menu_label("settings.ai.show_oz_changelog", "Show Oz changelog in new agent conversation view"), + crate::menu_label("settings.ai.hide_oz_changelog", "Hide Oz changelog in new agent conversation view"), ), builder(SettingsAction::AI( AISettingsPageAction::ToggleShowOzUpdatesInZeroState, @@ -545,7 +557,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ ToggleSettingActionPair::new( - "Rules", + crate::menu_label("settings.ai.rules", "Rules"), builder(SettingsAction::AI(AISettingsPageAction::ToggleRules)), &(context.clone() & id!(flags::IS_ANY_AI_ENABLED)), flags::AI_RULES_FLAG, @@ -553,7 +565,7 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::WarpAi) .with_enabled(|| FeatureFlag::AIRules.is_enabled()), ToggleSettingActionPair::new( - "Suggested Rules", + crate::menu_label("settings.ai.suggested_rules", "Suggested Rules"), builder(SettingsAction::AI( AISettingsPageAction::ToggleRuleSuggestions, )), @@ -565,7 +577,7 @@ pub fn init_actions_from_parent_view( FeatureFlag::AIRules.is_enabled() && FeatureFlag::SuggestedRules.is_enabled() }), ToggleSettingActionPair::new( - "Warp Drive as agent context", + crate::menu_label("settings.ai.warp_drive_context", "Warp Drive as agent context"), builder(SettingsAction::AI( AISettingsPageAction::ToggleWarpDriveContext, )), @@ -575,7 +587,7 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::WarpAi) .with_enabled(|| FeatureFlag::AIRules.is_enabled()), ToggleSettingActionPair::new( - "Auto-spawn servers from third-party agents", + crate::menu_label("settings.ai.auto_spawn_servers", "Auto-spawn servers from third-party agents"), builder(SettingsAction::AI(AISettingsPageAction::ToggleFileBasedMcp)), &(context.clone() & id!(flags::IS_ANY_AI_ENABLED)), flags::FILE_BASED_MCP_FLAG, @@ -592,7 +604,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings( vec![ ToggleSettingActionPair::new( - "Warp credit fallback", + crate::menu_label("settings.ai.warp_credit_fallback", "Warp credit fallback"), builder(SettingsAction::AI( AISettingsPageAction::ToggleCanUseWarpCreditsForFallback, )), @@ -936,7 +948,7 @@ impl AISettingsPageView { let expanded = host_native_absolute_path(s, &None, &None); Path::new(&expanded).is_dir() }); - input.set_placeholder_text("e.g. ~/code-repos/repo", ctx); + input.set_placeholder_text(crate::menu_label("settings.ai.placeholder_directory_allowlist", "e.g. ~/code-repos/repo"), ctx); input }); Self::update_editor_interaction_state( @@ -975,7 +987,7 @@ impl AISettingsPageView { }; let mut editor = EditorView::new(options, ctx); - editor.set_placeholder_text("Commands, comma separated", ctx); + editor.set_placeholder_text(crate::menu_label("settings.ai.commands_comma_separated", "Commands, comma separated"), ctx); let current_value = AISettings::as_ref(ctx) .autodetection_command_denylist @@ -997,7 +1009,7 @@ impl AISettingsPageView { let command_execution_allowlist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. ls .*", ctx); + input.set_placeholder_text(crate::menu_label("settings.ai.placeholder_command_allowlist", "e.g. ls .*"), ctx); input }); Self::update_editor_interaction_state( @@ -1029,7 +1041,7 @@ impl AISettingsPageView { let command_execution_denylist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. rm .*", ctx); + input.set_placeholder_text(crate::menu_label("settings.ai.placeholder_command_denylist", "e.g. rm .*"), ctx); input }); Self::update_editor_interaction_state( @@ -1061,7 +1073,7 @@ impl AISettingsPageView { let cli_agent_footer_command_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("command (supports regex)", ctx); + input.set_placeholder_text(crate::menu_label("settings.ai.placeholder_cli_command", "command (supports regex)"), ctx); input }); // The coding agent footer command editor is always enabled, @@ -1379,15 +1391,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + crate::menu_label("settings.ai.permission.agent_decides", "Agent decides"), AISettingsPageAction::SetApplyCodeDiffs(ActionPermission::AgentDecides), ), DropdownItem::new( - "Always allow", + crate::menu_label("settings.ai.permission.always_allow", "Always allow"), AISettingsPageAction::SetApplyCodeDiffs(ActionPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + crate::menu_label("settings.ai.permission.always_ask", "Always ask"), AISettingsPageAction::SetApplyCodeDiffs(ActionPermission::AlwaysAsk), ), ], @@ -1409,15 +1421,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + crate::menu_label("settings.ai.permission.agent_decides", "Agent decides"), AISettingsPageAction::SetReadFiles(ActionPermission::AgentDecides), ), DropdownItem::new( - "Always allow", + crate::menu_label("settings.ai.permission.always_allow", "Always allow"), AISettingsPageAction::SetReadFiles(ActionPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + crate::menu_label("settings.ai.permission.always_ask", "Always ask"), AISettingsPageAction::SetReadFiles(ActionPermission::AlwaysAsk), ), ], @@ -1439,15 +1451,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + crate::menu_label("settings.ai.permission.agent_decides", "Agent decides"), AISettingsPageAction::SetExecuteCommands(ActionPermission::AgentDecides), ), DropdownItem::new( - "Always allow", + crate::menu_label("settings.ai.permission.always_allow", "Always allow"), AISettingsPageAction::SetExecuteCommands(ActionPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + crate::menu_label("settings.ai.permission.always_ask", "Always ask"), AISettingsPageAction::SetExecuteCommands(ActionPermission::AlwaysAsk), ), ], @@ -1469,15 +1481,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Always allow", + crate::menu_label("settings.ai.permission.always_allow", "Always allow"), AISettingsPageAction::SetWriteToPty(WriteToPtyPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + crate::menu_label("settings.ai.permission.always_ask", "Always ask"), AISettingsPageAction::SetWriteToPty(WriteToPtyPermission::AlwaysAsk), ), DropdownItem::new( - "Ask on first write", + crate::menu_label("settings.ai.permission.ask_on_first_write", "Ask on first write"), AISettingsPageAction::SetWriteToPty(WriteToPtyPermission::AskOnFirstWrite), ), ], @@ -1499,15 +1511,15 @@ impl AISettingsPageView { dropdown.set_items( vec![ DropdownItem::new( - "Agent decides", + crate::menu_label("settings.ai.permission.agent_decides", "Agent decides"), AISettingsPageAction::SetMCPPermissions(ActionPermission::AgentDecides), ), DropdownItem::new( - "Always allow", + crate::menu_label("settings.ai.permission.always_allow", "Always allow"), AISettingsPageAction::SetMCPPermissions(ActionPermission::AlwaysAllow), ), DropdownItem::new( - "Always ask", + crate::menu_label("settings.ai.permission.always_ask", "Always ask"), AISettingsPageAction::SetMCPPermissions(ActionPermission::AlwaysAsk), ), ], @@ -1526,7 +1538,7 @@ impl AISettingsPageView { let mut dropdown = FilterableDropdown::new(ctx); dropdown.set_top_bar_max_width(AI_SETTINGS_DROPDOWN_WIDTH); dropdown.set_menu_width(AI_SETTINGS_DROPDOWN_WIDTH, ctx); - dropdown.set_menu_header_to_static("Select MCP servers"); + dropdown.set_menu_header_to_static(crate::menu_label("settings.ai.select_mcp_servers", "Select MCP servers")); dropdown }); Self::refresh_mcp_allowlist_dropdown(&mcp_allowlist_dropdown, ctx); @@ -1540,7 +1552,7 @@ impl AISettingsPageView { let mut dropdown = FilterableDropdown::new(ctx); dropdown.set_top_bar_max_width(AI_SETTINGS_DROPDOWN_WIDTH); dropdown.set_menu_width(AI_SETTINGS_DROPDOWN_WIDTH, ctx); - dropdown.set_menu_header_to_static("Select MCP servers"); + dropdown.set_menu_header_to_static(crate::menu_label("settings.ai.select_mcp_servers", "Select MCP servers")); dropdown }); Self::refresh_mcp_denylist_dropdown(&mcp_denylist_dropdown, ctx); @@ -1588,7 +1600,7 @@ impl AISettingsPageView { let expanded = host_native_absolute_path(s, &None, &None); Path::new(&expanded).is_dir() }); - input.set_placeholder_text("e.g. ~/code-repos/repo", ctx); + input.set_placeholder_text(crate::menu_label("settings.ai.placeholder_directory_allowlist", "e.g. ~/code-repos/repo"), ctx); input }); @@ -1623,7 +1635,7 @@ impl AISettingsPageView { let command_denylist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. rm .*", ctx); + input.set_placeholder_text(crate::menu_label("settings.ai.placeholder_command_denylist", "e.g. rm .*"), ctx); input }); Self::update_editor_interaction_state( @@ -1661,7 +1673,7 @@ impl AISettingsPageView { let command_allowlist_editor = ctx.add_typed_action_view(|ctx| { let mut input = SubmittableTextInput::new(ctx).validate_on_edit(|s| Regex::new(s).is_ok()); - input.set_placeholder_text("e.g. ls .*", ctx); + input.set_placeholder_text(crate::menu_label("settings.ai.placeholder_command_allowlist", "e.g. ls .*"), ctx); input }); Self::update_editor_interaction_state( @@ -1725,7 +1737,7 @@ impl AISettingsPageView { } let add_profile_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Add Profile", SecondaryTheme) + ActionButton::new(crate::menu_label("settings.ai.add_profile", "Add Profile"), SecondaryTheme) .with_icon(Icon::Plus) .with_size(ButtonSize::Small) .on_click(|ctx| { @@ -1741,7 +1753,7 @@ impl AISettingsPageView { let custom_inference_controls_enabled = is_any_ai_enabled && UserWorkspaces::as_ref(ctx).is_custom_inference_enabled(ctx); let custom_inference_add_button = ctx.add_typed_action_view(|_| { - ActionButton::new("+ Add custom model", SecondaryTheme) + ActionButton::new(crate::menu_label("settings.ai.add_custom_model", "+ Add custom model"), SecondaryTheme) .with_size(ButtonSize::Small) .on_click(|ctx| { ctx.dispatch_typed_action(AISettingsPageAction::OpenAddCustomEndpointModal); @@ -1759,7 +1771,7 @@ impl AISettingsPageView { let custom_endpoint_modal_view = ctx.add_typed_action_view(|ctx| { Modal::new( - Some("Add custom endpoint".to_string()), + Some(crate::menu_label("settings.ai.add_custom_endpoint", "Add custom endpoint").to_string()), custom_endpoint_modal_body.clone(), ctx, ) @@ -1863,11 +1875,11 @@ impl AISettingsPageView { let items = vec![ DropdownItem::new( - "New Tab", + crate::menu_label("settings.ai.new_tab", "New Tab"), AISettingsPageAction::SetConversationLayout(OpenConversationPreference::NewTab), ), DropdownItem::new( - "Split Pane", + crate::menu_label("settings.ai.split_pane", "Split Pane"), AISettingsPageAction::SetConversationLayout( OpenConversationPreference::SplitPane, ), @@ -1878,9 +1890,9 @@ impl AISettingsPageView { let current = *crate::util::file::external_editor::EditorSettings::as_ref(ctx) .open_conversation_layout_preference; match current { - OpenConversationPreference::NewTab => dropdown.set_selected_by_name("New Tab", ctx), + OpenConversationPreference::NewTab => dropdown.set_selected_by_name(crate::menu_label("settings.ai.new_tab", "New Tab"), ctx), OpenConversationPreference::SplitPane => { - dropdown.set_selected_by_name("Split Pane", ctx) + dropdown.set_selected_by_name(crate::menu_label("settings.ai.split_pane", "Split Pane"), ctx) } }; dropdown @@ -2182,11 +2194,19 @@ impl AISettingsPageView { } let provider_name = provider.display_name(); let current_default = Self::active_base_model_display_name(ctx); - let description = format!( - "You added your own {provider_name} API key, but your default model is currently set \ - to {current_default}, which won't work without Warp credits. Would you like to change \ - your default model?" - ); + let description = i18n::interpolate( + crate::menu_label( + "settings.ai.default_model_modal.api_key_description", + "You added your own {provider_name} API key, but your default model is currently set \ + to {current_default}, which won't work without Warp credits. Would you like to change \ + your default model?", + ), + &[ + ("provider_name", provider_name.to_string()), + ("current_default", current_default), + ], + ) + .into_owned(); self.show_set_default_model_modal(description, choices, ctx); } @@ -2228,12 +2248,19 @@ impl AISettingsPageView { return; } let current_default = Self::active_base_model_display_name(ctx); - let description = format!( - "You added the \"{}\" custom endpoint, but your default model is currently set to \ - {current_default}, which won't work without Warp credits. Would you like to change \ - your default model?", - endpoint.name - ); + let description = i18n::interpolate( + crate::menu_label( + "settings.ai.default_model_modal.custom_endpoint_description", + "You added the \"{endpoint_name}\" custom endpoint, but your default model is currently set to \ + {current_default}, which won't work without Warp credits. Would you like to change \ + your default model?", + ), + &[ + ("endpoint_name", endpoint.name.clone()), + ("current_default", current_default), + ], + ) + .into_owned(); self.show_set_default_model_modal(description, choices, ctx); } @@ -2265,7 +2292,7 @@ impl AISettingsPageView { (0..count) .map(|index| { let button = ctx.add_typed_action_view(move |_| { - ActionButton::new("Edit", SecondaryTheme) + ActionButton::new(crate::menu_label("settings.ai.edit", "Edit"), SecondaryTheme) .with_icon(Icon::Pencil) .with_size(ButtonSize::Small) .on_click(move |ctx| { @@ -2298,7 +2325,7 @@ impl AISettingsPageView { self.pending_remove_custom_endpoint_index = None; self.custom_endpoint_modal_state - .set_title(Some("Add custom endpoint".to_string()), ctx); + .set_title(Some(crate::menu_label("settings.ai.add_custom_endpoint", "Add custom endpoint").to_string()), ctx); self.custom_endpoint_modal_state.prefill(None, None, ctx); self.custom_endpoint_modal_state.open(ctx); ctx.emit(AISettingsPageEvent::ShowModal); @@ -2325,7 +2352,7 @@ impl AISettingsPageView { self.pending_remove_custom_endpoint_index = None; self.custom_endpoint_modal_state - .set_title(Some("Edit custom endpoint".to_string()), ctx); + .set_title(Some(crate::menu_label("settings.ai.edit_custom_endpoint", "Edit custom endpoint").to_string()), ctx); self.custom_endpoint_modal_state .prefill(endpoint.as_ref(), Some(index), ctx); self.custom_endpoint_modal_state.open(ctx); @@ -2384,7 +2411,7 @@ impl AISettingsPageView { let window_id = ctx.window_id(); crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let toast = crate::view_components::DismissibleToast::success( - "Endpoint added".to_string(), + crate::menu_label("settings.ai.endpoint_added", "Endpoint added").to_string(), ); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -2424,7 +2451,7 @@ impl AISettingsPageView { let window_id = ctx.window_id(); crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let toast = crate::view_components::DismissibleToast::success( - "Endpoint saved".to_string(), + crate::menu_label("settings.ai.endpoint_saved", "Endpoint saved").to_string(), ); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -2511,7 +2538,7 @@ impl AISettingsPageView { let window_id = ctx.window_id(); crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let toast = crate::view_components::DismissibleToast::success( - "Endpoint removed".to_string(), + crate::menu_label("settings.ai.endpoint_removed", "Endpoint removed").to_string(), ); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -3115,11 +3142,11 @@ impl AISettingsPageView { menu.set_items( vec![ DropdownItem::new( - "Read only", + crate::menu_label("settings.ai.permission.read_only", "Read only"), AISettingsPageAction::SetAutonomyReadonlyCommandsSetting, ), DropdownItem::new( - "Supervised", + crate::menu_label("settings.ai.permission.supervised", "Supervised"), AISettingsPageAction::SetAutonomySupervisedSetting, ), ], @@ -3298,10 +3325,10 @@ impl AISettingsPageView { AgentModeCodingPermissionsType::iter() .map(|t| { let display = match t { - AgentModeCodingPermissionsType::AlwaysAskBeforeReading => "Always ask", - AgentModeCodingPermissionsType::AlwaysAllowReading => "Always allow", + AgentModeCodingPermissionsType::AlwaysAskBeforeReading => crate::menu_label("settings.ai.permission.always_ask", "Always ask"), + AgentModeCodingPermissionsType::AlwaysAllowReading => crate::menu_label("settings.ai.permission.always_allow", "Always allow"), AgentModeCodingPermissionsType::AllowReadingSpecificFiles => { - "Allow in specific directories" + crate::menu_label("settings.ai.permission.allow_in_specific_directories", "Allow in specific directories") } }; DropdownItem::new(display, AISettingsPageAction::SetCodingPermission(t)) @@ -3494,7 +3521,7 @@ impl AISettingsPageView { } items.push( - MenuItemFields::new("Other") + MenuItemFields::new(crate::menu_label("settings.ai.other_category", "Other")) .with_on_select_action(DropdownAction::select_action_and_close( AISettingsPageAction::SetCLIAgentForCommand { pattern: pattern_clone.clone(), @@ -3507,15 +3534,15 @@ impl AISettingsPageView { dropdown.set_rich_items(items, ctx); dropdown.set_menu_header_text_override(|label| { - if label == "Other" { - "Select coding agent".to_string() + if label == crate::menu_label("settings.ai.other_category", "Other") { + crate::menu_label("settings.ai.select_coding_agent", "Select coding agent").to_string() } else { label.to_string() } }); let selected_name = if matches!(current_agent, CLIAgent::Unknown) { - "Other" + crate::menu_label("settings.ai.other_category", "Other") } else { current_agent.display_name() }; @@ -4629,7 +4656,7 @@ fn render_toolbar_layout_editor( let label = Container::new( appearance .ui_builder() - .span("Toolbar layout".to_string()) + .span(crate::menu_label("settings.ai.toolbar_layout", "Toolbar layout").to_string()) .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE), ..Default::default() @@ -4736,7 +4763,7 @@ impl SettingsWidget for GlobalAIWidget { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Text::new_inline( - "Warp Agent", + crate::menu_label("settings.warp_agent", "Warp Agent"), appearance.ui_font_family(), PRIMARY_HEADER_FONT_SIZE, ) @@ -4749,7 +4776,7 @@ impl SettingsWidget for GlobalAIWidget { row.add_child( ConstrainedBox::new( Container::new( - Text::new("Your organization disallows AI when the active pane contains content from a remote session", appearance.ui_font_family(), 12.) + Text::new(crate::menu_label("settings.ai.remote_session_org_policy", "Your organization disallows AI when the active pane contains content from a remote session"), appearance.ui_font_family(), 12.) .with_color(appearance.theme().ui_warning_color()) .finish() ) @@ -4770,7 +4797,7 @@ impl SettingsWidget for GlobalAIWidget { .with_child( Container::new( Text::new_inline( - "To use AI features, please create an account.", + crate::menu_label("settings.ai.to_use_ai_features_create_account", "To use AI features, please create an account."), appearance.ui_font_family(), 14., ) @@ -4801,7 +4828,7 @@ impl SettingsWidget for GlobalAIWidget { }), ..Default::default() }) - .with_text_label("Sign up".to_owned()) + .with_text_label(crate::menu_label("settings.ai.sign_up", "Sign up").to_owned()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( @@ -4867,9 +4894,9 @@ impl UsageWidget { } let request_count_label = if workspace_is_delinquent_due_to_payment_issue { - "Restricted due to billing issue".to_string() + crate::menu_label("settings.ai.restricted_due_to_billing", "Restricted due to billing issue").to_string() } else if is_unlimited { - "Unlimited".to_string() + crate::menu_label("settings.ai.unlimited", "Unlimited").to_string() } else { format!("{used}/{limit}") }; @@ -5016,7 +5043,7 @@ impl SettingsWidget for UsageWidget { .with_child( build_sub_header( appearance, - "Usage", + crate::menu_label("settings.ai.usage", "Usage"), Some(styles::header_font_color(true, app)), ) .finish(), @@ -5046,7 +5073,7 @@ impl SettingsWidget for UsageWidget { ); let request_usage_row = self.render_ai_usage_limit_row( - "Credits", + crate::menu_label("settings.ai.credits", "Credits"), request_limit_description, ai_request_usage_model.requests_used(), ai_request_usage_model.request_limit(), @@ -5065,19 +5092,19 @@ impl SettingsWidget for UsageWidget { let upgrade_url = UserWorkspaces::upgrade_link_for_team(team.uid); if has_admin_permissions { vec![ - FormattedTextFragment::hyperlink("Upgrade", upgrade_url), + FormattedTextFragment::hyperlink(crate::menu_label("settings.ai.upgrade", "Upgrade"), upgrade_url), FormattedTextFragment::plain_text(" to get more AI usage."), ] } else { // The /upgrade page says to contact their administrator. vec![ - FormattedTextFragment::hyperlink("Compare plans", upgrade_url), + FormattedTextFragment::hyperlink(crate::menu_label("settings.ai.compare_plans", "Compare plans"), upgrade_url), FormattedTextFragment::plain_text(" for more AI usage."), ] } } else { vec![ - FormattedTextFragment::hyperlink("Contact support", "mailto:support@warp.dev"), + FormattedTextFragment::hyperlink(crate::menu_label("settings.ai.contact_support", "Contact support"), "mailto:support@warp.dev"), FormattedTextFragment::plain_text(" for more AI usage."), ] } @@ -5085,7 +5112,7 @@ impl SettingsWidget for UsageWidget { let user_id = auth_state.user_id().unwrap_or_default(); let upgrade_url = UserWorkspaces::upgrade_link(user_id); vec![ - FormattedTextFragment::hyperlink("Upgrade", upgrade_url), + FormattedTextFragment::hyperlink(crate::menu_label("settings.ai.upgrade", "Upgrade"), upgrade_url), FormattedTextFragment::plain_text(" to get more AI usage."), ] }; @@ -5202,7 +5229,7 @@ impl ActiveAIWidget { Flex::column() .with_child( render_ai_setting_toggle::( - "Next Command", + crate::menu_label("settings.ai.next_command", "Next Command"), AISettingsPageAction::ToggleIntelligentAutosuggestions, *ai_settings.intelligent_autosuggestions_enabled_internal, is_toggleable, @@ -5212,7 +5239,7 @@ impl ActiveAIWidget { ), ) .with_child(render_ai_setting_description( - NEXT_COMMAND_DESCRIPTION, + next_command_description(), is_toggleable, app, )) @@ -5229,7 +5256,7 @@ impl ActiveAIWidget { Flex::column() .with_child( render_ai_setting_toggle::( - "Prompt Suggestions", + crate::menu_label("settings.ai.prompt_suggestions", "Prompt Suggestions"), AISettingsPageAction::TogglePromptSuggestions, *ai_settings.prompt_suggestions_enabled_internal, is_toggleable, @@ -5239,7 +5266,7 @@ impl ActiveAIWidget { ), ) .with_child(render_ai_setting_description( - PROMPT_SUGGESTIONS_DESCRIPTION, + prompt_suggestions_description(), is_toggleable, app, )) @@ -5256,7 +5283,7 @@ impl ActiveAIWidget { Flex::column() .with_child( render_ai_setting_toggle::( - "Suggested Code Banners", + crate::menu_label("settings.ai.suggested_code_banners", "Suggested Code Banners"), AISettingsPageAction::ToggleCodeSuggestions, *ai_settings.code_suggestions_enabled_internal, is_toggleable, @@ -5266,7 +5293,7 @@ impl ActiveAIWidget { ), ) .with_child(render_ai_setting_description( - SUGGESTED_CODE_BANNERS_DESCRIPTION, + suggested_code_banners_description(), is_toggleable, app, )) @@ -5284,7 +5311,7 @@ impl ActiveAIWidget { .with_child(render_ai_setting_toggle::< NaturalLanguageAutosuggestionsEnabled, >( - "Natural Language Autosuggestions", + crate::menu_label("settings.ai.natural_language_autosuggestions", "Natural Language Autosuggestions"), AISettingsPageAction::ToggleNaturalLanguageAutosuggestions, *ai_settings.natural_language_autosuggestions_enabled_internal, is_toggleable, @@ -5293,7 +5320,7 @@ impl ActiveAIWidget { app, )) .with_child(render_ai_setting_description( - NATURAL_LANGUAGE_AUTOSUGGESTIONS, + natural_language_autosuggestions(), is_toggleable, app, )) @@ -5310,7 +5337,7 @@ impl ActiveAIWidget { Flex::column() .with_child( render_ai_setting_toggle::( - "Shared Block Title Generation", + crate::menu_label("settings.ai.shared_block_title_generation", "Shared Block Title Generation"), AISettingsPageAction::ToggleSharedTitleGeneration, *ai_settings.shared_block_title_generation_enabled_internal, is_toggleable, @@ -5320,7 +5347,7 @@ impl ActiveAIWidget { ), ) .with_child(render_ai_setting_description( - SHARED_BLOCK_TITLE_GENERATION_DESCRIPTION, + shared_block_title_generation_description(), is_toggleable, app, )) @@ -5336,7 +5363,7 @@ impl ActiveAIWidget { let is_toggleable = ai_settings.is_active_ai_enabled(app); Flex::column() .with_child(render_ai_setting_toggle::( - "Commit & Pull Request Generation", + crate::menu_label("settings.ai.commit_and_pr_generation", "Commit & Pull Request Generation"), AISettingsPageAction::ToggleGitOperationsAutogen, *ai_settings.git_operations_autogen_enabled_internal, is_toggleable, @@ -5345,7 +5372,7 @@ impl ActiveAIWidget { app, )) .with_child(render_ai_setting_description( - GIT_OPERATIONS_AUTOGEN_DESCRIPTION, + git_operations_autogen_description(), is_toggleable, app, )) @@ -5387,7 +5414,7 @@ impl SettingsWidget for ActiveAIWidget { .with_child( build_sub_header( appearance, - "Active AI", + crate::menu_label("settings.ai.active_ai", "Active AI"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .finish(), @@ -5478,14 +5505,14 @@ impl SettingsWidget for AgentsWidget { agents_header.add_child( build_sub_header( appearance, - "Agents", + crate::menu_label("settings.ai.agents", "Agents"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) .finish(), ); agents_header.add_child(render_ai_setting_description( - "Set the boundaries for how your Agent operates. Choose what it can access, how much autonomy it has, and when it must ask for your approval. You can also fine-tune behavior around natural language input, codebase awareness, and more.", + crate::menu_label("settings.ai.agents_description", "Set the boundaries for how your Agent operates. Choose what it can access, how much autonomy it has, and when it must ask for your approval. You can also fine-tune behavior around natural language input, codebase awareness, and more."), ai_settings.is_any_ai_enabled(app), app, )); @@ -5524,7 +5551,7 @@ impl AgentsWidget { .with_child( build_sub_header( appearance, - "Profiles", + crate::menu_label("settings.ai.profiles", "Profiles"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .finish(), @@ -5532,7 +5559,7 @@ impl AgentsWidget { .with_child( Container::new( render_ai_setting_description( - "Profiles let you define how your Agent operates — from the actions it can take and when it needs approval, to the models it uses for tasks like coding and planning. You can also scope them to individual projects.", + crate::menu_label("settings.ai.profiles_description", "Profiles let you define how your Agent operates — from the actions it can take and when it needs approval, to the models it uses for tasks like coding and planning. You can also scope them to individual projects."), is_any_ai_enabled, app, ) @@ -5582,7 +5609,7 @@ impl AgentsWidget { let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app); let model_subheader = Container::new(render_custom_size_header( appearance, - "Models", + crate::menu_label("settings.ai.models", "Models"), 14.0, Some(styles::header_font_color(is_any_ai_enabled, app)), )) @@ -5626,7 +5653,7 @@ impl AgentsWidget { let max = cw.max; let label = Container::new(render_body_item_label::( - "Context window (tokens)".to_string(), + crate::menu_label("settings.ai.context_window_tokens", "Context window (tokens)").to_string(), None, None, LocalOnlyIconState::Hidden, @@ -5745,7 +5772,7 @@ impl AgentsWidget { let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app); let permissions_subheader = Container::new(render_custom_size_header( appearance, - "Permissions", + crate::menu_label("settings.ai.permissions", "Permissions"), 14.0, Some(styles::header_font_color(is_any_ai_enabled, app)), )) @@ -5755,7 +5782,7 @@ impl AgentsWidget { let code_diff_setting = BlocklistAIPermissions::as_ref(app).get_apply_code_diffs_setting(app, None); let code_diffs = self.render_execution_profile_dropdown( - "Apply code diffs", + crate::menu_label("settings.ai.apply_code_diffs", "Apply code diffs"), Icon::Code2, code_diff_setting.description(), &view.apply_code_diffs_dropdown_menu, @@ -5768,7 +5795,7 @@ impl AgentsWidget { BlocklistAIPermissions::as_ref(app).get_read_files_setting(app, None); let mut read_files_flex = Flex::column().with_main_axis_size(MainAxisSize::Min); read_files_flex.add_child(self.render_execution_profile_dropdown( - "Read files", + crate::menu_label("settings.ai.read_files", "Read files"), Icon::Notebook, read_files_setting.description(), &view.read_files_dropdown_menu, @@ -5798,7 +5825,7 @@ impl AgentsWidget { BlocklistAIPermissions::as_ref(app).get_execute_commands_setting(app, None); let mut execute_commands_flex = Flex::column().with_main_axis_size(MainAxisSize::Min); execute_commands_flex.add_child(self.render_execution_profile_dropdown( - "Execute commands", + crate::menu_label("settings.ai.execute_commands", "Execute commands"), Icon::Terminal, execute_commands_setting.description(), &view.execute_commands_dropdown_menu, @@ -5847,7 +5874,7 @@ impl AgentsWidget { { widget_children.push( Container::new(render_settings_info_banner( - "Some of your permissions are managed by your workspace.", + crate::menu_label("settings.ai.some_permissions_managed", "Some of your permissions are managed by your workspace."), None, appearance, )) @@ -5861,7 +5888,7 @@ impl AgentsWidget { let write_to_pty_setting = BlocklistAIPermissions::as_ref(app).get_write_to_pty_setting(app, None); let write_to_pty = self.render_execution_profile_dropdown( - "Interact with running commands", + crate::menu_label("settings.ai.interact_with_running_commands", "Interact with running commands"), Icon::Workflow, write_to_pty_setting.description(), &view.write_to_pty_autonomy_dropdown_menu, @@ -5997,8 +6024,8 @@ impl AgentsWidget { appearance, ); render_ai_list( - "Command denylist", - "Regular expressions to match commands that the Warp Agent should always ask permission to execute.", + crate::menu_label("settings.ai.command_denylist", "Command denylist"), + crate::menu_label("settings.ai.command_denylist_description", "Regular expressions to match commands that the Warp Agent should always ask permission to execute."), list, view, ai_settings, @@ -6032,8 +6059,8 @@ impl AgentsWidget { ); render_ai_list( - "Command allowlist", - "Regular expressions to match commands that can be automatically executed by the Warp Agent.", + crate::menu_label("settings.ai.command_allowlist", "Command allowlist"), + crate::menu_label("settings.ai.command_allowlist_description", "Regular expressions to match commands that can be automatically executed by the Warp Agent."), list, view, ai_settings, @@ -6070,8 +6097,8 @@ impl AgentsWidget { ); render_ai_list( - "Directory allowlist", - "Give the agent file access to certain directories.", + crate::menu_label("settings.ai.directory_allowlist", "Directory allowlist"), + crate::menu_label("settings.ai.directory_allowlist_description", "Give the agent file access to certain directories."), list, view, ai_settings, @@ -6113,7 +6140,7 @@ impl AgentsWidget { .finish(), appearance .ui_builder() - .span("Show model picker in prompt".to_string()) + .span(crate::menu_label("settings.ai.show_model_picker_in_prompt", "Show model picker in prompt").to_string()) .with_style(UiComponentStyles { font_color: Some( theme.sub_text_color(theme.surface_2()).into_solid(), @@ -6133,9 +6160,9 @@ impl AgentsWidget { render_dropdown_item( appearance, - "Base model", + crate::menu_label("settings.ai.base_model", "Base model"), Some( - "This model serves as the primary engine behind the Warp Agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization.", + crate::menu_label("settings.ai.base_model_description", "This model serves as the primary engine behind the Warp Agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization."), ), Some(show_in_prompt_checkbox), LocalOnlyIconState::Hidden, @@ -6155,7 +6182,7 @@ impl AgentsWidget { ) -> Box { let code_settings = CodeSettings::as_ref(app); let toggle = render_ai_setting_toggle::( - "Codebase Context", + crate::menu_label("settings.ai.codebase_context", "Codebase Context"), AISettingsPageAction::ToggleCodebaseContext, *code_settings.codebase_context_enabled, ai_settings.is_any_ai_enabled(app), @@ -6169,7 +6196,7 @@ impl AgentsWidget { "Allow the Warp Agent to generate an outline of your codebase that can be used for context. No code is ever stored on our servers. ", ), FormattedTextFragment::hyperlink( - "Learn more", + crate::menu_label("settings.ai.learn_more", "Learn more"), "https://docs.warp.dev/agent-platform/capabilities/codebase-context", ), ]; @@ -6222,7 +6249,7 @@ impl AgentsWidget { app: &AppContext, ) -> Box { let header = Container::new(render_body_item_label_with_icon::( - "Call MCP servers".into(), + crate::menu_label("settings.ai.call_mcp_servers", "Call MCP servers").into(), Icon::Dataflow, Some(styles::header_font_color( ai_settings.is_any_ai_enabled(app), @@ -6242,7 +6269,7 @@ impl AgentsWidget { "You haven't added any MCP servers yet. Once you do, you'll be able to control how much autonomy the Warp Agent has when interacting with them. ", ), FormattedTextFragment::hyperlink_action( - "Add a server", + crate::menu_label("settings.ai.add_mcp_server", "Add a server"), AISettingsPageAction::OpenMCPServerCollection, ), FormattedTextFragment::plain_text(" or "), @@ -6305,7 +6332,7 @@ impl AgentsWidget { BlocklistAIPermissions::as_ref(app).get_mcp_permissions_setting(app, None); let permission_setting = self.render_execution_profile_dropdown( - "Call MCP servers", + crate::menu_label("settings.ai.call_mcp_servers", "Call MCP servers"), Icon::Dataflow, current_mcp_setting.description(), &view.mcp_permissions_dropdown_menu, @@ -6319,8 +6346,8 @@ impl AgentsWidget { || current_mcp_setting == ActionPermission::AgentDecides { let allowlist = self.render_mcp_list( - "MCP allowlist", - "Allow the Warp Agent to call these MCP servers.", + crate::menu_label("settings.ai.mcp_allowlist", "MCP allowlist"), + crate::menu_label("settings.ai.mcp_allowlist_description", "Allow the Warp Agent to call these MCP servers."), &view.mcp_allowlist_dropdown, BlocklistAIPermissions::as_ref(app).get_mcp_allowlist(app, None), view.mcp_allowlist_mouse_state_handles.clone(), @@ -6336,8 +6363,8 @@ impl AgentsWidget { || current_mcp_setting == ActionPermission::AgentDecides { let denylist = self.render_mcp_list( - "MCP denylist", - "The Warp Agent will always ask for permission before calling any MCP servers on this list.", + crate::menu_label("settings.ai.mcp_denylist", "MCP denylist"), + crate::menu_label("settings.ai.mcp_denylist_description", "The Warp Agent will always ask for permission before calling any MCP servers on this list."), &view.mcp_denylist_dropdown, BlocklistAIPermissions::as_ref(app).get_mcp_denylist(app, None), view.mcp_denylist_mouse_state_handles.clone(), @@ -6447,7 +6474,7 @@ impl SettingsWidget for AIInputWidget { let input_header = build_sub_header( appearance, - "Input", + crate::menu_label("settings.ai.input", "Input"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -6464,7 +6491,7 @@ impl SettingsWidget for AIInputWidget { ); let show_input_hint_text = render_ai_setting_toggle::( - "Show input hint text", + crate::menu_label("settings.ai.show_input_hint_text", "Show input hint text"), AISettingsPageAction::ToggleShowInputHintText, *InputSettings::as_ref(app).show_hint_text, is_any_ai_enabled, @@ -6482,7 +6509,7 @@ impl SettingsWidget for AIInputWidget { if FeatureFlag::AgentTips.is_enabled() { let agent_tips_toggle = render_ai_setting_toggle::( - "Show agent tips", + crate::menu_label("settings.ai.show_agent_tips", "Show agent tips"), AISettingsPageAction::ToggleShowAgentTips, *InputSettings::as_ref(app).show_agent_tips, is_any_ai_enabled, @@ -6494,7 +6521,7 @@ impl SettingsWidget for AIInputWidget { } widget_children.push(render_ai_setting_toggle::( - "Include agent-executed commands in history", + crate::menu_label("settings.ai.include_agent_commands_in_history", "Include agent-executed commands in history"), AISettingsPageAction::ToggleIncludeAgentCommandsInHistory, *ai_settings.include_agent_commands_in_history, is_any_ai_enabled, @@ -6601,7 +6628,7 @@ impl AIInputWidget { section.add_children([ render_ai_setting_toggle::( - "Autodetect agent prompts in terminal input", + crate::menu_label("settings.ai.autodetect_agent_prompts", "Autodetect agent prompts in terminal input"), AISettingsPageAction::ToggleNLDInTerminal, ai_settings.is_nld_in_terminal_enabled(app), is_toggleable, @@ -6610,7 +6637,7 @@ impl AIInputWidget { app, ), render_ai_setting_toggle::( - "Autodetect terminal commands in agent input", + crate::menu_label("settings.ai.autodetect_terminal_commands", "Autodetect terminal commands in agent input"), AISettingsPageAction::ToggleAIInputAutoDetection, is_nld_enabled, is_toggleable, @@ -6660,7 +6687,7 @@ impl AIInputWidget { section.add_children([ render_ai_setting_toggle::( - "Natural language detection", + crate::menu_label("settings.ai.natural_language_detection", "Natural language detection"), AISettingsPageAction::ToggleAIInputAutoDetection, is_nld_enabled, is_toggleable, @@ -6694,13 +6721,13 @@ impl AIInputWidget { section .with_child(render_ai_setting_label::( - "Natural language denylist".to_owned(), + crate::menu_label("settings.ai.natural_language_denylist", "Natural language denylist").to_owned(), is_toggleable, &view.local_only_icon_tooltip_states, app, )) .with_child(render_ai_setting_description( - "Commands listed here will never trigger natural language detection.", + crate::menu_label("settings.ai.natural_language_denylist_description", "Commands listed here will never trigger natural language detection."), is_toggleable, app, )) @@ -6749,7 +6776,7 @@ impl SettingsWidget for MCPServersWidget { let header = build_sub_header( appearance, - "MCP Servers", + crate::menu_label("settings.ai.mcp_servers", "MCP Servers"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -6761,7 +6788,7 @@ impl SettingsWidget for MCPServersWidget { MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins. ", ), FormattedTextFragment::hyperlink( - "Learn more", + crate::menu_label("settings.ai.learn_more", "Learn more"), "https://docs.warp.dev/agent-platform/capabilities/mcp", ), ]; @@ -6790,7 +6817,7 @@ impl SettingsWidget for MCPServersWidget { Some( Flex::column() .with_child(render_ai_setting_toggle::( - "Auto-spawn servers from third-party agents", + crate::menu_label("settings.ai.auto_spawn_servers", "Auto-spawn servers from third-party agents"), AISettingsPageAction::ToggleFileBasedMcp, *ai_settings.file_based_mcp_enabled, is_any_ai_enabled, @@ -6841,7 +6868,7 @@ impl SettingsWidget for MCPServersWidget { }; let button = render_full_pane_width_ai_button( - "Manage MCP servers", + crate::menu_label("settings.ai.manage_mcp_servers", "Manage MCP servers"), is_any_ai_enabled, self.manage_mcp_servers_button.clone(), AISettingsPageAction::OpenMCPServerCollection, @@ -6878,7 +6905,7 @@ impl AIFactWidget { app: &warpui::AppContext, ) -> Box { let toggle = render_ai_setting_toggle::( - "Rules", + crate::menu_label("settings.ai.rules", "Rules"), AISettingsPageAction::ToggleRules, *ai_settings.memory_enabled, ai_settings.is_any_ai_enabled(app), @@ -6892,7 +6919,7 @@ impl AIFactWidget { "Rules help the Warp Agent follow your conventions, whether for codebases or specific workflows. ", ), FormattedTextFragment::hyperlink( - "Learn more", + crate::menu_label("settings.ai.learn_more", "Learn more"), "https://docs.warp.dev/agent-platform/capabilities/rules", ), ]; @@ -6929,7 +6956,7 @@ impl AIFactWidget { app: &warpui::AppContext, ) -> Box { let toggle = render_ai_setting_toggle::( - "Suggested Rules", + crate::menu_label("settings.ai.suggested_rules", "Suggested Rules"), AISettingsPageAction::ToggleRuleSuggestions, *ai_settings.rule_suggestions_enabled_internal, ai_settings.is_any_ai_enabled(app), @@ -6939,7 +6966,7 @@ impl AIFactWidget { ); let description = render_ai_setting_description( - "Let AI suggest rules to save based on your interactions.", + crate::menu_label("settings.ai.rule_suggestions_description", "Let AI suggest rules to save based on your interactions."), ai_settings.is_any_ai_enabled(app), app, ); @@ -6957,7 +6984,7 @@ impl AIFactWidget { app: &warpui::AppContext, ) -> Box { let toggle = render_ai_setting_toggle::( - "Warp Drive as agent context", + crate::menu_label("settings.ai.warp_drive_context", "Warp Drive as agent context"), AISettingsPageAction::ToggleWarpDriveContext, *ai_settings.warp_drive_context_enabled, ai_settings.is_any_ai_enabled(app), @@ -6967,7 +6994,7 @@ impl AIFactWidget { ); let description = render_ai_setting_description( - "The Warp Agent can leverage your Warp Drive Contents to tailor responses to your personal and team developer workflows and environments. This includes any Workflows, Notebooks, and Environment Variables.", + crate::menu_label("settings.ai.warp_drive_context_description", "The Warp Agent can leverage your Warp Drive Contents to tailor responses to your personal and team developer workflows and environments. This includes any Workflows, Notebooks, and Environment Variables."), ai_settings.is_any_ai_enabled(app), app, ); @@ -7001,14 +7028,14 @@ impl SettingsWidget for AIFactWidget { let header = build_sub_header( appearance, - "Knowledge", + crate::menu_label("settings.ai.knowledge", "Knowledge"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_margin_bottom(HEADER_PADDING) .finish(); let button = render_full_pane_width_ai_button( - "Manage rules", + crate::menu_label("settings.ai.manage_rules", "Manage rules"), is_any_ai_enabled, self.manage_rules_button.clone(), AISettingsPageAction::OpenAIFactCollection, @@ -7046,7 +7073,7 @@ impl VoiceWidget { let ai_settings = AISettings::as_ref(app); let is_toggleable = ai_settings.is_any_ai_enabled(app); let mut column = Flex::column().with_child(render_ai_setting_toggle::( - "Voice Input", + crate::menu_label("settings.ai.voice_input", "Voice Input"), AISettingsPageAction::ToggleVoiceInput, *ai_settings.voice_input_enabled_internal, is_toggleable, @@ -7089,8 +7116,8 @@ impl VoiceWidget { if ai_settings.is_voice_input_enabled(app) { column.add_child(render_dropdown_item( appearance, - "Key for Activating Voice Input", - Some("Press and hold to activate."), + crate::menu_label("settings.ai.key_for_voice_input", "Key for Activating Voice Input"), + Some(crate::menu_label("settings.ai.key_for_voice_input_description", "Press and hold to activate.")), None, LocalOnlyIconState::for_setting( VoiceInputToggleKey::storage_key(), @@ -7131,7 +7158,7 @@ impl SettingsWidget for VoiceWidget { .with_child( build_sub_header( appearance, - "Voice", + crate::menu_label("settings.ai.voice", "Voice"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -7262,7 +7289,7 @@ impl SettingsWidget for OtherAIWidget { .with_child( build_sub_header( appearance, - "Other", + crate::menu_label("settings.ai.other_category", "Other"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -7272,7 +7299,7 @@ impl SettingsWidget for OtherAIWidget { if FeatureFlag::AgentView.is_enabled() { let mut agent_view_column = Flex::column() .with_child(render_ai_setting_toggle::( - "Show Oz changelog in new conversation view", + crate::menu_label("settings.ai.show_oz_changelog", "Show Oz changelog in new conversation view"), AISettingsPageAction::ToggleShowOzUpdatesInZeroState, *ai_settings.should_show_oz_updates_in_zero_state, is_toggleable, @@ -7306,7 +7333,7 @@ impl SettingsWidget for OtherAIWidget { } column.add_child(render_ai_setting_toggle::( - "Show conversation history in tools panel", + crate::menu_label("settings.ai.show_conversation_history", "Show conversation history in tools panel"), AISettingsPageAction::ToggleShowConversationHistory, *ai_settings.show_conversation_history, is_toggleable, @@ -7317,8 +7344,8 @@ impl SettingsWidget for OtherAIWidget { column.add_child(render_dropdown_item( appearance, - "Agent thinking display", - Some("Controls how reasoning/thinking traces are displayed."), + crate::menu_label("settings.ai.agent_thinking_display", "Agent thinking display"), + Some(crate::menu_label("settings.ai.thinking_display_description", "Controls how reasoning/thinking traces are displayed.")), None, LocalOnlyIconState::for_setting( ThinkingDisplayMode::storage_key(), @@ -7353,7 +7380,7 @@ impl SettingsWidget for OtherAIWidget { column.add_child(render_dropdown_item( appearance, - "Preferred layout when opening existing agent conversations", + crate::menu_label("settings.ai.preferred_layout_open_conversation", "Preferred layout when opening existing agent conversations"), None, None, LocalOnlyIconState::for_setting( @@ -7405,7 +7432,7 @@ impl SettingsWidget for CLIAgentWidget { // global AI toggle, because these settings control third-party coding // agents (Claude Code, Codex, Gemini CLI) rather than Warp's own AI. let cli_agent_footer_toggle = render_ai_setting_toggle::( - "Show coding agent toolbar", + crate::menu_label("settings.ai.show_coding_agent_toolbar", "Show coding agent toolbar"), AISettingsPageAction::ToggleCLIAgentToolbar, *ai_settings.should_render_cli_agent_footer, true, @@ -7441,7 +7468,7 @@ impl SettingsWidget for CLIAgentWidget { .with_child( build_sub_header( appearance, - "Third party CLI agents", + crate::menu_label("settings.ai.third_party_cli_agents", "Third party CLI agents"), Some(styles::header_font_color(true, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -7466,14 +7493,14 @@ impl SettingsWidget for CLIAgentWidget { if FeatureFlag::CLIAgentRichInput.is_enabled() { // Setting 1: Auto show/hide rich input based on agent status let auto_show_toggle_label = render_body_item_label::( - "Auto show/hide Rich Input based on agent status".into(), + crate::menu_label("settings.ai.auto_show_hide_rich_input", "Auto show/hide Rich Input based on agent status").into(), Some(styles::header_font_color(true, app)), Some(AdditionalInfo { mouse_state: self.auto_toggle_rich_input_info_tooltip.clone(), on_click_action: None, secondary_text: None, tooltip_override_text: Some( - "Requires the Warp plugin for your coding agent".to_owned(), + crate::menu_label("settings.ai.requires_warp_plugin", "Requires the Warp plugin for your coding agent").to_owned(), ), }), LocalOnlyIconState::for_setting( @@ -7500,7 +7527,7 @@ impl SettingsWidget for CLIAgentWidget { column.add_child( render_ai_setting_toggle::( - "Auto open Rich Input when a coding agent session starts", + crate::menu_label("settings.ai.auto_open_rich_input", "Auto open Rich Input when a coding agent session starts"), AISettingsPageAction::ToggleAutoOpenRichInputOnCLIAgentStart, *ai_settings.auto_open_rich_input_on_cli_agent_start, true, @@ -7512,7 +7539,7 @@ impl SettingsWidget for CLIAgentWidget { // Setting 2: Auto dismiss rich input after prompt submission column.add_child(render_ai_setting_toggle::( - "Auto dismiss Rich Input after prompt submission", + crate::menu_label("settings.ai.auto_dismiss_rich_input", "Auto dismiss Rich Input after prompt submission"), AISettingsPageAction::ToggleAutoDismissRichInputAfterSubmit, *ai_settings.auto_dismiss_rich_input_after_submit, true, @@ -7539,7 +7566,7 @@ impl SettingsWidget for CLIAgentWidget { list_column.add_child( appearance .ui_builder() - .span("Commands that enable the toolbar".to_string()) + .span(crate::menu_label("settings.ai.commands_enable_toolbar", "Commands that enable the toolbar").to_string()) .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE), ..Default::default() @@ -7631,7 +7658,7 @@ impl SettingsWidget for CLIAgentWidget { let command_list_description = appearance .ui_builder() .paragraph( - "Add regex patterns to show the coding agent toolbar for matching commands.", + crate::menu_label("settings.ai.add_regex_patterns_toolbar", "Add regex patterns to show the coding agent toolbar for matching commands."), ) .with_style(UiComponentStyles { font_size: Some(appearance.ui_font_size()), @@ -7732,7 +7759,7 @@ impl SettingsWidget for AgentAttributionWidget { .switch(self.toggle.clone()) .check(state.is_enabled) .with_tooltip(TooltipConfig { - text: "This option is enforced by your organization's settings and cannot be customized.".to_string(), + text: crate::menu_label("settings.ai.org_enforced_tooltip", "This option is enforced by your organization's settings and cannot be customized.").to_string(), styles: ui_builder.default_tool_tip_styles(), }) .disable() @@ -7758,7 +7785,7 @@ impl SettingsWidget for AgentAttributionWidget { let toggle_row = build_toggle_element( render_body_item_label::( - "Enable agent attribution".to_string(), + crate::menu_label("settings.ai.enable_agent_attribution", "Enable agent attribution").to_string(), Some(styles::header_font_color(!state.is_disabled, app)), None, LocalOnlyIconState::Hidden, @@ -7775,7 +7802,7 @@ impl SettingsWidget for AgentAttributionWidget { .with_child( build_sub_header( appearance, - "Agent Attribution", + crate::menu_label("settings.ai.agent_attribution", "Agent Attribution"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -7783,7 +7810,7 @@ impl SettingsWidget for AgentAttributionWidget { ) .with_child(toggle_row) .with_child(render_ai_setting_description( - "Oz can add attribution to commit messages and pull requests it creates", + crate::menu_label("settings.ai.agent_attribution_description", "Oz can add attribution to commit messages and pull requests it creates"), !state.is_disabled, app, )) @@ -7835,7 +7862,7 @@ impl SettingsWidget for CloudAgentComputerUseWidget { .switch(self.toggle.clone()) .check(is_checked) .with_tooltip(TooltipConfig { - text: "This option is enforced by your organization's settings and cannot be customized.".to_string(), + text: crate::menu_label("settings.ai.org_enforced_tooltip", "This option is enforced by your organization's settings and cannot be customized.").to_string(), styles: ui_builder.default_tool_tip_styles(), }) .disable() @@ -7863,7 +7890,7 @@ impl SettingsWidget for CloudAgentComputerUseWidget { let toggle_row = build_toggle_element( render_body_item_label::( - "Computer use in Cloud Agents".to_string(), + crate::menu_label("settings.ai.computer_use_in_cloud_agents", "Computer use in Cloud Agents").to_string(), Some(styles::header_font_color(!is_disabled, app)), None, LocalOnlyIconState::Hidden, @@ -7880,7 +7907,7 @@ impl SettingsWidget for CloudAgentComputerUseWidget { .with_child( build_sub_header( appearance, - "Experimental", + crate::menu_label("settings.ai.experimental", "Experimental"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -7888,7 +7915,7 @@ impl SettingsWidget for CloudAgentComputerUseWidget { ) .with_child(toggle_row) .with_child(render_ai_setting_description( - "Enable computer use in cloud agent conversations started from the Warp app.", + crate::menu_label("settings.ai.computer_use_description", "Enable computer use in cloud agent conversations started from the Warp app."), !is_disabled, app, )) @@ -7934,7 +7961,7 @@ impl SettingsWidget for CloudHandoffWidget { let is_force_disabled = !is_any_ai_enabled || cloud_convos_off; let tooltip_text = if cloud_convos_off { - "Cloud handoff requires cloud conversations to be enabled." + crate::menu_label("settings.ai.cloud_handoff_requires_cloud", "Cloud handoff requires cloud conversations to be enabled.") } else { "" }; @@ -7963,7 +7990,7 @@ impl SettingsWidget for CloudHandoffWidget { let handoff_row = build_toggle_element( render_body_item_label::( - "Cloud handoff".to_string(), + crate::menu_label("settings.ai.cloud_handoff", "Cloud handoff").to_string(), Some(styles::header_font_color(!is_force_disabled, app)), None, LocalOnlyIconState::Hidden, @@ -7980,7 +8007,7 @@ impl SettingsWidget for CloudHandoffWidget { .with_child( build_sub_header( appearance, - "Cloud Handoff", + crate::menu_label("settings.ai.cloud_handoff_header", "Cloud Handoff"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -7988,7 +8015,7 @@ impl SettingsWidget for CloudHandoffWidget { ) .with_child(handoff_row) .with_child(render_ai_setting_description( - "Hand off local agent conversations to a cloud agent.", + crate::menu_label("settings.ai.cloud_handoff_description", "Hand off local agent conversations to a cloud agent."), !is_force_disabled, app, )); @@ -8037,7 +8064,7 @@ impl SettingsWidget for CloudHandoffWidget { let ampersand_row = build_toggle_element( render_body_item_label::( - "Use & to trigger handoff".to_string(), + crate::menu_label("settings.ai.use_ampersand_handoff", "Use & to trigger handoff").to_string(), Some(styles::header_font_color(true, app)), None, LocalOnlyIconState::Hidden, @@ -8051,7 +8078,7 @@ impl SettingsWidget for CloudHandoffWidget { column.add_child(ampersand_row); column.add_child(render_ai_setting_description( - "Type & as the first character to enter cloud handoff compose mode.", + crate::menu_label("settings.ai.ampersand_handoff_description", "Type & as the first character to enter cloud handoff compose mode."), true, app, )); @@ -8348,21 +8375,21 @@ impl ApiKeysWidget { let mut column = Flex::column().with_spacing(16.); column.add_child(self.render_api_key_input( appearance, - "OpenAI API key", + crate::menu_label("settings.ai.openai_api_key", "OpenAI API key"), self.openai_api_key_editor.clone(), is_enabled, app, )); column.add_child(self.render_api_key_input( appearance, - "Anthropic API key", + crate::menu_label("settings.ai.anthropic_api_key", "Anthropic API key"), self.anthropic_api_key_editor.clone(), is_enabled, app, )); column.add_child(self.render_api_key_input( appearance, - "Google API key", + crate::menu_label("settings.ai.google_api_key", "Google API key"), self.google_api_key_editor.clone(), is_enabled, app, @@ -8376,7 +8403,7 @@ impl ApiKeysWidget { FormattedTextFragment::plain_text( "Use your own API keys from model providers for Warp Agent. You can also add custom endpoints to use third-party models. Custom endpoints must support the OpenAI-compatible Chat Completions API. API keys are stored only on your device, never on Warp's servers. They're used to make requests to your chosen model provider. Using auto models or models from providers you have not provided API keys for will consume Warp credits. ", ), - FormattedTextFragment::hyperlink("Learn more", CUSTOM_INFERENCE_LEARN_MORE_URL), + FormattedTextFragment::hyperlink(crate::menu_label("settings.ai.learn_more", "Learn more"), CUSTOM_INFERENCE_LEARN_MORE_URL), ]; let description = FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(text_fragments)]), @@ -8700,7 +8727,7 @@ impl ApiKeysWidget { let ai_settings = AISettings::as_ref(app); let toggle = render_ai_setting_toggle::( - "Warp credit fallback", + crate::menu_label("settings.ai.warp_credit_fallback", "Warp credit fallback"), AISettingsPageAction::ToggleCanUseWarpCreditsForFallback, *ai_settings.can_use_warp_credits_for_fallback, ai_settings.is_any_ai_enabled(app), @@ -8710,7 +8737,7 @@ impl ApiKeysWidget { ); let description = render_ai_setting_description( - "When enabled, agent requests may be routed to one of Warp's provided models in the event of an error. Warp will prioritize using your API keys over your Warp credits.", + crate::menu_label("settings.ai.warp_credit_fallback_description", "When enabled, agent requests may be routed to one of Warp's provided models in the event of an error. Warp will prioritize using your API keys over your Warp credits."), ai_settings.is_any_ai_enabled(app), app, ); @@ -8748,13 +8775,13 @@ impl SettingsWidget for ApiKeysWidget { let mut column = Flex::column().with_child(render_separator(appearance)); if show_custom_inference { - // Header row: "Custom inference" + info icon on left, "+ Add custom model" on right + // Header row: crate::menu_label("settings.ai.custom_inference", "Custom inference") + info icon on left, crate::menu_label("settings.ai.add_custom_model", "+ Add custom model") on right let header_left = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( build_sub_header( appearance, - "Custom inference", + crate::menu_label("settings.ai.custom_inference", "Custom inference"), Some(styles::header_font_color( custom_inference_controls_enabled, app, @@ -8783,11 +8810,11 @@ impl SettingsWidget for ApiKeysWidget { // Description with Learn more link column.add_child(self.render_custom_inference_description(app)); } else { - // Fallback: old "API Keys" header only + // Fallback: old crate::menu_label("settings.ai.api_keys", "API Keys") header only column.add_child( build_sub_header( appearance, - "API Keys", + crate::menu_label("settings.ai.api_keys", "API Keys"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) @@ -8805,7 +8832,7 @@ impl SettingsWidget for ApiKeysWidget { column.add_child( Container::new( Text::new_inline( - "Custom endpoints", + crate::menu_label("settings.ai.custom_endpoints", "Custom endpoints"), appearance.ui_font_family(), CONTENT_FONT_SIZE, ) @@ -9063,7 +9090,7 @@ impl AwsBedrockWidget { }); let refresh_credentials_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Refresh", SecondaryTheme) + ActionButton::new(crate::menu_label("settings.ai.refresh", "Refresh"), SecondaryTheme) .with_icon(Icon::RefreshCw04) .with_size(ButtonSize::Small) .on_click(|ctx| { @@ -9166,16 +9193,16 @@ impl AwsBedrockWidget { let are_credentials_enabled = user_workspaces.is_aws_bedrock_credentials_enabled(app); let is_usage_enabled = is_section_enabled && are_credentials_enabled; let toggle_description = if is_admin_enforced { - "Warp loads and sends local AWS CLI credentials for Bedrock-supported models. This setting is managed by your organization.".to_string() + crate::menu_label("settings.ai.aws_bedrock_managed_by_org", "Warp loads and sends local AWS CLI credentials for Bedrock-supported models. This setting is managed by your organization.").to_string() } else { - "Warp loads and sends local AWS CLI credentials for Bedrock-supported models." + crate::menu_label("settings.ai.aws_bedrock_description", "Warp loads and sends local AWS CLI credentials for Bedrock-supported models.") .to_string() }; let mut column = Flex::column().with_spacing(16.).with_child( Flex::column() .with_child(render_ai_setting_toggle::( - "Use AWS Bedrock credentials", + crate::menu_label("settings.ai.use_aws_bedrock_credentials", "Use AWS Bedrock credentials"), AISettingsPageAction::ToggleAwsBedrockCredentialsEnabled, are_credentials_enabled, is_toggleable, @@ -9307,14 +9334,14 @@ impl AwsBedrockWidget { ); column.add_child(render_input( appearance, - "Login Command", + crate::menu_label("settings.ai.login_command", "Login Command"), self.aws_auth_refresh_command_editor.clone(), is_usage_enabled, app, )); column.add_child(render_input( appearance, - "AWS Profile", + crate::menu_label("settings.ai.aws_profile", "AWS Profile"), self.aws_auth_refresh_profile_editor.clone(), is_usage_enabled, app, @@ -9323,7 +9350,7 @@ impl AwsBedrockWidget { let auto_login_enabled = *AISettings::as_ref(app).aws_bedrock_auto_login.value(); let toggle = render_ai_setting_toggle::( - "Automatically run login command", + crate::menu_label("settings.ai.automatically_run_login_command", "Automatically run login command"), AISettingsPageAction::ToggleAwsBedrockAutoLogin, auto_login_enabled, is_usage_enabled, @@ -9332,7 +9359,7 @@ impl AwsBedrockWidget { app, ); let description = render_ai_setting_description( - "When enabled, the login command will run automatically when AWS Bedrock credentials expire.", + crate::menu_label("settings.ai.auto_login_description", "When enabled, the login command will run automatically when AWS Bedrock credentials expire."), is_usage_enabled, app, ); @@ -9375,7 +9402,7 @@ impl SettingsWidget for AwsBedrockWidget { .with_child( build_sub_header( appearance, - "AWS Bedrock", + crate::menu_label("settings.ai.aws_bedrock", "AWS Bedrock"), Some(styles::header_font_color(is_any_ai_enabled, app)), ) .with_padding_bottom(HEADER_PADDING) diff --git a/app/src/settings_view/appearance_page.rs b/app/src/settings_view/appearance_page.rs index 7ea7fb386c..7188660fdd 100644 --- a/app/src/settings_view/appearance_page.rs +++ b/app/src/settings_view/appearance_page.rs @@ -117,10 +117,11 @@ const MIN_NEW_WINDOW_ROWS_OR_COLS: u16 = 5; const MAX_NEW_WINDOW_ROWS_OR_COLS: u16 = 2000; fn default_font_label(is_ai_font: bool) -> String { + let default_label = crate::menu_label("settings.appearance.default_label", "default"); if is_ai_font { - format!("{} (default)", AIFontName::default_value()) + format!("{} ({})", AIFontName::default_value(), default_label) } else { - format!("{} (default)", MonospaceFontName::default_value()) + format!("{} ({})", MonospaceFontName::default_value(), default_label) } } @@ -132,7 +133,7 @@ pub fn init_actions_from_parent_view( // Add all the toggle settings from the Appearance Page that you want to show up on the Command Palette here. let mut toggle_binding_pairs = vec![ ToggleSettingActionPair::new( - "compact mode", + crate::menu_label("settings.appearance.toggle.compact_mode", "compact mode"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleCompactMode, )), @@ -140,7 +141,7 @@ pub fn init_actions_from_parent_view( flags::COMPACT_MODE_CONTEXT_FLAG, ), ToggleSettingActionPair::new( - "themes: sync with OS", + crate::menu_label("settings.appearance.toggle.themes_sync_os", "themes: sync with OS"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleRespectSystemTheme, )), @@ -151,7 +152,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "cursor blink", + crate::menu_label("settings.appearance.toggle.cursor_blink", "cursor blink"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleCursorBlink, )), @@ -167,7 +168,10 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "jump to bottom of block button", + crate::menu_label( + "settings.appearance.toggle.jump_to_bottom_of_block_button", + "jump to bottom of block button", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleJumpToBottomOfBlockButton, )), @@ -183,7 +187,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "block dividers", + crate::menu_label("settings.appearance.toggle.block_dividers", "block dividers"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleShowBlockDividers, )), @@ -198,7 +202,10 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "dim inactive panes", + crate::menu_label( + "settings.appearance.toggle.dim_inactive_panes", + "dim inactive panes", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleDimInactivePanes, )), @@ -207,7 +214,11 @@ pub fn init_actions_from_parent_view( )); app.register_fixed_bindings(vec![FixedBinding::empty( - "Start Input at the Top".to_string(), + crate::menu_label( + "settings.appearance.binding.start_input_top", + "Start Input at the Top", + ) + .to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetInputMode { new_mode: InputMode::Waterfall, @@ -219,7 +230,11 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::Settings.as_str())]); app.register_fixed_bindings(vec![FixedBinding::empty( - "Pin Input to the Top".to_string(), + crate::menu_label( + "settings.appearance.binding.pin_input_top", + "Pin Input to the Top", + ) + .to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetInputMode { new_mode: InputMode::PinnedToTop, @@ -231,7 +246,11 @@ pub fn init_actions_from_parent_view( .with_group(bindings::BindingGroup::Settings.as_str())]); app.register_fixed_bindings(vec![FixedBinding::empty( - "Pin Input to the Bottom".to_string(), + crate::menu_label( + "settings.appearance.binding.pin_input_bottom", + "Pin Input to the Bottom", + ) + .to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetInputMode { new_mode: InputMode::PinnedToBottom, @@ -243,7 +262,11 @@ pub fn init_actions_from_parent_view( // Add command palette entry for toggling between Warp and Classic input modes app.register_fixed_bindings(vec![FixedBinding::empty( - "Toggle Input Mode (Warp/Classic)".to_string(), + crate::menu_label( + "settings.appearance.binding.toggle_input_mode", + "Toggle Input Mode (Warp/Classic)", + ) + .to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleInputMode, )), @@ -251,7 +274,10 @@ pub fn init_actions_from_parent_view( ) .with_group(bindings::BindingGroup::Settings.as_str())]); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "open new windows with custom size", + crate::menu_label( + "settings.appearance.toggle.open_new_windows_custom_size", + "open new windows with custom size", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleOpenWindowsAtCustomSize, )), @@ -260,7 +286,10 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "window blur acrylic texture", + crate::menu_label( + "settings.appearance.toggle.window_blur_acrylic_texture", + "window blur acrylic texture", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleBlurTexture, )), @@ -269,7 +298,10 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "tools panel visibility across tabs", + crate::menu_label( + "settings.appearance.toggle.tools_panel_visibility", + "tools panel visibility across tabs", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleLeftPanelVisibility, )), @@ -278,7 +310,10 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "agent font matching terminal font", + crate::menu_label( + "settings.appearance.toggle.agent_font_match_terminal", + "agent font matching terminal font", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleMatchAIToTerminalFontFamily, )), @@ -287,7 +322,10 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "notebook font size matching terminal font size", + crate::menu_label( + "settings.appearance.toggle.notebook_font_match_terminal", + "notebook font size matching terminal font size", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleMatchNotebookToMonospaceFontSize, )), @@ -297,7 +335,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "tab indicators", + crate::menu_label("settings.appearance.toggle.tab_indicators", "tab indicators"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleTabIndicators, )), @@ -315,8 +353,14 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::custom( SettingActionPairDescriptions::new( - "Show code review button in tab bar", - "Hide code review button in tab bar", + crate::menu_label( + "settings.appearance.toggle.show_code_review_button_in_tab_bar", + "Show code review button in tab bar", + ), + crate::menu_label( + "settings.appearance.toggle.hide_code_review_button_in_tab_bar", + "Hide code review button in tab bar", + ), ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleShowCodeReviewButton, @@ -337,7 +381,10 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "focus follows mouse", + crate::menu_label( + "settings.appearance.toggle.focus_follows_mouse", + "focus follows mouse", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleFocusPaneOnHover, )), @@ -355,7 +402,11 @@ pub fn init_actions_from_parent_view( // Add bindings for each visibility option. app.register_fixed_bindings([ FixedBinding::empty( - "Always show tab bar".to_string(), + crate::menu_label( + "settings.appearance.binding.always_show_tab_bar", + "Always show tab bar", + ) + .to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetWorkspaceDecorationVisibility( WorkspaceDecorationVisibility::AlwaysShow, @@ -365,7 +416,11 @@ pub fn init_actions_from_parent_view( ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::empty( - "Hide tab bar if fullscreen".to_string(), + crate::menu_label( + "settings.appearance.binding.hide_tab_bar_if_fullscreen", + "Hide tab bar if fullscreen", + ) + .to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetWorkspaceDecorationVisibility( WorkspaceDecorationVisibility::HideFullscreen, @@ -375,7 +430,11 @@ pub fn init_actions_from_parent_view( ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::empty( - "Only show tab bar on hover".to_string(), + crate::menu_label( + "settings.appearance.binding.only_show_tab_bar_on_hover", + "Only show tab bar on hover", + ) + .to_string(), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::SetWorkspaceDecorationVisibility( WorkspaceDecorationVisibility::OnHover, @@ -389,7 +448,7 @@ pub fn init_actions_from_parent_view( // Add a toggle alias for "Zen mode". toggle_binding_pairs.push( ToggleSettingActionPair::new( - "zen mode", + crate::menu_label("settings.appearance.toggle.zen_mode", "zen mode"), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleWorkspaceDecorationVisibility, )), @@ -406,7 +465,10 @@ pub fn init_actions_from_parent_view( if FeatureFlag::VerticalTabs.is_enabled() { toggle_binding_pairs.push(ToggleSettingActionPair::new( - "vertical tab layout", + crate::menu_label( + "settings.appearance.toggle.vertical_tab_layout", + "vertical tab layout", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleVerticalTabs, )), @@ -414,7 +476,10 @@ pub fn init_actions_from_parent_view( flags::USE_VERTICAL_TABS_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "show vertical tabs panel in restored windows", + crate::menu_label( + "settings.appearance.toggle.show_vertical_tabs_restored", + "show vertical tabs panel in restored windows", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleShowVerticalTabPanelInRestoredWindows, )), @@ -422,7 +487,10 @@ pub fn init_actions_from_parent_view( flags::SHOW_VERTICAL_TAB_PANEL_IN_RESTORED_WINDOWS_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "latest user prompt as conversation title in tab names", + crate::menu_label( + "settings.appearance.toggle.latest_prompt_conversation_title", + "latest user prompt as conversation title in tab names", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleUseLatestUserPromptAsConversationTitleInTabNames, )), @@ -433,7 +501,10 @@ pub fn init_actions_from_parent_view( if FeatureFlag::Ligatures.is_enabled() { toggle_binding_pairs.push(ToggleSettingActionPair::new( - "ligature rendering", + crate::menu_label( + "settings.appearance.toggle.ligature_rendering", + "ligature rendering", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleLigatureRendering, )), @@ -443,7 +514,10 @@ pub fn init_actions_from_parent_view( } toggle_binding_pairs.push(ToggleSettingActionPair::new( - "preserve active tab color for new tabs", + crate::menu_label( + "settings.appearance.toggle.preserve_active_tab_color", + "preserve active tab color for new tabs", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::TogglePreserveActiveTabColor, )), @@ -452,7 +526,10 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "custom padding in alt-screen", + crate::menu_label( + "settings.appearance.toggle.custom_padding_alt_screen", + "custom padding in alt-screen", + ), builder(SettingsAction::AppearancePageToggle( AppearancePageAction::ToggleAltScreenPadding, )), @@ -1324,7 +1401,7 @@ impl AppearanceSettingsPageView { fn build_page(ctx: &mut ViewContext) -> PageType { let mut categories = vec![Category::new( - "Themes", + crate::menu_label("settings.appearance.category.themes", "Themes"), vec![ Box::new(CreateCustomThemeWidget::default()), Box::new(ThemeSelectWidget::default()), @@ -1333,7 +1410,7 @@ impl AppearanceSettingsPageView { if AppIconSettings::as_ref(ctx).is_supported_on_current_platform() { categories.push(Category::new( - "Icon", + crate::menu_label("settings.appearance.category.icon", "Icon"), vec![Box::new(CustomAppIconWidget::default())], )); } @@ -1377,7 +1454,10 @@ impl AppearanceSettingsPageView { } if !window_settings_widgets.is_empty() { - categories.push(Category::new("Window", window_settings_widgets)); + categories.push(Category::new( + crate::menu_label("settings.appearance.category.window", "Window"), + window_settings_widgets, + )); } // Create the Input category with all widgets @@ -1389,10 +1469,13 @@ impl AppearanceSettingsPageView { Box::new(InputModeWidget::default()), ]; - categories.push(Category::new("Input", category_widgets)); + categories.push(Category::new( + crate::menu_label("settings.appearance.category.input", "Input"), + category_widgets, + )); categories.push(Category::new( - "Panes", + crate::menu_label("settings.appearance.category.panes", "Panes"), vec![ Box::new(DimInactivePanesWidget::default()), Box::new(FocusFollowsMouseWidget::default()), @@ -1406,7 +1489,10 @@ impl AppearanceSettingsPageView { if FeatureFlag::MinimalistUI.is_enabled() { block_settings_widgets.push(Box::new(ShowBlockDividersWidget::default())); } - categories.push(Category::new("Blocks", block_settings_widgets)); + categories.push(Category::new( + crate::menu_label("settings.appearance.category.blocks", "Blocks"), + block_settings_widgets, + )); let font_settings = FontSettings::as_ref(ctx); let mut text_settings_widgets: Vec>> = vec![ @@ -1435,10 +1521,13 @@ impl AppearanceSettingsPageView { text_settings_widgets.push(Box::new(LigaturesWidget::default())); } - categories.push(Category::new("Text", text_settings_widgets)); + categories.push(Category::new( + crate::menu_label("settings.appearance.category.text", "Text"), + text_settings_widgets, + )); categories.push(Category::new( - "Cursor", + crate::menu_label("settings.appearance.category.cursor", "Cursor"), vec![ Box::new(CursorTypeWidget::default()), Box::new(BlinkingCursorWidget::default()), @@ -1487,10 +1576,13 @@ impl AppearanceSettingsPageView { tab_settings_widgets.push(Box::new(DirectoryTabColorsWidget { add_picker })); } - categories.push(Category::new("Tabs", tab_settings_widgets)); + categories.push(Category::new( + crate::menu_label("settings.appearance.category.tabs", "Tabs"), + tab_settings_widgets, + )); categories.push(Category::new( - "Full-screen Apps", + crate::menu_label("settings.appearance.category.full_screen_apps", "Full-screen Apps"), vec![Box::new(AltScreenPaddingWidget::default())], )); @@ -1604,48 +1696,129 @@ impl AppearanceSettingsPageView { fn input_mode_dropdown_item_label(val: InputMode) -> &'static str { match val { - InputMode::PinnedToBottom => "Pin to the bottom (Warp mode)", - InputMode::PinnedToTop => "Pin to the top (Reverse mode)", - InputMode::Waterfall => "Start at the top (Classic mode)", + InputMode::PinnedToBottom => crate::menu_label( + "settings.appearance.dropdown.input_mode.pin_bottom_warp", + "Pin to the bottom (Warp mode)", + ), + InputMode::PinnedToTop => crate::menu_label( + "settings.appearance.dropdown.input_mode.pin_top_reverse", + "Pin to the top (Reverse mode)", + ), + InputMode::Waterfall => crate::menu_label( + "settings.appearance.dropdown.input_mode.start_top_classic", + "Start at the top (Classic mode)", + ), } } fn app_icon_dropdown_item_label(val: AppIcon) -> &'static str { match val { - AppIcon::Aurora => "Aurora", - AppIcon::Default => "Default", - AppIcon::Classic1 => "Classic 1", - AppIcon::Classic2 => "Classic 2", - AppIcon::Classic3 => "Classic 3", - AppIcon::Comets => "Comets", - AppIcon::GlassSky => "Glass Sky", - AppIcon::Glitch => "Glitch", - AppIcon::Cow => "Cow", - AppIcon::Glow => "Glow", - AppIcon::Holographic => "Holographic", - AppIcon::Mono => "Mono", - AppIcon::Neon => "Neon", - AppIcon::Original => "Original", - AppIcon::Starburst => "Starburst", - AppIcon::Sticker => "Sticker", - AppIcon::WarpOne => "Warp 1", + AppIcon::Aurora => crate::menu_label( + "settings.appearance.dropdown.app_icon.aurora", + "Aurora", + ), + AppIcon::Default => crate::menu_label( + "settings.appearance.dropdown.app_icon.default", + "Default", + ), + AppIcon::Classic1 => crate::menu_label( + "settings.appearance.dropdown.app_icon.classic1", + "Classic 1", + ), + AppIcon::Classic2 => crate::menu_label( + "settings.appearance.dropdown.app_icon.classic2", + "Classic 2", + ), + AppIcon::Classic3 => crate::menu_label( + "settings.appearance.dropdown.app_icon.classic3", + "Classic 3", + ), + AppIcon::Comets => crate::menu_label( + "settings.appearance.dropdown.app_icon.comets", + "Comets", + ), + AppIcon::GlassSky => crate::menu_label( + "settings.appearance.dropdown.app_icon.glass_sky", + "Glass Sky", + ), + AppIcon::Glitch => crate::menu_label( + "settings.appearance.dropdown.app_icon.glitch", + "Glitch", + ), + AppIcon::Cow => crate::menu_label( + "settings.appearance.dropdown.app_icon.cow", + "Cow", + ), + AppIcon::Glow => crate::menu_label( + "settings.appearance.dropdown.app_icon.glow", + "Glow", + ), + AppIcon::Holographic => crate::menu_label( + "settings.appearance.dropdown.app_icon.holographic", + "Holographic", + ), + AppIcon::Mono => crate::menu_label( + "settings.appearance.dropdown.app_icon.mono", + "Mono", + ), + AppIcon::Neon => crate::menu_label( + "settings.appearance.dropdown.app_icon.neon", + "Neon", + ), + AppIcon::Original => crate::menu_label( + "settings.appearance.dropdown.app_icon.original", + "Original", + ), + AppIcon::Starburst => crate::menu_label( + "settings.appearance.dropdown.app_icon.starburst", + "Starburst", + ), + AppIcon::Sticker => crate::menu_label( + "settings.appearance.dropdown.app_icon.sticker", + "Sticker", + ), + AppIcon::WarpOne => crate::menu_label( + "settings.appearance.dropdown.app_icon.warp_one", + "Warp 1", + ), } } fn thin_strokes_dropdown_item_label(val: ThinStrokes) -> &'static str { match val { - ThinStrokes::Never => "Never", - ThinStrokes::OnLowDpiDisplays => "On low-DPI displays", - ThinStrokes::OnHighDpiDisplays => "On high-DPI displays", - ThinStrokes::Always => "Always", + ThinStrokes::Never => crate::menu_label( + "settings.appearance.dropdown.thin_strokes.never", + "Never", + ), + ThinStrokes::OnLowDpiDisplays => crate::menu_label( + "settings.appearance.dropdown.thin_strokes.on_low_dpi", + "On low-DPI displays", + ), + ThinStrokes::OnHighDpiDisplays => crate::menu_label( + "settings.appearance.dropdown.thin_strokes.on_high_dpi", + "On high-DPI displays", + ), + ThinStrokes::Always => crate::menu_label( + "settings.appearance.dropdown.thin_strokes.always", + "Always", + ), } } fn enforce_minimum_contrast_dropdown_item_label(val: EnforceMinimumContrast) -> &'static str { match val { - EnforceMinimumContrast::Always => "Always", - EnforceMinimumContrast::OnlyNamedColors => "Only for named colors", - EnforceMinimumContrast::Never => "Never", + EnforceMinimumContrast::Always => crate::menu_label( + "settings.appearance.dropdown.enforce_minimum_contrast.always", + "Always", + ), + EnforceMinimumContrast::OnlyNamedColors => crate::menu_label( + "settings.appearance.dropdown.enforce_minimum_contrast.only_named_colors", + "Only for named colors", + ), + EnforceMinimumContrast::Never => crate::menu_label( + "settings.appearance.dropdown.enforce_minimum_contrast.never", + "Never", + ), } } @@ -1653,9 +1826,18 @@ impl AppearanceSettingsPageView { value: WorkspaceDecorationVisibility, ) -> &'static str { match value { - WorkspaceDecorationVisibility::AlwaysShow => "Always", - WorkspaceDecorationVisibility::HideFullscreen => "When windowed", - WorkspaceDecorationVisibility::OnHover => "Only on hover", + WorkspaceDecorationVisibility::AlwaysShow => crate::menu_label( + "settings.appearance.dropdown.workspace_decoration_visibility.always", + "Always", + ), + WorkspaceDecorationVisibility::HideFullscreen => crate::menu_label( + "settings.appearance.dropdown.workspace_decoration_visibility.when_windowed", + "When windowed", + ), + WorkspaceDecorationVisibility::OnHover => crate::menu_label( + "settings.appearance.dropdown.workspace_decoration_visibility.only_on_hover", + "Only on hover", + ), } } @@ -1663,8 +1845,14 @@ impl AppearanceSettingsPageView { value: TabCloseButtonPosition, ) -> &'static str { match value { - TabCloseButtonPosition::Right => "Right", - TabCloseButtonPosition::Left => "Left", + TabCloseButtonPosition::Right => crate::menu_label( + "settings.appearance.dropdown.tab_close_button_position.right", + "Right", + ), + TabCloseButtonPosition::Left => crate::menu_label( + "settings.appearance.dropdown.tab_close_button_position.left", + "Left", + ), } } @@ -2703,7 +2891,11 @@ impl SettingsWidget for CreateCustomThemeWidget { appearance .ui_builder() .link( - "Create your own custom theme".to_string(), + crate::menu_label( + "settings.appearance.create_custom_theme", + "Create your own custom theme", + ) + .to_string(), Some("https://docs.warp.dev/terminal/appearance/custom-themes".to_string()), None, self.mouse_state.clone(), @@ -2738,9 +2930,18 @@ impl ThemeSelectWidget { ) -> Box { let theme: WarpTheme = WarpConfig::as_ref(app).theme_config().theme(&theme_kind); let mode_ui_label = match theme_chooser_mode { - ThemeChooserMode::SystemLight => "Light", - ThemeChooserMode::SystemDark => "Dark", - ThemeChooserMode::SystemAgnostic => "Current theme", + ThemeChooserMode::SystemLight => crate::menu_label( + "settings.appearance.theme_mode.light", + "Light", + ), + ThemeChooserMode::SystemDark => crate::menu_label( + "settings.appearance.theme_mode.dark", + "Dark", + ), + ThemeChooserMode::SystemAgnostic => crate::menu_label( + "settings.appearance.theme_mode.current", + "Current theme", + ), }; ConstrainedBox::new( @@ -2859,7 +3060,11 @@ impl SettingsWidget for ThemeSelectWidget { Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child(render_body_item::( - "Sync with OS".into(), + crate::menu_label( + "settings.appearance.label.sync_with_os", + "Sync with OS", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( UseSystemTheme::storage_key(), @@ -2887,8 +3092,11 @@ impl SettingsWidget for ThemeSelectWidget { appearance .ui_builder() .span( - "Automatically switch between light and dark themes when your system does." - .to_string(), + crate::menu_label( + "settings.appearance.label.sync_os_description", + "Automatically switch between light and dark themes when your system does.", + ) + .to_string(), ) .with_style( UiComponentStyles::default().set_margin(Coords::default().bottom(10.)), @@ -2940,8 +3148,14 @@ impl SettingsWidget for CustomAppIconWidget { let dropdown = render_dropdown_item( appearance, - "Customize your app icon", - show_bundle_warning.then_some("Changing the app icon requires the app to be bundled."), + crate::menu_label( + "settings.appearance.label.customize_app_icon", + "Customize your app icon", + ), + show_bundle_warning.then_some(crate::menu_label( + "settings.appearance.label.app_icon_change_warning", + "Changing the app icon requires the app to be bundled.", + )), None, LocalOnlyIconState::Hidden, None, @@ -2949,7 +3163,11 @@ impl SettingsWidget for CustomAppIconWidget { ); let show_dock_icon_toggle = render_body_item::( - "Show Warp in Dock".into(), + crate::menu_label( + "settings.appearance.label.show_warp_in_dock", + "Show Warp in Dock", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( ShowDockIconState::storage_key(), @@ -2989,7 +3207,10 @@ impl SettingsWidget for CustomAppIconWidget { appearance .ui_builder() .wrappable_text( - "You may need to restart Warp for MacOS to apply the preferred icon style.", + crate::menu_label( + "settings.appearance.label.restart_for_icon_macos", + "You may need to restart Warp for MacOS to apply the preferred icon style.", + ), true, ) .with_style(UiComponentStyles { @@ -3045,7 +3266,11 @@ impl SettingsWidget for CustomWindowSizeWidget { let row_border_color: Option = (!view.valid_new_window_rows).then(|| themes::theme::Fill::error().into()); let mut column = Flex::column().with_child(render_body_item::( - "Open new windows with custom size".into(), + crate::menu_label( + "settings.appearance.label.open_new_windows_custom_size", + "Open new windows with custom size", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( OpenWindowsAtCustomSize::storage_key(), @@ -3069,7 +3294,8 @@ impl SettingsWidget for CustomWindowSizeWidget { if *window_settings.open_windows_at_custom_size.value() { column.add_child( Container::new(render_body_item::( - "Columns".into(), + crate::menu_label("settings.appearance.label.columns", "Columns") + .to_owned(), None, // We show the local-only icon for this with the toggle, not the individual inputs. LocalOnlyIconState::Hidden, @@ -3105,7 +3331,7 @@ impl SettingsWidget for CustomWindowSizeWidget { ); column.add_child( Container::new(render_body_item::( - "Rows".into(), + crate::menu_label("settings.appearance.label.rows", "Rows").to_owned(), None, // We show the local-only icon for this with the toggle, not the individual inputs. LocalOnlyIconState::Hidden, @@ -3170,7 +3396,11 @@ impl SettingsWidget for WindowOpacityWidget { return Flex::column() .with_child( Container::new(render_body_item_label::( - "Window Opacity:".to_owned(), + crate::menu_label( + "settings.appearance.label.window_opacity", + "Window Opacity:", + ) + .to_owned(), None, None, LocalOnlyIconState::Hidden, @@ -3182,7 +3412,10 @@ impl SettingsWidget for WindowOpacityWidget { .with_child( Container::new( FormattedTextElement::from_str( - "Transparency is not supported with your graphics drivers.", + crate::menu_label( + "settings.appearance.label.transparency_not_supported", + "Transparency is not supported with your graphics drivers.", + ), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -3197,7 +3430,14 @@ impl SettingsWidget for WindowOpacityWidget { let opacity_value = *window_settings.background_opacity; let mut col = Flex::column().with_child(render_body_item::( - format!("Window Opacity: {opacity_value}"), + format!( + "{} {}", + crate::menu_label( + "settings.appearance.label.window_opacity_label", + "Window Opacity:" + ), + opacity_value + ), // TODO(CORE-3384) add AdditionalInfo here. None, LocalOnlyIconState::for_setting( @@ -3233,9 +3473,10 @@ impl SettingsWidget for WindowOpacityWidget { // Skip showing the warning for OpenGL since WGPU often incorrectly reports it as not // supporting alpha. if !window.supports_transparency() && window.graphics_backend() != GraphicsBackend::Gl { - let mut message = Cow::Borrowed( + let mut message: Cow = Cow::Owned(crate::menu_label( + "settings.appearance.label.transparency_may_not_support", "The selected graphics settings may not support rendering transparent windows.", - ); + ).to_string()); let gpu_settings = GPUSettings::as_ref(app); if (gpu_settings .prefer_low_power_gpu @@ -3245,10 +3486,11 @@ impl SettingsWidget for WindowOpacityWidget { .preferred_backend .is_supported_on_current_platform() { - message.to_mut().push_str( + message.to_mut().push_str(crate::menu_label( + "settings.appearance.label.try_changing_graphics_backend", " Try changing the settings for the graphics backend or integrated GPU in \ Features > System.", - ); + )); } col.add_child( @@ -3302,7 +3544,14 @@ impl SettingsWidget for WindowBlurWidget { Flex::column() .with_child(render_body_item::( - format!("Window Blur Radius: {blur_value}"), + format!( + "{} {}", + crate::menu_label( + "settings.appearance.label.window_blur_radius_label", + "Window Blur Radius:" + ), + blur_value + ), Some(label_info), LocalOnlyIconState::for_setting( BackgroundBlurRadius::storage_key(), @@ -3359,7 +3608,11 @@ impl SettingsWidget for WindowBlurTextureWidget { let window_settings = WindowSettings::as_ref(app); let use_blur_texture = *window_settings.background_blur_texture; let mut col = Flex::column().with_child(render_body_item::( - "Use Window Blur (Acrylic texture)".to_string(), + crate::menu_label( + "settings.appearance.label.use_window_blur_acrylic", + "Use Window Blur (Acrylic texture)", + ) + .to_string(), None, LocalOnlyIconState::for_setting( BackgroundBlurTexture::storage_key(), @@ -3385,7 +3638,10 @@ impl SettingsWidget for WindowBlurTextureWidget { col.add_child( Container::new( FormattedTextElement::from_str( - "The selected hardware may not support rendering transparent windows.", + crate::menu_label( + "settings.appearance.label.transparency_hardware_may_not_support", + "The selected hardware may not support rendering transparent windows.", + ), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -3423,7 +3679,11 @@ impl SettingsWidget for ToolsPanelStateScopeWidget { let is_enabled = *window_settings.left_panel_visibility_across_tabs; render_body_item::( - "Tools panel visibility is consistent across tabs".to_string(), + crate::menu_label( + "settings.appearance.label.tools_panel_visibility_across_tabs", + "Tools panel visibility is consistent across tabs", + ) + .to_string(), None, LocalOnlyIconState::for_setting( LeftPanelVisibilityAcrossTabs::storage_key(), @@ -3478,8 +3738,14 @@ impl SettingsWidget for InputTypeWidget { .radio_buttons( self.radio_buttons_states.clone(), vec![ - RadioButtonItem::text("Warp"), - RadioButtonItem::text("Shell (PS1)"), + RadioButtonItem::text(crate::menu_label( + "settings.appearance.label.radio_warp", + "Warp", + )), + RadioButtonItem::text(crate::menu_label( + "settings.appearance.label.radio_shell_ps1", + "Shell (PS1)", + )), ], view.input_type_radio_state.clone(), Some(input_type as usize), @@ -3499,7 +3765,7 @@ impl SettingsWidget for InputTypeWidget { .finish(); render_body_item::( - "Input type".into(), + crate::menu_label("settings.appearance.label.input_type", "Input type").to_owned(), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -3528,7 +3794,7 @@ impl SettingsWidget for InputModeWidget { ) -> Box { render_dropdown_item( appearance, - "Input position", + crate::menu_label("settings.appearance.label.input_position", "Input position"), None, None, LocalOnlyIconState::for_setting( @@ -3650,7 +3916,11 @@ impl SettingsWidget for DimInactivePanesWidget { app: &AppContext, ) -> Box { render_body_item::( - "Dim inactive panes".into(), + crate::menu_label( + "settings.appearance.label.dim_inactive_panes", + "Dim inactive panes", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( ShouldDimInactivePanes::storage_key(), @@ -3693,7 +3963,11 @@ impl SettingsWidget for FocusFollowsMouseWidget { app: &AppContext, ) -> Box { render_body_item::( - "Focus follows mouse".into(), + crate::menu_label( + "settings.appearance.label.focus_follows_mouse", + "Focus follows mouse", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( FocusPaneOnHover::storage_key(), @@ -3741,7 +4015,8 @@ impl SettingsWidget for CompactModeWidget { ); render_body_item::( - "Compact mode".into(), + crate::menu_label("settings.appearance.label.compact_mode", "Compact mode") + .to_owned(), None, LocalOnlyIconState::for_setting( Spacing::storage_key(), @@ -3788,7 +4063,11 @@ impl SettingsWidget for JumpToBottomOfBlockWidget { .show_jump_to_bottom_of_block_button .value(); render_body_item::( - "Show Jump to Bottom of Block button".into(), + crate::menu_label( + "settings.appearance.label.show_jump_to_bottom_of_block", + "Show Jump to Bottom of Block button", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( ShowJumpToBottomOfBlockButton::storage_key(), @@ -3835,7 +4114,11 @@ impl SettingsWidget for ShowBlockDividersWidget { let block_list_settings = BlockListSettings::as_ref(app); let enabled = block_list_settings.show_block_dividers.value(); render_body_item::( - "Show block dividers".into(), + crate::menu_label( + "settings.appearance.label.show_block_dividers", + "Show block dividers", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( ShowBlockDividers::storage_key(), @@ -3881,7 +4164,7 @@ impl SettingsWidget for AIFontWidget { let mut ai_font_row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center); let mut ai_font = Flex::column(); ai_font.add_child(render_body_item_label::( - "Agent font".to_string(), + crate::menu_label("settings.appearance.label.agent_font", "Agent font").to_string(), None, None, LocalOnlyIconState::for_setting( @@ -3917,7 +4200,13 @@ impl SettingsWidget for AIFontWidget { ai_font_row.add_child( appearance .ui_builder() - .span("Match terminal".to_string()) + .span( + crate::menu_label( + "settings.appearance.label.match_terminal", + "Match terminal", + ) + .to_string(), + ) .build() .with_margin_left(2.) .with_margin_right(16.) @@ -3945,7 +4234,10 @@ impl TerminalFontWidget { line_height.add_child( appearance .ui_builder() - .label("Line height".to_string()) + .label( + crate::menu_label("settings.appearance.label.line_height", "Line height") + .to_string(), + ) .with_style(UiComponentStyles { margin: Some(Coords { left: 12., @@ -4012,7 +4304,13 @@ impl TerminalFontWidget { font_size: Some(appearance.ui_font_size() * 0.8), ..Default::default() }) - .with_text_label("Reset to default".to_string()); + .with_text_label( + crate::menu_label( + "settings.appearance.label.reset_to_default", + "Reset to default", + ) + .to_string(), + ); button .build() @@ -4043,7 +4341,11 @@ impl SettingsWidget for TerminalFontWidget { // Terminal Font let mut terminal_font = Flex::column(); terminal_font.add_child(render_body_item_label::( - "Terminal font".to_string(), + crate::menu_label( + "settings.appearance.label.terminal_font", + "Terminal font", + ) + .to_string(), None, None, LocalOnlyIconState::for_setting( @@ -4086,7 +4388,13 @@ impl SettingsWidget for TerminalFontWidget { 1., appearance .ui_builder() - .span("View all available system fonts".to_string()) + .span( + crate::menu_label( + "settings.appearance.label.view_all_available_system_fonts", + "View all available system fonts", + ) + .to_string(), + ) .build() .with_margin_left(2.) .finish(), @@ -4107,7 +4415,10 @@ impl SettingsWidget for TerminalFontWidget { font_weight.add_child( appearance .ui_builder() - .label("Font weight".to_string()) + .label( + crate::menu_label("settings.appearance.label.font_weight", "Font weight") + .to_string(), + ) .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE), ..Default::default() @@ -4130,7 +4441,13 @@ impl SettingsWidget for TerminalFontWidget { font_size.add_child( appearance .ui_builder() - .label("Font size (px)".to_string()) + .label( + crate::menu_label( + "settings.appearance.label.font_size_px", + "Font size (px)", + ) + .to_string(), + ) .with_style(UiComponentStyles { margin: Some(Coords { left: 2., @@ -4214,7 +4531,13 @@ impl SettingsWidget for NotebookFontSizeWidget { Align::new( appearance .ui_builder() - .span("Notebook font size".to_string()) + .span( + crate::menu_label( + "settings.appearance.label.notebook_font_size", + "Notebook font size", + ) + .to_string(), + ) .build() .with_margin_right(16.) .finish(), @@ -4240,7 +4563,13 @@ impl SettingsWidget for NotebookFontSizeWidget { .with_child( appearance .ui_builder() - .span("Match terminal".to_string()) + .span( + crate::menu_label( + "settings.appearance.label.match_terminal", + "Match terminal", + ) + .to_string(), + ) .build() .with_margin_left(2.) .with_margin_right(16.) @@ -4298,7 +4627,7 @@ impl SettingsWidget for ThinStrokesWidget { ) -> Box { render_dropdown_item( appearance, - "Use thin strokes", + crate::menu_label("settings.appearance.label.use_thin_strokes", "Use thin strokes"), None, None, LocalOnlyIconState::for_setting( @@ -4331,7 +4660,10 @@ impl SettingsWidget for MinimumContrastWidget { ) -> Box { render_dropdown_item( appearance, - "Enforce minimum contrast", + crate::menu_label( + "settings.appearance.label.enforce_minimum_contrast", + "Enforce minimum contrast", + ), None, None, LocalOnlyIconState::for_setting( @@ -4369,12 +4701,22 @@ impl SettingsWidget for LigaturesWidget { let ligature_rendering_enabled = ligature_rendering.value(); render_body_item::( - "Show ligatures in terminal".into(), + crate::menu_label( + "settings.appearance.label.show_ligatures_in_terminal", + "Show ligatures in terminal", + ) + .to_owned(), Some(AdditionalInfo { mouse_state: self.info_mouse_state.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some("Ligatures may reduce performance".to_string()), + tooltip_override_text: Some( + crate::menu_label( + "settings.appearance.label.ligatures_tooltip", + "Ligatures may reduce performance", + ) + .to_string(), + ), }), LocalOnlyIconState::for_setting( LigatureRenderingEnabled::storage_key(), @@ -4434,7 +4776,7 @@ impl SettingsWidget for CursorTypeWidget { let cursor_display_types: Vec = all::().collect(); render_body_item::( - "Cursor type".into(), + crate::menu_label("settings.appearance.label.cursor_type", "Cursor type").to_owned(), None, LocalOnlyIconState::for_setting( CursorBlinkEnabled::storage_key(), @@ -4449,7 +4791,13 @@ impl SettingsWidget for CursorTypeWidget { .with_child( appearance .ui_builder() - .span("Cursor type is disabled in Vim mode".to_string()) + .span( + crate::menu_label( + "settings.appearance.label.cursor_type_disabled_vim", + "Cursor type is disabled in Vim mode", + ) + .to_string(), + ) .build() .finish(), ) @@ -4503,7 +4851,11 @@ impl SettingsWidget for BlinkingCursorWidget { let settings = AppEditorSettings::as_ref(app); let cursor_blink = &settings.cursor_blink; render_body_item::( - "Blinking cursor".into(), + crate::menu_label( + "settings.appearance.label.blinking_cursor", + "Blinking cursor", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( CursorBlinkEnabled::storage_key(), @@ -4545,7 +4897,10 @@ impl SettingsWidget for TabCloseButtonPositionWidget { ) -> Box { render_dropdown_item( appearance, - "Tab close button position", + crate::menu_label( + "settings.appearance.label.tab_close_button_position", + "Tab close button position", + ), None, None, LocalOnlyIconState::for_setting( @@ -4581,7 +4936,11 @@ impl SettingsWidget for TabIndicatorWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Show tab indicators".into(), + crate::menu_label( + "settings.appearance.label.show_tab_indicators", + "Show tab indicators", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( ShowIndicatorsButton::storage_key(), @@ -4626,7 +4985,11 @@ impl SettingsWidget for CodeReviewButtonWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Show code review button".into(), + crate::menu_label( + "settings.appearance.label.show_code_review_button", + "Show code review button", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( ShowCodeReviewButton::storage_key(), @@ -4671,7 +5034,11 @@ impl SettingsWidget for PreserveActiveTabColorWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Preserve active tab color for new tabs".into(), + crate::menu_label( + "settings.appearance.label.preserve_active_tab_color", + "Preserve active tab color for new tabs", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( PreserveActiveTabColor::storage_key(), @@ -4716,7 +5083,11 @@ impl SettingsWidget for VerticalTabsWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Use vertical tab layout".into(), + crate::menu_label( + "settings.appearance.label.use_vertical_tab_layout", + "Use vertical tab layout", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( UseVerticalTabs::storage_key(), @@ -4761,7 +5132,11 @@ impl SettingsWidget for ShowVerticalTabPanelInRestoredWindowsWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Show vertical tabs panel in restored windows".into(), + crate::menu_label( + "settings.appearance.label.show_vertical_tabs_restored", + "Show vertical tabs panel in restored windows", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( ShowVerticalTabPanelInRestoredWindows::storage_key(), @@ -4783,8 +5158,11 @@ impl SettingsWidget for ShowVerticalTabPanelInRestoredWindowsWidget { }) .finish(), Some( - "When enabled, reopening or restoring a window opens the vertical tabs panel even if it was closed when the window was last saved." - .to_string(), + crate::menu_label( + "settings.appearance.label.vertical_tabs_restored_description", + "When enabled, reopening or restoring a window opens the vertical tabs panel even if it was closed when the window was last saved.", + ) + .to_string(), ), ) } @@ -4811,7 +5189,11 @@ impl SettingsWidget for HideTitleBarSearchBarInVerticalTabsWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Hide search bar in vertical tab layout".into(), + crate::menu_label( + "settings.appearance.label.hide_search_bar_vertical_tabs", + "Hide search bar in vertical tab layout", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( HideTitleBarSearchBarInVerticalTabs::storage_key(), @@ -4833,8 +5215,11 @@ impl SettingsWidget for HideTitleBarSearchBarInVerticalTabsWidget { }) .finish(), Some( - "When using the vertical tab layout, hide the search bar in the title bar. Search stays available via the command palette and keyboard shortcuts." - .to_string(), + crate::menu_label( + "settings.appearance.label.hide_search_bar_vertical_tabs_description", + "When using the vertical tab layout, hide the search bar in the title bar. Search stays available via the command palette and keyboard shortcuts.", + ) + .to_string(), ), ) } @@ -4861,7 +5246,11 @@ impl SettingsWidget for UseLatestUserPromptAsConversationTitleInTabNamesWidget { let tab_settings = TabSettings::as_ref(app); render_body_item::( - "Use latest user prompt as conversation title in tab names".into(), + crate::menu_label( + "settings.appearance.label.latest_prompt_conversation_title", + "Use latest user prompt as conversation title in tab names", + ) + .to_owned(), None, LocalOnlyIconState::for_setting( UseLatestUserPromptAsConversationTitleInTabNames::storage_key(), @@ -4886,8 +5275,11 @@ impl SettingsWidget for UseLatestUserPromptAsConversationTitleInTabNamesWidget { }) .finish(), Some( - "Show the latest user prompt instead of the generated conversation title for Oz and third-party agent sessions in vertical tabs." - .to_string(), + crate::menu_label( + "settings.appearance.label.latest_prompt_conversation_title_description", + "Show the latest user prompt instead of the generated conversation title for Oz and third-party agent sessions in vertical tabs.", + ) + .to_string(), ), ) } @@ -4910,7 +5302,11 @@ impl SettingsWidget for EditToolbarWidget { _app: &AppContext, ) -> Box { let label = render_body_item_label::( - "Header toolbar layout".to_string(), + crate::menu_label( + "settings.appearance.label.header_toolbar_layout", + "Header toolbar layout", + ) + .to_string(), None, None, LocalOnlyIconState::Hidden, @@ -5016,7 +5412,10 @@ impl SettingsWidget for DirectoryTabColorsWidget { .with_spacing(4.) .with_child( Text::new( - "Directory tab colors", + crate::menu_label( + "settings.appearance.label.directory_tab_colors", + "Directory tab colors", + ), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -5026,7 +5425,10 @@ impl SettingsWidget for DirectoryTabColorsWidget { ) .with_child( Text::new( - "Automatically color tabs based on the directory or repo you're working in.", + crate::menu_label( + "settings.appearance.label.directory_tab_colors_description", + "Automatically color tabs based on the directory or repo you're working in.", + ), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -5082,7 +5484,11 @@ impl SettingsWidget for DirectoryTabColorsWidget { }; let is_selected = current_color == tab_color; let tooltip_text = match ansi_id { - None => "Default (no color)".to_string(), + None => crate::menu_label( + "settings.appearance.label.default_no_color_tooltip", + "Default (no color)", + ) + .to_string(), Some(id) => id.to_string(), }; let dir_path_clone = PathBuf::from(&dir_path); @@ -5163,7 +5569,10 @@ impl SettingsWidget for ZenModeWidget { ) -> Box { render_dropdown_item( appearance, - "Show the tab bar", + crate::menu_label( + "settings.appearance.label.show_the_tab_bar", + "Show the tab bar", + ), None, None, LocalOnlyIconState::for_setting( @@ -5200,7 +5609,11 @@ impl SettingsWidget for AltScreenPaddingWidget { let terminal_settings = &TerminalSettings::as_ref(app); let theme = appearance.theme(); let mut column = Flex::column().with_child(render_body_item::( - "Use custom padding in alt-screen".into(), + crate::menu_label( + "settings.appearance.label.use_custom_padding_alt_screen", + "Use custom padding in alt-screen", + ) + .to_owned(), Some(AdditionalInfo { mouse_state: self.additional_info_mouse_state.clone(), on_click_action: Some(AppearancePageAction::OpenUrl( @@ -5261,7 +5674,10 @@ impl SettingsWidget for AltScreenPaddingWidget { Container::new( Align::new( Text::new( - "Uniform padding (px)", + crate::menu_label( + "settings.appearance.label.uniform_padding_px", + "Uniform padding (px)", + ), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -5319,8 +5735,11 @@ impl SettingsWidget for ZoomLevelWidget { render_dropdown_item( appearance, - "Zoom", - Some("Adjusts the default zoom level across all windows"), + crate::menu_label("settings.appearance.label.zoom", "Zoom"), + Some(crate::menu_label( + "settings.appearance.label.zoom_secondary", + "Adjusts the default zoom level across all windows", + )), Some(reset_button), LocalOnlyIconState::for_setting( crate::window_settings::ZoomLevel::storage_key(), diff --git a/app/src/settings_view/billing_and_usage_page.rs b/app/src/settings_view/billing_and_usage_page.rs index 39b8cda394..50d478cb4e 100644 --- a/app/src/settings_view/billing_and_usage_page.rs +++ b/app/src/settings_view/billing_and_usage_page.rs @@ -66,43 +66,145 @@ use crate::workspaces::workspace::{CustomerType, Workspace}; use crate::{send_telemetry_from_ctx, WorkspaceAction}; const HEADER_FONT_SIZE: f32 = 16.; -const OVERAGE_USAGE_LINK_TEXT: &str = "View details on overage usage"; -const OVERAGE_TOGGLE_ADMIN_HEADER: &str = "Enable premium model usage overages"; -const OVERAGE_TOGGLE_USER_HEADER_ENABLED: &str = "Premium model usage overages are enabled"; -const OVERAGE_TOGGLE_USER_HEADER_DISABLED: &str = "Premium model usage overages are not enabled"; -const OVERAGE_TOGGLE_DESCRIPTION: &str = "Continue using premium models beyond your plan's limits. Usage is charged in $20 increments up to your spending limit, with any remaining balance charged on your scheduled billing date."; -const OVERAGE_TOGGLE_USER_DESCRIPTION: &str = - "Ask a team admin to enable overages for more AI usage."; - -const SORT_MENU_ITEM_DISPLAY_NAME_A_Z_LABEL: &str = "A to Z"; -const SORT_MENU_ITEM_DISPLAY_NAME_Z_A_LABEL: &str = "Z to A"; -const SORT_MENU_ITEM_REQUEST_USAGE_ASCENDING_LABEL: &str = "Usage ascending"; -const SORT_MENU_ITEM_REQUEST_USAGE_DESCENDING_LABEL: &str = "Usage descending"; - -const AUTO_RELOAD_EXCEED_LIMIT_WARNING_STRING: &str = - "Auto reload is disabled, as the next reload would exceed your monthly spend limit. Increase your limit to use auto reload."; -const AUTO_RELOAD_DELINQUENT_WARNING_STRING: &str = - "Restricted due to billing issue. Update your payment method to purchase add-on credits."; -const RESTRICTED_BILLING_USAGE_WARNING_STRING: &str = - "Auto reload is disabled due to recent failed reload. Please update your payment method and try again."; - -const OVERVIEW_TAB_TEXT: &str = "Overview"; -const USAGE_HISTORY_TAB_TEXT: &str = "Usage History"; - -const ENTERPRISE_USAGE_CALLOUT_HEADER: &str = "Usage reporting is currently limited"; -const ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_PREFIX: &str = - "Enterprise credit usage isn't fully available in this view yet. For the most accurate spend tracking, "; -const ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_LINK: &str = "visit the admin panel"; -const ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_SUFFIX: &str = "."; -const ENTERPRISE_USAGE_CALLOUT_BODY_NON_ADMIN: &str = - "Enterprise credit usage isn't fully available in this view yet. Contact a team admin for detailed usage reporting."; - -const ADDON_CREDITS_DESCRIPTION: &str = "Add-on credits are purchased in prepaid packages that roll over each billing cycle and expire after one year. The more you purchase, the better the per-credit rate. Once your base plan credits are used, add-on credits will be consumed."; -const ADDITIONAL_ADDON_CREDITS_DESCRIPTION_FOR_TEAM: &str = - "Purchased add-on credits are shared across your team."; - -// Cloud agent trial widget constants. -const AMBIENT_AGENT_TRIAL_TITLE: &str = "Cloud agent trial"; + +// ── i18n helpers ────────────────────────────────────────────────────── +// Converted from `const` to `fn` so translation can happen at runtime. +fn overage_usage_link_text() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.overage_usage_link", + "View details on overage usage", + ) +} +fn overage_toggle_admin_header() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.overage_toggle_admin_header", + "Enable premium model usage overages", + ) +} +fn overage_toggle_user_header_enabled() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.overage_toggle_user_header_enabled", + "Premium model usage overages are enabled", + ) +} +fn overage_toggle_user_header_disabled() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.overage_toggle_user_header_disabled", + "Premium model usage overages are not enabled", + ) +} +fn overage_toggle_description() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.overage_toggle_description", + "Continue using premium models beyond your plan's limits. Usage is charged in $20 increments up to your spending limit, with any remaining balance charged on your scheduled billing date.", + ) +} +fn overage_toggle_user_description() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.overage_toggle_user_description", + "Ask a team admin to enable overages for more AI usage.", + ) +} + +fn sort_menu_item_display_name_a_z_label() -> &'static str { + crate::menu_label("settings.billing_and_usage.sort_a_to_z", "A to Z") +} +fn sort_menu_item_display_name_z_a_label() -> &'static str { + crate::menu_label("settings.billing_and_usage.sort_z_to_a", "Z to A") +} +fn sort_menu_item_request_usage_ascending_label() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.sort_usage_ascending", + "Usage ascending", + ) +} +fn sort_menu_item_request_usage_descending_label() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.sort_usage_descending", + "Usage descending", + ) +} + +fn auto_reload_exceed_limit_warning_string() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.auto_reload_exceed_limit", + "Auto reload is disabled, as the next reload would exceed your monthly spend limit. Increase your limit to use auto reload.", + ) +} +fn auto_reload_delinquent_warning_string() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.auto_reload_delinquent", + "Restricted due to billing issue. Update your payment method to purchase add-on credits.", + ) +} +fn restricted_billing_usage_warning_string() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.restricted_billing_usage", + "Auto reload is disabled due to recent failed reload. Please update your payment method and try again.", + ) +} + +fn overview_tab_text() -> &'static str { + crate::menu_label("settings.billing_and_usage.overview_tab", "Overview") +} +fn usage_history_tab_text() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.usage_history_tab", + "Usage History", + ) +} + +fn enterprise_usage_callout_header() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.enterprise_usage_callout_header", + "Usage reporting is currently limited", + ) +} +fn enterprise_usage_callout_body_admin_prefix() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.enterprise_usage_callout_body_admin_prefix", + "Enterprise credit usage isn't fully available in this view yet. For the most accurate spend tracking, ", + ) +} +fn enterprise_usage_callout_body_admin_link() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.enterprise_usage_callout_body_admin_link", + "visit the admin panel", + ) +} +fn enterprise_usage_callout_body_admin_suffix() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.enterprise_usage_callout_body_admin_suffix", + ".", + ) +} +fn enterprise_usage_callout_body_non_admin() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.enterprise_usage_callout_body_non_admin", + "Enterprise credit usage isn't fully available in this view yet. Contact a team admin for detailed usage reporting.", + ) +} + +fn addon_credits_description() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.addon_credits_description", + "Add-on credits are purchased in prepaid packages that roll over each billing cycle and expire after one year. The more you purchase, the better the per-credit rate. Once your base plan credits are used, add-on credits will be consumed.", + ) +} +fn additional_addon_credits_description_for_team() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.additional_addon_credits_description_for_team", + "Purchased add-on credits are shared across your team.", + ) +} + +// Cloud agent trial widget. +fn ambient_agent_trial_title() -> &'static str { + crate::menu_label( + "settings.billing_and_usage.cloud_agent_trial", + "Cloud agent trial", + ) +} /// The threshold below which we only show the "Buy more" button (not "New agent"). use crate::ai::request_usage_model::AMBIENT_AGENT_TRIAL_CREDIT_THRESHOLD; @@ -115,9 +217,17 @@ pub fn create_discount_badge(discount: u32, appearance: &Appearance) -> Box Self { match label { - OVERVIEW_TAB_TEXT => BillingUsageTab::Overview, - USAGE_HISTORY_TAB_TEXT => BillingUsageTab::UsageHistory, + l if l == overview_tab_text() => BillingUsageTab::Overview, + l if l == usage_history_tab_text() => BillingUsageTab::UsageHistory, _ => BillingUsageTab::Overview, } } pub fn label(&self) -> &str { match self { - BillingUsageTab::Overview => OVERVIEW_TAB_TEXT, - BillingUsageTab::UsageHistory => USAGE_HISTORY_TAB_TEXT, + BillingUsageTab::Overview => overview_tab_text(), + BillingUsageTab::UsageHistory => usage_history_tab_text(), } } } @@ -288,7 +398,13 @@ impl BillingAndUsagePageView { let overage_limit_modal_view = ctx.add_typed_action_view(|ctx| { Modal::new( - Some("Overage spending limit".to_string()), + Some( + crate::menu_label( + "settings.billing_and_usage.overage_spending_limit", + "Overage spending limit", + ) + .to_string(), + ), overage_limit_modal, ctx, ) @@ -312,7 +428,13 @@ impl BillingAndUsagePageView { let addon_credit_modal_view = ctx.add_typed_action_view(|ctx| { Modal::new( - Some("Monthly spending limit".to_string()), + Some( + crate::menu_label( + "settings.billing_and_usage.monthly_spending_limit", + "Monthly spending limit", + ) + .to_string(), + ), addon_credit_modal, ctx, ) @@ -340,7 +462,11 @@ impl BillingAndUsagePageView { }); let load_more_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Load more", SecondaryTheme).on_click(|ctx| { + ActionButton::new( + crate::menu_label("settings.billing_and_usage.load_more", "Load more"), + SecondaryTheme, + ) + .on_click(|ctx| { ctx.dispatch_typed_action(BillingAndUsagePageAction::RenderMoreUsageEntries); }) }); @@ -445,7 +571,10 @@ impl BillingAndUsagePageView { } UserWorkspacesEvent::UpdateWorkspaceSettingsRejected(_err) => { self.show_toast( - "Failed to update workspace settings", + crate::menu_label( + "settings.billing_and_usage.failed_update_workspace_settings", + "Failed to update workspace settings", + ), ToastFlavor::Error, ctx, ); @@ -458,7 +587,10 @@ impl BillingAndUsagePageView { UserWorkspacesEvent::PurchaseAddonCreditsSuccess => { self.purchase_addon_credits_loading = false; self.show_toast( - "Successfully purchased add-on credits", + crate::menu_label( + "settings.billing_and_usage.successfully_purchased_addon_credits", + "Successfully purchased add-on credits", + ), ToastFlavor::Success, ctx, ); @@ -830,22 +962,22 @@ impl TypedActionView for BillingAndUsagePageView { // Build four menu items with checkmark for selected state let sort_options = [ ( - SORT_MENU_ITEM_DISPLAY_NAME_A_Z_LABEL, + sort_menu_item_display_name_a_z_label(), SortKey::DisplayName, SortOrder::Asc, ), ( - SORT_MENU_ITEM_DISPLAY_NAME_Z_A_LABEL, + sort_menu_item_display_name_z_a_label(), SortKey::DisplayName, SortOrder::Desc, ), ( - SORT_MENU_ITEM_REQUEST_USAGE_ASCENDING_LABEL, + sort_menu_item_request_usage_ascending_label(), SortKey::Requests, SortOrder::Asc, ), ( - SORT_MENU_ITEM_REQUEST_USAGE_DESCENDING_LABEL, + sort_menu_item_request_usage_descending_label(), SortKey::Requests, SortOrder::Desc, ), @@ -1109,18 +1241,27 @@ impl BillingAndUsagePageView { let fg = theme.foreground().into_solid(); let bg = theme.background().into_solid(); - let title = Text::new_inline(AMBIENT_AGENT_TRIAL_TITLE, appearance.ui_font_family(), 14.) - .with_color(theme.active_ui_text_color().into()) - .with_style(Properties::default().weight(Weight::Semibold)) - .finish(); + let title = Text::new_inline( + ambient_agent_trial_title(), + appearance.ui_font_family(), + 14., + ) + .with_color(theme.active_ui_text_color().into()) + .with_style(Properties::default().weight(Weight::Semibold)) + .finish(); let credits_text = if credits_remaining == 1 { - "1 credit remaining".to_string() + crate::menu_label( + "settings.billing_and_usage.credit_remaining_singular", + "1 credit remaining", + ) + .to_string() } else { - format!( - "{} credits remaining", - credits_remaining.separate_with_commas() + crate::menu_label( + "settings.billing_and_usage.credit_remaining_plural", + "{count} credits remaining", ) + .replace("{count}", &credits_remaining.separate_with_commas()) }; let credits_label = Text::new_inline(credits_text, appearance.ui_font_family(), 12.) .with_color(blended_colors::text_sub(theme, theme.surface_1())) @@ -1141,7 +1282,10 @@ impl BillingAndUsagePageView { ButtonVariant::Secondary, self.ambient_trial_new_agent_button.clone(), ) - .with_text_label("New agent".to_string()) + .with_text_label( + crate::menu_label("settings.billing_and_usage.new_agent", "New agent") + .to_string(), + ) .with_style(UiComponentStyles { font_color: Some(bg), background: Some(fg.into()), @@ -1178,7 +1322,10 @@ impl BillingAndUsagePageView { ButtonVariant::Secondary, self.ambient_trial_buy_more_button.clone(), ) - .with_text_label("Buy more".to_string()) + .with_text_label( + crate::menu_label("settings.billing_and_usage.buy_more", "Buy more") + .to_string(), + ) .with_style(UiComponentStyles { background: Some(bg.into()), font_size: Some(14.), @@ -1264,16 +1411,16 @@ impl BillingAndUsagePageView { let enabled_and_not_delinquent = enabled && !is_delinquent; let (header_text, description_text) = if has_admin_permissions { - (OVERAGE_TOGGLE_ADMIN_HEADER, OVERAGE_TOGGLE_DESCRIPTION) + (overage_toggle_admin_header(), overage_toggle_description()) } else if enabled { ( - OVERAGE_TOGGLE_USER_HEADER_ENABLED, - OVERAGE_TOGGLE_DESCRIPTION, + overage_toggle_user_header_enabled(), + overage_toggle_description(), ) } else { ( - OVERAGE_TOGGLE_USER_HEADER_DISABLED, - OVERAGE_TOGGLE_USER_DESCRIPTION, + overage_toggle_user_header_disabled(), + overage_toggle_user_description(), ) }; @@ -1370,7 +1517,7 @@ impl BillingAndUsagePageView { let spend_limit_text = if let Some(cents) = usage_settings.max_monthly_spend_cents { format!("${:.2}", cents as f64 / 100.0) } else { - "Not set".to_string() + crate::menu_label("settings.billing_and_usage.not_set", "Not set").to_string() }; let info_icon = render_info_icon( @@ -1380,13 +1527,20 @@ impl BillingAndUsagePageView { on_click_action: None, secondary_text: None, tooltip_override_text: Some( - "Sets the monthly overage spending limit beyond the plan amount".to_string(), + crate::menu_label( + "settings.billing_and_usage.sets_monthly_overage_limit", + "Sets the monthly overage spending limit beyond the plan amount", + ) + .to_string(), ), }, ); let label = Text::new_inline( - "Monthly overage spending limit", + crate::menu_label( + "settings.billing_and_usage.monthly_overage_spending_limit", + "Monthly overage spending limit", + ), appearance.ui_font_family(), 12., ) @@ -1455,7 +1609,7 @@ impl BillingAndUsagePageView { appearance .ui_builder() .link( - OVERAGE_USAGE_LINK_TEXT.to_string(), + overage_usage_link_text().to_string(), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action( @@ -1615,10 +1769,17 @@ impl BillingAndUsagePageView { let ui_builder = appearance.ui_builder(); let theme = appearance.theme(); - let header = Text::new_inline("Add-on credits", appearance.ui_font_family(), 16.) - .with_color(fg.into()) - .with_style(Properties::default().weight(Weight::Bold)) - .finish(); + let header = Text::new_inline( + crate::menu_label( + "settings.billing_and_usage.add_on_credits", + "Add-on credits", + ), + appearance.ui_font_family(), + 16., + ) + .with_color(fg.into()) + .with_style(Properties::default().weight(Weight::Bold)) + .finish(); let credits_value = Text::new_inline( bonus_credit_balance.separate_with_commas(), @@ -1671,9 +1832,27 @@ impl BillingAndUsagePageView { .current_team() .is_some_and(|team| team.billing_metadata.is_on_legacy_paid_plan()); let (link_text, suffix) = if is_legacy_paid { - ("Switch to the Build plan", " to purchase add-on credits.") + ( + crate::menu_label( + "settings.billing_and_usage.switch_to_build", + "Switch to the Build plan", + ), + crate::menu_label( + "settings.billing_and_usage.to_purchase_addon_credits", + " to purchase add-on credits.", + ), + ) } else { - ("Upgrade to the Build plan", " to purchase add-on credits.") + ( + crate::menu_label( + "settings.billing_and_usage.upgrade_to_build", + "Upgrade to the Build plan", + ), + crate::menu_label( + "settings.billing_and_usage.to_purchase_addon_credits", + " to purchase add-on credits.", + ), + ) }; let text_fragments = vec![ @@ -1713,7 +1892,10 @@ impl BillingAndUsagePageView { // they're on an Enterprise-like plan. For admins, we show them a message to contact their // Account Executive. (false, false, true) => { - let paragraph_text = "Contact your Account Executive for more add-on credits."; + let paragraph_text = crate::menu_label( + "settings.billing_and_usage.contact_account_executive", + "Contact your Account Executive for more add-on credits.", + ); Some( ui_builder .paragraph(paragraph_text) @@ -1728,7 +1910,10 @@ impl BillingAndUsagePageView { // Every other case relates to not being a team admin. If you aren't an admin, we show // a generic message telling you to talk to them. (_, _, false) => { - let paragraph_text = "Contact a team admin to purchase add-on credits."; + let paragraph_text = crate::menu_label( + "settings.billing_and_usage.contact_team_admin", + "Contact a team admin to purchase add-on credits.", + ); Some( ui_builder .paragraph(paragraph_text) @@ -1765,9 +1950,13 @@ impl BillingAndUsagePageView { .unwrap_or(1); let paragraph_text = if team_member_count > 1 { - format!("{ADDON_CREDITS_DESCRIPTION} {ADDITIONAL_ADDON_CREDITS_DESCRIPTION_FOR_TEAM}") + format!( + "{} {}", + addon_credits_description(), + additional_addon_credits_description_for_team() + ) } else { - ADDON_CREDITS_DESCRIPTION.to_string() + addon_credits_description().to_string() }; let paragraph = ui_builder .paragraph(paragraph_text) @@ -1785,7 +1974,11 @@ impl BillingAndUsagePageView { on_click_action: None, secondary_text: None, tooltip_override_text: Some( - "Sets the monthly limit spent on add-on credits".to_string(), + crate::menu_label( + "settings.billing_and_usage.sets_monthly_limit_spent", + "Sets the monthly limit spent on add-on credits", + ) + .to_string(), ), }, ); @@ -1795,12 +1988,21 @@ impl BillingAndUsagePageView { .addon_credits_settings .max_monthly_spend_cents .map(|cents| format!("${:.2}", cents as f64 / 100.0)) - .unwrap_or_else(|| "$200.00".to_string()); + .unwrap_or_else(|| { + crate::menu_label("settings.billing_and_usage.default_spend_limit", "$200.00") + .to_string() + }); let monthly_spend_row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_children([ - ui_builder.span("Monthly spend limit").build().finish(), + ui_builder + .span(crate::menu_label( + "settings.billing_and_usage.monthly_spend_limit", + "Monthly spend limit", + )) + .build() + .finish(), Shrinkable::new(1., Align::new(info_icon).left().finish()).finish(), icon_button( appearance, @@ -1829,15 +2031,26 @@ impl BillingAndUsagePageView { let cost_cents = bonus_grants.cents_spent; let cost_dollars = cost_cents as f64 / 100.0; - let label = - Text::new_inline("Purchased this month", appearance.ui_font_family(), 12.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(); + let label = Text::new_inline( + crate::menu_label( + "settings.billing_and_usage.purchased_this_month", + "Purchased this month", + ), + appearance.ui_font_family(), + 12., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(); let credits_text = if credits_purchased == 1 { - "1 credit".to_string() + crate::menu_label("settings.billing_and_usage.credit_singular", "1 credit") + .to_string() } else { - format!("{} credits", credits_purchased.separate_with_commas()) + crate::menu_label( + "settings.billing_and_usage.credit_plural", + "{count} credits", + ) + .replace("{count}", &credits_purchased.separate_with_commas()) }; let credits_component = Container::new( @@ -1896,7 +2109,10 @@ impl BillingAndUsagePageView { let auto_reload_amount = selected_option .map(|option| option.credits.to_string()) .filter(|_| auto_reload_enabled) - .unwrap_or("your selected".to_string()); + .unwrap_or_else(|| { + crate::menu_label("settings.billing_and_usage.your_selected", "your selected") + .to_string() + }); let auto_reload_switch = ui_builder .switch(self.auto_reload_switch.clone()) .check(auto_reload_enabled); @@ -1914,17 +2130,20 @@ impl BillingAndUsagePageView { .finish() }; + let auto_reload_description = crate::menu_label( + "settings.billing_and_usage.auto_reload_description", + "When enabled, auto reload will automatically purchase {amount} credits when your add-on credit balance reaches 100 credits remaining.", + ) + .replace("{amount}", &auto_reload_amount); + let auto_reload_switch = Container::new(render_body_item::( - "Auto reload".into(), + crate::menu_label("settings.billing_and_usage.auto_reload", "Auto reload").into(), None, Default::default(), Default::default(), appearance, auto_reload_switch, - Some(format!( - "When enabled, auto reload will automatically purchase {auto_reload_amount} \ - credits when your add-on credit balance reaches 100 credits remaining." - )), + Some(auto_reload_description), )) .with_padding_right(-TOGGLE_BUTTON_RIGHT_PADDING) .finish(); @@ -1983,9 +2202,9 @@ impl BillingAndUsagePageView { }; let button_text = if purchase_addon_credits_loading { - "Buying…".to_string() + crate::menu_label("settings.billing_and_usage.buying", "Buying…").to_string() } else { - "Buy".to_string() + crate::menu_label("settings.billing_and_usage.buy", "Buy").to_string() }; let would_exceed_limit = selected_option.is_some_and(|option| { @@ -2052,12 +2271,12 @@ impl BillingAndUsagePageView { if delinquent_due_to_payment_issue { card_content_upper.add_child(self.render_warning_row( appearance, - AUTO_RELOAD_DELINQUENT_WARNING_STRING.to_string(), + auto_reload_delinquent_warning_string().to_string(), )); } else if would_exceed_limit { card_content_upper.add_child(self.render_warning_row( appearance, - AUTO_RELOAD_EXCEED_LIMIT_WARNING_STRING.to_string(), + auto_reload_exceed_limit_warning_string().to_string(), )); } let card_upper = Container::new(card_content_upper.finish()) @@ -2076,14 +2295,20 @@ impl BillingAndUsagePageView { .finish(); let mut card_content_lower_children = vec![ - ui_builder.span("One-time purchase").build().finish(), + ui_builder + .span(crate::menu_label( + "settings.billing_and_usage.one_time_purchase", + "One-time purchase", + )) + .build() + .finish(), buy_row.finish(), ]; if delinquent_due_to_payment_issue { card_content_lower_children.push(self.render_warning_row( appearance, - AUTO_RELOAD_DELINQUENT_WARNING_STRING.to_string(), + auto_reload_delinquent_warning_string().to_string(), )); } else if workspace .billing_metadata @@ -2091,18 +2316,25 @@ impl BillingAndUsagePageView { { card_content_lower_children.push(self.render_warning_row( appearance, - RESTRICTED_BILLING_USAGE_WARNING_STRING.to_string(), + restricted_billing_usage_warning_string().to_string(), )); } else if would_exceed_limit { let warning_fragments = vec![ - FormattedTextFragment::plain_text( + FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.reloading_would_exceed", "Reloading would exceed your monthly limit. ", - ), + )), FormattedTextFragment::hyperlink_action( - "Increase your limit", + crate::menu_label( + "settings.billing_and_usage.increase_your_limit", + "Increase your limit", + ), BillingAndUsagePageAction::ShowAddOnCreditModal, ), - FormattedTextFragment::plain_text(" to continue."), + FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.to_continue", + " to continue.", + )), ]; card_content_lower_children .push(self.render_warning_row_with_link(appearance, warning_fragments)); @@ -2159,24 +2391,40 @@ impl BillingAndUsagePageView { if let (Some(count), Some(cost)) = (total_overages_count, total_overages_cost) { if count == 1 { ( - "1 credit".to_string(), + crate::menu_label("settings.billing_and_usage.credit_singular", "1 credit") + .to_string(), format!("${:.2}", cost as f64 / 100.0), ) } else { ( - format!("{} credits", count.separate_with_commas()), + crate::menu_label( + "settings.billing_and_usage.credit_plural", + "{count} credits", + ) + .replace("{count}", &count.separate_with_commas()), format!("${:.2}", cost as f64 / 100.0), ) } } else { - ("0 credits".to_string(), "$0.00".to_string()) + ( + crate::menu_label("settings.billing_and_usage.zero_credits", "0 credits") + .to_string(), + crate::menu_label("settings.billing_and_usage.zero_cost", "$0.00").to_string(), + ) }; let mut left_side_component = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center); - let label = Text::new_inline("Total overages", appearance.ui_font_family(), 12.) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish(); + let label = Text::new_inline( + crate::menu_label( + "settings.billing_and_usage.total_overages", + "Total overages", + ), + appearance.ui_font_family(), + 12., + ) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish(); left_side_component.add_child(Container::new(label).with_margin_right(8.).finish()); @@ -2201,7 +2449,11 @@ impl BillingAndUsagePageView { if let Some(period_end) = total_overages_period_end { let local_period_end = period_end.with_timezone(&Local); let formatted_date = local_period_end.format("%b %d at %-I:%M %p").to_string(); - let billing_date_text = format!("Usage resets on {formatted_date}"); + let billing_date_text = crate::menu_label( + "settings.billing_and_usage.usage_resets_on", + "Usage resets on {date}", + ) + .replace("{date}", &formatted_date); left_side_component.add_child( Container::new( Text::new_inline(billing_date_text, appearance.ui_font_family(), 12.) @@ -2256,8 +2508,14 @@ impl BillingAndUsagePageView { on_click_action: None, secondary_text: None, tooltip_override_text: match info.is_current_user { - true => Some("Your credit limit is prorated because you joined midway through the billing cycle.".to_string()), - false => Some("This credit limit is prorated because this user joined midway through the billing cycle.".to_string()), + true => Some(crate::menu_label( + "settings.billing_and_usage.credit_limit_prorated_self", + "Your credit limit is prorated because you joined midway through the billing cycle.", + ).to_string()), + false => Some(crate::menu_label( + "settings.billing_and_usage.credit_limit_prorated_other", + "This credit limit is prorated because this user joined midway through the billing cycle.", + ).to_string()), }, }, )) @@ -2278,17 +2536,24 @@ impl BillingAndUsagePageView { } let request_count_label = if workspace_is_delinquent_due_to_payment_issue { - "Restricted due to billing issue".to_string() + crate::menu_label( + "settings.billing_and_usage.restricted_due_to_billing", + "Restricted due to billing issue", + ) + .to_string() } else { match divisor { - Some(Divisor::Unlimited) => { - format!("{}/Unlimited", used.separate_with_commas()) - } - Some(Divisor::Limit(limit)) => format!( - "{}/{}", - used.separate_with_commas(), - limit.separate_with_commas() - ), + Some(Divisor::Unlimited) => crate::menu_label( + "settings.billing_and_usage.used_over_unlimited", + "{used}/Unlimited", + ) + .replace("{used}", &used.separate_with_commas()), + Some(Divisor::Limit(limit)) => crate::menu_label( + "settings.billing_and_usage.used_over_limit", + "{used}/{limit}", + ) + .replace("{used}", &used.separate_with_commas()) + .replace("{limit}", &limit.separate_with_commas()), None => used.separate_with_commas(), } }; @@ -2365,9 +2630,12 @@ impl BillingAndUsagePageView { ) .finish() } else { - let header = "Credits"; - let description = - format!("This is the {refresh_duration} limit of AI credits for your account."); + let header = crate::menu_label("settings.billing_and_usage.credits_header", "Credits"); + let description = crate::menu_label( + "settings.billing_and_usage.credit_limit_description", + "This is the {duration} limit of AI credits for your account.", + ) + .replace("{duration}", &refresh_duration); let request_usage_description = FormattedTextElement::from_str( description, @@ -2508,12 +2776,20 @@ impl BillingAndUsagePageView { .with_main_axis_alignment(MainAxisAlignment::Center) .with_child( Container::new( - Text::new_inline("Last 30 days".to_string(), appearance.ui_font_family(), 14.) - .with_color(blended_colors::text_sub( - appearance.theme(), - appearance.theme().surface_1(), - )) - .finish(), + Text::new_inline( + crate::menu_label( + "settings.billing_and_usage.last_30_days", + "Last 30 days", + ) + .to_string(), + appearance.ui_font_family(), + 14., + ) + .with_color(blended_colors::text_sub( + appearance.theme(), + appearance.theme().surface_1(), + )) + .finish(), ) .with_vertical_margin(12.) .finish(), @@ -2621,19 +2897,29 @@ impl BillingAndUsagePageView { ) .with_child( Container::new( - Text::new("No usage history", appearance.ui_font_family(), 14.) - .with_color(blended_colors::text_sub( - appearance.theme(), - appearance.theme().surface_1(), - )) - .finish(), + Text::new( + crate::menu_label( + "settings.billing_and_usage.no_usage_history", + "No usage history", + ), + appearance.ui_font_family(), + 14., + ) + .with_color(blended_colors::text_sub( + appearance.theme(), + appearance.theme().surface_1(), + )) + .finish(), ) .with_margin_bottom(4.) .finish(), ) .with_child( Text::new( - "Kick off an agent task to view usage history here.", + crate::menu_label( + "settings.billing_and_usage.kick_off_agent_task", + "Kick off an agent task to view usage history here.", + ), appearance.ui_font_family(), 14., ) @@ -2680,7 +2966,7 @@ impl BillingAndUsagePageView { .finish(); let header = Text::new_inline( - ENTERPRISE_USAGE_CALLOUT_HEADER, + enterprise_usage_callout_header(), appearance.ui_font_family(), 16., ) @@ -2698,12 +2984,12 @@ impl BillingAndUsagePageView { let body = if has_admin_permissions { let admin_panel_url = AdminActions::admin_panel_link_for_team(team_uid); let text_fragments = vec![ - FormattedTextFragment::plain_text(ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_PREFIX), + FormattedTextFragment::plain_text(enterprise_usage_callout_body_admin_prefix()), FormattedTextFragment::hyperlink( - ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_LINK, + enterprise_usage_callout_body_admin_link(), admin_panel_url, ), - FormattedTextFragment::plain_text(ENTERPRISE_USAGE_CALLOUT_BODY_ADMIN_SUFFIX), + FormattedTextFragment::plain_text(enterprise_usage_callout_body_admin_suffix()), ]; FormattedTextElement::new( FormattedText::new([FormattedTextLine::Line(text_fragments)]), @@ -2721,7 +3007,7 @@ impl BillingAndUsagePageView { } else { appearance .ui_builder() - .paragraph(ENTERPRISE_USAGE_CALLOUT_BODY_NON_ADMIN) + .paragraph(enterprise_usage_callout_body_non_admin()) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(bg).into()), font_size: Some(12.), @@ -2778,7 +3064,10 @@ impl BillingAndUsagePageView { .with_child( appearance .ui_builder() - .paragraph(format!("Resets {formatted_next_refresh_time}")) + .paragraph( + crate::menu_label("settings.billing_and_usage.resets_at", "Resets {time}") + .replace("{time}", formatted_next_refresh_time), + ) .with_style(UiComponentStyles { font_color: Some(blended_colors::text_sub( appearance.theme(), @@ -2828,8 +3117,13 @@ impl BillingAndUsagePageView { let hoverable = Hoverable::new(self.sort_icon_mouse_state.clone(), |mouse_state| { if mouse_state.is_hovered() { - let tooltip = - appearance.ui_builder().tool_tip("Sort by".to_string()); + let tooltip = appearance.ui_builder().tool_tip( + crate::menu_label( + "settings.billing_and_usage.sort_by", + "Sort by", + ) + .to_string(), + ); button.add_positioned_overlay_child( tooltip.build().finish(), @@ -2892,7 +3186,7 @@ impl BillingAndUsagePageView { .with_child( build_sub_header( appearance, - "Usage", + crate::menu_label("settings.billing_and_usage.usage_section", "Usage"), Some( appearance .theme() @@ -2943,15 +3237,18 @@ impl BillingAndUsagePageView { None }; - usage.add_child(self.render_ai_usage_limit_row( - "Team total".to_string(), - team_total_used, - team_divisor, - ai_request_usage_model.refresh_duration_to_string(), - workspace_is_delinquent_due_to_payment_issue, - appearance, - None, - )); + usage.add_child( + self.render_ai_usage_limit_row( + crate::menu_label("settings.billing_and_usage.team_total", "Team total") + .to_string(), + team_total_used, + team_divisor, + ai_request_usage_model.refresh_duration_to_string(), + workspace_is_delinquent_due_to_payment_issue, + appearance, + None, + ), + ); let divider = Container::new( ConstrainedBox::new(Empty::new().finish()) .with_height(1.) @@ -3071,18 +3368,25 @@ impl BillingAndUsagePageView { if has_admin_permissions { vec![ FormattedTextFragment::hyperlink_action( - "Manage billing", + crate::menu_label( + "settings.billing_and_usage.manage_billing", + "Manage billing", + ), BillingAndUsagePageAction::GenerateStripeBillingPortalLink { team_uid: team.uid, }, ), - FormattedTextFragment::plain_text(" to regain access to AI features."), + FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.to_regain_access_to_ai", + " to regain access to AI features.", + )), ] } else { // Non-admin team member - show message to contact admin - vec![FormattedTextFragment::plain_text( + vec![FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.contact_team_admin_billing", "Contact your team admin to resolve billing issues.", - )] + ))] } } else if team.billing_metadata.can_upgrade_to_higher_tier_plan() { let upgrade_url = UserWorkspaces::upgrade_link_for_team(team.uid); @@ -3091,39 +3395,67 @@ impl BillingAndUsagePageView { if team.billing_metadata.is_on_legacy_paid_plan() { vec![ FormattedTextFragment::hyperlink( - "Switch to the Build plan", + crate::menu_label( + "settings.billing_and_usage.switch_to_build", + "Switch to the Build plan", + ), upgrade_url, ), - FormattedTextFragment::plain_text( + FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.for_more_flexible_pricing", " for a more flexible pricing model.", - ), + )), ] } else { let mut fragments = vec![FormattedTextFragment::hyperlink( - "Upgrade to the Build plan", + crate::menu_label( + "settings.billing_and_usage.upgrade_to_build", + "Upgrade to the Build plan", + ), upgrade_url, )]; if team.billing_metadata.is_byo_api_key_enabled() { - fragments.push(FormattedTextFragment::plain_text(" or ")); + fragments.push(FormattedTextFragment::plain_text( + crate::menu_label( + "settings.billing_and_usage.or_separator", + " or ", + ), + )); fragments.push(FormattedTextFragment::hyperlink_action( - "bring your own key", + crate::menu_label( + "settings.billing_and_usage.bring_your_own_key", + "bring your own key", + ), BillingAndUsagePageAction::NavigateToByokSettings, )); } - fragments.push(FormattedTextFragment::plain_text( + fragments.push(FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.for_increased_ai_access", " for increased access to AI features.", - )); + ))); fragments } } else { let upgrade_text = match team.billing_metadata.customer_type { - CustomerType::Prosumer => "Upgrade to Turbo plan", - CustomerType::Turbo => "Upgrade to Lightspeed plan", - _ => "Upgrade", + CustomerType::Prosumer => crate::menu_label( + "settings.billing_and_usage.upgrade_to_turbo", + "Upgrade to Turbo plan", + ), + CustomerType::Turbo => crate::menu_label( + "settings.billing_and_usage.upgrade_to_lightspeed", + "Upgrade to Lightspeed plan", + ), + _ => crate::menu_label( + "settings.billing_and_usage.upgrade_plan", + "Upgrade", + ), }; vec![ FormattedTextFragment::hyperlink(upgrade_text, upgrade_url), - FormattedTextFragment::plain_text(" to get more AI usage."), + FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.to_get_more_ai_usage", + " to get more AI usage.", + )), ] } } else { @@ -3132,19 +3464,28 @@ impl BillingAndUsagePageView { } else if team.billing_metadata.is_on_build_plan() { vec![ FormattedTextFragment::hyperlink( - "Upgrade to Max", + crate::menu_label( + "settings.billing_and_usage.upgrade_to_max", + "Upgrade to Max", + ), UserWorkspaces::upgrade_link_for_team(team.uid), ), - FormattedTextFragment::plain_text(" for more AI credits."), + FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.for_more_ai_credits", + " for more AI credits.", + )), ] } else if team.billing_metadata.is_on_build_max_plan() { vec![ FormattedTextFragment::hyperlink( - "Switch to Business", + crate::menu_label("settings.billing_and_usage.switch_to_business", "Switch to Business"), UserWorkspaces::upgrade_link_for_team(team.uid), ), FormattedTextFragment::plain_text( - " for security features like SSO and automatically applied zero data retention.", + crate::menu_label( + "settings.billing_and_usage.for_security_features", + " for security features like SSO and automatically applied zero data retention.", + ), ), ] } else if team.billing_metadata.is_on_build_business_plan() @@ -3152,15 +3493,30 @@ impl BillingAndUsagePageView { { vec![ FormattedTextFragment::hyperlink( - "Upgrade to Enterprise", + crate::menu_label( + "settings.billing_and_usage.upgrade_to_enterprise", + "Upgrade to Enterprise", + ), "mailto:sales@warp.dev", ), - FormattedTextFragment::plain_text(" for custom limits and dedicated support."), + FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.for_custom_limits", + " for custom limits and dedicated support.", + )), ] } else if !team.billing_metadata.is_usage_based_pricing_toggleable() { vec![ - FormattedTextFragment::hyperlink("Contact support", "mailto:support@warp.dev"), - FormattedTextFragment::plain_text(" for more AI usage."), + FormattedTextFragment::hyperlink( + crate::menu_label( + "settings.billing_and_usage.contact_support", + "Contact support", + ), + "mailto:support@warp.dev", + ), + FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.for_more_ai_usage", + " for more AI usage.", + )), ] } else { vec![] @@ -3169,19 +3525,29 @@ impl BillingAndUsagePageView { let user_id = auth_state.user_id().unwrap_or_default(); let upgrade_url = UserWorkspaces::upgrade_link(user_id); let mut fragments = vec![FormattedTextFragment::hyperlink( - "Upgrade to the Build plan", + crate::menu_label( + "settings.billing_and_usage.upgrade_to_build", + "Upgrade to the Build plan", + ), upgrade_url, )]; if UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app) { - fragments.push(FormattedTextFragment::plain_text(" or ")); + fragments.push(FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.or_separator", + " or ", + ))); fragments.push(FormattedTextFragment::hyperlink_action( - "bring your own key", + crate::menu_label( + "settings.billing_and_usage.bring_your_own_key", + "bring your own key", + ), BillingAndUsagePageAction::NavigateToByokSettings, )); } - fragments.push(FormattedTextFragment::plain_text( + fragments.push(FormattedTextFragment::plain_text(crate::menu_label( + "settings.billing_and_usage.for_more_credits_access", " for more credits and access to more models.", - )); + ))); fragments }; @@ -3342,7 +3708,9 @@ impl BillingAndUsagePageView { self.anonymous_user_sign_up_button.clone(), ) .with_style(button_styles) - .with_text_label("Sign up".to_owned()) + .with_text_label( + crate::menu_label("settings.billing_and_usage.sign_up", "Sign up").to_owned(), + ) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(BillingAndUsagePageAction::SignupAnonymousUser); @@ -3354,7 +3722,10 @@ impl BillingAndUsagePageView { .with_cross_axis_alignment(CrossAxisAlignment::End); let current_user_id = auth_state.user_id().unwrap_or_default(); - plan_info.add_child(render_customer_type_badge(appearance, "Free".into())); + plan_info.add_child(render_customer_type_badge( + appearance, + crate::menu_label("settings.billing_and_usage.free", "Free").into(), + )); plan_info.add_child( Container::new( appearance @@ -3363,7 +3734,10 @@ impl BillingAndUsagePageView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Compare plans", + crate::menu_label( + "settings.billing_and_usage.compare_plans", + "Compare plans", + ), Icon::CoinsStacked.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -3402,10 +3776,14 @@ impl BillingAndUsagePageView { } fn render_plan_header_text(&self, appearance: &Appearance) -> Box { - Text::new_inline("Plan", appearance.ui_font_family(), HEADER_FONT_SIZE) - .with_style(Properties::default().weight(Weight::Bold)) - .with_color(appearance.theme().active_ui_text_color().into()) - .finish() + Text::new_inline( + crate::menu_label("settings.billing_and_usage.plan", "Plan"), + appearance.ui_font_family(), + HEADER_FONT_SIZE, + ) + .with_style(Properties::default().weight(Weight::Bold)) + .with_color(appearance.theme().active_ui_text_color().into()) + .finish() } fn render_team_admin_actions( @@ -3428,7 +3806,10 @@ impl BillingAndUsagePageView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Manage billing", + crate::menu_label( + "settings.billing_and_usage.manage_billing", + "Manage billing", + ), Icon::CoinsStacked.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -3486,7 +3867,10 @@ impl BillingAndUsagePageView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Open admin panel", + crate::menu_label( + "settings.billing_and_usage.open_admin_panel", + "Open admin panel", + ), Icon::Users.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -3513,7 +3897,10 @@ impl BillingAndUsagePageView { ) -> (Box, Box) { let current_user_id = auth_state.user_id().unwrap_or_default(); - let plan_badge = render_customer_type_badge(appearance, "Free".into()); + let plan_badge = render_customer_type_badge( + appearance, + crate::menu_label("settings.billing_and_usage.free", "Free").into(), + ); let badge_element = Container::new(plan_badge).with_margin_right(16.).finish(); @@ -3524,7 +3911,10 @@ impl BillingAndUsagePageView { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Compare plans", + crate::menu_label( + "settings.billing_and_usage.compare_plans", + "Compare plans", + ), Icon::CoinsStacked.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, diff --git a/app/src/settings_view/custom_router_view.rs b/app/src/settings_view/custom_router_view.rs index c31053dfa7..974b319346 100644 --- a/app/src/settings_view/custom_router_view.rs +++ b/app/src/settings_view/custom_router_view.rs @@ -48,8 +48,11 @@ impl CustomRouterView { pub fn new(router: CustomModelRouter, ctx: &mut ViewContext) -> Self { let is_any_ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx); let open_file_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Open file", SecondaryTheme) - .with_icon(Icon::File) + ActionButton::new( + crate::menu_label("settings.custom_router.view.open_file", "Open file"), + SecondaryTheme, + ) + .with_icon(Icon::File) .with_size(ButtonSize::Small) .with_height(HEADER_BUTTON_HEIGHT) .on_click(|ctx| { @@ -61,7 +64,7 @@ impl CustomRouterView { }); let edit_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Edit", SecondaryTheme) + ActionButton::new(crate::menu_label("common.edit", "Edit"), SecondaryTheme) .with_icon(Icon::Pencil) .with_size(ButtonSize::Small) .with_height(HEADER_BUTTON_HEIGHT) @@ -74,7 +77,7 @@ impl CustomRouterView { }); let delete_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Delete", DangerSecondaryTheme) + ActionButton::new(crate::menu_label("common.delete", "Delete"), DangerSecondaryTheme) .with_icon(Icon::Trash) .with_size(ButtonSize::Small) .with_height(HEADER_BUTTON_HEIGHT) @@ -178,8 +181,14 @@ impl View for CustomRouterView { // Type label row let type_label = match &self.router.routing { - CustomModelRouting::Complexity(_) => "Complexity-based routing", - CustomModelRouting::Prompt(_) => "Prompt-based routing", + CustomModelRouting::Complexity(_) => crate::menu_label( + "settings.custom_router.view.complexity_based_routing", + "Complexity-based routing", + ), + CustomModelRouting::Prompt(_) => crate::menu_label( + "settings.custom_router.view.prompt_based_routing", + "Prompt-based routing", + ), }; let type_row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) @@ -257,7 +266,7 @@ fn render_targets_row( match routing { CustomModelRouting::Complexity(c) => { flex.add_child(render_model_line( - "Default:", + crate::menu_label("settings.custom_router.view.label_default", "Default:"), model_display_name(&c.default, app), appearance, sub_color, @@ -265,7 +274,7 @@ fn render_targets_row( if let Some(easy) = &c.easy { flex.add_child( Container::new(render_model_line( - "Easy:", + crate::menu_label("settings.custom_router.view.label_easy", "Easy:"), model_display_name(easy, app), appearance, sub_color, @@ -277,7 +286,7 @@ fn render_targets_row( if let Some(medium) = &c.medium { flex.add_child( Container::new(render_model_line( - "Medium:", + crate::menu_label("settings.custom_router.view.label_medium", "Medium:"), model_display_name(medium, app), appearance, sub_color, @@ -289,7 +298,7 @@ fn render_targets_row( if let Some(hard) = &c.hard { flex.add_child( Container::new(render_model_line( - "Hard:", + crate::menu_label("settings.custom_router.view.label_hard", "Hard:"), model_display_name(hard, app), appearance, sub_color, @@ -301,7 +310,7 @@ fn render_targets_row( } CustomModelRouting::Prompt(p) => { flex.add_child(render_model_line( - "Default:", + crate::menu_label("settings.custom_router.view.label_default", "Default:"), model_display_name(&p.default_model, app), appearance, sub_color, @@ -309,9 +318,20 @@ fn render_targets_row( let rule_count = p.rules.len(); if rule_count > 0 { let label = if rule_count == 1 { - "1 rule".to_string() + crate::menu_label( + "settings.custom_router.view.rules_count_one", + "1 rule", + ) + .to_string() } else { - format!("{rule_count} rules") + i18n::interpolate( + crate::menu_label( + "settings.custom_router.view.rules_count_other", + "{n} rules", + ), + &[("n", rule_count.to_string())], + ) + .into_owned() }; flex.add_child( Container::new( diff --git a/app/src/settings_view/execution_profile_view.rs b/app/src/settings_view/execution_profile_view.rs index e34ec58f4f..e2fd2bd782 100644 --- a/app/src/settings_view/execution_profile_view.rs +++ b/app/src/settings_view/execution_profile_view.rs @@ -53,7 +53,7 @@ impl ExecutionProfileView { }); let edit_button = ctx.add_typed_action_view(|_ctx| { - ActionButton::new("Edit", SecondaryTheme) + ActionButton::new(crate::menu_label("common.edit", "Edit"), SecondaryTheme) .with_icon(Icon::Pencil) .with_size(ButtonSize::Small) .on_click(|ctx| { @@ -117,14 +117,14 @@ impl View for ExecutionProfileView { .as_ref() .and_then(|id| llm_preferences.get_llm_info(id)) .map(|info| info.display_name.clone()) - .unwrap_or_else(|| "Auto".to_string()); + .unwrap_or_else(|| crate::menu_label("settings.execution_profile.auto", "Auto").to_string()); let computer_use_model = profile .computer_use_model .as_ref() .and_then(|id| llm_preferences.get_llm_info(id)) .map(|info| info.display_name.clone()) - .unwrap_or_else(|| "Auto".to_string()); + .unwrap_or_else(|| crate::menu_label("settings.execution_profile.auto", "Auto").to_string()); Container::new( Flex::column() @@ -150,7 +150,7 @@ impl View for ExecutionProfileView { let mut model_flex = Flex::column(); model_flex.add_child( Container::new( - Text::new("MODELS", appearance.ui_font_family(), 10.) + Text::new(crate::menu_label("settings.execution_profile.models_section", "MODELS"), appearance.ui_font_family(), 10.) .with_color(appearance.theme().disabled_ui_text_color().into()) .finish(), ) @@ -160,7 +160,7 @@ impl View for ExecutionProfileView { model_flex.add_child(with_standard_vertical_margin( render_model_line_with_icon( Icon::Lightning, - "Base model:", + crate::menu_label("settings.execution_profile.base_model", "Base model:"), base_model, appearance, is_any_ai_enabled, @@ -169,7 +169,7 @@ impl View for ExecutionProfileView { model_flex.add_child(with_standard_vertical_margin( render_model_line_with_icon( Icon::Terminal, - "Full terminal use:", + crate::menu_label("settings.execution_profile.full_terminal_use", "Full terminal use:"), cli_agent_model, appearance, is_any_ai_enabled, @@ -179,7 +179,7 @@ impl View for ExecutionProfileView { model_flex.add_child(with_standard_vertical_margin( render_model_line_with_icon( Icon::Laptop, - "Computer use:", + crate::menu_label("settings.execution_profile.computer_use_model", "Computer use:"), computer_use_model, appearance, is_any_ai_enabled, @@ -196,7 +196,7 @@ impl View for ExecutionProfileView { let mut permissions_column = Flex::column() .with_child( Container::new( - Text::new("PERMISSIONS", appearance.ui_font_family(), 10.) + Text::new(crate::menu_label("settings.execution_profile.permissions_section", "PERMISSIONS"), appearance.ui_font_family(), 10.) .with_color( appearance.theme().disabled_ui_text_color().into(), ) @@ -208,7 +208,7 @@ impl View for ExecutionProfileView { .with_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Code2, - "Apply code diffs:", + crate::menu_label("settings.execution_profile.apply_code_diffs", "Apply code diffs:"), &profile.apply_code_diffs, appearance, is_any_ai_enabled, @@ -217,7 +217,7 @@ impl View for ExecutionProfileView { .with_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Notebook, - "Read files:", + crate::menu_label("settings.execution_profile.read_files", "Read files:"), &profile.read_files, appearance, is_any_ai_enabled, @@ -237,7 +237,7 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Terminal, - "Execute commands:", + crate::menu_label("settings.execution_profile.execute_commands", "Execute commands:"), &profile.execute_commands, appearance, is_any_ai_enabled, @@ -276,7 +276,7 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_write_to_pty_permission_line_with_icon( Icon::Workflow, - "Interact with running commands:", + crate::menu_label("settings.execution_profile.interact_with_running_commands", "Interact with running commands:"), &profile.write_to_pty, appearance, is_any_ai_enabled, @@ -287,7 +287,7 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_computer_use_permission_line_with_icon( Icon::Laptop, - "Computer use:", + crate::menu_label("settings.execution_profile.computer_use_permission", "Computer use:"), &profile.computer_use, appearance, is_any_ai_enabled, @@ -298,7 +298,7 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_ask_user_question_permission_line_with_icon( Icon::MessageText, - "Ask questions:", + crate::menu_label("settings.execution_profile.ask_questions", "Ask questions:"), &profile.ask_user_question, appearance, is_any_ai_enabled, @@ -307,7 +307,7 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_run_agents_permission_line_with_icon( Icon::Workflow, - "Run agents:", + crate::menu_label("settings.execution_profile.run_agents", "Run agents:"), &profile.run_agents, appearance, is_any_ai_enabled, @@ -317,7 +317,7 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_action_permission_line_with_icon( Icon::Dataflow, - "Call MCP servers:", + crate::menu_label("settings.execution_profile.call_mcp_servers", "Call MCP servers:"), &profile.mcp_permissions, appearance, is_any_ai_enabled, @@ -361,7 +361,7 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_bool_permission_line_with_icon( Icon::Globe, - "Call web tools:", + crate::menu_label("settings.execution_profile.call_web_tools", "Call web tools:"), profile.web_search_enabled, appearance, is_any_ai_enabled, @@ -372,7 +372,7 @@ impl View for ExecutionProfileView { permissions_column.add_child(with_standard_vertical_margin( render_bool_permission_line_with_icon( Icon::Compass, - "Auto-sync plans to Warp Drive:", + crate::menu_label("settings.execution_profile.autosync_plans_to_warp_drive", "Auto-sync plans to Warp Drive:"), profile.autosync_plans_to_warp_drive, appearance, is_any_ai_enabled, @@ -424,7 +424,7 @@ where let items_vec: Vec = items.into_iter().map(|item| item.to_string()).collect(); if items_vec.is_empty() { return Container::new( - Text::new("None", appearance.ui_font_family(), 12.) + Text::new(crate::menu_label("settings.execution_profile.none", "None"), appearance.ui_font_family(), 12.) .with_color(appearance.theme().disabled_ui_text_color().into()) .finish(), ) @@ -695,10 +695,10 @@ fn render_action_permission_line_with_icon( is_ai_enabled: bool, ) -> Box { let permission_text = match permission { - ActionPermission::AgentDecides => "Agent decides", - ActionPermission::AlwaysAllow => "Always allow", - ActionPermission::AlwaysAsk => "Always ask", - ActionPermission::Unknown => "Unknown", + ActionPermission::AgentDecides => crate::menu_label("settings.execution_profile.permission_agent_decides", "Agent decides"), + ActionPermission::AlwaysAllow => crate::menu_label("settings.execution_profile.permission_always_allow", "Always allow"), + ActionPermission::AlwaysAsk => crate::menu_label("settings.execution_profile.permission_always_ask", "Always ask"), + ActionPermission::Unknown => crate::menu_label("settings.execution_profile.permission_unknown", "Unknown"), }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -711,10 +711,10 @@ fn render_write_to_pty_permission_line_with_icon( is_ai_enabled: bool, ) -> Box { let permission_text = match permission { - WriteToPtyPermission::AlwaysAllow => "Always allow", - WriteToPtyPermission::AlwaysAsk => "Always ask", - WriteToPtyPermission::AskOnFirstWrite => "Ask on first write", - WriteToPtyPermission::Unknown => "Unknown", + WriteToPtyPermission::AlwaysAllow => crate::menu_label("settings.execution_profile.permission_always_allow", "Always allow"), + WriteToPtyPermission::AlwaysAsk => crate::menu_label("settings.execution_profile.permission_always_ask", "Always ask"), + WriteToPtyPermission::AskOnFirstWrite => crate::menu_label("settings.execution_profile.permission_ask_on_first_write", "Ask on first write"), + WriteToPtyPermission::Unknown => crate::menu_label("settings.execution_profile.permission_unknown", "Unknown"), }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -728,9 +728,9 @@ fn render_computer_use_permission_line_with_icon( ) -> Box { let permission_text = match permission { crate::ai::execution_profiles::ComputerUsePermission::Never - | crate::ai::execution_profiles::ComputerUsePermission::Unknown => "Never", - crate::ai::execution_profiles::ComputerUsePermission::AlwaysAsk => "Always ask", - crate::ai::execution_profiles::ComputerUsePermission::AlwaysAllow => "Always allow", + | crate::ai::execution_profiles::ComputerUsePermission::Unknown => crate::menu_label("settings.execution_profile.permission_never", "Never"), + crate::ai::execution_profiles::ComputerUsePermission::AlwaysAsk => crate::menu_label("settings.execution_profile.permission_always_ask", "Always ask"), + crate::ai::execution_profiles::ComputerUsePermission::AlwaysAllow => crate::menu_label("settings.execution_profile.permission_always_allow", "Always allow"), }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -743,11 +743,11 @@ fn render_ask_user_question_permission_line_with_icon( is_ai_enabled: bool, ) -> Box { let permission_text = match permission { - AskUserQuestionPermission::Never => "Never ask", + AskUserQuestionPermission::Never => crate::menu_label("settings.execution_profile.permission_never_ask", "Never ask"), AskUserQuestionPermission::AskExceptInAutoApprove | AskUserQuestionPermission::Unknown => { - "Ask unless auto-approve" + crate::menu_label("settings.execution_profile.permission_ask_unless_auto_approve", "Ask unless auto-approve") } - AskUserQuestionPermission::AlwaysAsk => "Always ask", + AskUserQuestionPermission::AlwaysAsk => crate::menu_label("settings.execution_profile.permission_always_ask", "Always ask"), }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -760,9 +760,9 @@ fn render_run_agents_permission_line_with_icon( is_ai_enabled: bool, ) -> Box { let permission_text = match permission { - RunAgentsPermission::NeverAllow | RunAgentsPermission::Unknown => "Never", - RunAgentsPermission::AlwaysAllow => "Always allow", - RunAgentsPermission::AlwaysAsk => "Always ask", + RunAgentsPermission::NeverAllow | RunAgentsPermission::Unknown => crate::menu_label("settings.execution_profile.permission_never", "Never"), + RunAgentsPermission::AlwaysAllow => crate::menu_label("settings.execution_profile.permission_always_allow", "Always allow"), + RunAgentsPermission::AlwaysAsk => crate::menu_label("settings.execution_profile.permission_always_ask", "Always ask"), }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -774,7 +774,11 @@ fn render_bool_permission_line_with_icon( appearance: &Appearance, is_ai_enabled: bool, ) -> Box { - let permission_text = if enabled { "On" } else { "Off" }; + let permission_text = if enabled { + crate::menu_label("settings.execution_profile.on", "On") + } else { + crate::menu_label("settings.execution_profile.off", "Off") + }; render_permission_line_with_icon(icon, label, permission_text, appearance, is_ai_enabled) } @@ -785,7 +789,7 @@ fn render_directory_allowlist( ) -> Box { with_standard_vertical_margin(render_pathbuf_allowlist_row( Icon::Check, - "Directory allowlist:".to_string(), + crate::menu_label("settings.execution_profile.directory_allowlist", "Directory allowlist:").to_string(), &profile.directory_allowlist, appearance, is_ai_enabled, @@ -799,7 +803,7 @@ fn render_command_allowlist( ) -> Box { with_standard_vertical_margin(render_command_predicate_row( Icon::Check, - "Command allowlist:".to_string(), + crate::menu_label("settings.execution_profile.command_allowlist", "Command allowlist:").to_string(), &profile.command_allowlist, appearance, is_ai_enabled, @@ -813,7 +817,7 @@ fn render_command_denylist( ) -> Box { with_standard_vertical_margin(render_command_predicate_row( Icon::SlashCircle, - "Command denylist:".to_string(), + crate::menu_label("settings.execution_profile.command_denylist", "Command denylist:").to_string(), &profile.command_denylist, appearance, is_ai_enabled, @@ -828,7 +832,7 @@ fn render_mcp_allowlist( ) -> Box { with_standard_vertical_margin(render_mcp_uuid_row( Icon::Check, - "MCP allowlist:".to_string(), + crate::menu_label("settings.execution_profile.mcp_allowlist", "MCP allowlist:").to_string(), &profile.mcp_allowlist, appearance, app, @@ -844,7 +848,7 @@ fn render_mcp_denylist( ) -> Box { with_standard_vertical_margin(render_mcp_uuid_row( Icon::SlashCircle, - "MCP denylist:".to_string(), + crate::menu_label("settings.execution_profile.mcp_denylist", "MCP denylist:").to_string(), &profile.mcp_denylist, appearance, app, diff --git a/app/src/settings_view/features_page.rs b/app/src/settings_view/features_page.rs index fcec718d5a..57970df387 100644 --- a/app/src/settings_view/features_page.rs +++ b/app/src/settings_view/features_page.rs @@ -112,11 +112,19 @@ use crate::{report_if_error, send_telemetry_from_ctx, themes, GlobalResourceHand cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { - static EXTRA_META_KEYS_LEFT_TEXT: &str = "Left Option key is Meta"; - static EXTRA_META_KEYS_RIGHT_TEXT: &str = "Right Option key is Meta"; + fn extra_meta_keys_left_text() -> &'static str { + crate::menu_label("settings.features.left_option_meta", "Left Option key is Meta") + } + fn extra_meta_keys_right_text() -> &'static str { + crate::menu_label("settings.features.right_option_meta", "Right Option key is Meta") + } } else { - static EXTRA_META_KEYS_LEFT_TEXT: &str = "Left Alt key is Meta"; - static EXTRA_META_KEYS_RIGHT_TEXT: &str = "Right Alt key is Meta"; + fn extra_meta_keys_left_text() -> &'static str { + crate::menu_label("settings.features.left_alt_meta", "Left Alt key is Meta") + } + fn extra_meta_keys_right_text() -> &'static str { + crate::menu_label("settings.features.right_alt_meta", "Right Alt key is Meta") + } } } @@ -130,7 +138,7 @@ pub fn init_actions_from_parent_view( // Add all of the toggle settings from the Features Page that you want to show up on the Command Palette here. let mut toggle_binding_pairs = vec![ ToggleSettingActionPair::new( - "copy on select within the terminal", + crate::menu_label("settings.features.toggle.copy_on_select", "copy on select within the terminal"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCopyOnSelect, )), @@ -138,7 +146,7 @@ pub fn init_actions_from_parent_view( flags::COPY_ON_SELECT_CONTEXT_FLAG, ), ToggleSettingActionPair::new( - "linux selection clipboard", + crate::menu_label("settings.features.toggle.linux_selection_clipboard", "linux selection clipboard"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLinuxClipboardSelection, )), @@ -151,7 +159,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "autocomplete quotes, parentheses, and brackets", + crate::menu_label("settings.features.toggle.autocomplete_symbols", "autocomplete quotes, parentheses, and brackets"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAutocompleteSymbols, )), @@ -164,7 +172,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "restore windows, tabs, and panes on startup", + crate::menu_label("settings.features.toggle.restore_session", "restore windows, tabs, and panes on startup"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleRestoreSession, )), @@ -172,7 +180,7 @@ pub fn init_actions_from_parent_view( flags::RESTORE_SESSION_CONTEXT_FLAG, ), ToggleSettingActionPair::new( - EXTRA_META_KEYS_LEFT_TEXT, + extra_meta_keys_left_text(), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLeftMetaKey, )), @@ -185,7 +193,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - EXTRA_META_KEYS_RIGHT_TEXT, + extra_meta_keys_right_text(), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleRightMetaKey, )), @@ -198,7 +206,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "scroll reporting", + crate::menu_label("settings.features.toggle.scroll_reporting", "scroll reporting"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleScrollReporting, )), @@ -211,7 +219,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "completions while typing", + crate::menu_label("settings.features.toggle.completions_while_typing", "completions while typing"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCompletionsOpenWhileTyping, )), @@ -224,7 +232,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "command corrections", + crate::menu_label("settings.features.toggle.command_corrections", "command corrections"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCommandCorrections, )), @@ -237,7 +245,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "error underlining", + crate::menu_label("settings.features.toggle.error_underlining", "error underlining"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleErrorUnderlining, )), @@ -250,7 +258,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "syntax highlighting", + crate::menu_label("settings.features.toggle.syntax_highlighting", "syntax highlighting"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSyntaxHighlighting, )), @@ -263,7 +271,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "audible terminal bell", + crate::menu_label("settings.features.toggle.audible_bell", "audible terminal bell"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleUseAudibleBell, )), @@ -276,7 +284,7 @@ pub fn init_actions_from_parent_view( .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( - "autosuggestions", + crate::menu_label("settings.features.toggle.autosuggestions", "autosuggestions"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAutosuggestions, )), @@ -284,7 +292,7 @@ pub fn init_actions_from_parent_view( flags::AUTOSUGGESTIONS_ENABLED_FLAG, ), ToggleSettingActionPair::new( - "autosuggestion keybinding hint", + crate::menu_label("settings.features.toggle.autosuggestion_keybinding_hint", "autosuggestion keybinding hint"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAutosuggestionKeybindingHint, )), @@ -292,7 +300,7 @@ pub fn init_actions_from_parent_view( flags::AUTOSUGGESTION_KEYBINDING_HINT_FLAG, ), ToggleSettingActionPair::new( - "autosuggestion ignore button", + crate::menu_label("settings.features.toggle.autosuggestion_ignore_button", "autosuggestion ignore button"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowAutosuggestionIgnoreButton, )), @@ -303,7 +311,7 @@ pub fn init_actions_from_parent_view( ]; toggle_binding_pairs.push(ToggleSettingActionPair::new( - "reuse existing SSH ControlMaster in the Warp SSH wrapper", + crate::menu_label("settings.features.toggle.ssh_reuse_control_master", "reuse existing SSH ControlMaster in the Warp SSH wrapper"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSshReuseControlMaster, )), @@ -312,7 +320,7 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "show tooltip on click on links", + crate::menu_label("settings.features.toggle.link_tooltip", "show tooltip on click on links"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLinkTooltip, )), @@ -321,7 +329,7 @@ pub fn init_actions_from_parent_view( )); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "long-running command notifications", + crate::menu_label("settings.features.toggle.long_running_notifications", "long-running command notifications"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLongRunningNotifications, )), @@ -336,7 +344,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "agent task completion notifications", + crate::menu_label("settings.features.toggle.agent_task_completed_notifications", "agent task completion notifications"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAgentTaskCompletedNotifications, )), @@ -351,7 +359,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "needs-attention notifications", + crate::menu_label("settings.features.toggle.needs_attention_notifications", "needs-attention notifications"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleNeedsAttentionNotifications, )), @@ -367,7 +375,7 @@ pub fn init_actions_from_parent_view( #[cfg(target_os = "macos")] toggle_binding_pairs.push( ToggleSettingActionPair::new( - "notification sounds", + crate::menu_label("settings.features.toggle.notification_sounds", "notification sounds"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleNotificationSound, )), @@ -382,7 +390,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "in-app agent notifications", + crate::menu_label("settings.features.toggle.in_app_agent_notifications", "in-app agent notifications"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAgentInAppNotifications, )), @@ -394,7 +402,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "quit warning modal", + crate::menu_label("settings.features.toggle.quit_warning_modal", "quit warning modal"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowWarningBeforeQuitting, )), @@ -409,7 +417,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "mouse reporting", + crate::menu_label("settings.features.toggle.mouse_reporting", "mouse reporting"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleMouseReporting, )), @@ -425,7 +433,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "alias expansion", + crate::menu_label("settings.features.toggle.alias_expansion", "alias expansion"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAliasExpansion, )), @@ -441,7 +449,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "middle-click paste", + crate::menu_label("settings.features.toggle.middle_click_paste", "middle-click paste"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleMiddleClickPaste, )), @@ -457,7 +465,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "code as default editor", + crate::menu_label("settings.features.toggle.code_as_default_editor", "code as default editor"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCodeAsDefaultEditor, )), @@ -473,7 +481,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "input hint text", + crate::menu_label("settings.features.toggle.input_hint_text", "input hint text"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowInputHintText, )), @@ -489,7 +497,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "editing commands with Vim keybindings", + crate::menu_label("settings.features.toggle.vim_keybindings", "editing commands with Vim keybindings"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleVimMode, )), @@ -505,7 +513,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "Vim unnamed register as system clipboard", + crate::menu_label("settings.features.toggle.vim_unnamed_register", "Vim unnamed register as system clipboard"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleVimUnnamedSystemClipboard, )), @@ -521,7 +529,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "Vim status bar", + crate::menu_label("settings.features.toggle.vim_status_bar", "Vim status bar"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleVimStatusBar, )), @@ -537,7 +545,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "focus reporting", + crate::menu_label("settings.features.toggle.focus_reporting", "focus reporting"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleFocusReporting, )), @@ -552,7 +560,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "smart select", + crate::menu_label("settings.features.toggle.smart_select", "smart select"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSmartSelection, )), @@ -562,7 +570,7 @@ pub fn init_actions_from_parent_view( if FeatureFlag::AgentView.is_enabled() && AISettings::as_ref(app).is_any_ai_enabled(app) { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "help block in new sessions", + crate::menu_label("settings.features.toggle.help_block_in_new_sessions", "help block in new sessions"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowTerminalZeroStateBlock, )), @@ -579,7 +587,7 @@ pub fn init_actions_from_parent_view( toggle_binding_pairs.push( ToggleSettingActionPair::new( - "terminal input message line", + crate::menu_label("settings.features.toggle.terminal_input_message_line", "terminal input message line"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowTerminalInputMessageLine, )), @@ -590,7 +598,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push( ToggleSettingActionPair::new( - "'@' context menu in terminal mode", + crate::menu_label("settings.features.toggle.at_context_menu_terminal", "'@' context menu in terminal mode"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAtContextMenuInTerminalMode, )), @@ -605,7 +613,7 @@ pub fn init_actions_from_parent_view( ); toggle_binding_pairs.push(ToggleSettingActionPair::new( - "preserve input focus on block selection", + crate::menu_label("settings.features.toggle.preserve_input_focus_on_block_selection", "preserve input focus on block selection"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::TogglePreserveInputFocusOnBlockSelection, )), @@ -616,7 +624,7 @@ pub fn init_actions_from_parent_view( if FeatureFlag::AgentView.is_enabled() && AISettings::as_ref(app).is_any_ai_enabled(app) { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "slash commands in terminal mode", + crate::menu_label("settings.features.toggle.slash_commands_terminal", "slash commands in terminal mode"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSlashCommandsInTerminalMode, )), @@ -633,7 +641,7 @@ pub fn init_actions_from_parent_view( if FeatureFlag::AIContextMenuCode.is_enabled() { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "codebase symbols in the '@' context menu", + crate::menu_label("settings.features.toggle.outline_codebase_symbols", "codebase symbols in the '@' context menu"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleOutlineCodebaseSymbolsForAtContextMenu, )), @@ -649,7 +657,7 @@ pub fn init_actions_from_parent_view( } toggle_binding_pairs.push( ToggleSettingActionPair::new( - "global workflows in Command Search", + crate::menu_label("settings.features.toggle.global_workflows_search", "global workflows in Command Search"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleGlobalWorkflowsInUniversalSearch, )), @@ -666,7 +674,7 @@ pub fn init_actions_from_parent_view( if GPUState::as_ref(app).is_low_power_gpu_available() { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "integrated GPU rendering (low power)", + crate::menu_label("settings.features.toggle.integrated_gpu", "integrated GPU rendering (low power)"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::TogglePreferLowPowerGPU, )), @@ -686,7 +694,7 @@ pub fn init_actions_from_parent_view( if windowing_system_is_customizable(app) { toggle_binding_pairs.push( ToggleSettingActionPair::new( - "Wayland for window management", + crate::menu_label("settings.features.toggle.wayland_window_management", "Wayland for window management"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleForceX11, )), @@ -704,7 +712,7 @@ pub fn init_actions_from_parent_view( ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings(toggle_binding_pairs, app); app.register_fixed_bindings([FixedBinding::empty( - "Configure Global Hotkey", + crate::menu_label("settings.features.configure_global_hotkey", "Configure Global Hotkey"), WorkspaceAction::ScrollToSettingsWidget { page: SettingsSection::Features, widget_id: GlobalHotkeyWidget::static_widget_id(), @@ -714,7 +722,7 @@ pub fn init_actions_from_parent_view( if DefaultTerminal::can_warp_become_default() { app.register_fixed_bindings([FixedBinding::empty( - "Make Warp the default terminal", + crate::menu_label("settings.features.make_default_terminal", "Make Warp the default terminal"), builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::MakeWarpDefaultTerminal, )), @@ -861,14 +869,19 @@ fn max_max_grid_size() -> usize { fn block_maximum_rows_description() -> String { let max_rows = if ChannelState::enable_debug_features() { - "10 million" + crate::menu_label("settings.features.ten_million", "10 million") } else { - "1 million" + crate::menu_label("settings.features.one_million", "1 million") }; - format!( - "Setting the limit above 100k lines may impact performance. Maximum rows supported is {max_rows}." + i18n::interpolate( + crate::menu_label( + "settings.features.block_max_rows_warning", + "Setting the limit above 100k lines may impact performance. Maximum rows supported is {max_rows}.", + ), + &[("max_rows", max_rows.to_string())], ) + .into_owned() } fn to_string(b: bool) -> String { @@ -2304,22 +2317,22 @@ impl FeaturesPageView { let mut dropdown = Dropdown::new(ctx); let top = DropdownItem::new( - "Pin to top", + crate::menu_label("settings.features.pin_top", "Pin to top"), FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Top), ); let bottom = DropdownItem::new( - "Pin to bottom", + crate::menu_label("settings.features.pin_bottom", "Pin to bottom"), FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Bottom), ); let left = DropdownItem::new( - "Pin to left", + crate::menu_label("settings.features.pin_left", "Pin to left"), FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Left), ); let right = DropdownItem::new( - "Pin to right", + crate::menu_label("settings.features.pin_right", "Pin to right"), FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Right), ); @@ -3462,7 +3475,7 @@ impl FeaturesPageView { self.graphics_backend_dropdown.update(ctx, |dropdown, ctx| { if let Some(window) = ctx.windows().platform_window(ctx.window_id()) { let mut items = vec![DropdownItem::new( - "Default", + crate::menu_label("settings.features.default", "Default"), FeaturesPageAction::SetPreferredGraphicsBackend(None), )]; items.extend(window.supported_backends().into_iter().map(|backend| { @@ -3478,7 +3491,7 @@ impl FeaturesPageView { gpu_settings .preferred_backend .map(|backend| backend.to_label()) - .unwrap_or("Default"), + .unwrap_or_else(|| crate::menu_label("settings.features.default", "Default")), ctx, ); }); @@ -3574,8 +3587,8 @@ impl FeaturesPageView { fn new_tab_placement_dropdown_item_label(val: NewTabPlacement) -> &'static str { match val { - NewTabPlacement::AfterAllTabs => "After all tabs", - NewTabPlacement::AfterCurrentTab => "After current tab", + NewTabPlacement::AfterAllTabs => crate::menu_label("settings.features.new_tab_after_all", "After all tabs"), + NewTabPlacement::AfterCurrentTab => crate::menu_label("settings.features.new_tab_after_current", "After current tab"), } } @@ -3790,7 +3803,7 @@ impl FeaturesPageView { .with_child( Container::new( Text::new_inline( - "Width %", + crate::menu_label("settings.features.width_pct", "Width %"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -3828,7 +3841,7 @@ impl FeaturesPageView { .with_child( Container::new( Text::new_inline( - "Height %", + crate::menu_label("settings.features.height_pct", "Height %"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -3906,7 +3919,7 @@ impl FeaturesPageView { .with_child( appearance .ui_builder() - .span("Autohides on loss of keyboard focus") + .span(crate::menu_label("settings.features.autohides_on_focus_loss", "Autohides on loss of keyboard focus")) .build() .with_margin_left(5.) .finish(), @@ -4002,7 +4015,7 @@ impl FeaturesPageView { Container::new( Align::new( Text::new_inline( - "When a command takes longer than", + crate::menu_label("settings.features.command_longer_than", "When a command takes longer than"), appearance.ui_font_family(), font_size, ) @@ -4038,7 +4051,7 @@ impl FeaturesPageView { Container::new( Align::new( Text::new_inline( - "seconds to complete", + crate::menu_label("settings.features.seconds_to_complete", "seconds to complete"), appearance.ui_font_family(), font_size, ) @@ -4120,7 +4133,7 @@ impl FeaturesPageView { Shrinkable::new( 2., Align::new( - Text::new_inline("Keybinding", appearance.ui_font_family(), 13.) + Text::new_inline(crate::menu_label("settings.features.keybinding", "Keybinding"), appearance.ui_font_family(), 13.) .with_color(appearance.theme().active_ui_text_color().into()) .finish(), ) @@ -4141,7 +4154,7 @@ impl FeaturesPageView { } else { appearance .ui_builder() - .paragraph("Click to set global hotkey".to_string()) + .paragraph(crate::menu_label("settings.features.click_to_set_hotkey", "Click to set global hotkey").to_string()) .build() .finish() }) @@ -4196,7 +4209,7 @@ impl FeaturesPageView { padding: Some(Coords::default().right(10.)), ..Default::default() }) - .with_text_label("Cancel".to_string()) + .with_text_label(crate::menu_label("common.cancel", "Cancel").to_string()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(cancel_action.clone()); @@ -4208,7 +4221,7 @@ impl FeaturesPageView { appearance .ui_builder() .button(ButtonVariant::Text, save_button_mouse_state) - .with_text_label("Save".to_string()) + .with_text_label(crate::menu_label("common.save", "Save").to_string()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(save_action.clone()); @@ -4232,7 +4245,7 @@ impl FeaturesPageView { 2., Align::new( Text::new_inline( - "Press new keyboard shortcut", + crate::menu_label("settings.features.press_new_shortcut", "Press new keyboard shortcut"), appearance.ui_font_family(), 13., ) @@ -4283,7 +4296,7 @@ impl FeaturesPageView { } Container::new( - Text::new_inline("Change keybinding", appearance.ui_font_family(), 12.) + Text::new_inline(crate::menu_label("settings.features.change_keybinding", "Change keybinding"), appearance.ui_font_family(), 12.) .with_color(button_color) .finish(), ) @@ -4449,7 +4462,7 @@ fn init_display_count_dropdown( ctx: &mut ViewContext>, ) { let no_preference = DropdownItem::new( - "Active Screen", + crate::menu_label("settings.features.active_screen", "Active Screen"), //|| { FeaturesPageAction::QuakeEditorSetPinScreen(None), //} ); @@ -4473,7 +4486,7 @@ fn init_display_count_dropdown( Some(idx) if idx.is_valid_given_display_count(display_count) => { dropdown.set_selected_by_name(format!("{idx}"), ctx) } - _ => dropdown.set_selected_by_name("Active Screen", ctx), + _ => dropdown.set_selected_by_name(crate::menu_label("settings.features.active_screen", "Active Screen"), ctx), }; } @@ -4498,7 +4511,7 @@ impl SettingsWidget for NativeRedirectWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Open links in desktop app".into(), + crate::menu_label("settings.features.open_links_desktop", "Open links in desktop app").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, @@ -4568,7 +4581,7 @@ impl SettingsWidget for SessionRestorationWidget { .finish(); let labeled_switch = render_body_item::( - "Restore windows, tabs, and panes on startup".into(), + crate::menu_label("settings.features.restore_session", "Restore windows, tabs, and panes on startup").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -4603,7 +4616,7 @@ impl SettingsWidget for SessionRestorationWidget { let link = ui_builder .link( - "See docs.".to_owned(), + crate::menu_label("settings.features.see_docs", "See docs.").to_owned(), Some("https://docs.warp.dev/terminal/sessions/session-restoration".to_owned()), None, self.docs_link.clone(), @@ -4653,7 +4666,7 @@ impl SettingsWidget for SnackbarHeaderWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Show sticky command header".into(), + crate::menu_label("settings.features.sticky_command_header", "Show sticky command header").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -4706,7 +4719,7 @@ impl SettingsWidget for LinkTooltipWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Show tooltip on click on links".into(), + crate::menu_label("settings.features.link_tooltip", "Show tooltip on click on links").into(), None, LocalOnlyIconState::for_setting( LinkTooltip::storage_key(), @@ -4775,7 +4788,7 @@ impl SettingsWidget for QuitWarningModalWidget { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Show warning before quitting/logging out".into(), + crate::menu_label("settings.features.quit_warning_modal", "Show warning before quitting/logging out").into(), None, LocalOnlyIconState::for_setting( ShowWarningBeforeQuitting::storage_key(), @@ -4822,9 +4835,9 @@ impl SettingsWidget for LoginItemWidget { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); #[cfg(target_os = "macos")] - let label = "Start Warp at login (requires macOS 13+)"; + let label = crate::menu_label("settings.features.start_at_login_macos", "Start Warp at login (requires macOS 13+)"); #[cfg(not(target_os = "macos"))] - let label = "Start Warp at login"; + let label = crate::menu_label("settings.features.start_at_login", "Start Warp at login"); render_body_item::( label.into(), None, @@ -4873,7 +4886,7 @@ impl SettingsWidget for QuitWhenAllWindowsClosedWidget { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Quit when all windows are closed".into(), + crate::menu_label("settings.features.quit_when_closed", "Quit when all windows are closed").into(), None, LocalOnlyIconState::for_setting( QuitOnLastWindowClosed::storage_key(), @@ -4920,7 +4933,7 @@ impl SettingsWidget for ShowChangelogWidget { let changelog_settings = ChangelogSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Show changelog toast after updates".into(), + crate::menu_label("settings.features.show_changelog", "Show changelog toast after updates").into(), None, LocalOnlyIconState::for_setting( ShowChangelogAfterUpdate::storage_key(), @@ -4993,7 +5006,7 @@ impl SettingsWidget for MouseScrollMultiplierWidget { } else { appearance .ui_builder() - .wrappable_text("Allowed Values: 1-20", true) + .wrappable_text(crate::menu_label("settings.features.allowed_values_1_20", "Allowed Values: 1-20"), true) .with_style(UiComponentStyles { font_color: Some(themes::theme::Fill::error().into_solid()), ..Default::default() @@ -5006,13 +5019,13 @@ impl SettingsWidget for MouseScrollMultiplierWidget { .finish(); render_body_item::( - "Lines scrolled by mouse wheel interval".into(), + crate::menu_label("settings.features.mouse_scroll_lines", "Lines scrolled by mouse wheel interval").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, tooltip_override_text: Some( - "Supports floating point values between 1 and 20.".to_string(), + crate::menu_label("settings.features.mouse_scroll_tooltip", "Supports floating point values between 1 and 20.").to_string(), ), }), LocalOnlyIconState::for_setting( @@ -5053,7 +5066,7 @@ impl SettingsWidget for AutoOpenCodeReviewPaneWidget { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Auto open code review panel".into(), + crate::menu_label("settings.features.auto_open_code_review", "Auto open code review panel").into(), None, LocalOnlyIconState::for_setting( AutoOpenCodeReviewPaneOnFirstAgentChange::storage_key(), @@ -5074,7 +5087,7 @@ impl SettingsWidget for AutoOpenCodeReviewPaneWidget { ctx.dispatch_typed_action(FeaturesPageAction::ToggleAutoOpenCodeReviewPane); }) .finish(), - Some("When this setting is on, the code review panel will open on the first accepted diff of a conversation".into()), + Some(crate::menu_label("settings.features.auto_open_code_review_description", "When this setting is on, the code review panel will open on the first accepted diff of a conversation").into()), ) } } @@ -5101,7 +5114,7 @@ impl SettingsWidget for DefaultTerminalWidget { let default_terminal = DefaultTerminal::as_ref(app); if default_terminal.is_warp_default() { ui_builder - .wrappable_text("Warp is the default terminal", true) + .wrappable_text(crate::menu_label("settings.features.warp_is_default", "Warp is the default terminal"), true) .with_style(UiComponentStyles { font_color: Some(appearance.theme().disabled_ui_text_color().into()), margin: Some(Coords::default().bottom(16.)), @@ -5112,7 +5125,7 @@ impl SettingsWidget for DefaultTerminalWidget { } else { ui_builder .link( - "Make Warp the default terminal".to_string(), + crate::menu_label("settings.features.make_default_terminal", "Make Warp the default terminal").to_string(), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(FeaturesPageAction::MakeWarpDefaultTerminal); @@ -5165,7 +5178,7 @@ impl SettingsWidget for BlockLimitWidget { .finish(); render_body_item::( - "Maximum rows in a block".into(), + crate::menu_label("settings.features.max_block_rows", "Maximum rows in a block").into(), None, LocalOnlyIconState::for_setting( MaximumGridSize::storage_key(), @@ -5207,7 +5220,7 @@ impl SettingsWidget for DesktopNotificationsWidget { let ui_builder = appearance.ui_builder(); let mut column = Flex::column(); column.add_child(render_body_item::( - "Receive desktop notifications from Warp".into(), + crate::menu_label("settings.features.receive_desktop_notifications", "Receive desktop notifications from Warp").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl(NOTIFICATIONS_DOCS_URL.into())), @@ -5248,7 +5261,7 @@ impl SettingsWidget for DesktopNotificationsWidget { session_settings .notifications .is_agent_task_completed_enabled, - "Notify when an agent completes a task", + crate::menu_label("settings.features.notify_agent_completes", "Notify when an agent completes a task"), FeaturesPageAction::ToggleAgentTaskCompletedNotifications, view.button_mouse_states .agent_task_completed_notifications_checkbox @@ -5261,7 +5274,7 @@ impl SettingsWidget for DesktopNotificationsWidget { ), view.render_notification_toggle( session_settings.notifications.is_needs_attention_enabled, - "Notify when a command or agent needs your attention to continue", + crate::menu_label("settings.features.notify_needs_attention", "Notify when a command or agent needs your attention to continue"), FeaturesPageAction::ToggleNeedsAttentionNotifications, view.button_mouse_states .agent_needs_attention_notifications_checkbox @@ -5273,7 +5286,7 @@ impl SettingsWidget for DesktopNotificationsWidget { { view.render_notification_toggle( session_settings.notifications.play_notification_sound, - "Play notification sounds", + crate::menu_label("settings.features.play_notification_sounds", "Play notification sounds"), FeaturesPageAction::ToggleNotificationSound, view.button_mouse_states.notification_sound_checkbox.clone(), appearance, @@ -5288,7 +5301,7 @@ impl SettingsWidget for DesktopNotificationsWidget { let ai_settings = AISettings::as_ref(app); let show_agent_notifications = *ai_settings.show_agent_notifications; column.add_child(render_body_item::( - "Show in-app agent notifications".into(), + crate::menu_label("settings.features.in_app_agent_notifications", "Show in-app agent notifications").into(), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, @@ -5327,7 +5340,7 @@ impl SettingsWidget for DesktopNotificationsWidget { .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Text::new_inline( - "Toast notifications stay visible for", + crate::menu_label("settings.features.toast_visible_for", "Toast notifications stay visible for"), appearance.ui_font_family(), font_size, ) @@ -5356,7 +5369,7 @@ impl SettingsWidget for DesktopNotificationsWidget { .finish(), ) .with_child( - Text::new_inline("seconds", appearance.ui_font_family(), font_size) + Text::new_inline(crate::menu_label("settings.features.seconds", "seconds"), appearance.ui_font_family(), font_size) .with_color(font_color.into()) .finish(), ) @@ -5392,7 +5405,7 @@ impl SettingsWidget for StartupShellWidget { .with_children([ render_sub_sub_header( appearance, - "Default shell for new sessions".to_string(), + crate::menu_label("settings.features.default_shell", "Default shell for new sessions").to_string(), Some(LocalOnlyIconState::for_setting( StartupShellOverride::storage_key(), StartupShellOverride::sync_to_cloud(), @@ -5431,7 +5444,7 @@ impl SettingsWidget for WorkingDirectoryWidget { .with_children([ render_sub_sub_header( appearance, - "Working directory for new sessions".to_string(), + crate::menu_label("settings.features.working_directory", "Working directory for new sessions").to_string(), Some(LocalOnlyIconState::for_setting( WorkingDirectoryConfig::storage_key(), WorkingDirectoryConfig::sync_to_cloud(), @@ -5489,7 +5502,7 @@ impl SettingsWidget for ConfirmCloseSharedSessionWidget { let ui_builder = appearance.ui_builder(); let session_settings = SessionSettings::as_ref(app); render_body_item::( - "Confirm before closing shared session".into(), + crate::menu_label("settings.features.confirm_close_session", "Confirm before closing shared session").into(), None, LocalOnlyIconState::for_setting( ShouldConfirmCloseSession::storage_key(), @@ -5542,7 +5555,7 @@ impl SettingsWidget for ExtraMetaKeysWidget { .borrow_mut(); Flex::column() .with_child(render_body_item::( - EXTRA_META_KEYS_LEFT_TEXT.into(), + extra_meta_keys_left_text().into(), None, LocalOnlyIconState::for_setting( crate::terminal::keys_settings::ExtraMetaKeys::storage_key(), @@ -5563,7 +5576,7 @@ impl SettingsWidget for ExtraMetaKeysWidget { None, )) .with_child(render_body_item::( - EXTRA_META_KEYS_RIGHT_TEXT.into(), + extra_meta_keys_right_text().into(), None, LocalOnlyIconState::for_setting( crate::terminal::keys_settings::ExtraMetaKeys::storage_key(), @@ -5607,7 +5620,7 @@ impl SettingsWidget for GlobalHotkeyWidget { let ui_builder = appearance.ui_builder(); if app.is_wayland() { column.add_child(render_body_item::( - "Global hotkey:".to_owned(), + crate::menu_label("settings.features.global_hotkey", "Global hotkey:").to_owned(), None, // Fine not to show local only icon state for this, as it's not a supported setting. LocalOnlyIconState::Hidden, @@ -5616,12 +5629,12 @@ impl SettingsWidget for GlobalHotkeyWidget { Flex::row() .with_children([ ui_builder - .span("Not supported on Wayland. ") + .span(crate::menu_label("settings.features.not_supported_wayland", "Not supported on Wayland. ")) .build() .finish(), ui_builder .link( - "See docs.".to_owned(), + crate::menu_label("settings.features.see_docs", "See docs.").to_owned(), Some( "https://docs.warp.dev/terminal/windows/global-hotkey" .to_owned(), @@ -5643,7 +5656,7 @@ impl SettingsWidget for GlobalHotkeyWidget { || { render_dropdown_item( appearance, - "Global hotkey:", + crate::menu_label("settings.features.global_hotkey", "Global hotkey:"), None, None, LocalOnlyIconState::for_setting( @@ -5750,7 +5763,7 @@ impl SettingsWidget for AutocompleteSymbolsWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Autocomplete quotes, parentheses, and brackets".into(), + crate::menu_label("settings.features.autocomplete_symbols", "Autocomplete quotes, parentheses, and brackets").into(), None, LocalOnlyIconState::for_setting( AutocompleteSymbols::storage_key(), @@ -5799,7 +5812,7 @@ impl SettingsWidget for CodeEditorLineNumberModeWidget { || { render_dropdown_item( appearance, - "Code editor line numbers:", + crate::menu_label("settings.features.code_editor_line_numbers", "Code editor line numbers:"), None, None, LocalOnlyIconState::for_setting( @@ -5839,7 +5852,7 @@ impl SettingsWidget for ErrorUnderliningWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Error underlining for commands".into(), + crate::menu_label("settings.features.error_underlining", "Error underlining for commands").into(), None, LocalOnlyIconState::for_setting( ErrorUnderliningEnabled::storage_key(), @@ -5885,7 +5898,7 @@ impl SettingsWidget for SyntaxHighlightingWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Syntax highlighting for commands".into(), + crate::menu_label("settings.features.syntax_highlighting", "Syntax highlighting for commands").into(), None, LocalOnlyIconState::for_setting( SyntaxHighlighting::storage_key(), @@ -5931,7 +5944,7 @@ impl SettingsWidget for CompletionsMenuWhileTypingWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Open completions menu as you type".into(), + crate::menu_label("settings.features.completions_while_typing", "Open completions menu as you type").into(), None, LocalOnlyIconState::for_setting( CompletionsOpenWhileTyping::storage_key(), @@ -5981,7 +5994,7 @@ impl SettingsWidget for CommandCorrectionsWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Suggest corrected commands".into(), + crate::menu_label("settings.features.command_corrections", "Suggest corrected commands").into(), None, LocalOnlyIconState::for_setting( CommandCorrections::storage_key(), @@ -6028,7 +6041,7 @@ impl SettingsWidget for AliasExpansionWidget { let alias_expansion_settings = AliasExpansionSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Expand aliases as you type".into(), + crate::menu_label("settings.features.alias_expansion", "Expand aliases as you type").into(), None, LocalOnlyIconState::for_setting( AliasExpansionEnabled::storage_key(), @@ -6075,7 +6088,7 @@ impl SettingsWidget for MiddleClickPasteWidget { let ui_builder = appearance.ui_builder(); let selection_settings = SelectionSettings::as_ref(app); render_body_item::( - "Middle-click to paste".into(), + crate::menu_label("settings.features.middle_click_paste", "Middle-click to paste").into(), None, LocalOnlyIconState::for_setting( MiddleClickPasteEnabled::storage_key(), @@ -6127,7 +6140,7 @@ impl SettingsWidget for VimModeWidget { let app_editor_settings = AppEditorSettings::as_ref(app); let vim_mode_enabled = *app_editor_settings.vim_mode.value(); column.add_child(render_body_item::( - "Edit code and commands with Vim keybindings".into(), + crate::menu_label("settings.features.vim_keybindings", "Edit code and commands with Vim keybindings").into(), None, LocalOnlyIconState::for_setting( VimModeEnabled::storage_key(), @@ -6175,7 +6188,7 @@ impl SettingsWidget for VimModeWidget { app, ), clipboard_switch, - "Set unnamed register as system clipboard".into(), + crate::menu_label("settings.features.vim_unnamed_register", "Set unnamed register as system clipboard").into(), ); let vim_status_bar = *app_editor_settings.vim_status_bar.value(); @@ -6199,7 +6212,7 @@ impl SettingsWidget for VimModeWidget { app, ), status_bar_switch, - "Show Vim status bar".into(), + crate::menu_label("settings.features.vim_status_bar", "Show Vim status bar").into(), ); column.add_child(render_group( @@ -6232,7 +6245,7 @@ impl SettingsWidget for AtContextMenuInTerminalModeWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable '@' context menu in terminal mode".into(), + crate::menu_label("settings.features.at_context_menu_terminal", "Enable '@' context menu in terminal mode").into(), None, LocalOnlyIconState::for_setting( AtContextMenuInTerminalMode::storage_key(), @@ -6288,7 +6301,7 @@ impl SettingsWidget for SlashCommandsInTerminalModeWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable slash commands in terminal mode".into(), + crate::menu_label("settings.features.slash_commands_terminal", "Enable slash commands in terminal mode").into(), None, LocalOnlyIconState::for_setting( EnableSlashCommandsInTerminal::storage_key(), @@ -6340,7 +6353,7 @@ impl SettingsWidget for OutlineCodebaseSymbolsForAtContextMenuWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Outline codebase symbols for '@' context menu".into(), + crate::menu_label("settings.features.outline_codebase_symbols", "Outline codebase symbols for '@' context menu").into(), None, LocalOnlyIconState::for_setting( OutlineCodebaseSymbolsForAtContextMenu::storage_key(), @@ -6392,7 +6405,7 @@ impl SettingsWidget for ShowTerminalInputMessageLineWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Show terminal input message line".into(), + crate::menu_label("settings.features.terminal_input_message_line", "Show terminal input message line").into(), None, LocalOnlyIconState::for_setting( ShowTerminalInputMessageBar::storage_key(), @@ -6440,7 +6453,7 @@ impl SettingsWidget for PreserveInputFocusOnBlockSelectionWidget { ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( - "Preserve input focus on block selection".into(), + crate::menu_label("settings.features.preserve_input_focus_on_block_selection", "Preserve input focus on block selection").into(), None, LocalOnlyIconState::for_setting( PreserveInputFocusOnBlockSelection::storage_key(), @@ -6493,7 +6506,7 @@ impl SettingsWidget for AutosuggestionKeybindingHintWidget { let autosuggestion_keybinding_hint = *app_editor_settings.autosuggestion_keybinding_hint.value(); column.add_child(render_body_item::( - "Show autosuggestion keybinding hint".into(), + crate::menu_label("settings.features.autosuggestion_hint", "Show autosuggestion keybinding hint").into(), None, LocalOnlyIconState::for_setting( AutosuggestionKeybindingHint::storage_key(), @@ -6549,7 +6562,7 @@ impl SettingsWidget for AutosuggestionIgnoreButtonWidget { .show_autosuggestion_ignore_button .value(); column.add_child(render_body_item::( - "Show autosuggestion ignore button".into(), + crate::menu_label("settings.features.autosuggestion_ignore", "Show autosuggestion ignore button").into(), None, LocalOnlyIconState::for_setting( ShowAutosuggestionIgnoreButton::storage_key(), @@ -6594,36 +6607,45 @@ impl TabKeyBehaviorWidget { TabBehavior::Completions if view.autosuggestions_keystroke.is_empty() => { // If the "Accept autosuggestions" keybinding is unbound, the // user can always still accept with right arrow. - Some("→ accepts autosuggestions.".into()) + Some(crate::menu_label("settings.features.right_arrow_accepts", "→ accepts autosuggestions.").to_string()) } - TabBehavior::Completions => Some(format!( - "{} accepts autosuggestions.", - *view.autosuggestions_keystroke - )), + TabBehavior::Completions => Some(i18n::interpolate( + crate::menu_label( + "settings.features.accepts_autosuggestions", + "{} accepts autosuggestions.", + ), + &[("keystroke", view.autosuggestions_keystroke.to_string())], + ).into_owned()), TabBehavior::Autosuggestions if *input_settings.completions_open_while_typing.value() => { if view.completions_keystroke.is_empty() { - Some("Completions open as you type.".into()) + Some(crate::menu_label("settings.features.completions_open_typing", "Completions open as you type.").to_string()) } else { - Some(format!( - "Completions open as you type (or {}).", - *view.completions_keystroke - )) + Some(i18n::interpolate( + crate::menu_label( + "settings.features.completions_open_typing_or", + "Completions open as you type (or {}).", + ), + &[("keystroke", view.completions_keystroke.to_string())], + ).into_owned()) } } TabBehavior::Autosuggestions if view.completions_keystroke.is_empty() => { - Some("Opening the completion menu is unbound.".into()) + Some(crate::menu_label("settings.features.completions_unbound", "Opening the completion menu is unbound.").to_string()) } - TabBehavior::Autosuggestions => Some(format!( - "{} opens completion menu.", - *view.completions_keystroke - )), + TabBehavior::Autosuggestions => Some(i18n::interpolate( + crate::menu_label( + "settings.features.opens_completion_menu", + "{} opens completion menu.", + ), + &[("keystroke", view.completions_keystroke.to_string())], + ).into_owned()), TabBehavior::UserDefined => None, }; let other_keybinding_name = match *view.tab_behavior { - TabBehavior::Completions => Some("Accept Autosuggestion"), - TabBehavior::Autosuggestions => Some("Open Completions Menu"), + TabBehavior::Completions => Some(crate::menu_label("settings.features.accept_autosuggestion", "Accept Autosuggestion")), + TabBehavior::Autosuggestions => Some(crate::menu_label("settings.features.open_completions_menu", "Open Completions Menu")), TabBehavior::UserDefined => None, }; @@ -6676,7 +6698,7 @@ impl SettingsWidget for TabKeyBehaviorWidget { .with_child( appearance .ui_builder() - .span("Tab key behavior") + .span(crate::menu_label("settings.features.tab_key_behavior", "Tab key behavior")) .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE + 1.), ..Default::default() @@ -6736,7 +6758,7 @@ impl SettingsWidget for CtrlTabBehaviorWidget { || { render_dropdown_item( appearance, - "Ctrl+Tab behavior:", + crate::menu_label("settings.features.ctrl_tab_behavior", "Ctrl+Tab behavior:"), None, None, LocalOnlyIconState::for_setting( @@ -6779,7 +6801,7 @@ impl SettingsWidget for MouseReportingWidget { let reporting_settings = AltScreenReporting::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable Mouse Reporting".into(), + crate::menu_label("settings.features.mouse_reporting", "Enable Mouse Reporting").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -6834,7 +6856,7 @@ impl SettingsWidget for ScrollReportingWidget { let reporting_settings = AltScreenReporting::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable Scroll Reporting".into(), + crate::menu_label("settings.features.scroll_reporting", "Enable Scroll Reporting").into(), None, LocalOnlyIconState::for_setting( ScrollReportingEnabled::storage_key(), @@ -6892,7 +6914,7 @@ impl SettingsWidget for FocusReportingWidget { let reporting_settings = AltScreenReporting::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( - "Enable Focus Reporting".into(), + crate::menu_label("settings.features.focus_reporting", "Enable Focus Reporting").into(), None, LocalOnlyIconState::for_setting( FocusReportingEnabled::storage_key(), @@ -6939,7 +6961,7 @@ impl SettingsWidget for AudibleBellWidget { let ui_builder = appearance.ui_builder(); let terminal_settings = TerminalSettings::as_ref(app); render_body_item::( - "Use Audible Bell".into(), + crate::menu_label("settings.features.audible_bell", "Use Audible Bell").into(), None, LocalOnlyIconState::for_setting( UseAudibleBell::storage_key(), @@ -6983,7 +7005,7 @@ impl SmartSelectWidget { Flex::column() .with_child( ui_builder - .label("Characters considered part of a word".to_string()) + .label(crate::menu_label("settings.features.word_char_allowlist", "Characters considered part of a word").to_string()) .with_style(UiComponentStyles { margin: Some(Coords { top: 10.0, @@ -7044,7 +7066,7 @@ impl SettingsWidget for SmartSelectWidget { let selection = SemanticSelection::as_ref(app); let mut column = Flex::column(); column.add_child(render_body_item::( - "Double-click smart selection".into(), + crate::menu_label("settings.features.smart_select", "Double-click smart selection").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -7120,7 +7142,7 @@ impl SettingsWidget for ShowTerminalZeroStateBlockWidget { let ui_builder = appearance.ui_builder(); let terminal_settings = TerminalSettings::as_ref(app); render_body_item::( - "Show help block in new sessions".into(), + crate::menu_label("settings.features.show_help_block", "Show help block in new sessions").into(), None, LocalOnlyIconState::for_setting( ShowTerminalZeroStateBlock::storage_key(), @@ -7162,7 +7184,7 @@ impl SettingsWidget for CopyOnSelectWidget { let ui_builder = appearance.ui_builder(); let copy_on_select_enabled = SelectionSettings::as_ref(app).copy_on_select_enabled(); render_body_item::( - "Copy on select".into(), + crate::menu_label("settings.features.copy_on_select", "Copy on select").into(), None, LocalOnlyIconState::for_setting( CopyOnSelect::storage_key(), @@ -7206,8 +7228,8 @@ impl SettingsWidget for Osc52ClipboardAccessWidget { ) -> Box { render_dropdown_item( appearance, - "Clipboard access (OSC 52)", - Some("Controls whether programs running in the terminal can read or write your system clipboard."), + crate::menu_label("settings.features.clipboard_osc52", "Clipboard access (OSC 52)"), + Some(crate::menu_label("settings.features.clipboard_osc52_tooltip", "Controls whether programs running in the terminal can read or write your system clipboard.")), None, LocalOnlyIconState::for_setting( Osc52ClipboardAccessSetting::storage_key(), @@ -7242,7 +7264,7 @@ impl SettingsWidget for NewTabPlacementWidget { ) -> Box { render_dropdown_item( appearance, - "New tab placement", + crate::menu_label("settings.features.new_tab_placement", "New tab placement"), None, None, LocalOnlyIconState::for_setting( @@ -7277,7 +7299,7 @@ impl SettingsWidget for DefaultSessionModeWidget { app: &AppContext, ) -> Box { let label = render_dropdown_item_label( - "Default mode for new sessions".to_string(), + crate::menu_label("settings.features.default_session_mode", "Default mode for new sessions").to_string(), None, LocalOnlyIconState::for_setting( DefaultSessionMode::storage_key(), @@ -7331,7 +7353,7 @@ impl SettingsWidget for WorkflowsInCommandSearch { let ui_builder = appearance.ui_builder(); let workflow_settings = CommandSearchSettings::as_ref(app); render_body_item::( - "Show Global Workflows in Command Search (ctrl-r)".into(), + crate::menu_label("settings.features.global_workflows_search", "Show Global Workflows in Command Search (ctrl-r)").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( @@ -7386,13 +7408,13 @@ impl SettingsWidget for LinuxSelectionClipboardWidget { app: &AppContext, ) -> Box { render_body_item::( - "Honor linux selection clipboard".into(), + crate::menu_label("settings.features.linux_selection_clipboard", "Honor linux selection clipboard").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, tooltip_override_text: Some( - "Whether the Linux primary clipboard should be supported.".into(), + crate::menu_label("settings.features.linux_clipboard_tooltip", "Whether the Linux primary clipboard should be supported.").into(), ), }), LocalOnlyIconState::for_setting( @@ -7440,7 +7462,7 @@ impl SettingsWidget for GPUWidget { ) -> Box { let gpu_settings = GPUSettings::as_ref(app); let mut col = Flex::column().with_child(render_body_item::( - "Prefer rendering new windows with integrated GPU (low power)".into(), + crate::menu_label("settings.features.integrated_gpu", "Prefer rendering new windows with integrated GPU (low power)").into(), None, LocalOnlyIconState::for_setting( PreferLowPowerGPU::storage_key(), @@ -7470,7 +7492,7 @@ impl SettingsWidget for GPUWidget { Container::new( appearance .ui_builder() - .wrappable_text("Changes will apply to new windows.", true) + .wrappable_text(crate::menu_label("settings.features.changes_new_windows", "Changes will apply to new windows."), true) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(theme.background()).into_solid()), ..Default::default() @@ -7510,12 +7532,12 @@ impl SettingsWidget for WindowSystemWidget { let mut children = Flex::column(); let force_x11 = *LinuxAppConfiguration::as_ref(app).force_x11.value(); children.add_child(render_body_item::( - "Use Wayland for window management".into(), + crate::menu_label("settings.features.wayland_window_management", "Use Wayland for window management").into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, - tooltip_override_text: Some("Enables the use of Wayland".to_string()), + tooltip_override_text: Some(crate::menu_label("settings.features.wayland_tooltip", "Enables the use of Wayland").to_string()), }), LocalOnlyIconState::for_setting( ForceX11::storage_key(), @@ -7540,12 +7562,18 @@ impl SettingsWidget for WindowSystemWidget { None, )); - let mut secondary_text = + let mut secondary_text = crate::menu_label( + "settings.features.wayland_description", "Enabling this setting disables global hotkey support. When disabled, text \ - may be blurry if your Wayland compositor is using fraction scaling (ex: 125%)." - .to_string(); + may be blurry if your Wayland compositor is using fraction scaling (ex: 125%).", + ) + .to_string(); if view.force_x11_changed { - secondary_text.push_str("\n\nRestart Warp for changes to take effect."); + secondary_text.push_str("\n\n"); + secondary_text.push_str(crate::menu_label( + "settings.features.restart_for_changes", + "Restart Warp for changes to take effect.", + )); } let warp_theme = appearance.theme(); children.add_child( @@ -7586,7 +7614,7 @@ impl SettingsWidget for GraphicsBackendWidget { let theme = appearance.theme(); let dropdown = render_dropdown_item( appearance, - "Preferred graphics backend", + crate::menu_label("settings.features.graphics_backend", "Preferred graphics backend"), None, None, LocalOnlyIconState::for_setting( @@ -7607,7 +7635,16 @@ impl SettingsWidget for GraphicsBackendWidget { col.add_child( appearance .ui_builder() - .wrappable_text(format!("Current backend: {}", backend.to_label()), true) + .wrappable_text( + i18n::interpolate( + crate::menu_label( + "settings.features.current_backend", + "Current backend: {backend}", + ), + &[("backend", backend.to_label().to_string())], + ), + true, + ) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(theme.background()).into_solid()), ..Default::default() @@ -7621,7 +7658,7 @@ impl SettingsWidget for GraphicsBackendWidget { Container::new( appearance .ui_builder() - .wrappable_text("Changes will apply to new windows.", true) + .wrappable_text(crate::menu_label("settings.features.changes_new_windows", "Changes will apply to new windows."), true) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(theme.background()).into_solid()), ..Default::default() @@ -7664,7 +7701,7 @@ impl SettingsWidget for AsyncFindWidget { let ui_builder = appearance.ui_builder(); let label = render_body_item_label::( - "Asynchronous find".into(), + crate::menu_label("settings.features.asynchronous_find", "Asynchronous find").into(), None, None, LocalOnlyIconState::for_setting( @@ -7700,8 +7737,11 @@ impl SettingsWidget for AsyncFindWidget { switch, appearance, Some( - "Use an improved implementation of find to keep the UI responsive while searching for matches on large outputs." - .into(), + crate::menu_label( + "settings.features.asynchronous_find_tooltip", + "Use an improved implementation of find to keep the UI responsive while searching for matches on large outputs.", + ) + .into(), ), ) } diff --git a/app/src/settings_view/mcp_servers_page.rs b/app/src/settings_view/mcp_servers_page.rs index 23ee824983..a7d56ab18e 100644 --- a/app/src/settings_view/mcp_servers_page.rs +++ b/app/src/settings_view/mcp_servers_page.rs @@ -50,7 +50,9 @@ pub enum InstallOrigin { Deeplink, } -const PAGE_TITLE_TEXT: &str = "MCP Servers"; +fn page_title_text() -> &'static str { + crate::menu_label("settings.mcp_servers_page.title", "MCP Servers") +} #[derive(Debug, Default, Copy, Clone)] pub enum MCPServersSettingsPage { #[default] @@ -102,7 +104,7 @@ impl MCPServersSettingsPageView { Self { page: PageType::new_monolith( MCPServersSettingsWidget::default(), - Some(PAGE_TITLE_TEXT), + Some(page_title_text()), true, ), current_page: MCPServersSettingsPage::default(), @@ -148,8 +150,19 @@ impl MCPServersSettingsPageView { ctx: &mut ViewContext, ) { let message = match server_name { - Some(name) => format!("Successfully logged out of {name} MCP server"), - None => "Successfully logged out of MCP server".to_string(), + Some(name) => i18n::interpolate( + crate::menu_label( + "settings.mcp_servers_page.logout_success", + "Successfully logged out of {name} MCP server", + ), + &[("name", name)], + ) + .into_owned(), + None => crate::menu_label( + "settings.mcp_servers_page.logout_success_unnamed", + "Successfully logged out of MCP server", + ) + .to_string(), }; match item_id { ServerCardItemId::TemplatableMCP(_) => { @@ -316,7 +329,11 @@ impl MCPServersSettingsPageView { "Ignoring MCP deeplink autoinstall for '{autoinstall_param}': installation modal already open" ); self.add_error_toast( - "Finish the current MCP install before opening another install link.".to_string(), + crate::menu_label( + "settings.mcp_servers_page.install_modal_already_open", + "Finish the current MCP install before opening another install link.", + ) + .to_string(), ctx, ); return; @@ -331,7 +348,17 @@ impl MCPServersSettingsPageView { log::warn!( "Unrecognized autoinstall value '{autoinstall_param}': no matching gallery item found" ); - self.add_error_toast(format!("Unknown MCP server '{autoinstall_param}'"), ctx); + self.add_error_toast( + i18n::interpolate( + crate::menu_label( + "settings.mcp_servers_page.unknown_server", + "Unknown MCP server '{name}'", + ), + &[("name", autoinstall_param.to_string())], + ) + .into_owned(), + ctx, + ); return; }; @@ -359,7 +386,14 @@ impl MCPServersSettingsPageView { // gallery entry cannot be turned into a valid template. Surface the // failure to the user rather than silently returning. self.add_error_toast( - format!("MCP server '{gallery_title}' cannot be installed from this link."), + i18n::interpolate( + crate::menu_label( + "settings.mcp_servers_page.cannot_install_from_link", + "MCP server '{name}' cannot be installed from this link.", + ), + &[("name", gallery_title)], + ) + .into_owned(), ctx, ); return; @@ -572,7 +606,7 @@ impl SettingsWidget for MCPServersSettingsWidget { type View = MCPServersSettingsPageView; fn search_terms(&self) -> &str { - "mcp servers" + crate::menu_label("settings.mcp_servers_page.search_terms", "mcp servers") } fn render( diff --git a/app/src/settings_view/platform_page.rs b/app/src/settings_view/platform_page.rs index dabdf7b87a..9777fb7c94 100644 --- a/app/src/settings_view/platform_page.rs +++ b/app/src/settings_view/platform_page.rs @@ -171,7 +171,7 @@ impl PlatformPageView { ..Default::default() }; let mut editor = EditorView::single_line(options, ctx); - editor.set_placeholder_text("Search API keys", ctx); + editor.set_placeholder_text(crate::menu_label("settings.platform.search_api_keys", "Search API keys"), ctx); editor }); ctx.subscribe_to_view(&api_key_search_editor, |me, _, event, ctx| { @@ -186,7 +186,7 @@ impl PlatformPageView { }); let create_api_key_modal_view = ctx.add_typed_action_view(|ctx| { - Modal::new(Some("New API key".to_string()), create_api_key_body, ctx) + Modal::new(Some(crate::menu_label("settings.platform.new_api_key", "New API key").to_string()), create_api_key_body, ctx) .with_modal_style(UiComponentStyles { width: Some(MODAL_WIDTH), height: Some(MODAL_HEIGHT), @@ -237,7 +237,7 @@ impl PlatformPageView { fn show_create_api_key_modal(&mut self, ctx: &mut ViewContext) { self.create_api_key_modal_state - .set_title(Some("New API key".to_string()), ctx); + .set_title(Some(crate::menu_label("settings.platform.new_api_key", "New API key").to_string()), ctx); self.create_api_key_modal_state.open(ctx); ctx.emit(PlatformPageViewEvent::ShowCreateApiKeyModal); } @@ -266,7 +266,7 @@ impl PlatformPageView { } CreateApiKeyModalEvent::Created { api_key } => { self.create_api_key_modal_state - .set_title(Some("Save your key".to_string()), ctx); + .set_title(Some(crate::menu_label("settings.platform.save_your_key", "Save your key").to_string()), ctx); let ui_key = APIKeyProperties::from(api_key); self.ensure_expire_button_for_key(ctx, ui_key.uid.clone()); self.api_keys.push(ui_key); @@ -320,7 +320,7 @@ impl PlatformPageView { let window_id = ctx.window_id(); crate::ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let toast = crate::view_components::DismissibleToast::success( - "API key deleted".to_string(), + crate::menu_label("settings.platform.api_key_deleted", "API key deleted").to_string(), ); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); @@ -489,8 +489,8 @@ impl PlatformPageWidget { appearance: &Appearance, ) -> Box { let text = vec![ - FormattedTextFragment::plain_text("Create and manage API keys to allow other Oz cloud agents to access your Warp account.\nFor more information, visit the "), - FormattedTextFragment::hyperlink("Documentation.", API_KEY_DOCS_URL), + FormattedTextFragment::plain_text(crate::menu_label("settings.platform.description_prefix", "Create and manage API keys to allow other Oz cloud agents to access your Warp account.\nFor more information, visit the ")), + FormattedTextFragment::hyperlink(crate::menu_label("settings.platform.documentation_link", "Documentation."), API_KEY_DOCS_URL), ]; let text_element = FormattedTextElement::new( @@ -524,7 +524,7 @@ impl PlatformPageWidget { Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( - Text::new_inline("Oz Cloud API Keys", appearance.ui_font_family(), 16.) + Text::new_inline(crate::menu_label("settings.platform.oz_cloud_api_keys", "Oz Cloud API Keys"), appearance.ui_font_family(), 16.) .with_style(Properties::default().weight(Weight::Bold)) .with_color(appearance.theme().active_ui_text_color().into()) .with_clip(ClipConfig::end()) @@ -537,7 +537,7 @@ impl PlatformPageWidget { ButtonVariant::Outlined, self.create_api_key_button_mouse_state.clone(), ) - .with_text_label("+ Create API Key".to_string()) + .with_text_label(crate::menu_label("settings.platform.create_api_key_button", "+ Create API Key").to_string()) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(PlatformPageAction::ShowCreateApiKeyModal); @@ -603,29 +603,29 @@ impl PlatformPageWidget { .with_main_axis_size(MainAxisSize::Max); header_row.add_child(self.render_resizable_header_cell( appearance, - "Name", + crate::menu_label("settings.platform.column_name", "Name"), view.api_key_table_column_widths.name.clone(), API_KEY_NAME_COLUMN_MIN_WIDTH, min_non_resizable_columns_width, table_width_chrome, )); header_row.add_child( - ConstrainedBox::new(self.render_header_cell(appearance, "Key")) + ConstrainedBox::new(self.render_header_cell(appearance, crate::menu_label("settings.platform.column_key", "Key"))) .with_width(API_KEY_KEY_COLUMN_WIDTH) .finish(), ); if show_scope_column { header_row.add_child( - Expanded::new(1., self.render_header_cell(appearance, "Scope")).finish(), + Expanded::new(1., self.render_header_cell(appearance, crate::menu_label("settings.platform.column_scope", "Scope"))).finish(), ); } header_row - .add_child(Expanded::new(1., self.render_header_cell(appearance, "Created")).finish()); + .add_child(Expanded::new(1., self.render_header_cell(appearance, crate::menu_label("settings.platform.column_created", "Created"))).finish()); header_row.add_child( - Expanded::new(1., self.render_header_cell(appearance, "Last used")).finish(), + Expanded::new(1., self.render_header_cell(appearance, crate::menu_label("settings.platform.column_last_used", "Last used"))).finish(), ); header_row.add_child( - Expanded::new(1., self.render_header_cell(appearance, "Expires at")).finish(), + Expanded::new(1., self.render_header_cell(appearance, crate::menu_label("settings.platform.column_expires_at", "Expires at"))).finish(), ); header_row.add_child(Expanded::new(0.5, self.render_header_cell(appearance, "")).finish()); @@ -722,11 +722,11 @@ impl PlatformPageWidget { let last_used = key .last_used_at .map(format_approx_duration_from_now_utc) - .unwrap_or_else(|| "Never".to_owned()); + .unwrap_or_else(|| crate::menu_label("settings.platform.never", "Never").to_owned()); let expires_at = key .expires_at .map(|dt| format!("{}", dt.format("%b %-d, %Y"))) - .unwrap_or_else(|| "Never".to_owned()); + .unwrap_or_else(|| crate::menu_label("settings.platform.never", "Never").to_owned()); let name_column_width = view.api_key_table_column_widths.name_width(); let key_column_width = API_KEY_KEY_COLUMN_WIDTH; let mut row = Flex::row() @@ -767,9 +767,9 @@ impl PlatformPageWidget { ); if FeatureFlag::TeamApiKeys.is_enabled() || FeatureFlag::NamedAgents.is_enabled() { let scope_display = match key.scope { - ApiKeyScope::Personal => "Personal", - ApiKeyScope::Team => "Team", - ApiKeyScope::Agent => "Agent", + ApiKeyScope::Personal => crate::menu_label("settings.platform.scope_personal", "Personal"), + ApiKeyScope::Team => crate::menu_label("settings.platform.scope_team", "Team"), + ApiKeyScope::Agent => crate::menu_label("settings.platform.scope_agent", "Agent"), }; row.add_child( Expanded::new( @@ -857,7 +857,7 @@ impl PlatformPageWidget { .with_child( Container::new( Text::new( - "No API Keys", + crate::menu_label("settings.platform.no_api_keys", "No API Keys"), appearance.ui_font_family(), SUBHEADER_FONT_SIZE, ) @@ -871,7 +871,7 @@ impl PlatformPageWidget { .with_child( Container::new( Text::new( - "Create a key to manage external access to Warp", + crate::menu_label("settings.platform.no_api_keys_description", "Create a key to manage external access to Warp"), appearance.ui_font_family(), CONTENT_FONT_SIZE, ) @@ -892,7 +892,7 @@ impl PlatformPageWidget { fn render_no_search_results(&self, appearance: &Appearance) -> Box { Container::new( Text::new( - "No API keys match your search", + crate::menu_label("settings.platform.no_api_keys_match_search", "No API keys match your search"), appearance.ui_font_family(), CONTENT_FONT_SIZE, ) diff --git a/app/src/settings_view/privacy_page.rs b/app/src/settings_view/privacy_page.rs index 8f6b9bdcad..9e66d30188 100644 --- a/app/src/settings_view/privacy_page.rs +++ b/app/src/settings_view/privacy_page.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use std::sync::LazyLock; use std::time::Duration; use pathfinder_geometry::vector::vec2f; @@ -61,36 +60,81 @@ use crate::{report_if_error, send_telemetry_from_ctx}; const FONT_SIZE: f32 = 12.; -const SAFE_MODE_TITLE: &str = "Secret redaction"; -static SAFE_MODE_DESCRIPTION: LazyLock<&'static str> = LazyLock::new(|| { - "When this setting is enabled, Warp will scan blocks, the contents of \ +// ── i18n helpers ────────────────────────────────────────────────────── +fn privacy_page_title() -> &'static str { + crate::menu_label("settings.privacy.title", "Privacy") +} +fn safe_mode_title() -> &'static str { + crate::menu_label("settings.privacy.secret_redaction", "Secret redaction") +} +fn safe_mode_description() -> &'static str { + crate::menu_label( + "settings.privacy.secret_redaction_description", + "When this setting is enabled, Warp will scan blocks, the contents of \ Warp Drive objects, and Oz prompts for potential sensitive \ information and prevent saving or sending this data to any \ - servers. You can customize this list via regexes." -}); -const USER_SECRET_REGEX_TITLE: &str = "Custom secret redaction"; -const USER_SECRET_REGEX_DESCRIPTION: &str = - "Use regex to define additional secrets or data you'd like to redact. This will take effect \ - when the next command runs. You can use the inline (?i) flag as a prefix to your regex \ - to make it case-insensitive."; -const TELEMETRY_DESCRIPTION_OLD: &str = - "App analytics help us make the product better for you. We only collect \ - app usage metadata, never console input or output."; -const TELEMETRY_TITLE: &str = "Help improve Warp"; -const TELEMETRY_DESCRIPTION: &str = - "App analytics help us make the product better for you. We may collect \ - certain console interactions to improve Warp's AI capabilities."; + servers. You can customize this list via regexes.", + ) +} +fn user_secret_regex_title() -> &'static str { + crate::menu_label( + "settings.privacy.custom_secret_redaction", + "Custom secret redaction", + ) +} +fn user_secret_regex_description() -> &'static str { + crate::menu_label( + "settings.privacy.custom_secret_redaction_description", + "Use regex to define additional secrets or data you'd like to redact. This will take effect \ + when the next command runs. You can use the inline (?i) flag as a prefix to your regex \ + to make it case-insensitive.", + ) +} +fn telemetry_title() -> &'static str { + crate::menu_label("settings.privacy.help_improve_warp", "Help improve Warp") +} +fn telemetry_description() -> &'static str { + crate::menu_label( + "settings.privacy.help_improve_warp_description", + "App analytics help us make the product better for you. We may collect \ + certain console interactions to improve Warp's AI capabilities.", + ) +} +fn telemetry_description_enterprise() -> &'static str { + crate::menu_label( + "settings.privacy.help_improve_warp_description_enterprise", + "App analytics help us make the product better for you. We only collect \ + app usage metadata, never console input or output.", + ) +} const TELEMETRY_DOCS_URL: &str = "https://docs.warp.dev/support-and-community/privacy-and-security/privacy#what-telemetry-data-does-warp-collect-and-why"; - -const DATA_MANAGEMENT_TITLE: &str = "Manage your data"; -const DATA_MANAGEMENT_DESCRIPTION: &str = - "At any time, you may choose to delete your Warp account permanently. \ - You will no longer be able to use Warp."; -const DATA_MANAGEMENT_LINK_TEXT: &str = "Visit the data management page"; - -const PRIVACY_POLICY_TITLE: &str = "Privacy policy"; -const PRIVACY_POLICY_LINK_TEXT: &str = "Read Warp's privacy policy"; +fn data_management_title() -> &'static str { + crate::menu_label("settings.privacy.manage_your_data", "Manage your data") +} +fn data_management_description() -> &'static str { + crate::menu_label( + "settings.privacy.manage_your_data_description", + "At any time, you may choose to delete your Warp account permanently. \ + You will no longer be able to use Warp.", + ) +} +fn data_management_link_text() -> &'static str { + crate::menu_label( + "settings.privacy.visit_data_management_page", + "Visit the data management page", + ) +} +fn privacy_policy_title() -> &'static str { + crate::menu_label("settings.privacy.privacy_policy_title", "Privacy Policy") +} +fn privacy_policy_link_text() -> &'static str { + crate::menu_label( + "settings.privacy.read_privacy_policy", + "Read Warp's privacy policy", + ) +} +// ─────────────────────────────────────────────────────────────────────── pub fn data_management_url(custom_token: Option<&str>) -> String { match custom_token { @@ -154,7 +198,7 @@ impl PrivacyPageView { }); let add_regex_modal_view = ctx.add_typed_action_view(|ctx| { - Modal::new(Some("Add regex pattern".to_string()), add_regex_body, ctx) + Modal::new(Some(crate::menu_label("settings.privacy.add_regex_pattern", "Add regex pattern").to_string()), add_regex_body, ctx) .with_modal_style(UiComponentStyles { width: Some(600.), height: Some(400.), @@ -234,7 +278,7 @@ impl PrivacyPageView { } widgets.push(Box::new(DataManagementWidget::default())); widgets.push(Box::new(PrivacyPolicyWidget::default())); - PageType::new_uncategorized(widgets, Some("Privacy")) + PageType::new_uncategorized(widgets, Some(privacy_page_title())) } fn update_button_states( @@ -758,7 +802,7 @@ impl SecretRedactionWidget { .count(); let personal_tab = self.render_tab( - "Personal".to_string(), + crate::menu_label("settings.privacy.personal", "Personal").to_string(), personal_count, SecretRedactionTab::Personal, active_tab == SecretRedactionTab::Personal, @@ -769,7 +813,7 @@ impl SecretRedactionWidget { let is_enterprise_tab_active = active_tab == SecretRedactionTab::Enterprise; let enterprise_tab = self.render_tab( - "Enterprise".to_string(), + crate::menu_label("settings.privacy.enterprise", "Enterprise").to_string(), enterprise_count, SecretRedactionTab::Enterprise, is_enterprise_tab_active, @@ -785,7 +829,7 @@ impl SecretRedactionWidget { if is_enterprise_tab_active { row.add_child(Shrinkable::new(1., Empty::new().finish()).finish()); row.add_child(self.render_info( - "Enterprise secret redaction cannot be modified.".to_string(), + crate::menu_label("settings.privacy.enterprise_secret_redaction_locked", "Enterprise secret redaction cannot be modified.").to_string(), appearance, )); } @@ -904,7 +948,7 @@ impl SecretRedactionWidget { if enterprise_regex_list.is_empty() { return ui_builder - .paragraph("No enterprise regexes have been configured by your organization.") + .paragraph(crate::menu_label("settings.privacy.no_enterprise_regexes", "No enterprise regexes have been configured by your organization.")) .with_style(UiComponentStyles { font_color: Some(description_text_color), ..Default::default() @@ -1004,7 +1048,7 @@ impl SecretRedactionWidget { .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( - self.render_section_title("Recommended".to_string(), appearance), + self.render_section_title(crate::menu_label("settings.privacy.recommended", "Recommended").to_string(), appearance), ) .with_child( Container::new( @@ -1014,7 +1058,7 @@ impl SecretRedactionWidget { self.add_all_button_mouse_state.clone(), ) .with_text_and_icon_label(Self::add_button( - "Add all", appearance, + crate::menu_label("settings.privacy.add_all", "Add all"), appearance, )) .with_style(Self::add_button_style()) .build() @@ -1172,7 +1216,7 @@ impl SettingsWidget for SecretRedactionWidget { .with_child( Shrinkable::new( 1.0, - render_sub_header(appearance, SAFE_MODE_TITLE, Some(local_only_icon_state)), + render_sub_header(appearance, safe_mode_title(), Some(local_only_icon_state)), ) .finish(), ) @@ -1180,7 +1224,7 @@ impl SettingsWidget for SecretRedactionWidget { Container::new({ if is_enterprise_enabled { self.render_info( - "Enabled by your organization.".to_string(), + crate::menu_label("settings.privacy.enabled_by_organization", "Enabled by your organization.").to_string(), appearance, ) } else { @@ -1207,7 +1251,7 @@ impl SettingsWidget for SecretRedactionWidget { .with_child(secret_redaction_title_row) .with_child( ui_builder - .paragraph((*SAFE_MODE_DESCRIPTION).to_owned()) + .paragraph(safe_mode_description()) .with_style(UiComponentStyles { font_color: Some(description_text_color), font_size: Some(FONT_SIZE + 1.), // One size up from current 12px to 13px @@ -1233,7 +1277,7 @@ impl SettingsWidget for SecretRedactionWidget { // Create the label with local-only icon if needed let label_with_icon = super::settings_page::render_dropdown_item_label( - "Secret visual redaction mode".to_string(), + crate::menu_label("settings.privacy.secret_visual_redaction_mode", "Secret visual redaction mode").to_string(), None, local_only_icon_state, None, @@ -1247,7 +1291,7 @@ impl SettingsWidget for SecretRedactionWidget { Container::new( ui_builder .paragraph( - "Choose how secrets are visually presented in the block list while keeping them searchable. This setting only affects what you see in the block list.", + crate::menu_label("settings.privacy.secret_visual_redaction_mode_description", "Choose how secrets are visually presented in the block list while keeping them searchable. This setting only affects what you see in the block list."), ) .with_style(UiComponentStyles { font_color: Some(description_text_color), @@ -1296,11 +1340,11 @@ impl SettingsWidget for SecretRedactionWidget { 1., Flex::column() .with_child(self.render_section_title( - USER_SECRET_REGEX_TITLE.to_string(), + user_secret_regex_title().to_string(), appearance, )) .with_child(self.render_description( - USER_SECRET_REGEX_DESCRIPTION.to_owned(), + user_secret_regex_description().to_string(), appearance, if privacy_settings.user_secret_regex_list.iter().count() > 0 { 10. @@ -1318,7 +1362,7 @@ impl SettingsWidget for SecretRedactionWidget { ButtonVariant::Secondary, self.add_regex_button_mouse_state.clone(), ) - .with_text_and_icon_label(Self::add_button("Add regex", appearance)) + .with_text_and_icon_label(Self::add_button(crate::menu_label("settings.privacy.add_regex", "Add regex"), appearance)) .with_style(Self::add_button_style()) .build() .on_click(move |ctx, _, _| { @@ -1396,8 +1440,7 @@ impl AppAnalyticsWidget { let mut stack = Stack::new().with_child(badge); if is_hovered { let tooltip = ui_builder.tool_tip( - "Your administrator has enabled zero data retention for your team. User generated content will never be collected." - .to_string(), + crate::menu_label("settings.privacy.zdr_enabled_description", "Your administrator has enabled zero data retention for your team. User generated content will never be collected.").to_string(), ); stack.add_positioned_child( tooltip.build().finish(), @@ -1453,9 +1496,9 @@ impl SettingsWidget for AppAnalyticsWidget { .is_some_and(|w| w.billing_metadata.customer_type == CustomerType::Enterprise); // Keep the old description for enterprise users because we do not collect block input/output for them. let description = if is_enterprise { - TELEMETRY_DESCRIPTION_OLD + telemetry_description_enterprise() } else { - TELEMETRY_DESCRIPTION + telemetry_description() }; let org_setting = UserWorkspaces::handle(app) @@ -1474,7 +1517,7 @@ impl SettingsWidget for AppAnalyticsWidget { Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child(render_body_item_label::( - TELEMETRY_TITLE.into(), + telemetry_title().into(), None, None, LocalOnlyIconState::Hidden, @@ -1485,7 +1528,7 @@ impl SettingsWidget for AppAnalyticsWidget { .finish() } else { render_body_item_label::( - TELEMETRY_TITLE.into(), + telemetry_title().into(), None, None, LocalOnlyIconState::Hidden, @@ -1507,7 +1550,7 @@ impl SettingsWidget for AppAnalyticsWidget { } else { switch .with_tooltip(TooltipConfig { - text: "This setting is managed by your organization.".to_string(), + text: crate::menu_label("settings.privacy.managed_by_organization", "This setting is managed by your organization.").to_string(), styles: ui_builder.default_tool_tip_styles(), }) .disable() @@ -1542,7 +1585,7 @@ impl SettingsWidget for AppAnalyticsWidget { Align::new( ui_builder .link( - "Read more about Warp's use of data".into(), + crate::menu_label("settings.privacy.read_more_about_data_use", "Read more about Warp's use of data").into(), Some(TELEMETRY_DOCS_URL.into()), None, self.docs_link_mouse_state.clone(), @@ -1592,7 +1635,7 @@ impl SettingsWidget for CrashReportsWidget { let privacy_settings = PrivacySettings::as_ref(app); Flex::column() .with_child(render_body_item::( - "Send crash reports".into(), + crate::menu_label("settings.privacy.send_crash_reports", "Send crash reports").into(), None, // Crash report state is always synced to cloud, so no need to show local only icon. LocalOnlyIconState::Hidden, @@ -1611,8 +1654,7 @@ impl SettingsWidget for CrashReportsWidget { .with_child( ui_builder .paragraph( - "Crash reports assist with debugging and stability improvements." - .to_owned(), + crate::menu_label("settings.privacy.send_crash_reports_description", "Crash reports assist with debugging and stability improvements."), ) .with_style(UiComponentStyles { font_color: Some( @@ -1696,7 +1738,7 @@ impl SettingsWidget for CloudConversationStorageWidget { } else { switch .with_tooltip(TooltipConfig { - text: "This setting is managed by your organization.".to_string(), + text: crate::menu_label("settings.privacy.managed_by_organization", "This setting is managed by your organization.").to_string(), styles: ui_builder.default_tool_tip_styles(), }) .disable() @@ -1706,7 +1748,7 @@ impl SettingsWidget for CloudConversationStorageWidget { Flex::column() .with_child(render_body_item::( - "Store AI conversations in the cloud".into(), + crate::menu_label("settings.privacy.store_ai_conversations_in_cloud", "Store AI conversations in the cloud").into(), None, LocalOnlyIconState::Hidden, toggle_state, @@ -1718,15 +1760,10 @@ impl SettingsWidget for CloudConversationStorageWidget { ui_builder .paragraph( if is_checked { - "Agent conversations can be shared with others and are retained \ - when you log in on different devices. This data is only stored \ - for product functionality, and Warp will not use it for analytics." + crate::menu_label("settings.privacy.cloud_conversation_storage_description", "Agent conversations can be shared with others and are retained when you log in on different devices. This data is only stored for product functionality, and Warp will not use it for analytics.").to_string() } else { - "Agent conversations are only stored locally on your machine, are \ - lost upon logout, and cannot be shared. Note: conversation data \ - for ambient agents are still stored in the cloud." - } - .to_owned(), + crate::menu_label("settings.privacy.local_conversation_storage_description", "Agent conversations are only stored locally on your machine, are lost upon logout, and cannot be shared. Note: conversation data for ambient agents are still stored in the cloud.").to_string() + }, ) .with_style(UiComponentStyles { font_color: Some( @@ -1770,7 +1807,7 @@ impl SettingsWidget for NetworkLogWidget { let ui_builder = appearance.ui_builder(); Flex::column() .with_child(render_body_item::( - "Network log console".into(), + crate::menu_label("settings.privacy.network_log_console", "Network log console").into(), None, // Not rendering a setting, so no need to show local only icon state. LocalOnlyIconState::Hidden, @@ -1782,10 +1819,7 @@ impl SettingsWidget for NetworkLogWidget { .with_child( ui_builder .paragraph( - "We've built a native console that allows you to view all communications \ - from Warp to external servers to ensure you feel comfortable that your \ - work is always kept safe." - .to_owned(), + crate::menu_label("settings.privacy.network_log_console_description", "We've built a native console that allows you to view all communications from Warp to external servers to ensure you feel comfortable that your work is always kept safe."), ) .with_style(UiComponentStyles { font_color: Some( @@ -1808,7 +1842,7 @@ impl SettingsWidget for NetworkLogWidget { Align::new( ui_builder .link( - "View network logging".to_owned(), + crate::menu_label("settings.privacy.view_network_logging", "View network logging").to_string(), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(PrivacyPageAction::LaunchNetworkLogging); @@ -1848,7 +1882,7 @@ impl SettingsWidget for DataManagementWidget { let ui_builder = appearance.ui_builder(); Flex::column() .with_child(render_body_item::( - DATA_MANAGEMENT_TITLE.into(), + data_management_title().into(), None, // Not rendering a setting, so no need to show local only icon state. LocalOnlyIconState::Hidden, @@ -1859,7 +1893,7 @@ impl SettingsWidget for DataManagementWidget { )) .with_child( ui_builder - .paragraph(DATA_MANAGEMENT_DESCRIPTION) + .paragraph(data_management_description()) .with_style(UiComponentStyles { font_color: Some( appearance @@ -1882,7 +1916,7 @@ impl SettingsWidget for DataManagementWidget { appearance .ui_builder() .link( - DATA_MANAGEMENT_LINK_TEXT.into(), + data_management_link_text().into(), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action( @@ -1923,7 +1957,7 @@ impl SettingsWidget for PrivacyPolicyWidget { ) -> Box { Flex::column() .with_child(render_body_item::( - PRIVACY_POLICY_TITLE.into(), + privacy_policy_title().into(), None, // Not rendering a setting, so no need to show local only icon state. LocalOnlyIconState::Hidden, @@ -1937,7 +1971,7 @@ impl SettingsWidget for PrivacyPolicyWidget { appearance .ui_builder() .link( - PRIVACY_POLICY_LINK_TEXT.into(), + privacy_policy_link_text().into(), Some(PRIVACY_POLICY_URL.into()), None, self.link_mouse_state.clone(), diff --git a/app/src/settings_view/scripting_page.rs b/app/src/settings_view/scripting_page.rs index 5d12fd2ec8..d33d98e194 100644 --- a/app/src/settings_view/scripting_page.rs +++ b/app/src/settings_view/scripting_page.rs @@ -71,7 +71,7 @@ impl ScriptingSettingsPageView { vec![Box::new(LocalControlModeWidget)]; Self { - page: PageType::new_uncategorized(widgets, Some("Scripting")), + page: PageType::new_uncategorized(widgets, Some(crate::menu_label("settings.scripting.title", "Scripting"))), local_only_icon_tooltip_states: RefCell::new(HashMap::new()), local_control_mode_dropdown, #[cfg(target_os = "macos")] @@ -120,9 +120,10 @@ impl ScriptingSettingsPageView { match result { Ok(()) => { let command_name = ChannelState::channel().warpctrl_command_name(); - let message = format!( - "Successfully installed the Warp Control CLI! You can now run '{command_name}' from the command line." - ); + let message = i18n::interpolate( + crate::menu_label("settings.scripting.install_success", "Successfully installed the Warp Control CLI! You can now run '{command_name}' from the command line."), + &[("command_name", command_name.to_string())], + ).into_owned(); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::success(message), @@ -132,7 +133,10 @@ impl ScriptingSettingsPageView { }); } Err(error) => { - let message = format!("Failed to install Warp Control command: {error}"); + let message = i18n::interpolate( + crate::menu_label("settings.scripting.install_failed", "Failed to install Warp Control command: {error}"), + &[("error", error.to_string())], + ).into_owned(); log::warn!("{message}"); ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_persistent_toast( @@ -231,11 +235,11 @@ impl SettingsWidget for WarpControlCliInstallWidget { let installed = cli_install::is_warpctrl_installed(); let disabled = view.warpctrl_installing || installed; let label = if view.warpctrl_installing { - "Installing…" + crate::menu_label("settings.scripting.installing", "Installing…") } else if installed { - "Installed" + crate::menu_label("settings.scripting.installed", "Installed") } else { - "Install" + crate::menu_label("settings.scripting.install", "Install") }; let mut button = appearance .ui_builder() @@ -259,13 +263,13 @@ impl SettingsWidget for WarpControlCliInstallWidget { }; render_body_item::( - "Warp Control CLI command".into(), + crate::menu_label("settings.scripting.warp_control_cli_command", "Warp Control CLI command").into(), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, appearance, button, - Some("Install the warpctrl command for scripting Warp from your terminal.".to_owned()), + Some(crate::menu_label("settings.scripting.warp_control_cli_description", "Install the warpctrl command for scripting Warp from your terminal.").to_owned()), ) } } @@ -285,7 +289,7 @@ impl SettingsWidget for LocalControlModeWidget { app: &AppContext, ) -> Box { render_body_item::( - "warpctrl CLI".into(), + crate::menu_label("settings.scripting.warpctrl_cli", "warpctrl CLI").into(), None, LocalOnlyIconState::for_setting( LocalControlModeSetting::storage_key(), @@ -296,7 +300,7 @@ impl SettingsWidget for LocalControlModeWidget { ToggleState::Enabled, appearance, ChildView::new(&view.local_control_mode_dropdown).finish(), - Some("warpctrl allows for scripting Warp's UI. Use with care.".to_owned()), + Some(crate::menu_label("settings.scripting.warpctrl_cli_description", "warpctrl allows for scripting Warp's UI. Use with care.").to_owned()), ) } } diff --git a/app/src/settings_view/set_default_model_modal.rs b/app/src/settings_view/set_default_model_modal.rs index 42b9198674..68ea9ad16f 100644 --- a/app/src/settings_view/set_default_model_modal.rs +++ b/app/src/settings_view/set_default_model_modal.rs @@ -67,13 +67,24 @@ impl SetDefaultModelModalBody { }); let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Not now", NakedTheme).on_click(|ctx| { + ActionButton::new( + crate::menu_label("settings.modals.set_default_model.not_now", "Not now"), + NakedTheme, + ) + .on_click(|ctx| { ctx.dispatch_typed_action(SetDefaultModelModalBodyAction::Cancel); }) }); let save_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Change default model", PrimaryTheme).on_click(|ctx| { + ActionButton::new( + crate::menu_label( + "settings.modals.set_default_model.change_default_model", + "Change default model", + ), + PrimaryTheme, + ) + .on_click(|ctx| { ctx.dispatch_typed_action(SetDefaultModelModalBodyAction::Save); }) }); diff --git a/app/src/settings_view/teams_page.rs b/app/src/settings_view/teams_page.rs index 93e4538f23..15240afb56 100644 --- a/app/src/settings_view/teams_page.rs +++ b/app/src/settings_view/teams_page.rs @@ -81,19 +81,82 @@ use crate::workspaces::workspace::{ }; const TEAM_MEMBERS_HEADER_POSITION_ID: &str = "team_settings:team_members_header"; -// Styling for team create page -const TEAM_NAME_EDITOR_PLACEHOLDER_TEXT: &str = "Team name"; -const CREATE_TEAM_BUTTON_LEFT_PADDING: f32 = 10.; -const CREATE_TEAM_DESCRIPTION: &str = "When you create a team, you can collaborate on agent-driven development by sharing cloud agent runs, environments, automations, and artifacts. You can also create a shared knowledge store for teammates and agents alike."; +// ── i18n helpers ────────────────────────────────────────────────────── +// Converted from `const` to `fn` so translation can happen at runtime. +fn team_name_editor_placeholder_text() -> &'static str { + crate::menu_label("settings.teams.team_name", "Team name") +} +fn create_team_description() -> &'static str { + crate::menu_label( + "settings.teams.create_team_description", + "When you create a team, you can collaborate on agent-driven development by sharing cloud agent runs, environments, automations, and artifacts. You can also create a shared knowledge store for teammates and agents alike.", + ) +} +fn leave_team_button_label() -> &'static str { + crate::menu_label("settings.teams.leave_team", "Leave team") +} +fn delete_team_button_label() -> &'static str { + crate::menu_label("settings.teams.delete_team", "Delete team") +} +fn create_team_button_label() -> &'static str { + crate::menu_label("settings.teams.create", "Create") +} +fn approve_domains_placeholder() -> &'static str { + crate::menu_label( + "settings.teams.domains_comma_separated", + "Domains, comma separated", + ) +} +fn emails_placeholder() -> &'static str { + crate::menu_label( + "settings.teams.emails_comma_separated", + "Emails, comma separated", + ) +} +fn approve_domains_button_label() -> &'static str { + crate::menu_label("settings.teams.set", "Set") +} +fn send_email_invites_button_label() -> &'static str { + crate::menu_label("settings.teams.invite", "Invite") +} +fn invalid_domains_instructions() -> &'static str { + crate::menu_label( + "settings.teams.invalid_domains_instructions", + "Some of the provided domains are invalid, or have already been added.", + ) +} +fn invite_link_toggle_instructions() -> &'static str { + crate::menu_label( + "settings.teams.invite_link_toggle_instructions", + "As an admin, you can choose whether to enable or disable the ability for team members to invite others by invitation link.", + ) +} +fn invite_link_domain_restrictions_instructions() -> &'static str { + crate::menu_label( + "settings.teams.invite_link_domain_restrictions_instructions", + "Restrict by domain — only allow users with emails at specific domains to join your team through the invite link.", + ) +} +fn invite_by_email_expiry_instructions() -> &'static str { + crate::menu_label( + "settings.teams.email_invite_expiry_instructions", + "Email invitations are valid for 7 days.", + ) +} +fn invalid_emails_instructions() -> &'static str { + crate::menu_label( + "settings.teams.invalid_emails_instructions", + "Some of the provided email addresses are invalid, already invited, or members of the team.", + ) +} +fn offline_text() -> &'static str { + crate::menu_label("settings.teams.offline", "You are offline.") +} +// ─────────────────────────────────────────────────────────────────────── + +const CREATE_TEAM_BUTTON_LEFT_PADDING: f32 = 10.; // Styling for team management page -const LEAVE_TEAM_BUTTON_LABEL: &str = "Leave team"; -const DELETE_TEAM_BUTTON_LABEL: &str = "Delete team"; -const CREATE_TEAM_BUTTON_LABEL: &str = "Create"; -const APPROVE_DOMAINS_PLACEHOLDER: &str = "Domains, comma separated"; -const EMAILS_PLACEHOLDER: &str = "Emails, comma separated"; -const APPROVE_DOMAINS_BUTTON_LABEL: &str = "Set"; -const SEND_EMAIL_INVITES_BUTTON_LABEL: &str = "Invite"; const BUTTON_WIDTH: f32 = 82.; const BUTTON_HEIGHT: f32 = 40.; const COPY_LINK_LEFT_PADDING: f32 = 7.; @@ -108,18 +171,6 @@ const SUBSUBSECTION_HEADER_FONT_SIZE: f32 = 14.; const OWNER_STATE_CHIP_ACCENT_OPACITY: u8 = 30; const INVITE_LINK_PREFIX: &str = "/team/"; -const INVALID_DOMAINS_INSTRUCTIONS: &str = - "Some of the provided domains are invalid, or have already been added."; - -const INVITE_LINK_TOGGLE_INSTRUCTIONS: &str = "As an admin, you can choose whether to enable or disable the ability for team members to invite others by invitation link."; -const INVITE_LINK_DOMAIN_RESTRICTIONS_INSTRUCTIONS: &str = - "Restrict by domain — only allow users with emails at specific domains to join your team through the invite link."; - -const INVITE_BY_EMAIL_EXPIRY_INSTRUCTIONS: &str = "Email invitations are valid for 7 days."; -const INVALID_EMAILS_INSTRUCTIONS: &str = - "Some of the provided email addresses are invalid, already invited, or members of the team."; - -const OFFLINE_TEXT: &str = "You are offline."; const MAX_CHIP_WIDTH: f32 = 280.; @@ -327,8 +378,11 @@ impl std::fmt::Display for TeamsInviteOption { f, "{}", match self { - TeamsInviteOption::Link => "Link", - TeamsInviteOption::Email => "Email", + TeamsInviteOption::Link => crate::menu_label("settings.teams.invite_option_link", "Link"), + TeamsInviteOption::Email => crate::menu_label( + "settings.teams.invite_option_email", + "Email" + ), }, ) } @@ -719,7 +773,7 @@ impl TeamsPageView { let font_size = appearance.ui_font_size(); let create_team_editor = Self::editor( |me, event, ctx| me.handle_editor_event(event, ctx), - TEAM_NAME_EDITOR_PLACEHOLDER_TEXT, + team_name_editor_placeholder_text(), font_size, ctx, ); @@ -727,7 +781,7 @@ impl TeamsPageView { let approve_domains_block_editor = ctx.add_typed_action_view(|ctx| { WordBlockEditorView::new( ctx, - APPROVE_DOMAINS_PLACEHOLDER, + approve_domains_placeholder(), font_size, vec![',', ' '], MAX_CHIP_WIDTH, @@ -741,7 +795,7 @@ impl TeamsPageView { let email_invites_block_editor = ctx.add_typed_action_view(|ctx| { WordBlockEditorView::new( ctx, - EMAILS_PLACEHOLDER, + emails_placeholder(), font_size, vec![',', ' '], MAX_CHIP_WIDTH, @@ -777,7 +831,7 @@ impl TeamsPageView { .to_string(); let rename_team_editor = ctx.add_typed_action_view(|ctx| { let mut input = ClickableTextInput::new(team_name, ctx); - input.set_placeholder_text("Your new team name", ctx); + input.set_placeholder_text(crate::menu_label("settings.teams.your_new_team_name", "Your new team name"), ctx); input }); ctx.subscribe_to_view(&rename_team_editor, |me, _, event, ctx| { @@ -801,7 +855,7 @@ impl TeamsPageView { }); let transfer_ownership_modal = ctx.add_typed_action_view(|ctx| { Modal::new( - Some("Transfer team ownership?".to_string()), + Some(crate::menu_label("settings.teams.transfer_team_ownership", "Transfer team ownership?").to_string()), transfer_ownership_modal_body, ctx, ) @@ -919,7 +973,14 @@ impl TeamsPageView { } UserWorkspacesEvent::EmailInviteRejected(err) => { self.update_team_members_state(ctx); - self.show_error("Failed to send invite", Some(err), ctx) + self.show_error( + crate::menu_label( + "settings.teams.failed_to_send_invite", + "Failed to send invite", + ), + Some(err), + ctx, + ) } UserWorkspacesEvent::TeamsChanged => { self.update_team_members_state(ctx); @@ -932,25 +993,61 @@ impl TeamsPageView { ctx.emit(TeamsPageViewEvent::TeamsChanged); } UserWorkspacesEvent::ToggleInviteLinksSuccess => { - self.show_success("Toggled invite links", ctx); + self.show_success( + crate::menu_label( + "settings.teams.toggled_invite_links", + "Toggled invite links", + ), + ctx, + ); ctx.notify(); } UserWorkspacesEvent::ToggleInviteLinksRejected(err) => { - self.show_error("Failed to toggle invite links", Some(err), ctx); + self.show_error( + crate::menu_label( + "settings.teams.failed_to_toggle_invite_links", + "Failed to toggle invite links", + ), + Some(err), + ctx, + ); } UserWorkspacesEvent::ResetInviteLinks => { - self.show_success("Reset invite links", ctx); + self.show_success( + crate::menu_label( + "settings.teams.reset_invite_links_success", + "Reset invite links", + ), + ctx, + ); ctx.notify(); } UserWorkspacesEvent::ResetInviteLinksRejected(err) => { - self.show_error("Failed to reset invite links", Some(err), ctx); + self.show_error( + crate::menu_label( + "settings.teams.failed_to_reset_invite_links", + "Failed to reset invite links", + ), + Some(err), + ctx, + ); } UserWorkspacesEvent::DeleteTeamInvite => { self.update_team_members_state(ctx); - self.show_success("Deleted invite", ctx); + self.show_success( + crate::menu_label("settings.teams.deleted_invite", "Deleted invite"), + ctx, + ); } UserWorkspacesEvent::DeleteTeamInviteRejected(err) => { - self.show_error("Failed to delete invite", Some(err), ctx); + self.show_error( + crate::menu_label( + "settings.teams.failed_to_delete_invite", + "Failed to delete invite", + ), + Some(err), + ctx, + ); } UserWorkspacesEvent::AddDomainRestrictionsSuccess => { self.approve_domains_block_editor @@ -960,19 +1057,36 @@ impl TeamsPageView { self.update_approved_domains_state(ctx); } UserWorkspacesEvent::AddDomainRestrictionsRejected(err) => { - self.show_error("Failed to add domain restriction", Some(err), ctx) + self.show_error( + crate::menu_label( + "settings.teams.failed_to_add_domain_restriction", + "Failed to add domain restriction", + ), + Some(err), + ctx, + ) } UserWorkspacesEvent::DeleteDomainRestrictionSuccess => { self.update_approved_domains_state(ctx); } UserWorkspacesEvent::DeleteDomainRestrictionRejected(err) => { - self.show_error("Failed to delete domain restriction", Some(err), ctx) + self.show_error( + crate::menu_label( + "settings.teams.failed_to_delete_domain_restriction", + "Failed to delete domain restriction", + ), + Some(err), + ctx, + ) } UserWorkspacesEvent::GenerateUpgradeLink(upgrade_link) => { ctx.open_url(upgrade_link); } UserWorkspacesEvent::GenerateUpgradeLinkRejected(err) => self.show_error( - "Failed to generate upgrade link. Please contact us at feedback@warp.dev", + crate::menu_label( + "settings.teams.failed_to_generate_upgrade_link", + "Failed to generate upgrade link. Please contact us at feedback@warp.dev", + ), Some(err), ctx, ), @@ -980,16 +1094,32 @@ impl TeamsPageView { ctx.open_url(billing_session_link); } UserWorkspacesEvent::GenerateStripeBillingPortalLinkRejected(err) => self.show_error( - "Failed to generate billing link. Please contact us at feedback@warp.dev", + crate::menu_label( + "settings.teams.failed_to_generate_billing_link", + "Failed to generate billing link. Please contact us at feedback@warp.dev", + ), Some(err), ctx, ), UserWorkspacesEvent::ToggleTeamDiscoverabilitySuccess => { - self.show_success("Toggled team discoverability", ctx); + self.show_success( + crate::menu_label( + "settings.teams.toggled_team_discoverability", + "Toggled team discoverability", + ), + ctx, + ); ctx.notify(); } UserWorkspacesEvent::ToggleTeamDiscoverabilityRejected(err) => { - self.show_error("Failed to toggle team discoverability", Some(err), ctx); + self.show_error( + crate::menu_label( + "settings.teams.failed_to_toggle_team_discoverability", + "Failed to toggle team discoverability", + ), + Some(err), + ctx, + ); } UserWorkspacesEvent::JoinTeamWithTeamDiscoverySuccess => { // Force refresh of Warp Drive objects after joining a team @@ -1001,14 +1131,37 @@ impl TeamsPageView { .user_workspaces .as_ref(ctx) .current_team() - .map_or("Successfully joined team".to_string(), |team| { - format!("Successfully joined {}", team.name) - }); + .map_or_else( + || { + crate::menu_label( + "settings.teams.successfully_joined_team", + "Successfully joined team", + ) + .to_string() + }, + |team| { + i18n::interpolate( + crate::menu_label( + "settings.teams.successfully_joined_team_named", + "Successfully joined {name}", + ), + &[("name", team.name.clone())], + ) + .into_owned() + }, + ); self.show_success(message, ctx); ctx.notify(); } UserWorkspacesEvent::JoinTeamWithTeamDiscoveryRejected(err) => { - self.show_error("Failed to join team", Some(err), ctx); + self.show_error( + crate::menu_label( + "settings.teams.failed_to_join_team", + "Failed to join team", + ), + Some(err), + ctx, + ); } UserWorkspacesEvent::FetchDiscoverableTeamsSuccess(teams) => { self.discoverable_teams_states = teams @@ -1022,18 +1175,44 @@ impl TeamsPageView { log::error!("Failed to fetch discoverable teams: {e:?}"); } UserWorkspacesEvent::TransferTeamOwnershipSuccess => { - self.show_success("Successfully transferred team ownership", ctx); + self.show_success( + crate::menu_label( + "settings.teams.successfully_transferred_team_ownership", + "Successfully transferred team ownership", + ), + ctx, + ); ctx.notify(); } UserWorkspacesEvent::TransferTeamOwnershipRejected(err) => { - self.show_error("Failed to transfer team ownership", Some(err), ctx); + self.show_error( + crate::menu_label( + "settings.teams.failed_to_transfer_team_ownership", + "Failed to transfer team ownership", + ), + Some(err), + ctx, + ); } UserWorkspacesEvent::SetTeamMemberRoleSuccess => { self.update_team_members_state(ctx); - self.show_success("Successfully updated team member role", ctx); + self.show_success( + crate::menu_label( + "settings.teams.successfully_updated_team_member_role", + "Successfully updated team member role", + ), + ctx, + ); } UserWorkspacesEvent::SetTeamMemberRoleRejected(err) => { - self.show_error("Failed to update team member role", Some(err), ctx); + self.show_error( + crate::menu_label( + "settings.teams.failed_to_update_team_member_role", + "Failed to update team member role", + ), + Some(err), + ctx, + ); } UserWorkspacesEvent::UpdateWorkspaceSettingsSuccess => { // as of right now, this is only emitted on the billing & usage page @@ -1150,18 +1329,41 @@ impl TeamsPageView { ) { match event { TeamUpdateManagerEvent::LeaveError => { - let error = "Error leaving team".to_string(); + let error = crate::menu_label( + "settings.teams.error_leaving_team", + "Error leaving team", + ) + .to_string(); self.show_error(error, None, ctx); } TeamUpdateManagerEvent::LeaveSuccess => { - self.show_success("Successfully left team", ctx); + self.show_success( + crate::menu_label( + "settings.teams.successfully_left_team", + "Successfully left team", + ), + ctx, + ); ctx.notify(); } TeamUpdateManagerEvent::RenameTeamSuccess => { - self.show_success("Successfully renamed team", ctx) + self.show_success( + crate::menu_label( + "settings.teams.successfully_renamed_team", + "Successfully renamed team", + ), + ctx, + ) } TeamUpdateManagerEvent::RenameTeamError => { - self.show_error("Failed to rename team", None, ctx) + self.show_error( + crate::menu_label( + "settings.teams.failed_to_rename_team", + "Failed to rename team", + ), + None, + ctx, + ) } } } @@ -1445,7 +1647,14 @@ impl TeamsPageView { fn copy_invite_link(&mut self, link: &str, ctx: &mut ViewContext) { ctx.clipboard() .write(ClipboardContent::plain_text(link.to_string())); - self.show_toast("Link copied to clipboard!", ToastFlavor::Default, ctx); + self.show_toast( + crate::menu_label( + "settings.teams.link_copied_to_clipboard", + "Link copied to clipboard!", + ), + ToastFlavor::Default, + ctx, + ); } fn remove_user_from_team( @@ -1510,7 +1719,14 @@ impl TeamsPageView { // Verify no invalid domains before continuing let invalid_domains = editor.get_list_of_invalid_words(ctx); if !invalid_domains.is_empty() { - let error = format!("Invalid domains: {}", invalid_domains.len()); + let error = i18n::interpolate( + crate::menu_label( + "settings.teams.invalid_domains_count", + "Invalid domains: {count}", + ), + &[("count", invalid_domains.len().to_string())], + ) + .into_owned(); self.show_error(error, None, ctx); return; } @@ -1529,10 +1745,15 @@ impl TeamsPageView { .into_iter() .collect(); - self.show_success( - format!("Domain restrictions added: {}", unique_domains.len()), - ctx, - ); + let success_message = i18n::interpolate( + crate::menu_label( + "settings.teams.domain_restrictions_added_count", + "Domain restrictions added: {count}", + ), + &[("count", unique_domains.len().to_string())], + ) + .into_owned(); + self.show_success(success_message, ctx); self.user_workspaces .update(ctx, move |user_workspaces, ctx| { user_workspaces.add_invite_link_domain_restrictions(team_uid, unique_domains, ctx); @@ -1557,7 +1778,14 @@ impl TeamsPageView { // Verify no invalid emails before continuing let invalid_emails = editor.get_list_of_invalid_words(ctx); if !invalid_emails.is_empty() { - let error = format!("Invalid emails: {}", invalid_emails.len()); + let error = i18n::interpolate( + crate::menu_label( + "settings.teams.invalid_emails_count", + "Invalid emails: {count}", + ), + &[("count", invalid_emails.len().to_string())], + ) + .into_owned(); self.show_error(error, None, ctx); return; } @@ -1577,9 +1805,20 @@ impl TeamsPageView { .collect(); let message = if unique_emails.len() == 1 { - "Your invite is on the way!".to_string() + crate::menu_label( + "settings.teams.invite_on_its_way", + "Your invite is on the way!", + ) + .to_string() } else { - format!("Your {} invites are on the way!", unique_emails.len()) + i18n::interpolate( + crate::menu_label( + "settings.teams.invites_on_their_way", + "Your {count} invites are on the way!", + ), + &[("count", unique_emails.len().to_string())], + ) + .into_owned() }; self.show_success(message, ctx); self.user_workspaces @@ -1718,7 +1957,11 @@ impl TeamsPageView { let actions = if current_user_has_admin_permissions { vec![ItemAction { icon: Icon::X, - label: "Cancel invite".to_string(), + label: crate::menu_label( + "settings.teams.cancel_invite", + "Cancel invite", + ) + .to_string(), action: TeamsPageAction::DeletePendingEmailInvitation { team_uid: team.uid, invitee_email: email_invite.invitee_email.clone(), @@ -1755,7 +1998,11 @@ impl TeamsPageView { if current_user_has_owner_permissions && !team_member_has_owner_permissions { actions.push(ItemAction { icon: Icon::Users, - label: "Transfer ownership".to_string(), + label: crate::menu_label( + "settings.teams.transfer_ownership", + "Transfer ownership", + ) + .to_string(), action: TeamsPageAction::ShowTransferOwnershipModal { new_owner_email: member.email.clone(), new_owner_uid: member.uid, @@ -1772,7 +2019,11 @@ impl TeamsPageView { if team_member_has_admin_permissions { actions.push(ItemAction { icon: Icon::ArrowDown, - label: "Demote from admin".to_string(), + label: crate::menu_label( + "settings.teams.demote_from_admin", + "Demote from admin", + ) + .to_string(), action: TeamsPageAction::SetTeamMemberRole { team_uid: team.uid, user_uid: member.uid, @@ -1782,7 +2033,11 @@ impl TeamsPageView { } else { actions.push(ItemAction { icon: Icon::ArrowUp, - label: "Promote to admin".to_string(), + label: crate::menu_label( + "settings.teams.promote_to_admin", + "Promote to admin", + ) + .to_string(), action: TeamsPageAction::SetTeamMemberRole { team_uid: team.uid, user_uid: member.uid, @@ -1796,7 +2051,11 @@ impl TeamsPageView { if current_user_has_admin_permissions && !team_member_has_owner_permissions { actions.push(ItemAction { icon: Icon::X, - label: "Remove from team".to_string(), + label: crate::menu_label( + "settings.teams.remove_from_team", + "Remove from team", + ) + .to_string(), action: TeamsPageAction::RemoveUserFromTeam { user_uid: member.uid, team_uid: team.uid, @@ -1995,10 +2254,18 @@ impl TeamsWidget { .finish(); let title = match warning { - GrowTeamWarning::SeatCapReached => "Your team is full", - GrowTeamWarning::SeatCapExceeded => "You've exceeded your member limit", - GrowTeamWarning::PaymentPastDue => "Payment past due", - GrowTeamWarning::PaymentUnpaid => "Subscription unpaid", + GrowTeamWarning::SeatCapReached => { + crate::menu_label("settings.teams.team_full_title", "Your team is full") + } + GrowTeamWarning::SeatCapExceeded => { + crate::menu_label("settings.teams.you_exceeded_member_limit", "You've exceeded your member limit") + } + GrowTeamWarning::PaymentPastDue => { + crate::menu_label("settings.teams.payment_past_due", "Payment past due") + } + GrowTeamWarning::PaymentUnpaid => { + crate::menu_label("settings.teams.subscription_unpaid", "Subscription unpaid") + } }; let title_element = self.render_subsection_header(title.to_owned(), appearance); @@ -2010,15 +2277,17 @@ impl TeamsWidget { ); let body_prefix = match warning { - GrowTeamWarning::SeatCapReached => "You've reached your plan's member limit.", + GrowTeamWarning::SeatCapReached => { + crate::menu_label("settings.teams.seat_cap_reached", "You've reached your plan's member limit.") + } GrowTeamWarning::SeatCapExceeded => { - "You've exceeded your plan's member limit. Existing team members keep their access, but you won't be able to add new members." + crate::menu_label("settings.teams.seat_cap_exceeded", "You've exceeded your plan's member limit. Existing team members keep their access, but you won't be able to add new members.") } GrowTeamWarning::PaymentPastDue => { - "Team invites have been restricted due to a past-due payment." + crate::menu_label("settings.teams.payment_past_due_description", "Team invites have been restricted due to a past-due payment.") } GrowTeamWarning::PaymentUnpaid => { - "Team invites have been restricted due to an unpaid subscription." + crate::menu_label("settings.teams.subscription_unpaid_description", "Team invites have been restricted due to an unpaid subscription.") } }; @@ -2028,22 +2297,41 @@ impl TeamsWidget { ); let cta_sentence = if !has_admin_permissions { if is_delinquency { - "Contact a team admin to restore access." + crate::menu_label( + "settings.teams.contact_team_admin_to_restore", + "Contact a team admin to restore access.", + ) } else { - "Contact a team admin to grow the team." + crate::menu_label( + "settings.teams.contact_team_admin_to_grow", + "Contact a team admin to grow the team.", + ) } } else { match cta { - GrowTeamWarningCta::Upgrade => "Upgrade to grow your team.", - GrowTeamWarningCta::UpdateBilling => { - "Update your payment information to restore access." - } - GrowTeamWarningCta::ContactSupport => "Contact support to restore access.", + GrowTeamWarningCta::Upgrade => crate::menu_label( + "settings.teams.upgrade_to_grow_team", + "Upgrade to grow your team.", + ), + GrowTeamWarningCta::UpdateBilling => crate::menu_label( + "settings.teams.update_payment_to_restore", + "Update your payment information to restore access.", + ), + GrowTeamWarningCta::ContactSupport => crate::menu_label( + "settings.teams.contact_support_to_restore", + "Contact support to restore access.", + ), GrowTeamWarningCta::None => { if is_delinquency { - "Contact support to restore access." + crate::menu_label( + "settings.teams.contact_support_to_restore", + "Contact support to restore access.", + ) } else { - "Contact sales to grow your team." + crate::menu_label( + "settings.teams.contact_sales_to_grow", + "Contact sales to grow your team.", + ) } } } @@ -2073,16 +2361,17 @@ impl TeamsWidget { // mouse state handle is fine because at most one CTA shows at a time. if let Some((cta_label, cta_action)) = match cta { GrowTeamWarningCta::Upgrade => Some(( - "Upgrade", + crate::menu_label("settings.teams.upgrade", "Upgrade"), TeamsPageAction::GenerateUpgradeLink { team_uid: team.uid }, )), GrowTeamWarningCta::UpdateBilling => Some(( - "Update billing", + crate::menu_label("settings.teams.update_billing", "Update billing"), TeamsPageAction::GenerateStripeBillingPortalLink { team_uid: team.uid }, )), - GrowTeamWarningCta::ContactSupport => { - Some(("Contact support", TeamsPageAction::ContactSupport)) - } + GrowTeamWarningCta::ContactSupport => Some(( + crate::menu_label("settings.teams.contact_support", "Contact support"), + TeamsPageAction::ContactSupport, + )), GrowTeamWarningCta::None => None, } { let cta_mouse_state = self @@ -2141,11 +2430,26 @@ impl TeamsWidget { ) -> (&'static str, &'static str) { if billing_metadata.customer_type == CustomerType::Business { ( - "Upgrade to Enterprise", - " for an unlimited team member limit.", + crate::menu_label( + "settings.teams.upgrade_to_enterprise", + "Upgrade to Enterprise", + ), + crate::menu_label( + "settings.teams.for_unlimited_team_member_limit", + " for an unlimited team member limit.", + ), ) } else { - ("Upgrade to Business", " for a higher team member limit.") + ( + crate::menu_label( + "settings.teams.upgrade_to_business", + "Upgrade to Business", + ), + crate::menu_label( + "settings.teams.for_higher_team_member_limit", + " for a higher team member limit.", + ), + ) } } @@ -2157,21 +2461,41 @@ impl TeamsWidget { has_admin_permissions: bool, ) -> Box { let prorated_message = if has_admin_permissions { - "You'll be charged for a portion of the team member's usage of Warp." + crate::menu_label( + "settings.teams.prorated_charge_info", + "You'll be charged for a portion of the team member's usage of Warp.", + ) } else { - "Your admin will be charged for a portion of the team member's usage of Warp." + crate::menu_label( + "settings.teams.prorated_charge_info_admin", + "Your admin will be charged for a portion of the team member's usage of Warp.", + ) }; let additional_members_cost_money_msg = if let Some((monthly_cost, yearly_cost)) = self.get_per_seat_costs(team_metadata, pricing_info_model) { - format!( - "Additional members are billed at your plan's per-user rate: ${monthly_cost:.0}/month or ${yearly_cost:.0}/year, depending on your billing interval. {prorated_message}" + i18n::interpolate( + crate::menu_label( + "settings.teams.additional_members_billed", + "Additional members are billed at your plan's per-user rate: ${monthly:.0}/month or ${yearly:.0}/year, depending on your billing interval. {prorated}", + ), + &[ + ("monthly", format!("{monthly_cost:.0}")), + ("yearly", format!("{yearly_cost:.0}")), + ("prorated", prorated_message.to_string()), + ], ) + .into_owned() } else { - format!( - "Additional members are billed at your plan's per-user rate. {prorated_message}" + i18n::interpolate( + crate::menu_label( + "settings.teams.additional_members_billed_no_rate", + "Additional members are billed at your plan's per-user rate. {prorated}", + ), + &[("prorated", prorated_message.to_string())], ) + .into_owned() }; let horizontal_padding = 16.; @@ -2390,7 +2714,7 @@ impl TeamsWidget { left_side.add_child( Container::new(self.render_delinquency_badge( appearance, - "PAST DUE".into(), + crate::menu_label("settings.teams.past_due", "PAST DUE").into(), themes::theme::Fill::from(*PAST_DUE_BADGE_COLOR).into(), )) .with_margin_left(8.) @@ -2401,7 +2725,7 @@ impl TeamsWidget { left_side.add_child( Container::new(self.render_delinquency_badge( appearance, - "UNPAID".into(), + crate::menu_label("settings.teams.unpaid", "UNPAID").into(), themes::theme::Fill::from(*UNPAID_BADGE_COLOR).into(), )) .with_margin_left(8.) @@ -2433,7 +2757,7 @@ impl TeamsWidget { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Contact support", + crate::menu_label("settings.teams.contact_support", "Contact support"), Icon::Phone.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -2462,7 +2786,7 @@ impl TeamsWidget { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Manage billing", + crate::menu_label("settings.teams.manage_billing", "Manage billing"), Icon::CoinsStacked.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -2493,7 +2817,7 @@ impl TeamsWidget { .with_text_and_icon_label( TextAndIcon::new( TextAndIconAlignment::IconFirst, - "Open admin panel", + crate::menu_label("settings.teams.open_admin_panel", "Open admin panel"), Icon::Users.to_warpui_icon(appearance.theme().accent()), MainAxisSize::Min, MainAxisAlignment::Center, @@ -2527,12 +2851,18 @@ impl TeamsWidget { // If the team is upgradeable to self-serve tier, show them the upgrade link. if team.billing_metadata.can_upgrade_to_higher_tier_plan() { let description = if team.billing_metadata.can_upgrade_to_build_plan() { - "Upgrade to Build" + crate::menu_label("settings.teams.upgrade_to_build", "Upgrade to Build") } else { match team.billing_metadata.customer_type { - CustomerType::Prosumer => "Upgrade to Turbo plan", - CustomerType::Turbo => "Upgrade to Lightspeed plan", - _ => "Compare plans", + CustomerType::Prosumer => crate::menu_label( + "settings.teams.upgrade_to_turbo_plan", + "Upgrade to Turbo plan", + ), + CustomerType::Turbo => crate::menu_label( + "settings.teams.upgrade_to_lightspeed_plan", + "Upgrade to Lightspeed plan", + ), + _ => crate::menu_label("settings.teams.compare_plans", "Compare plans"), } }; billing_links.add_child( @@ -2573,8 +2903,10 @@ impl TeamsWidget { ) -> Box { let mut section = Flex::column(); let sub_header_text = match team.billing_metadata.customer_type { - CustomerType::Free => "Free plan usage limits", - _ => "Plan usage limits", + CustomerType::Free => { + crate::menu_label("settings.teams.free_plan_usage_limits", "Free plan usage limits") + } + _ => crate::menu_label("settings.teams.plan_usage_limits", "Plan usage limits"), }; section.add_child(self.render_subsection_header(sub_header_text.into(), appearance)); @@ -2585,7 +2917,7 @@ impl TeamsWidget { if !policy.is_unlimited { let mut shared_notebooks_column = Flex::column(); shared_notebooks_column.add_child( - self.render_plan_usage_header("Shared Notebooks".into(), appearance), + self.render_plan_usage_header(crate::menu_label("settings.teams.shared_notebooks", "Shared Notebooks").to_string(), appearance), ); let num_shared_notebooks = cloud_model .active_notebooks_in_space(Space::Team { team_uid: team.uid }, app) @@ -2610,7 +2942,7 @@ impl TeamsWidget { if !policy.is_unlimited { let mut shared_workflows_column = Flex::column(); shared_workflows_column.add_child( - self.render_plan_usage_header("Shared Workflows".into(), appearance), + self.render_plan_usage_header(crate::menu_label("settings.teams.shared_workflows", "Shared Workflows").to_string(), appearance), ); let num_shared_workflows = cloud_model .active_workflows_in_space(Space::Team { team_uid: team.uid }, app) @@ -2663,7 +2995,7 @@ impl TeamsWidget { invitation_section.add_child( Container::new( - self.render_subsection_header("Invite team members".to_owned(), appearance), + self.render_subsection_header(crate::menu_label("settings.teams.invite_team_members", "Invite team members").to_owned(), appearance), ) .with_padding_bottom(16.) .finish(), @@ -2735,13 +3067,13 @@ impl TeamsWidget { // Header + admin-only subtext on the left, toggle on the right. The // text is stacked so the toggle centers against the whole block. - let header = self.render_subsubsection_header("By link".to_owned(), appearance); + let header = self.render_subsubsection_header(crate::menu_label("settings.teams.by_link", "By link").to_owned(), appearance); let text_column = if has_admin_permissions { Flex::column() .with_child(header) .with_child( Container::new(self.render_sub_text( - INVITE_LINK_TOGGLE_INSTRUCTIONS.into(), + invite_link_toggle_instructions().to_string(), appearance, Some(Coords::uniform(0.).right(48.)), )) @@ -2793,7 +3125,7 @@ impl TeamsWidget { appearance .ui_builder() .link( - "Reset links".into(), + crate::menu_label("settings.teams.reset_links", "Reset links").into(), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action(TeamsPageAction::ResetInviteLinks { @@ -2839,7 +3171,7 @@ impl TeamsWidget { // "By email" subsection header section.add_child( - Container::new(self.render_subsubsection_header("By email".to_owned(), appearance)) + Container::new(self.render_subsubsection_header(crate::menu_label("settings.teams.by_email", "By email").to_owned(), appearance)) .with_padding_top(CONTENT_SEPARATION_PADDING) .with_padding_bottom(8.) .finish(), @@ -2851,7 +3183,7 @@ impl TeamsWidget { // the invitation section owns the explanation + recovery CTA. section.add_child( Container::new(self.render_sub_text( - INVITE_BY_EMAIL_EXPIRY_INSTRUCTIONS.into(), + invite_by_email_expiry_instructions().to_string(), appearance, Some(Coords::uniform(0.).right(48.)), )) @@ -2888,7 +3220,7 @@ impl TeamsWidget { { section.add_child( Container::new( - self.render_error_sub_text(INVALID_EMAILS_INSTRUCTIONS.into(), appearance), + self.render_error_sub_text(invalid_emails_instructions().to_string(), appearance), ) .with_padding_top(8.) .finish(), @@ -2912,7 +3244,7 @@ impl TeamsWidget { .with_main_axis_size(MainAxisSize::Max) .with_main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_cross_axis_alignment(CrossAxisAlignment::Center) - .with_child(self.render_subsection_header("Team members".to_owned(), appearance)) + .with_child(self.render_subsection_header(crate::menu_label("settings.teams.team_members", "Team members").to_owned(), appearance)) .with_child(self.render_team_members_count(team, appearance)) .finish(); section.add_child( @@ -2939,9 +3271,16 @@ impl TeamsWidget { fn render_team_members_count(&self, team: &Team, appearance: &Appearance) -> Box { let count = team.members.len(); let count_label = if count == 1 { - "1 team member".to_string() + crate::menu_label("settings.teams.team_member_count_one", "1 team member").to_string() } else { - format!("{count} team members") + i18n::interpolate( + crate::menu_label( + "settings.teams.team_member_count_other", + "{count} team members", + ), + &[("count", count.to_string())], + ) + .into_owned() }; // No capacity tooltip when the plan is unlimited (or workspace size @@ -2984,8 +3323,14 @@ impl TeamsWidget { }; let plan_display = team.billing_metadata.customer_type.to_display_string(); - let tooltip_text = - format!("Your plan ({plan_display}) has a maximum capacity of {cap} members."); + let tooltip_text = i18n::interpolate( + crate::menu_label( + "settings.teams.member_limit_info", + "Your plan ({plan}) has a maximum capacity of {cap} members.", + ), + &[("plan", plan_display.to_string()), ("cap", cap.to_string())], + ) + .into_owned(); let info_icon = Container::new( ConstrainedBox::new(Icon::Info.to_warpui_icon(muted_color).finish()) @@ -3038,7 +3383,11 @@ impl TeamsWidget { let team_uid = team.uid; let (link_text, suffix) = Self::outgrow_upgrade_line_copy(&team.billing_metadata); - let prefix = self.render_sub_text("Need more seats? ".to_string(), appearance, None); + let prefix = self.render_sub_text( + crate::menu_label("settings.teams.need_more_seats", "Need more seats? ").to_string(), + appearance, + None, + ); let link = appearance .ui_builder() .link( @@ -3079,7 +3428,7 @@ impl TeamsWidget { if has_admin_permissions { section.add_child( Container::new(self.render_sub_text( - INVITE_LINK_DOMAIN_RESTRICTIONS_INSTRUCTIONS.into(), + invite_link_domain_restrictions_instructions().to_string(), appearance, Some(Coords::uniform(0.).right(48.)), )) @@ -3116,7 +3465,7 @@ impl TeamsWidget { { section.add_child( Container::new( - self.render_error_sub_text(INVALID_DOMAINS_INSTRUCTIONS.into(), appearance), + self.render_error_sub_text(invalid_domains_instructions().to_string(), appearance), ) .with_padding_top(8.) .finish(), @@ -3132,7 +3481,7 @@ impl TeamsWidget { let actions = if has_admin_permissions { vec![ItemAction { icon: Icon::X, - label: "Remove domain".to_string(), + label: crate::menu_label("settings.teams.remove_domain", "Remove domain").to_string(), action: TeamsPageAction::DeleteDomainRestriction { domain_uid: domain_restriction.uid, team_uid: team.uid, @@ -3181,7 +3530,7 @@ impl TeamsWidget { (None, ButtonVariant::Basic) }; Container::new(self.render_button( - APPROVE_DOMAINS_BUTTON_LABEL, + approve_domains_button_label(), variant, self.mouse_state_handles.approve_domains_button.clone(), action, @@ -3211,7 +3560,7 @@ impl TeamsWidget { (None, ButtonVariant::Basic) }; Container::new(self.render_button( - SEND_EMAIL_INVITES_BUTTON_LABEL, + send_email_invites_button_label(), variant, self.mouse_state_handles.send_email_invites_button.clone(), action, @@ -3246,11 +3595,17 @@ impl TeamsWidget { ) -> Box { // Same layout as the "By link" header row: text column on the left, // toggle on the right. - let header = self.render_subsubsection_header("By discovery".to_owned(), appearance); + let header = self.render_subsubsection_header(crate::menu_label("settings.teams.by_discovery", "By discovery").to_owned(), appearance); let domain = current_user_email.split('@').nth(1).unwrap_or(""); - let team_discoverability_instructions = - format!("Allow Warp users with an @{domain} email to find and join the team."); + let team_discoverability_instructions = i18n::interpolate( + crate::menu_label( + "settings.teams.allow_users_with_domain", + "Allow Warp users with an @{domain} email to find and join the team.", + ), + &[("domain", domain.to_string())], + ) + .into_owned(); let subtext = self.render_sub_text( team_discoverability_instructions, appearance, @@ -3303,12 +3658,12 @@ impl TeamsWidget { let (label, action) = if is_team_owner { ( - DELETE_TEAM_BUTTON_LABEL, + delete_team_button_label(), TeamsPageAction::ShowDeleteTeamConfirmationDialog, ) } else { ( - LEAVE_TEAM_BUTTON_LABEL, + leave_team_button_label(), TeamsPageAction::ShowLeaveTeamConfirmationDialog, ) }; @@ -3387,7 +3742,7 @@ impl TeamsWidget { let link = appearance .ui_builder() .link( - "Manage plan".into(), + crate::menu_label("settings.teams.manage_plan", "Manage plan").into(), None, Some(Box::new(move |ctx| { ctx.dispatch_typed_action( @@ -3485,7 +3840,7 @@ impl TeamsWidget { pending_and_close_row.add_child( self.render_state_chip( appearance, - "EXPIRED".into(), + crate::menu_label("settings.teams.expired", "EXPIRED").into(), appearance.theme().ui_error_color(), themes::theme::Fill::from(appearance.theme().ui_error_color()) .with_opacity(30) @@ -3499,7 +3854,7 @@ impl TeamsWidget { pending_and_close_row.add_child( self.render_state_chip( appearance, - "PENDING".into(), + crate::menu_label("settings.teams.pending", "PENDING").into(), *EMAIL_INVITE_PENDING_COLOR, themes::theme::Fill::from(*EMAIL_INVITE_PENDING_COLOR) .with_opacity(30) @@ -3513,7 +3868,7 @@ impl TeamsWidget { pending_and_close_row.add_child( self.render_state_chip( appearance, - "OWNER".into(), + crate::menu_label("settings.teams.owner", "OWNER").into(), owner_state_chip_text_color(appearance.theme()), appearance .theme() @@ -3529,7 +3884,7 @@ impl TeamsWidget { pending_and_close_row.add_child( self.render_state_chip( appearance, - "ADMIN".into(), + crate::menu_label("settings.teams.admin", "ADMIN").into(), appearance .theme() .background() @@ -3685,7 +4040,14 @@ impl TeamsWidget { ); (link, true) } - None => ("Failed to load invite link.".into(), false), + None => ( + crate::menu_label( + "settings.teams.failed_to_load_invite_link", + "Failed to load invite link.", + ) + .into(), + false, + ), }; let theme = appearance.theme(); @@ -3980,13 +4342,13 @@ impl TeamsWidget { let mut page = Flex::column(); // Title, subtitle, and description - page.add_child(render_sub_header(appearance, "Teams".to_string(), None)); + page.add_child(render_sub_header(appearance, crate::menu_label("settings.teams.teams", "Teams").to_string(), None)); page.add_child( - self.render_sub_header_with_subtext_color(appearance, "Create a team".to_string()), + self.render_sub_header_with_subtext_color(appearance, crate::menu_label("settings.teams.create_team", "Create a team").to_string()), ); page.add_child( Container::new( - self.render_description(CREATE_TEAM_DESCRIPTION.to_string(), appearance), + self.render_description(create_team_description().to_string(), appearance), ) .with_padding_top(6.) .finish(), @@ -4009,10 +4371,20 @@ impl TeamsWidget { .with_margin_left(-4.) .finish(); let checkbox_row_text = if let Some(domain) = view.auth_state.user_email_domain() { - format!("Allow Warp users with an @{domain} email to find and join the team.") + i18n::interpolate( + crate::menu_label( + "settings.teams.allow_users_with_domain", + "Allow Warp users with an @{domain} email to find and join the team.", + ), + &[("domain", domain.to_string())], + ) + .into_owned() } else { - "Allow Warp users with the same email domain as you to find and join the team." - .to_string() + crate::menu_label( + "settings.teams.allow_users_with_same_domain", + "Allow Warp users with the same email domain as you to find and join the team.", + ) + .to_string() }; let checkbox_row = Container::new( Flex::row() @@ -4042,7 +4414,11 @@ impl TeamsWidget { page.add_child(render_separator(appearance)); page.add_child(self.render_sub_header_with_subtext_color( appearance, - "Or, join an existing team within your company".to_string(), + crate::menu_label( + "settings.teams.or_join_existing_team", + "Or, join an existing team within your company", + ) + .to_string(), )); // Team discovery @@ -4120,9 +4496,16 @@ impl TeamsWidget { // Number of teammates let teammate_string = if team_state.team.num_members == 1 { - "1 teammate".to_string() + crate::menu_label("settings.teams.teammate_count_one", "1 teammate").to_string() } else { - format!("{} teammates", team_state.team.num_members) + i18n::interpolate( + crate::menu_label( + "settings.teams.teammate_count_other", + "{count} teammates", + ), + &[("count", team_state.team.num_members.to_string())], + ) + .into_owned() }; single_team.add_child(self.render_sub_text(teammate_string, appearance, None)); @@ -4130,8 +4513,11 @@ impl TeamsWidget { single_team.add_child( Container::new( self.render_sub_text( - "Join this team and start collaborating on workflows, notebooks, and more." - .to_string(), + crate::menu_label( + "settings.teams.join_this_team", + "Join this team and start collaborating on workflows, notebooks, and more.", + ) + .to_string(), appearance, None, ), @@ -4262,7 +4648,7 @@ impl TeamsWidget { ButtonVariant::Accent, self.mouse_state_handles.create_team_button.clone(), ) - .with_centered_text_label(CREATE_TEAM_BUTTON_LABEL.to_owned()) + .with_centered_text_label(create_team_button_label().to_owned()) .with_style(UiComponentStyles { font_color: Some( appearance @@ -4311,7 +4697,7 @@ impl TeamsWidget { ) -> Box { if team_state.team.team_accepting_invites { self.render_button( - "Join", + crate::menu_label("settings.teams.join", "Join"), ButtonVariant::Accent, team_state.mouse_state_handle.clone(), Some(TeamsPageAction::JoinTeamWithTeamDiscovery { @@ -4341,7 +4727,7 @@ impl TeamsWidget { font_size: Some(14.), ..Default::default() }) - .with_centered_text_label("Contact Admin to request access".to_string()) + .with_centered_text_label(crate::menu_label("settings.teams.contact_admin_to_request_access", "Contact Admin to request access").to_string()) .disabled() .build() .finish() @@ -4386,7 +4772,7 @@ impl SettingsWidget for TeamsWidget { } else { appearance .ui_builder() - .span(OFFLINE_TEXT.to_string()) + .span(offline_text().to_string()) .build() .finish() }; diff --git a/app/src/settings_view/update_environment_form.rs b/app/src/settings_view/update_environment_form.rs index f04dd0c281..664f0475af 100644 --- a/app/src/settings_view/update_environment_form.rs +++ b/app/src/settings_view/update_environment_form.rs @@ -256,14 +256,14 @@ pub struct EnvironmentFormCopy { impl EnvironmentFormCopy { pub fn orchestration_modal() -> Self { Self { - name_placeholder: "e.g., dev-env", - repos_placeholder_authed: "Browse GitHub repos...", - repos_placeholder_unauthed: REPOS_PLACEHOLDER_UNAUTHED, - docker_image_label: "Docker image", - docker_image_placeholder: "e.g., node:20-alpine", - description_placeholder: DESCRIPTION_PLACEHOLDER, - setup_commands_placeholder: "e.g., node start", - setup_commands_helper: "Press Enter or click the submit button to add each command.", + name_placeholder: crate::menu_label("settings.update_environment_form.name_placeholder_orchestration", "e.g., dev-env"), + repos_placeholder_authed: crate::menu_label("settings.update_environment_form.repos_placeholder_browse", "Browse GitHub repos..."), + repos_placeholder_unauthed: repos_placeholder_unauthed(), + docker_image_label: crate::menu_label("settings.update_environment_form.docker_image_label", "Docker image"), + docker_image_placeholder: crate::menu_label("settings.update_environment_form.docker_image_placeholder_short", "e.g., node:20-alpine"), + description_placeholder: description_placeholder(), + setup_commands_placeholder: crate::menu_label("settings.update_environment_form.setup_commands_placeholder_short", "e.g., node start"), + setup_commands_helper: crate::menu_label("settings.update_environment_form.setup_commands_helper_short", "Press Enter or click the submit button to add each command."), show_description_character_count: false, } } @@ -272,14 +272,14 @@ impl EnvironmentFormCopy { impl Default for EnvironmentFormCopy { fn default() -> Self { Self { - name_placeholder: "Environment name", - repos_placeholder_authed: REPOS_PLACEHOLDER_AUTHED, - repos_placeholder_unauthed: REPOS_PLACEHOLDER_UNAUTHED, - docker_image_label: "Docker image reference", - docker_image_placeholder: "e.g. python:3.11, node:20-alpine", - description_placeholder: DESCRIPTION_PLACEHOLDER, - setup_commands_placeholder: "e.g. cd my-repo && pip install -r requirements.txt", - setup_commands_helper: "Setup commands run independently. Each command runs from the workspace root (/workspace). If a command depends on the previous one, combine them with &&.", + name_placeholder: crate::menu_label("settings.update_environment_form.name_placeholder", "Environment name"), + repos_placeholder_authed: repos_placeholder_authed(), + repos_placeholder_unauthed: repos_placeholder_unauthed(), + docker_image_label: crate::menu_label("settings.update_environment_form.docker_image_label_long", "Docker image reference"), + docker_image_placeholder: crate::menu_label("settings.update_environment_form.docker_image_placeholder", "e.g. python:3.11, node:20-alpine"), + description_placeholder: description_placeholder(), + setup_commands_placeholder: crate::menu_label("settings.update_environment_form.setup_commands_placeholder", "e.g. cd my-repo && pip install -r requirements.txt"), + setup_commands_helper: crate::menu_label("settings.update_environment_form.setup_commands_helper", "Setup commands run independently. Each command runs from the workspace root (/workspace). If a command depends on the previous one, combine them with &&."), show_description_character_count: true, } } @@ -361,9 +361,16 @@ pub struct UpdateEnvironmentForm { } const DESCRIPTION_MAX_CHARS: usize = 240; -const DESCRIPTION_PLACEHOLDER: &str = "e.g., this environment is for all front end focused agents"; -const REPOS_PLACEHOLDER_AUTHED: &str = "Enter repos (owner/repo format)"; -const REPOS_PLACEHOLDER_UNAUTHED: &str = "Paste repo URL(s)"; +// ── i18n helpers ────────────────────────────────────────────────────── +fn description_placeholder() -> &'static str { + crate::menu_label("settings.update_environment_form.description_placeholder", "e.g., this environment is for all front end focused agents") +} +fn repos_placeholder_authed() -> &'static str { + crate::menu_label("settings.update_environment_form.repos_placeholder_authed", "Enter repos (owner/repo format)") +} +fn repos_placeholder_unauthed() -> &'static str { + crate::menu_label("settings.update_environment_form.repos_placeholder_unauthed", "Paste repo URL(s)") +} const FORM_FIELD_SPACING: f32 = 20.; const FORM_LABEL_SPACING: f32 = 6.; const FORM_INPUT_HEIGHT: f32 = 36.; @@ -462,7 +469,7 @@ impl UpdateEnvironmentForm { // Create buttons let submit_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Create", PrimaryTheme) + ActionButton::new(crate::menu_label("common.create", "Create"), PrimaryTheme) .with_icon(Icon::Check) .on_click(|ctx| { ctx.dispatch_typed_action(UpdateEnvironmentFormAction::Submit); @@ -470,7 +477,7 @@ impl UpdateEnvironmentForm { }); let delete_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Delete environment", DangerSecondaryTheme) + ActionButton::new(crate::menu_label("settings.update_environment_form.delete_environment", "Delete environment"), DangerSecondaryTheme) .with_icon(Icon::Trash) .on_click(|ctx| { ctx.dispatch_typed_action(UpdateEnvironmentFormAction::Delete); @@ -478,7 +485,7 @@ impl UpdateEnvironmentForm { }); let cancel_button = ctx.add_typed_action_view(|_| { - ActionButton::new("Cancel", SecondaryTheme).on_click(|ctx| { + ActionButton::new(crate::menu_label("common.cancel", "Cancel"), SecondaryTheme).on_click(|ctx| { ctx.dispatch_typed_action(UpdateEnvironmentFormAction::Cancel); }) }); @@ -831,10 +838,10 @@ impl UpdateEnvironmentForm { fn update_submit_button_label(&mut self, ctx: &mut ViewContext) { let button_text = match (&self.mode, self.show_header) { - (EnvironmentFormMode::Create, true) => "Create", - (EnvironmentFormMode::Create, false) => "Create environment", - (EnvironmentFormMode::Edit { .. }, true) => "Save", - (EnvironmentFormMode::Edit { .. }, false) => "Save environment", + (EnvironmentFormMode::Create, true) => crate::menu_label("common.create", "Create"), + (EnvironmentFormMode::Create, false) => crate::menu_label("settings.update_environment_form.create_environment", "Create environment"), + (EnvironmentFormMode::Edit { .. }, true) => crate::menu_label("common.save", "Save"), + (EnvironmentFormMode::Edit { .. }, false) => crate::menu_label("settings.update_environment_form.save_environment", "Save environment"), }; self.submit_button.update(ctx, |button, ctx| { button.set_label(button_text, ctx); @@ -867,7 +874,7 @@ impl UpdateEnvironmentForm { self.remove_setup_command_mouse_states.clear(); // Update button text for Create mode self.submit_button.update(ctx, |button, ctx| { - button.set_label("Create", ctx); + button.set_label(crate::menu_label("common.create", "Create"), ctx); }); } EnvironmentFormInitArgs::Edit { @@ -908,7 +915,7 @@ impl UpdateEnvironmentForm { .collect(); // Update button text for Edit mode self.submit_button.update(ctx, |button, ctx| { - button.set_label("Save", ctx); + button.set_label(crate::menu_label("common.save", "Save"), ctx); }); } } @@ -1026,7 +1033,7 @@ impl UpdateEnvironmentForm { ..Default::default() }; let mut editor = EditorView::new(options, ctx); - editor.set_placeholder_text(DESCRIPTION_PLACEHOLDER, ctx); + editor.set_placeholder_text(description_placeholder(), ctx); editor }) } @@ -1354,7 +1361,7 @@ impl UpdateEnvironmentForm { } Ok(UserGithubInfoResult::Unknown) => { me.github_dropdown_state.load_error_message = Some( - "Couldn't load GitHub repos. You can paste repo URL(s), or retry." + crate::menu_label("settings.update_environment_form.load_repos_error", "Couldn't load GitHub repos. You can paste repo URL(s), or retry.") .to_string(), ); me.update_repos_input_placeholder(ctx); @@ -1362,7 +1369,7 @@ impl UpdateEnvironmentForm { Err(e) => { debug!("Failed to load GitHub repos: {e}"); me.github_dropdown_state.load_error_message = Some( - "Couldn't load GitHub repos. You can paste repo URL(s), or retry." + crate::menu_label("settings.update_environment_form.load_repos_error", "Couldn't load GitHub repos. You can paste repo URL(s), or retry.") .to_string(), ); me.update_repos_input_placeholder(ctx); @@ -1554,7 +1561,7 @@ impl UpdateEnvironmentForm { }; } warp_graphql::queries::suggest_cloud_environment_image::SuggestCloudEnvironmentImageResult::UserFacingError(_) => { - let error_message = "Failed to suggest a Docker image".to_string(); + let error_message = crate::menu_label("settings.update_environment_form.suggest_image_failed", "Failed to suggest a Docker image").to_string(); send_telemetry_from_ctx!( CloudAgentTelemetryEvent::ImageSuggestionFailed { error: error_message.clone(), @@ -1567,7 +1574,7 @@ impl UpdateEnvironmentForm { }; } warp_graphql::queries::suggest_cloud_environment_image::SuggestCloudEnvironmentImageResult::Unknown => { - let error_message = "Unknown response from suggestCloudEnvironmentImage".to_string(); + let error_message = crate::menu_label("settings.update_environment_form.suggest_image_unknown_response", "Unknown response from suggestCloudEnvironmentImage").to_string(); send_telemetry_from_ctx!( CloudAgentTelemetryEvent::ImageSuggestionFailed { error: error_message.clone(), @@ -1581,7 +1588,10 @@ impl UpdateEnvironmentForm { } }, Err(e) => { - let error_message = format!("Failed to suggest a Docker image: {}", e); + let error_message = i18n::interpolate( + crate::menu_label("settings.update_environment_form.suggest_image_failed_with_error", "Failed to suggest a Docker image: {error}"), + &[("error", e.to_string())], + ).into_owned(); send_telemetry_from_ctx!( CloudAgentTelemetryEvent::ImageSuggestionFailed { error: error_message.clone(), @@ -1649,7 +1659,7 @@ impl UpdateEnvironmentForm { theme.active_ui_text_color() }; - Text::new_inline("Share with team", font_family, font_size) + Text::new_inline(crate::menu_label("settings.update_environment_form.share_with_team", "Share with team"), font_family, font_size) .with_color(color.into()) .finish() }, @@ -1686,7 +1696,7 @@ impl UpdateEnvironmentForm { Some(render_warning_box( WarningBoxConfig::new( - "Personal environments cannot be used with external integrations or team API keys. For the best experience, use shared environments.", + crate::menu_label("settings.update_environment_form.share_warning", "Personal environments cannot be used with external integrations or team API keys. For the best experience, use shared environments."), ) .with_width(self.field_max_width), appearance, @@ -1731,8 +1741,8 @@ impl UpdateEnvironmentForm { fn render_header(&self, appearance: &Appearance, app: &AppContext) -> Box { let (title, button_handle) = match &self.mode { - EnvironmentFormMode::Create => ("Create environment", &self.submit_button), - EnvironmentFormMode::Edit { .. } => ("Edit environment", &self.submit_button), + EnvironmentFormMode::Create => (crate::menu_label("settings.update_environment_form.create_environment", "Create environment"), &self.submit_button), + EnvironmentFormMode::Edit { .. } => (crate::menu_label("settings.update_environment_form.edit_environment", "Edit environment"), &self.submit_button), }; let submit_actions = || self.render_submit_actions(appearance, app, button_handle); @@ -1858,7 +1868,7 @@ impl UpdateEnvironmentForm { .with_spacing(FORM_LABEL_SPACING); field.add_child(Self::render_form_label( - "Setup command(s)", + crate::menu_label("settings.update_environment_form.setup_commands_label", "Setup command(s)"), false, appearance, )); @@ -1929,7 +1939,7 @@ impl UpdateEnvironmentForm { field.add_child( Text::new( - "Description", + crate::menu_label("settings.update_environment_form.description_label", "Description"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -1961,7 +1971,10 @@ impl UpdateEnvironmentForm { .buffer_text(app) .chars() .count(); - let count_text = format!("{char_count} / {DESCRIPTION_MAX_CHARS} characters"); + let count_text = i18n::interpolate( + crate::menu_label("settings.update_environment_form.characters_count", "{count} / {max} characters"), + &[("count", char_count.to_string()), ("max", DESCRIPTION_MAX_CHARS.to_string())], + ).into_owned(); field.add_child( Text::new( count_text, @@ -1992,7 +2005,7 @@ impl UpdateEnvironmentForm { fn render_repos_field_label(&self, appearance: &Appearance) -> Box { let theme = appearance.theme(); Text::new( - "Repo(s)", + crate::menu_label("settings.update_environment_form.repos_label", "Repo(s)"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2022,7 +2035,7 @@ impl UpdateEnvironmentForm { .with_child( Container::new( Text::new( - "Loading...", + crate::menu_label("settings.update_environment_form.loading", "Loading..."), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2111,7 +2124,7 @@ impl UpdateEnvironmentForm { ) .with_child( Text::new( - "Auth with GitHub", + crate::menu_label("settings.update_environment_form.auth_with_github", "Auth with GitHub"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2159,7 +2172,7 @@ impl UpdateEnvironmentForm { .github_dropdown_state .load_error_message .clone() - .unwrap_or_else(|| "Failed to load GitHub repositories".to_string()); + .unwrap_or_else(|| { crate::menu_label("settings.update_environment_form.load_repositories_failed", "Failed to load GitHub repositories").to_string() }); let mut field = Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) @@ -2225,7 +2238,7 @@ impl UpdateEnvironmentForm { ) .with_child( Text::new( - "Retry", + crate::menu_label("common.retry", "Retry"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2468,7 +2481,7 @@ impl UpdateEnvironmentForm { fn render_repo_helper_text_row(&self, appearance: &Appearance) -> Box { let theme = appearance.theme(); let helper = Text::new( - "Type owner/repo and press Enter to add, or select from dropdown.", + crate::menu_label("settings.update_environment_form.repos_helper", "Type owner/repo and press Enter to add, or select from dropdown."), appearance.ui_font_family(), appearance.ui_font_size() * 0.85, ) @@ -2494,7 +2507,7 @@ impl UpdateEnvironmentForm { // Plain text part text_row.add_child( Text::new( - "Missing a repo?", + crate::menu_label("settings.update_environment_form.missing_repo", "Missing a repo?"), appearance.ui_font_family(), appearance.ui_font_size() * 0.85, ) @@ -2512,7 +2525,7 @@ impl UpdateEnvironmentForm { theme.accent() }; Text::new( - "Configure access on GitHub", + crate::menu_label("settings.update_environment_form.configure_access", "Configure access on GitHub"), appearance.ui_font_family(), appearance.ui_font_size() * 0.85, ) @@ -2681,7 +2694,7 @@ impl UpdateEnvironmentForm { content.add_child( Container::new( Text::new( - "No repositories found", + crate::menu_label("settings.update_environment_form.no_repositories", "No repositories found"), appearance.ui_font_family(), appearance.ui_font_size(), ) @@ -2966,7 +2979,7 @@ impl UpdateEnvironmentForm { let ui_builder = appearance.ui_builder().clone(); move || { ui_builder - .tool_tip(format!("Open image at {docker_hub_url}")) + .tool_tip(crate::menu_label("settings.update_environment_form.open_image_at", "Open image at {url}").replace("{url}", &docker_hub_url)) .build() .finish() } @@ -3090,12 +3103,12 @@ impl UpdateEnvironmentForm { let is_disabled = !self.can_suggest_image_for_current_repos(); let button_text = if is_loading { - "Generating…" + crate::menu_label("settings.update_environment_form.generating", "Generating…") } else { - "Suggest image" + crate::menu_label("settings.update_environment_form.suggest_image", "Suggest image") }; - let tooltip_text = "Warp will suggest a Docker image based on your selected repositories."; + let tooltip_text = crate::menu_label("settings.update_environment_form.suggest_image_tooltip", "Warp will suggest a Docker image based on your selected repositories."); let button = Hoverable::new( self.suggest_image_button_mouse_state.clone(), @@ -3214,7 +3227,7 @@ impl UpdateEnvironmentForm { let auth_url_with_next = self.auth_url_with_next(auth_url); let action = UpdateEnvironmentFormAction::OpenUrl(auth_url_with_next); let button = WarningBoxButtonConfig::new( - "Authenticate", + crate::menu_label("settings.update_environment_form.authenticate", "Authenticate"), self.suggest_image_auth_button_mouse_state.clone(), move |ctx| { ctx.dispatch_typed_action(action.clone()); @@ -3222,7 +3235,7 @@ impl UpdateEnvironmentForm { ); Some(render_warning_box( WarningBoxConfig::new( - "You need to grant access to your GitHub repos to suggest a Docker image", + crate::menu_label("settings.update_environment_form.suggest_image_auth_required", "You need to grant access to your GitHub repos to suggest a Docker image"), ) .with_width(self.field_max_width) .with_button(button), @@ -3248,7 +3261,7 @@ impl UpdateEnvironmentForm { ) -> Box { let action = UpdateEnvironmentFormAction::LaunchAgentForSelectedRepos; let button = WarningBoxButtonConfig::new( - "Launch agent", + crate::menu_label("settings.update_environment_form.launch_agent", "Launch agent"), self.suggest_image_launch_agent_button_mouse_state.clone(), move |ctx| { ctx.dispatch_typed_action(action.clone()); @@ -3257,7 +3270,7 @@ impl UpdateEnvironmentForm { render_warning_box( WarningBoxConfig::new( - "We couldn't find a good match. We recommend using a custom Docker image for these repos.", + crate::menu_label("settings.update_environment_form.suggest_image_no_match", "We couldn't find a good match. We recommend using a custom Docker image for these repos."), ) .with_description(reason) .with_icon(Icon::AlertTriangle) @@ -3522,7 +3535,7 @@ impl View for UpdateEnvironmentForm { // Form fields page.add_child(Self::render_form_field( - "Name", + crate::menu_label("settings.update_environment_form.name_label", "Name"), true, None, &self.name_editor, diff --git a/app/src/tab.rs b/app/src/tab.rs index 1a62be67cd..415f08cb98 100644 --- a/app/src/tab.rs +++ b/app/src/tab.rs @@ -57,7 +57,10 @@ use crate::BlocklistAIHistoryModel; pub const TAB_BAR_BORDER_HEIGHT: f32 = 1.0; pub(crate) const TAB_INDICATOR_HEIGHT: f32 = 14.0; -/// Label for the tab right-click menu's "Move to group" submenu parent. +/// Stable identifier for the tab right-click menu's "Move to group" submenu +/// parent. Used as a save-position anchor in `workspace/view.rs` +/// (NOT as a user-facing label — display text comes from +/// `crate::menu_label("workspace.tab_grouping.move_to_group", ...)`). pub const MOVE_TO_GROUP_LABEL: &str = "Move to group"; /// Decides which tab-group context-menu entries apply to a tab, based on its @@ -638,13 +641,22 @@ impl TabData { ); } if show_move_to_group { - menu_items.push(MenuItemFields::new_submenu(MOVE_TO_GROUP_LABEL).into_item()); + menu_items.push( + MenuItemFields::new_submenu(crate::menu_label( + "workspace.tab_grouping.move_to_group", + "Move to group", + )) + .into_item(), + ); } if show_remove_from_group { menu_items.push( - MenuItemFields::new("Remove from group") - .with_on_select_action(WorkspaceAction::RemoveTabFromGroup(index)) - .into_item(), + MenuItemFields::new(crate::menu_label( + "workspace.tab_grouping.remove_from_group", + "Remove from group", + )) + .with_on_select_action(WorkspaceAction::RemoveTabFromGroup(index)) + .into_item(), ); } menu_items diff --git a/app/src/terminal/input/slash_commands/mod.rs b/app/src/terminal/input/slash_commands/mod.rs index d28a2ba326..298cd1b233 100644 --- a/app/src/terminal/input/slash_commands/mod.rs +++ b/app/src/terminal/input/slash_commands/mod.rs @@ -1400,7 +1400,6 @@ pub(crate) fn slash_command_is_submitted_as_prompt(command: &StaticCommand) -> b || command.name == commands::PLAN.name || command.name == commands::ORCHESTRATE.name } - /// Returns true when the conversation with `conversation_id` is associated with an Oz /// `AmbientAgentTask`. Callers deciding between `/fork` and `/continue-locally` should also /// check the same `CLOUD_AGENT` context that gates `/continue-locally`. diff --git a/app/src/terminal/share_block_modal.rs b/app/src/terminal/share_block_modal.rs index b93bebcd54..eee5109350 100644 --- a/app/src/terminal/share_block_modal.rs +++ b/app/src/terminal/share_block_modal.rs @@ -66,8 +66,6 @@ const INNER_MARGIN: f32 = 20.; const MODAL_WIDTH: f32 = 862.; const BLOCK_TITLE_INPUT_WIDTH: f32 = 800.; -const BLOCK_TITLE_PLACEHOLDER: &str = "Title (optional)"; - // TODO(vorporeal): This is 12 in the specs, but I think our 14pt font is a bit // taller than 14pt? const VERTICAL_SEPARATOR_HEIGHT: f32 = 32.; @@ -77,15 +75,6 @@ const NEW_BUTTON_VERTICAL_PADDING: f32 = 10.; const NEW_BUTTON_HORIZONTAL_PADDING: f32 = 10.; const NEW_COPY_BUTTON_WIDTH: f32 = 80.; -const COMMAND_AND_OUTPUT_OPTION: (&str, DisplaySetting) = - ("Command and Output", DisplaySetting::CommandAndOutput); -const COMMAND_OPTION: (&str, DisplaySetting) = ("Command", DisplaySetting::Command); -const OUTPUT_OPTION: (&str, DisplaySetting) = ("Output", DisplaySetting::Output); - -/// This default title is helpful for screen readers. -const DEFAULT_EMBED_TITLE: &str = "embedded warp block"; -const BLOCK_CREATION_FAILED_MESSAGE: &str = "Something went wrong. Please try again."; - #[derive(PartialEq)] enum ShareRequestState { None, @@ -161,7 +150,7 @@ pub fn init(app: &mut AppContext) { FixedBinding::custom( CustomAction::Copy, ShareBlockModalAction::CopyLink, - "Copy", + crate::menu_label("terminal.share_block.keybinding_copy", "Copy"), id!(ShareBlockModal::ui_name()), ), FixedBinding::new( @@ -196,7 +185,10 @@ impl ShareBlockModal { }, ctx, ); - editor.set_placeholder_text(BLOCK_TITLE_PLACEHOLDER, ctx); + editor.set_placeholder_text( + crate::menu_label("terminal.share_block.title_placeholder", "Title (optional)"), + ctx, + ); editor }); ctx.subscribe_to_view(&block_title_editor, move |me, _, event, ctx| { @@ -220,9 +212,22 @@ impl ShareBlockModal { ..Default::default() }; - let embed_display_options = [COMMAND_AND_OUTPUT_OPTION, COMMAND_OPTION, OUTPUT_OPTION] - .map(|(name, display_setting)| (name.to_string(), display_setting)) - .to_vec(); + let embed_display_options = [ + ( + crate::menu_label("terminal.share_block.command_and_output", "Command and Output") + .to_string(), + DisplaySetting::CommandAndOutput, + ), + ( + crate::menu_label("terminal.share_block.command", "Command").to_string(), + DisplaySetting::Command, + ), + ( + crate::menu_label("terminal.share_block.output", "Output").to_string(), + DisplaySetting::Output, + ), + ] + .to_vec(); let ligature_handle = LigatureSettings::handle(ctx); ctx.subscribe_to_model(&ligature_handle, |_, _, _, ctx| ctx.notify()); @@ -383,7 +388,11 @@ impl ShareBlockModal { fn display_failure_toast(&mut self, ctx: &mut ViewContext) { ctx.emit(ShareBlockModalEvent::ShowToast { - message: BLOCK_CREATION_FAILED_MESSAGE.to_string(), + message: crate::menu_label( + "terminal.share_block.creation_failed_message", + "Something went wrong. Please try again.", + ) + .to_string(), flavor: ToastFlavor::Error, }); } @@ -500,7 +509,8 @@ impl ShareBlockModal { ); ctx.clipboard().write(ClipboardContent::plain_text(link)); ctx.emit(ShareBlockModalEvent::ShowToast { - message: "Link copied.".to_string(), + message: crate::menu_label("terminal.share_block.link_copied", "Link copied.") + .to_string(), flavor: ToastFlavor::Default, }); } @@ -523,7 +533,11 @@ impl ShareBlockModal { let width = ServerBlock::embed_pixel_width(block); let mut title = self.block_title_editor.as_ref(app).buffer_text(app); if title.is_empty() { - title = DEFAULT_EMBED_TITLE.to_string(); + title = crate::menu_label( + "terminal.share_block.embed_title_default", + "embedded warp block", + ) + .to_string(); } let embed_link = escape_html_attribute(&embed_link); let title = escape_html_attribute(&title); @@ -546,7 +560,11 @@ impl ShareBlockModal { ctx.clipboard() .write(ClipboardContent::plain_text(embed_snippet)); ctx.emit(ShareBlockModalEvent::ShowToast { - message: "Embed code copied.".to_string(), + message: crate::menu_label( + "terminal.share_block.embed_code_copied", + "Embed code copied.", + ) + .to_string(), flavor: ToastFlavor::Success, }); } @@ -626,7 +644,7 @@ impl ShareBlockModal { fn render_create_block_buttons_row(&self, appearance: &Appearance) -> Box { let create_link_button = self.render_create_block_button( appearance, - "Create link", + crate::menu_label("terminal.share_block.create_link", "Create link"), Icon::Link, ButtonVariant::Accent, self.mouse_state_handles @@ -636,7 +654,7 @@ impl ShareBlockModal { ); let get_embed_button = self.render_create_block_button( appearance, - "Get embed", + crate::menu_label("terminal.share_block.get_embed", "Get embed"), Icon::Code1, ButtonVariant::Basic, self.mouse_state_handles @@ -663,7 +681,8 @@ impl ShareBlockModal { TextAndIconAlignment::TextFirst, if let ShareRequestState::Pending(pending_share_type) = self.request_state { if pending_share_type == share_type { - "Creating block...".to_string() + crate::menu_label("terminal.share_block.creating_block", "Creating block...") + .to_string() } else { text_label.to_string() } @@ -736,9 +755,13 @@ impl ShareBlockModal { .finish(); col.add_child(link_button_row); } else { - let embed_snippet = self - .generate_embed_snippet(app) - .unwrap_or("Error generating embed snippet".to_string()); + let embed_snippet = self.generate_embed_snippet(app).unwrap_or( + crate::menu_label( + "terminal.share_block.error_generating_embed", + "Error generating embed snippet", + ) + .to_string(), + ); col.add_child(self.render_embed_label(appearance, embed_snippet)); col.add_child( Align::new( @@ -764,7 +787,13 @@ impl ShareBlockModal { .manage_permalinks_mouse_state .clone(), ) - .with_centered_text_label("Manage shared blocks".to_string()) + .with_centered_text_label( + crate::menu_label( + "terminal.share_block.manage_shared_blocks", + "Manage shared blocks", + ) + .to_string(), + ) .with_style( self.button_style_overrides(appearance) .set_font_size(12.) @@ -796,7 +825,7 @@ impl ShareBlockModal { ) -> Box { let text_and_icon = TextAndIcon::new( TextAndIconAlignment::TextFirst, - "Copy".to_string(), + crate::menu_label("terminal.share_block.copy", "Copy").to_string(), Icon::Copy.to_warpui_icon(appearance.theme().active_ui_text_color()), MainAxisSize::Max, MainAxisAlignment::Center, @@ -870,7 +899,7 @@ impl ShareBlockModal { if link_generated { self.block_title_editor.as_ref(app).buffer_text(app) } else { - "Share block".to_string() + crate::menu_label("terminal.share_block.share_block", "Share block").to_string() }, appearance.ui_font_family(), 24., @@ -958,7 +987,7 @@ impl ShareBlockModal { .finish(); let show_prompt_description = appearance .ui_builder() - .span("Show prompt".to_string()) + .span(crate::menu_label("terminal.share_block.show_prompt", "Show prompt").to_string()) .build() .with_margin_left(2.) .finish(); @@ -1059,7 +1088,13 @@ impl ShareBlockModal { let redact_secrets_description = appearance .ui_builder() - .span("Redact secrets (API keys, passwords, IP addresses, PII etc.)".to_string()) + .span( + crate::menu_label( + "terminal.share_block.redact_secrets", + "Redact secrets (API keys, passwords, IP addresses, PII etc.)", + ) + .to_string(), + ) .build() .with_margin_left(4.) .finish(); diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 4d95363881..94bcf348d6 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -254,7 +254,7 @@ use crate::ai::blocklist::{ BlocklistAIActionModel, BlocklistAIContextEvent, BlocklistAIContextModel, BlocklistAIController, BlocklistAIControllerEvent, BlocklistAIHistoryEvent, BlocklistAIHistoryModel, BlocklistAIInputEvent, BlocklistAIInputModel, ClientIdentifiers, - ConversationSelection, ConversationStatusUpdate, InputConfig, InputType, +ConversationSelection, ConversationStatusUpdate, InputConfig, InputType, InputTypeAutoDetectionSource, LegacyPassiveSuggestionsEvent, LegacyPassiveSuggestionsModel, MaaPassiveSuggestionsEvent, MaaPassiveSuggestionsModel, PassiveSuggestionsModels, PendingAttachment, PendingQueryState, QueuedQuery, QueuedQueryId, QueuedQueryModel, @@ -3959,12 +3959,13 @@ impl TerminalView { let osc52_clipboard_blocked_banner = ctx.add_typed_action_view(|_| { Banner::::new_with_buttons( - BannerTextContent::plain_text( + BannerTextContent::plain_text(crate::menu_label( + "terminal.osc52.banner_text", "A terminal program tried to access your clipboard. This is disabled by default for security reasons.", - ), + )), vec![ BannerTextButton::new( - "Allow".to_string(), + crate::menu_label("terminal.osc52.allow_button", "Allow").to_string(), Rc::new(|event_ctx, _ctx, _position| { event_ctx.dispatch_typed_action(BannerAction::::Action( TerminalAction::Osc52AllowBlockedClipboardOperation, @@ -3972,7 +3973,8 @@ impl TerminalView { }), ), BannerTextButton::new( - "Don't show again".to_string(), + crate::menu_label("terminal.osc52.dont_show_again", "Don't show again") + .to_string(), Rc::new(|event_ctx, _ctx, _position| { event_ctx.dispatch_typed_action( BannerAction::::Dismiss(DismissalType::Permanent), @@ -13473,7 +13475,6 @@ impl TerminalView { ctx.background_executor() .spawn(async move { session_clone2.load_all_builtins().await }) .detach(); - // If we were waiting for a successful warpification, it's come. Stop the timeout. self.warpify_state.abort_ssh_warpify_timeout(); @@ -15850,6 +15851,24 @@ impl TerminalView { self.enter_agent_view_after_pending_commands = false; } + #[cfg(not(target_family = "wasm"))] + pub(super) fn on_pty_spawn_failed( + &mut self, + pty_spawn_error: anyhow::Error, + ctx: &mut ViewContext, + ) { + self.pty_spawn_failed = true; + // Emit before the banner so the terminal driver can cancel its + // bootstrap wait immediately, without waiting for the 60 s timeout. + let reason = format!("{pty_spawn_error:#}"); + ctx.emit(Event::PtySpawnFailed { reason }); + self.insert_shell_process_terminated_banner( + shell_terminated_banner::TerminationType::PtySpawnFailure { pty_spawn_error }, + ctx, + ); + ctx.notify(); + } + /// Start a timer so that we can detect when a session does not bootstrap in a timely manner fn start_bootstrap_timer(&self, duration: Duration, ctx: &mut ViewContext) { let _ = ctx.spawn( @@ -21957,16 +21976,23 @@ impl TerminalView { return; } let text = match blocked_type { - Osc52ClipboardBlockedType::Write => { - "A terminal program tried to write to your clipboard. This is disabled by default for security reasons, to protect against malicious software." - } - Osc52ClipboardBlockedType::Read => { - "A terminal program tried to read your clipboard. This is disabled by default for security reasons, to protect against malicious software." - } + Osc52ClipboardBlockedType::Write => crate::menu_label( + "terminal.osc52.write_blocked_text", + "A terminal program tried to write to your clipboard. This is disabled by default for security reasons, to protect against malicious software.", + ), + Osc52ClipboardBlockedType::Read => crate::menu_label( + "terminal.osc52.read_blocked_text", + "A terminal program tried to read your clipboard. This is disabled by default for security reasons, to protect against malicious software.", + ), }; let button_label = match blocked_type { - Osc52ClipboardBlockedType::Write => "Allow clipboard writes", - Osc52ClipboardBlockedType::Read => "Allow clipboard reads and writes", + Osc52ClipboardBlockedType::Write => { + crate::menu_label("terminal.osc52.allow_writes", "Allow clipboard writes") + } + Osc52ClipboardBlockedType::Read => crate::menu_label( + "terminal.osc52.allow_reads_and_writes", + "Allow clipboard reads and writes", + ), }; self.osc52_clipboard_blocked_banner .update(ctx, |banner, ctx| { diff --git a/app/src/workspace/mod.rs b/app/src/workspace/mod.rs index a6d2f3ce9c..d610c39f67 100644 --- a/app/src/workspace/mod.rs +++ b/app/src/workspace/mod.rs @@ -111,7 +111,7 @@ pub fn init(app: &mut AppContext) { lsp::init(app); app.register_fixed_bindings([FixedBinding::empty( - "Dump debug info", + crate::menu_label("workspace.debug.dump_debug_info", "Dump debug info"), WorkspaceAction::DumpDebugInfo, id!("Workspace"), )]); @@ -139,25 +139,25 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:log_review_comment_send_status_for_active_tab", - "[Debug] Log review comment send status for active tab", + crate::menu_label("workspace.debug.log_review_comment_send_status", "[Debug] Log review comment send status for active tab"), WorkspaceAction::LogReviewCommentSendStatusForActiveTab, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:panic", - "Trigger a panic (for testing sentry-rust)", + crate::menu_label("workspace.debug.trigger_panic", "Trigger a panic (for testing sentry-rust)"), WorkspaceAction::Panic, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:open_view_tree_debug_view", - "Open view tree debugger", + crate::menu_label("workspace.debug.open_view_tree_debugger", "Open view tree debugger"), WorkspaceAction::OpenViewTreeDebugWindow, ) .with_context_predicate(id!("Workspace")), ]); app.register_fixed_bindings([FixedBinding::empty( - "[Debug] View first-time user experience", + crate::menu_label("workspace.debug.view_first_time_user_experience", "[Debug] View first-time user experience"), WorkspaceAction::AddGetStartedTab, id!("Workspace"), )]); @@ -167,55 +167,55 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:open_build_plan_migration_modal", - "[Debug] Open Build Plan Migration Modal", + crate::menu_label("workspace.debug.open_build_plan_migration_modal", "[Debug] Open Build Plan Migration Modal"), WorkspaceAction::OpenBuildPlanMigrationModal, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:reset_build_plan_migration_modal_state", - "[Debug] Reset Build Plan Migration Modal State", + crate::menu_label("workspace.debug.reset_build_plan_migration_modal_state", "[Debug] Reset Build Plan Migration Modal State"), WorkspaceAction::ResetBuildPlanMigrationModalState, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:debug_reset_aws_bedrock_login_banner_dismissed", - "[Debug] Un-dismiss AWS login banner", + crate::menu_label("workspace.debug.undismiss_aws_login_banner", "[Debug] Un-dismiss AWS login banner"), WorkspaceAction::DebugResetAwsBedrockLoginBannerDismissed, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:open_oz_launch_modal", - "[Debug] Open Oz Launch Modal", + crate::menu_label("workspace.debug.open_oz_launch_modal", "[Debug] Open Oz Launch Modal"), WorkspaceAction::OpenOzLaunchModal, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:reset_oz_launch_modal_state", - "[Debug] Reset Oz Launch Modal State", + crate::menu_label("workspace.debug.reset_oz_launch_modal_state", "[Debug] Reset Oz Launch Modal State"), WorkspaceAction::ResetOzLaunchModalState, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:open_openwarp_launch_modal", - "[Debug] Open OpenWarp Launch Modal", + crate::menu_label("workspace.debug.open_openwarp_launch_modal", "[Debug] Open OpenWarp Launch Modal"), WorkspaceAction::OpenOpenWarpLaunchModal, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:reset_openwarp_launch_modal_state", - "[Debug] Reset OpenWarp Launch Modal State", + crate::menu_label("workspace.debug.reset_openwarp_launch_modal_state", "[Debug] Reset OpenWarp Launch Modal State"), WorkspaceAction::ResetOpenWarpLaunchModalState, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:open_orchestration_launch_modal", - "[Debug] Open Orchestration Launch Modal", + crate::menu_label("workspace.debug.open_orchestration_launch_modal", "[Debug] Open Orchestration Launch Modal"), WorkspaceAction::OpenOrchestrationLaunchModal, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:reset_orchestration_launch_modal_state", - "[Debug] Reset Orchestration Launch Modal State", + crate::menu_label("workspace.debug.reset_orchestration_launch_modal_state", "[Debug] Reset Orchestration Launch Modal State"), WorkspaceAction::ResetOrchestrationLaunchModalState, ) .with_context_predicate(id!("Workspace")), @@ -251,25 +251,25 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:install_opencode_warp_plugin", - "[Debug] Install OpenCode Warp plugin", + crate::menu_label("workspace.debug.install_opencode_warp_plugin", "[Debug] Install OpenCode Warp plugin"), WorkspaceAction::InstallOpenCodeWarpPlugin, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:use_local_opencode_warp_plugin", - "[Debug] Use local OpenCode Warp plugin (testing only)", + crate::menu_label("workspace.debug.use_local_opencode_warp_plugin", "[Debug] Use local OpenCode Warp plugin (testing only)"), WorkspaceAction::UseLocalOpenCodeWarpPlugin, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:open_session_config_modal", - "[Debug] Open Session Config Modal", + crate::menu_label("workspace.debug.open_session_config_modal", "[Debug] Open Session Config Modal"), WorkspaceAction::ShowSessionConfigModal, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_hoa_onboarding_flow", - "[Debug] Start HOA Onboarding Flow", + crate::menu_label("workspace.debug.start_hoa_onboarding_flow", "[Debug] Start HOA Onboarding Flow"), WorkspaceAction::ShowHoaOnboardingFlow, ) .with_context_predicate(id!("Workspace")), @@ -280,7 +280,7 @@ pub fn init(app: &mut AppContext) { #[cfg(target_os = "macos")] app.register_editable_bindings([EditableBinding::new( "workspace:sample_process", - "Sample Process", + crate::menu_label("workspace.binding.sample_process", "Sample Process"), WorkspaceAction::SampleProcess, ) .with_context_predicate(id!("Workspace"))]); @@ -289,7 +289,7 @@ pub fn init(app: &mut AppContext) { { app.register_editable_bindings([EditableBinding::new( "workspace:dump_heap_profile", - "Dump heap profile (can only be done once)", + crate::menu_label("workspace.binding.dump_heap_profile", "Dump heap profile (can only be done once)"), WorkspaceAction::DumpHeapProfile, ) .with_context_predicate(id!("Workspace"))]); @@ -299,26 +299,26 @@ pub fn init(app: &mut AppContext) { FixedBinding::custom( CustomAction::CycleNextSession, WorkspaceAction::CycleNextSession, - "Switch to next tab", + crate::menu_label("workspace.binding.switch_to_next_tab", "Switch to next tab"), id!("Workspace") & id!("Workspace_MultipleTabs"), ), FixedBinding::custom( CustomAction::CyclePrevSession, WorkspaceAction::CyclePrevSession, - "Switch to previous tab", + crate::menu_label("workspace.binding.switch_to_previous_tab", "Switch to previous tab"), id!("Workspace") & id!("Workspace_MultipleTabs"), ), FixedBinding::custom( CustomAction::AddWindow, WorkspaceAction::AddWindow, - "Create New Window", + crate::menu_label("workspace.binding.create_new_window", "Create New Window"), id!("Workspace"), ) .with_enabled(|| ContextFlag::CreateNewSession.is_enabled()), FixedBinding::custom( CustomAction::NewFile, WorkspaceAction::NewCodeFile, - "New File", + crate::menu_label("workspace.binding.new_file", "New File"), id!("Workspace") & !id!("Workspace_ViewOnlySharedSession"), ), ]); @@ -328,21 +328,21 @@ pub fn init(app: &mut AppContext) { FixedBinding::custom( CustomAction::IncreaseZoom, WorkspaceAction::IncreaseZoom, - "Zoom In", + crate::menu_label("workspace.binding.zoom_in", "Zoom In"), id!("Workspace"), ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::custom( CustomAction::DecreaseZoom, WorkspaceAction::DecreaseZoom, - "Zoom Out", + crate::menu_label("workspace.binding.zoom_out", "Zoom Out"), id!("Workspace"), ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::custom( CustomAction::ResetZoom, WorkspaceAction::ResetZoom, - "Reset Zoom", + crate::menu_label("workspace.binding.reset_zoom", "Reset Zoom"), id!("Workspace"), ) .with_group(bindings::BindingGroup::Settings.as_str()), @@ -352,14 +352,14 @@ pub fn init(app: &mut AppContext) { FixedBinding::custom( CustomAction::IncreaseFontSize, WorkspaceAction::IncreaseFontSize, - "Increase font size", + crate::menu_label("workspace.binding.increase_font_size", "Increase font size"), id!("Workspace"), ) .with_group(bindings::BindingGroup::Settings.as_str()), FixedBinding::custom( CustomAction::DecreaseFontSize, WorkspaceAction::DecreaseFontSize, - "Decrease font size", + crate::menu_label("workspace.binding.decrease_font_size", "Decrease font size"), id!("Workspace"), ) .with_group(bindings::BindingGroup::Settings.as_str()), @@ -370,7 +370,7 @@ pub fn init(app: &mut AppContext) { app.register_fixed_bindings([FixedBinding::custom( CustomAction::SaveCurrentConfig, WorkspaceAction::OpenLaunchConfigSaveModal, - "Save new launch configuration", + crate::menu_label("workspace.binding.save_new_launch_configuration", "Save new launch configuration"), id!("Workspace"), )]); } @@ -405,7 +405,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:increase_zoom", - "Increase zoom level", + crate::menu_label("workspace.binding.increase_zoom_level", "Increase zoom level"), WorkspaceAction::IncreaseZoom, ) .with_context_predicate(id!("Workspace")) @@ -413,7 +413,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-="), EditableBinding::new( "workspace:decrease_zoom", - "Decrease zoom level", + crate::menu_label("workspace.binding.decrease_zoom_level", "Decrease zoom level"), WorkspaceAction::DecreaseZoom, ) .with_context_predicate(id!("Workspace")) @@ -421,14 +421,14 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl--"), EditableBinding::new( "workspace:reset_zoom", - "Reset zoom level to default", + crate::menu_label("workspace.binding.reset_zoom_level", "Reset zoom level to default"), WorkspaceAction::ResetZoom, ) .with_group(bindings::BindingGroup::Settings.as_str()) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:increase_font_size", - "Increase font size", + crate::menu_label("workspace.binding.increase_font_size", "Increase font size"), WorkspaceAction::IncreaseFontSize, ) .with_context_predicate(id!("Workspace")) @@ -436,7 +436,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("alt-shift->"), EditableBinding::new( "workspace:decrease_font_size", - "Decrease font size", + crate::menu_label("workspace.binding.decrease_font_size", "Decrease font size"), WorkspaceAction::DecreaseFontSize, ) .with_context_predicate(id!("Workspace")) @@ -444,7 +444,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("alt-shift-<"), EditableBinding::new( "workspace:reset_font_size", - "Reset font size to default", + crate::menu_label("workspace.binding.reset_font_size", "Reset font size to default"), WorkspaceAction::ResetFontSize, ) .with_group(bindings::BindingGroup::Settings.as_str()) @@ -454,7 +454,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:increase_font_size", - "Increase font size", + crate::menu_label("workspace.binding.increase_font_size", "Increase font size"), WorkspaceAction::IncreaseFontSize, ) .with_context_predicate(id!("Workspace")) @@ -462,7 +462,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-="), EditableBinding::new( "workspace:decrease_font_size", - "Decrease font size", + crate::menu_label("workspace.binding.decrease_font_size", "Decrease font size"), WorkspaceAction::DecreaseFontSize, ) .with_context_predicate(id!("Workspace")) @@ -470,7 +470,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl--"), EditableBinding::new( "workspace:reset_font_size", - "Reset font size to default", + crate::menu_label("workspace.binding.reset_font_size", "Reset font size to default"), WorkspaceAction::ResetFontSize, ) .with_group(bindings::BindingGroup::Settings.as_str()) @@ -485,7 +485,7 @@ pub fn init(app: &mut AppContext) { FixedBinding::custom( CustomAction::ToggleProjectExplorer, WorkspaceAction::ToggleProjectExplorer, - BindingDescription::new("Toggle project explorer") + BindingDescription::new(crate::menu_label("workspace.binding.toggle_project_explorer", "Toggle project explorer")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Project Explorer"), id!("Workspace") & id!(flags::SHOW_PROJECT_EXPLORER), ), @@ -494,14 +494,14 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:show_theme_chooser", - "Open theme picker", + crate::menu_label("workspace.binding.open_theme_picker", "Open theme picker"), WorkspaceAction::ShowThemeChooserForActiveTheme, ) .with_context_predicate(id!("Workspace")) .with_group(bindings::BindingGroup::Settings.as_str()), EditableBinding::new( TOGGLE_TAB_CONFIGS_MENU_BINDING_NAME, - "Open tab configs menu", + crate::menu_label("workspace.binding.open_tab_configs_menu", "Open tab configs menu"), WorkspaceAction::ToggleTabConfigsMenu, ) .with_context_predicate(id!("Workspace")) @@ -509,7 +509,7 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-alt-shift-T"), EditableBinding::new( "workspace:activate_first_tab", - "Switch to 1st tab", + crate::menu_label("workspace.binding.switch_to_first_tab", "Switch to 1st tab"), WorkspaceAction::ActivateTabByNumber(1), ) .with_context_predicate(id!("Workspace")) @@ -517,7 +517,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-1"), EditableBinding::new( "workspace:activate_second_tab", - "Switch to 2nd tab", + crate::menu_label("workspace.binding.switch_to_second_tab", "Switch to 2nd tab"), WorkspaceAction::ActivateTabByNumber(2), ) .with_context_predicate(id!("Workspace")) @@ -525,7 +525,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-2"), EditableBinding::new( "workspace:activate_third_tab", - "Switch to 3rd tab", + crate::menu_label("workspace.binding.switch_to_third_tab", "Switch to 3rd tab"), WorkspaceAction::ActivateTabByNumber(3), ) .with_context_predicate(id!("Workspace")) @@ -533,7 +533,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-3"), EditableBinding::new( "workspace:activate_fourth_tab", - "Switch to 4th tab", + crate::menu_label("workspace.binding.switch_to_fourth_tab", "Switch to 4th tab"), WorkspaceAction::ActivateTabByNumber(4), ) .with_context_predicate(id!("Workspace")) @@ -541,7 +541,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-4"), EditableBinding::new( "workspace:activate_fifth_tab", - "Switch to 5th tab", + crate::menu_label("workspace.binding.switch_to_fifth_tab", "Switch to 5th tab"), WorkspaceAction::ActivateTabByNumber(5), ) .with_context_predicate(id!("Workspace")) @@ -549,7 +549,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-5"), EditableBinding::new( "workspace:activate_sixth_tab", - "Switch to 6th tab", + crate::menu_label("workspace.binding.switch_to_sixth_tab", "Switch to 6th tab"), WorkspaceAction::ActivateTabByNumber(6), ) .with_context_predicate(id!("Workspace")) @@ -557,7 +557,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-6"), EditableBinding::new( "workspace:activate_seventh_tab", - "Switch to 7th tab", + crate::menu_label("workspace.binding.switch_to_seventh_tab", "Switch to 7th tab"), WorkspaceAction::ActivateTabByNumber(7), ) .with_context_predicate(id!("Workspace")) @@ -565,7 +565,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-7"), EditableBinding::new( "workspace:activate_eighth_tab", - "Switch to 8th tab", + crate::menu_label("workspace.binding.switch_to_eighth_tab", "Switch to 8th tab"), WorkspaceAction::ActivateTabByNumber(8), ) .with_context_predicate(id!("Workspace")) @@ -573,7 +573,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-8"), EditableBinding::new( "workspace:activate_last_tab", - "Switch to last tab", + crate::menu_label("workspace.binding.switch_to_last_tab", "Switch to last tab"), WorkspaceAction::ActivateLastTab, ) .with_context_predicate(id!("Workspace")) @@ -581,7 +581,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding("cmdorctrl-9"), EditableBinding::new( "workspace:activate_prev_tab", - "Activate previous tab", + crate::menu_label("workspace.binding.activate_previous_tab", "Activate previous tab"), WorkspaceAction::ActivatePrevTab, ) .with_context_predicate( @@ -591,7 +591,7 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-pageup"), EditableBinding::new( "workspace:activate_next_tab", - "Activate next tab", + crate::menu_label("workspace.binding.activate_next_tab", "Activate next tab"), WorkspaceAction::ActivateNextTab, ) .with_context_predicate( @@ -602,7 +602,7 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-pagedown"), EditableBinding::new( "pane_group:navigate_prev", - "Activate previous pane", + crate::menu_label("workspace.binding.activate_previous_pane", "Activate previous pane"), WorkspaceAction::NavigatePrevPaneOrPanel, ) .with_context_predicate(id!("Workspace")) @@ -610,15 +610,22 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::ActivatePreviousPane), EditableBinding::new( "pane_group:navigate_next", - "Activate next pane", + crate::menu_label("workspace.binding.activate_next_pane", "Activate next pane"), WorkspaceAction::NavigateNextPaneOrPanel, ) .with_context_predicate(id!("Workspace")) .with_group(bindings::BindingGroup::Navigation.as_str()) .with_custom_action(CustomAction::ActivateNextPane), + EditableBinding::new( + "workspace:toggle_mouse_reporting", + crate::menu_label("workspace.binding.toggle_mouse_reporting", "Toggle Mouse Reporting"), + WorkspaceAction::ToggleMouseReporting, + ) + .with_group(bindings::BindingGroup::Settings.as_str()) + .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:create_team_notebook", - BindingDescription::new("Create a new team notebook") + BindingDescription::new(crate::menu_label("workspace.binding.create_team_notebook", "Create a new team notebook")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "New Team Notebook"), WorkspaceAction::CreateTeamNotebook, ) @@ -632,7 +639,7 @@ pub fn init(app: &mut AppContext) { .with_group(bindings::BindingGroup::Notebooks.as_str()), EditableBinding::new( "workspace:create_personal_notebook", - BindingDescription::new("Create a new personal notebook") + BindingDescription::new(crate::menu_label("workspace.binding.create_personal_notebook", "Create a new personal notebook")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "New Personal Notebook"), WorkspaceAction::CreatePersonalNotebook, ) @@ -641,7 +648,7 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Workspace") & id!(flags::ENABLE_WARP_DRIVE)), EditableBinding::new( "workspace:create_team_workflow", - BindingDescription::new("Create a new team workflow") + BindingDescription::new(crate::menu_label("workspace.binding.create_team_workflow", "Create a new team workflow")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "New Team Workflow"), WorkspaceAction::CreateTeamWorkflow, ) @@ -655,7 +662,7 @@ pub fn init(app: &mut AppContext) { .with_group(bindings::BindingGroup::Workflow.as_str()), EditableBinding::new( "workspace:create_personal_workflow", - BindingDescription::new("Create a new personal workflow") + BindingDescription::new(crate::menu_label("workspace.binding.create_personal_workflow", "Create a new personal workflow")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "New Personal Workflow"), WorkspaceAction::CreatePersonalWorkflow, ) @@ -664,7 +671,7 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Workspace") & id!(flags::ENABLE_WARP_DRIVE)), EditableBinding::new( "workspace:create_team_folder", - BindingDescription::new("Create a new team folder") + BindingDescription::new(crate::menu_label("workspace.binding.create_team_folder", "Create a new team folder")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "New Team Folder"), WorkspaceAction::CreateTeamFolder, ) @@ -677,7 +684,7 @@ pub fn init(app: &mut AppContext) { .with_group(bindings::BindingGroup::Folders.as_str()), EditableBinding::new( "workspace:create_personal_folder", - BindingDescription::new("Create a new personal folder") + BindingDescription::new(crate::menu_label("workspace.binding.create_personal_folder", "Create a new personal folder")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "New Personal Folder"), WorkspaceAction::CreatePersonalFolder, ) @@ -685,7 +692,7 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Workspace") & id!(flags::ENABLE_WARP_DRIVE) & id!("IsOnline")), EditableBinding::new( NEW_TAB_BINDING_NAME, - BindingDescription::new("Create new tab"), + BindingDescription::new(crate::menu_label("workspace.binding.create_new_tab", "Create new tab")), WorkspaceAction::AddDefaultTab, ) .with_context_predicate(id!("Workspace") & !id!("Workspace_PaneDragging")) @@ -693,7 +700,7 @@ pub fn init(app: &mut AppContext) { .with_enabled(|| ContextFlag::CreateNewSession.is_enabled()), EditableBinding::new( NEW_TERMINAL_TAB_BINDING_NAME, - BindingDescription::new("New Terminal Tab"), + BindingDescription::new(crate::menu_label("workspace.binding.new_terminal_tab", "New Terminal Tab")), WorkspaceAction::AddTerminalTab { hide_homepage: false, }, @@ -703,7 +710,7 @@ pub fn init(app: &mut AppContext) { .with_enabled(|| ContextFlag::CreateNewSession.is_enabled()), EditableBinding::new( NEW_AGENT_TAB_BINDING_NAME, - BindingDescription::new("New Agent Tab"), + BindingDescription::new(crate::menu_label("workspace.binding.new_agent_tab", "New Agent Tab")), WorkspaceAction::AddAgentTab, ) .with_group(bindings::BindingGroup::WarpAi.as_str()) @@ -713,7 +720,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( NEW_AMBIENT_AGENT_TAB_BINDING_NAME, - BindingDescription::new("New Cloud Agent Tab"), + BindingDescription::new(crate::menu_label("workspace.binding.new_cloud_agent_tab", "New Cloud Agent Tab")), WorkspaceAction::AddAmbientAgentTab, ) .with_group(bindings::BindingGroup::WarpAi.as_str()) @@ -725,14 +732,14 @@ pub fn init(app: &mut AppContext) { }), EditableBinding::new( "workspace:toggle_left_panel", - BindingDescription::new("Open Left Panel"), + BindingDescription::new(crate::menu_label("workspace.binding.open_left_panel", "Open Left Panel")), WorkspaceAction::ToggleLeftPanel, ) .with_context_predicate(id!("Workspace")) .with_custom_action(CustomAction::ToggleWarpDrive), EditableBinding::new( TOGGLE_RIGHT_PANEL_BINDING_NAME, - BindingDescription::new("Toggle code review") + BindingDescription::new(crate::menu_label("workspace.binding.toggle_code_review", "Toggle code review")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Toggle Code Review"), WorkspaceAction::ToggleRightPanel, ) @@ -742,7 +749,7 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-shift-+"), EditableBinding::new( TOGGLE_VERTICAL_TABS_PANEL_BINDING_NAME, - BindingDescription::new("Toggle vertical tabs panel") + BindingDescription::new(crate::menu_label("workspace.binding.toggle_vertical_tabs_panel", "Toggle vertical tabs panel")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Toggle Vertical Tabs Panel"), WorkspaceAction::ToggleVerticalTabsPanel, ) @@ -752,7 +759,7 @@ pub fn init(app: &mut AppContext) { .with_key_binding(cmd_or_ctrl_shift("b")), EditableBinding::new( LEFT_PANEL_PROJECT_EXPLORER_BINDING_NAME, - BindingDescription::new("Left Panel: Project explorer"), + BindingDescription::new(crate::menu_label("workspace.binding.left_panel_project_explorer", "Left Panel: Project explorer")), WorkspaceAction::ToggleProjectExplorer, ) .with_group(bindings::BindingGroup::Navigation.as_str()) @@ -760,7 +767,7 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::ToggleProjectExplorer), EditableBinding::new( LEFT_PANEL_AGENT_CONVERSATIONS_BINDING_NAME, - BindingDescription::new("Left Panel: Agent conversations"), + BindingDescription::new(crate::menu_label("workspace.binding.left_panel_agent_conversations", "Left Panel: Agent conversations")), WorkspaceAction::ToggleConversationListView, ) .with_group(bindings::BindingGroup::Navigation.as_str()) @@ -769,7 +776,7 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::ToggleConversationListView), EditableBinding::new( LEFT_PANEL_GLOBAL_SEARCH_BINDING_NAME, - BindingDescription::new("Left Panel: Global search"), + BindingDescription::new(crate::menu_label("workspace.binding.left_panel_global_search", "Left Panel: Global search")), WorkspaceAction::ToggleGlobalSearch, ) .with_group(bindings::BindingGroup::Navigation.as_str()) @@ -778,7 +785,10 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::ToggleGlobalSearch), EditableBinding::new( "file_tree:toggle_hidden_files", - BindingDescription::new("Toggle hidden files in Project Explorer"), + BindingDescription::new(crate::menu_label( + "workspace.binding.toggle_hidden_files_in_project_explorer", + "Toggle hidden files in Project Explorer", + )), WorkspaceAction::ToggleHiddenFiles, ) .with_group(bindings::BindingGroup::Navigation.as_str()) @@ -787,7 +797,7 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("ctrl-shift->"), EditableBinding::new( LEFT_PANEL_WARP_DRIVE_BINDING_NAME, - BindingDescription::new("Left Panel: Warp Drive"), + BindingDescription::new(crate::menu_label("workspace.binding.left_panel_warp_drive", "Left Panel: Warp Drive")), WorkspaceAction::ToggleWarpDrive, ) .with_group(bindings::BindingGroup::Navigation.as_str()) @@ -796,14 +806,14 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("alt-4"), EditableBinding::new( TOGGLE_PROJECT_EXPLORER_BINDING_NAME, - BindingDescription::new("Toggle project explorer") + BindingDescription::new(crate::menu_label("workspace.binding.toggle_project_explorer", "Toggle project explorer")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Project Explorer"), WorkspaceAction::ToggleProjectExplorer, ) .with_context_predicate(id!("Workspace") & id!(flags::SHOW_PROJECT_EXPLORER)), EditableBinding::new( OPEN_GLOBAL_SEARCH_BINDING_NAME, - BindingDescription::new("Open global search") + BindingDescription::new(crate::menu_label("workspace.binding.open_global_search", "Open global search")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Global Search"), WorkspaceAction::OpenGlobalSearch, ) @@ -813,14 +823,14 @@ pub fn init(app: &mut AppContext) { .with_linux_or_windows_key_binding("alt-shift-F"), EditableBinding::new( TOGGLE_WARP_DRIVE_BINDING_NAME, - BindingDescription::new("Toggle Warp Drive") + BindingDescription::new(crate::menu_label("workspace.binding.toggle_warp_drive", "Toggle Warp Drive")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Warp Drive"), WorkspaceAction::ToggleWarpDrive, ) .with_context_predicate(id!("Workspace") & id!(flags::ENABLE_WARP_DRIVE)), EditableBinding::new( TOGGLE_CONVERSATION_LIST_VIEW_BINDING_NAME, - BindingDescription::new("Toggle Agent conversation list view").with_custom_description( + BindingDescription::new(crate::menu_label("workspace.binding.toggle_agent_conversation_list_view", "Toggle Agent conversation list view")).with_custom_description( bindings::MAC_MENUS_CONTEXT, "Agent conversation list view", ), @@ -833,15 +843,15 @@ pub fn init(app: &mut AppContext) { .with_group(bindings::BindingGroup::WarpAi.as_str()), EditableBinding::new( "workspace:close_panel", - BindingDescription::new("Close focused panel") - .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Close focused panel"), + BindingDescription::new(crate::menu_label("workspace.binding.close_focused_panel", "Close focused panel")) + .with_custom_description(bindings::MAC_MENUS_CONTEXT, crate::menu_label("workspace.binding.close_focused_panel", "Close focused panel")), WorkspaceAction::ClosePanel, ) .with_context_predicate(id!("Workspace")) .with_custom_action(CustomAction::CloseCurrentSession), EditableBinding::new( "workspace:toggle_command_palette", - BindingDescription::new("Toggle command palette") + BindingDescription::new(crate::menu_label("workspace.binding.toggle_command_palette", "Toggle command palette")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Command Palette"), WorkspaceAction::TogglePalette { mode: PaletteMode::Command, @@ -853,8 +863,8 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::CommandPalette), EditableBinding::new( "workspace:move_tab_left", - BindingDescription::new("Move tab left") - .with_dynamic_override(|ctx| uses_vertical_tabs(ctx).then(|| "move tab up".into())), + BindingDescription::new(crate::menu_label("workspace.binding.move_tab_left", "Move tab left")) + .with_dynamic_override(|ctx| uses_vertical_tabs(ctx).then(|| crate::menu_label("workspace.binding.move_tab_up", "move tab up").to_string())), WorkspaceAction::MoveActiveTabLeft, ) .with_group(bindings::BindingGroup::Navigation.as_str()) @@ -867,8 +877,8 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::MoveTabLeft), EditableBinding::new( "workspace:move_tab_right", - BindingDescription::new("Move tab right").with_dynamic_override(|ctx| { - uses_vertical_tabs(ctx).then(|| "move tab down".into()) + BindingDescription::new(crate::menu_label("workspace.binding.move_tab_right", "Move tab right")).with_dynamic_override(|ctx| { + uses_vertical_tabs(ctx).then(|| crate::menu_label("workspace.binding.move_tab_down", "move tab down").to_string()) }), WorkspaceAction::MoveActiveTabRight, ) @@ -882,7 +892,7 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::MoveTabRight), EditableBinding::new( "workspace:toggle_keybindings_page", - "Toggle keyboard shortcuts", + crate::menu_label("workspace.binding.toggle_keyboard_shortcuts", "Toggle keyboard shortcuts"), WorkspaceAction::ToggleKeybindingsPage, ) .with_group(bindings::BindingGroup::KeyboardShortcuts.as_str()) @@ -890,7 +900,7 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::ToggleKeybindingsPage), EditableBinding::new( "workspace:show_keybinding_settings", - "Open keybindings editor", + crate::menu_label("workspace.binding.open_keybindings_editor", "Open keybindings editor"), WorkspaceAction::ConfigureKeybindingSettings { keybinding_name: None, }, @@ -900,7 +910,7 @@ pub fn init(app: &mut AppContext) { .with_mac_key_binding("cmd-ctrl-k"), EditableBinding::new( "workspace:toggle_block_snackbar", - "Toggle sticky command header", + crate::menu_label("workspace.binding.toggle_sticky_command_header", "Toggle sticky command header"), WorkspaceAction::ToggleBlockSnackbar, ) .with_group(bindings::BindingGroup::Settings.as_str()) @@ -912,14 +922,14 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:set_a11y_concise_verbosity_level", - "[a11y] Set concise accessibility announcements", + crate::menu_label("workspace.debug.set_a11y_concise", "[a11y] Set concise accessibility announcements"), WorkspaceAction::SetA11yVerbosityLevel(AccessibilityVerbosity::Concise), ) .with_context_predicate(id!("Workspace")) .with_key_binding("cmdorctrl-alt-c"), EditableBinding::new( "workspace:set_a11y_verbose_verbosity_level", - "[a11y] Set verbose accessibility announcements", + crate::menu_label("workspace.debug.set_a11y_verbose", "[a11y] Set verbose accessibility announcements"), WorkspaceAction::SetA11yVerbosityLevel(AccessibilityVerbosity::Verbose), ) .with_context_predicate(id!("Workspace")) @@ -929,7 +939,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "workspace:rename_active_tab", - "Rename the current tab", + crate::menu_label("workspace.binding.rename_current_tab", "Rename the current tab"), WorkspaceAction::RenameActiveTab, ) .with_group(bindings::BindingGroup::Settings.as_str()) @@ -943,7 +953,7 @@ pub fn init(app: &mut AppContext) { // reachable via the binding registry. app.register_editable_bindings([EditableBinding::new( "workspace:rename_active_pane", - "Rename the current pane", + crate::menu_label("workspace.binding.rename_current_pane", "Rename the current pane"), WorkspaceAction::RenameActivePane, ) .with_group(bindings::BindingGroup::Settings.as_str()) @@ -953,7 +963,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:new_tab_group", - "Create new tab group", + crate::menu_label("workspace.binding.create_new_tab_group", "Create new tab group"), // Reuse the new-session dropdown's action, not a dedicated variant. WorkspaceAction::SelectNewSessionMenuItem(NewSessionMenuItem::CreateNewTabGroup), ) @@ -962,7 +972,10 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Workspace") & !id!("Workspace_PaneDragging")), EditableBinding::new( "workspace:new_tab_group_from_active_or_selected_tabs", - "Create tab group from active or selected tab(s)", + crate::menu_label( + "workspace.binding.create_tab_group_from_active", + "Create tab group from active or selected tab(s)", + ), WorkspaceAction::NewTabGroupFromActiveOrSelectedTabs, ) .with_enabled(|| FeatureFlag::GroupedTabs.is_enabled()) @@ -974,7 +987,10 @@ pub fn init(app: &mut AppContext) { // offered, matching the multi-tab right-click menu. EditableBinding::new( "workspace:remove_active_or_selected_tabs_from_group", - "Remove active or selected tab(s) from group", + crate::menu_label( + "workspace.binding.remove_active_from_group", + "Remove active or selected tab(s) from group", + ), WorkspaceAction::RemoveActiveOrSelectedTabsFromGroup, ) .with_enabled(|| FeatureFlag::GroupedTabs.is_enabled()) @@ -992,7 +1008,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:pin_active_tab", - "Pin current tab", + crate::menu_label("workspace.binding.pin_current_tab", "Pin current tab"), WorkspaceAction::PinActiveTab, ) .with_enabled(|| FeatureFlag::PinnedTabs.is_enabled()) @@ -1002,7 +1018,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "workspace:unpin_active_tab", - "Unpin current tab", + crate::menu_label("workspace.binding.unpin_current_tab", "Unpin current tab"), WorkspaceAction::UnpinActiveTab, ) .with_enabled(|| FeatureFlag::PinnedTabs.is_enabled()) @@ -1012,7 +1028,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "workspace:pin_active_tab_group", - "Pin current tab group", + crate::menu_label("workspace.binding.pin_current_tab_group", "Pin current tab group"), WorkspaceAction::PinActiveTabGroup, ) .with_enabled(|| { @@ -1027,7 +1043,10 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "workspace:unpin_active_tab_group", - "Unpin current tab group", + crate::menu_label( + "workspace.binding.unpin_current_tab_group", + "Unpin current tab group", + ), WorkspaceAction::UnpinActiveTabGroup, ) .with_enabled(|| { @@ -1045,7 +1064,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:terminate_app", - "Quit Warp", + crate::menu_label("workspace.binding.quit_warp", "Quit Warp"), WorkspaceAction::TerminateApp, ) .with_context_predicate(id!("Workspace")) @@ -1053,8 +1072,8 @@ pub fn init(app: &mut AppContext) { .with_enabled(|| ContextFlag::CloseWindow.is_enabled()), EditableBinding::new( "workspace:close_window", - BindingDescription::new("Close Window") - .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Close Window"), + BindingDescription::new(crate::menu_label("workspace.binding.close_window", "Close Window")) + .with_custom_description(bindings::MAC_MENUS_CONTEXT, crate::menu_label("workspace.binding.close_window", "Close Window")), WorkspaceAction::CloseWindow, ) .with_mac_key_binding("cmd-shift-W") @@ -1064,7 +1083,7 @@ pub fn init(app: &mut AppContext) { .with_enabled(|| ContextFlag::CloseWindow.is_enabled()), EditableBinding::new( "workspace:close_active_tab", - "Close the current tab", + crate::menu_label("workspace.binding.close_current_tab", "Close the current tab"), WorkspaceAction::CloseActiveTab, ) .with_custom_action(CustomAction::CloseTab) @@ -1074,7 +1093,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "workspace:close_other_tabs", - "Close other tabs", + crate::menu_label("workspace.binding.close_other_tabs", "Close other tabs"), WorkspaceAction::CloseNonActiveTabs, ) .with_custom_action(CustomAction::CloseOtherTabs) @@ -1082,8 +1101,8 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:close_tabs_right_active_tab", - BindingDescription::new("Close tabs to the right").with_dynamic_override(|ctx| { - uses_vertical_tabs(ctx).then(|| "close tabs below".into()) + BindingDescription::new(crate::menu_label("workspace.binding.close_tabs_to_the_right", "Close tabs to the right")).with_dynamic_override(|ctx| { + uses_vertical_tabs(ctx).then(|| crate::menu_label("workspace.binding.close_tabs_below", "close tabs below").to_string()) }), WorkspaceAction::CloseTabsRightActiveTab, ) @@ -1094,21 +1113,21 @@ pub fn init(app: &mut AppContext) { // (i.e. whether notifications are already on or off). EditableBinding::new( "workspace:toggle_notifications_on", - "Turn notifications on", + crate::menu_label("workspace.binding.turn_notifications_on", "Turn notifications on"), WorkspaceAction::ToggleNotifications, ) .with_group(bindings::BindingGroup::Notifications.as_str()) .with_context_predicate(id!("Workspace") & !id!("Notifications_Enabled")), EditableBinding::new( "workspace:toggle_notifications_off", - "Turn notifications off", + crate::menu_label("workspace.binding.turn_notifications_off", "Turn notifications off"), WorkspaceAction::ToggleNotifications, ) .with_group(bindings::BindingGroup::Notifications.as_str()) .with_context_predicate(id!("Workspace") & id!("Notifications_Enabled")), EditableBinding::new( "workspace:toggle_navigation_palette", - BindingDescription::new("Toggle navigation palette") + BindingDescription::new(crate::menu_label("workspace.binding.toggle_navigation_palette", "Toggle navigation palette")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Navigation Palette"), WorkspaceAction::TogglePalette { mode: PaletteMode::Navigation, @@ -1120,7 +1139,7 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::NavigationPalette), EditableBinding::new( "workspace:toggle_launch_config_palette", - "Launch configuration palette", + crate::menu_label("workspace.binding.launch_configuration_palette", "Launch configuration palette"), WorkspaceAction::TogglePalette { mode: PaletteMode::LaunchConfig, source: PaletteSource::Keybinding, @@ -1131,7 +1150,7 @@ pub fn init(app: &mut AppContext) { .with_enabled(|| ContextFlag::LaunchConfigurations.is_enabled()), EditableBinding::new( "workspace:toggle_files_palette", - "Toggle Files Palette", + crate::menu_label("workspace.binding.toggle_files_palette", "Toggle Files Palette"), WorkspaceAction::TogglePalette { mode: PaletteMode::Files, source: PaletteSource::Keybinding, @@ -1141,7 +1160,7 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::FilesPalette), EditableBinding::new( "workspace:open_launch_config_save_modal", - "Save new launch configuration", + crate::menu_label("workspace.binding.save_new_launch_configuration", "Save new launch configuration"), WorkspaceAction::OpenLaunchConfigSaveModal, ) .with_context_predicate(id!("Workspace")) @@ -1150,7 +1169,7 @@ pub fn init(app: &mut AppContext) { EditableBinding::new( // If you rename this name, please update the name in command_palette/action/data_source.rs "workspace:search_drive", - "Search Warp Drive", + crate::menu_label("workspace.binding.search_warp_drive", "Search Warp Drive"), WorkspaceAction::OpenPalette { mode: PaletteMode::WarpDrive, source: PaletteSource::Keybinding, @@ -1165,7 +1184,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:update_and_relaunch", - "Install update and relaunch", + crate::menu_label("workspace.binding.install_update_and_relaunch", "Install update and relaunch"), // TODO(vorporeal): I wonder if we should change wording here? WorkspaceAction::ApplyUpdate, ) @@ -1174,7 +1193,7 @@ pub fn init(app: &mut AppContext) { .with_enabled(|| ContextFlag::PromptForVersionUpdates.is_enabled()), EditableBinding::new( "workspace:check_for_updates", - "Check for updates", + crate::menu_label("workspace.binding.check_for_updates", "Check for updates"), WorkspaceAction::CheckForUpdate, ) .with_group(bindings::BindingGroup::AutoUpdate.as_str()) @@ -1185,7 +1204,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "workspace:log_out", - "Log out", + crate::menu_label("workspace.binding.log_out", "Log out"), WorkspaceAction::LogOut, ) .with_group(bindings::BindingGroup::Settings.as_str()) @@ -1194,7 +1213,7 @@ pub fn init(app: &mut AppContext) { if !FeatureFlag::AvatarInTabBar.is_enabled() { app.register_editable_bindings([EditableBinding::new( "workspace:toggle_resource_center", - "Toggle resource center", + crate::menu_label("workspace.binding.toggle_resource_center", "Toggle resource center"), WorkspaceAction::ToggleResourceCenter, ) .with_group(bindings::BindingGroup::Navigation.as_str()) @@ -1205,7 +1224,7 @@ pub fn init(app: &mut AppContext) { if cfg!(not(target_family = "wasm")) { app.register_editable_bindings([EditableBinding::new( "workspace:export_all_warp_drive_objects", - "Export all Warp Drive objects", + crate::menu_label("workspace.binding.export_warp_drive_objects", "Export all Warp Drive objects"), WorkspaceAction::ExportAllWarpDriveObjects, ) .with_group(bindings::BindingGroup::Settings.as_str()) @@ -1218,14 +1237,20 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:install_cli", - "Install Oz CLI globally for use outside of Warp", + crate::menu_label( + "workspace.binding.install_oz_cli", + "Install Oz CLI globally for use outside of Warp", + ), WorkspaceAction::InstallOz, ) .with_group(bindings::BindingGroup::Settings.as_str()) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:uninstall_cli", - "Undo global Oz CLI installation (oz will still work within Warp)", + crate::menu_label( + "workspace.binding.uninstall_oz_cli", + "Undo global Oz CLI installation (oz will still work within Warp)", + ), WorkspaceAction::UninstallOz, ) .with_group(bindings::BindingGroup::Settings.as_str()) @@ -1253,11 +1278,11 @@ pub fn init(app: &mut AppContext) { if FeatureFlag::Changelog.is_enabled() { app.register_editable_bindings([ - // Always show the "View latest changelog" action in the command palette, + // Always show the crate::menu_label("workspace.binding.view_latest_changelog", "View latest changelog") action in the command palette, // but without a keybinding when the update toast is not visible. EditableBinding::new( "workspace:view_changelog", - "View latest changelog", + crate::menu_label("workspace.binding.view_latest_changelog", "View latest changelog"), WorkspaceAction::ViewLatestChangelog, ) .with_context_predicate(id!("Workspace") & !id!("UpdateToastVisible")) @@ -1268,7 +1293,7 @@ pub fn init(app: &mut AppContext) { // When the update toast is visible, register the keybinding as well. EditableBinding::new( "workspace:view_changelog", - "View latest changelog", + crate::menu_label("workspace.binding.view_latest_changelog", "View latest changelog"), WorkspaceAction::ViewLatestChangelog, ) .with_context_predicate(id!("Workspace") & id!("UpdateToastVisible")) @@ -1296,7 +1321,7 @@ pub fn init(app: &mut AppContext) { .with_custom_action(CustomAction::NewAgentModePane), EditableBinding::new( "workspace:toggle_ai_assistant", - "Toggle Warp AI", + crate::menu_label("workspace.binding.toggle_warp_ai", "Toggle Warp AI"), WorkspaceAction::ToggleAIAssistant, ) .with_enabled(|| !FeatureFlag::AgentMode.is_enabled()) @@ -1310,7 +1335,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:create_team_env_vars", - BindingDescription::new("Create new team environment variables") + BindingDescription::new(crate::menu_label("workspace.binding.create_team_env_vars", "Create new team environment variables")) .with_custom_description( bindings::MAC_MENUS_CONTEXT, "New Team Environment Variables", @@ -1327,7 +1352,7 @@ pub fn init(app: &mut AppContext) { .with_group(bindings::BindingGroup::EnvVarCollection.as_str()), EditableBinding::new( "workspace:create_personal_env_vars", - BindingDescription::new("Create new personal environment variables") + BindingDescription::new(crate::menu_label("workspace.binding.create_personal_env_vars", "Create new personal environment variables")) .with_custom_description( bindings::MAC_MENUS_CONTEXT, "New Personal Environment Variables", @@ -1339,7 +1364,7 @@ pub fn init(app: &mut AppContext) { .with_context_predicate(id!("Workspace") & id!(flags::ENABLE_WARP_DRIVE)), EditableBinding::new( "workspace:create_personal_ai_prompt", - BindingDescription::new("Create a new personal prompt") + BindingDescription::new(crate::menu_label("workspace.binding.create_personal_prompt", "Create a new personal prompt")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "New Personal Prompt"), WorkspaceAction::CreatePersonalAIPrompt, ) @@ -1350,7 +1375,7 @@ pub fn init(app: &mut AppContext) { ), EditableBinding::new( "workspace:create_team_ai_prompt", - BindingDescription::new("Create a new team prompt") + BindingDescription::new(crate::menu_label("workspace.binding.create_team_prompt", "Create a new team prompt")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "New Team Prompt"), WorkspaceAction::CreateTeamAIPrompt, ) @@ -1368,14 +1393,14 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:shift_focus_left", - "Switch Focus to Left Panel", + crate::menu_label("workspace.binding.switch_focus_left", "Switch Focus to Left Panel"), WorkspaceAction::FocusLeftPanel, ) .with_context_predicate(id!("Workspace")) .with_key_binding("cmdorctrl-shift-("), EditableBinding::new( "workspace:shift_focus_right", - "Switch Focus to Right Panel", + crate::menu_label("workspace.binding.switch_focus_right", "Switch Focus to Right Panel"), WorkspaceAction::FocusRightPanel, ) .with_context_predicate(id!("Workspace")) @@ -1385,13 +1410,13 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:import_to_personal_drive", - "Import To Personal Drive", + crate::menu_label("workspace.binding.import_to_personal_drive", "Import To Personal Drive"), WorkspaceAction::ImportToPersonalDrive, ) .with_context_predicate(id!("Workspace") & id!(flags::ENABLE_WARP_DRIVE)), EditableBinding::new( "workspace:import_to_team_drive", - "Import To Team Drive", + crate::menu_label("workspace.binding.import_to_team_drive", "Import To Team Drive"), WorkspaceAction::ImportToTeamDrive, ) .with_context_predicate( @@ -1405,7 +1430,7 @@ pub fn init(app: &mut AppContext) { if ChannelState::enable_debug_features() { app.register_editable_bindings([EditableBinding::new( "workspace:copy_access_token_to_clipboard", - "Copy access token to clipboard", + crate::menu_label("workspace.binding.copy_access_token", "Copy access token to clipboard"), WorkspaceAction::CopyAccessTokenToClipboard, ) .with_context_predicate(id!("Workspace"))]); @@ -1414,7 +1439,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:open_repository", - BindingDescription::new("Open repository") + BindingDescription::new(crate::menu_label("workspace.binding.open_repository", "Open repository")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Open Repository"), WorkspaceAction::OpenRepository { path: None }, ) @@ -1423,8 +1448,8 @@ pub fn init(app: &mut AppContext) { .with_group(bindings::BindingGroup::Folders.as_str()), EditableBinding::new( "workspace:open_ai_fact_collection", - BindingDescription::new("Open AI Rules") - .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Open AI Rules"), + BindingDescription::new(crate::menu_label("workspace.binding.open_ai_rules", "Open AI Rules")) + .with_custom_description(bindings::MAC_MENUS_CONTEXT, crate::menu_label("workspace.binding.open_ai_rules", "Open AI Rules")), WorkspaceAction::OpenAIFactCollection, ) .with_enabled(|| FeatureFlag::AIRules.is_enabled()) @@ -1435,8 +1460,8 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "workspace:open_mcp_servers", - BindingDescription::new("Open MCP Servers") - .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Open MCP Servers"), + BindingDescription::new(crate::menu_label("workspace.binding.open_mcp_servers", "Open MCP Servers")) + .with_custom_description(bindings::MAC_MENUS_CONTEXT, crate::menu_label("workspace.binding.open_mcp_servers", "Open MCP Servers")), WorkspaceAction::OpenMCPServerCollection, ) .with_enabled(|| { @@ -1448,7 +1473,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "workspace:jump_to_latest_toast", - "Jump to latest agent task", + crate::menu_label("workspace.binding.jump_to_latest_agent_task", "Jump to latest agent task"), WorkspaceAction::JumpToLatestToast, ) .with_enabled(|| FeatureFlag::AgentMode.is_enabled()) @@ -1459,7 +1484,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( TOGGLE_NOTIFICATION_MAILBOX_BINDING_NAME, - "Toggle notification mailbox", + crate::menu_label("workspace.binding.toggle_notification_mailbox", "Toggle notification mailbox"), WorkspaceAction::ToggleNotificationMailbox { select_first: true }, ) .with_enabled(|| FeatureFlag::HOANotifications.is_enabled()) @@ -1473,7 +1498,7 @@ pub fn init(app: &mut AppContext) { app.register_editable_bindings([EditableBinding::new( "workspace:toggle_agent_management_view", - "Toggle the agent management view", + crate::menu_label("workspace.binding.toggle_agent_management_view", "Toggle the agent management view"), WorkspaceAction::ToggleAgentManagementView, ) .with_enabled(|| FeatureFlag::AgentManagementView.is_enabled()) @@ -1490,7 +1515,7 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:show_settings", - BindingDescription::new("Open Settings") + BindingDescription::new(crate::menu_label("workspace.binding.open_settings", "Open Settings")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Settings"), WorkspaceAction::ShowSettings, ) @@ -1499,7 +1524,7 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { .with_custom_action(CustomAction::ShowSettings), EditableBinding::new( "workspace:show_settings_account_page", - "Open Settings: Account", + crate::menu_label("workspace.binding.open_settings_account", "Open Settings: Account"), WorkspaceAction::ShowSettingsPage(SettingsSection::Account), ) .with_context_predicate(id!("Workspace")) @@ -1507,7 +1532,7 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { .with_custom_action(CustomAction::ShowAccount), EditableBinding::new( "workspace:show_settings_appearance_page", - BindingDescription::new("Open Settings: Appearance") + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_appearance", "Open Settings: Appearance")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Appearance..."), WorkspaceAction::ShowSettingsPage(SettingsSection::Appearance), ) @@ -1516,14 +1541,14 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { .with_custom_action(CustomAction::ShowAppearance), EditableBinding::new( "workspace:show_settings_features_page", - "Open Settings: Features", + crate::menu_label("workspace.binding.open_settings_features", "Open Settings: Features"), WorkspaceAction::ShowSettingsPage(SettingsSection::Features), ) .with_group(bindings::BindingGroup::Settings.as_str()) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_settings_shared_blocks_page", - BindingDescription::new("Open Settings: Shared Blocks") + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_shared_blocks", "Open Settings: Shared Blocks")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "View Shared Blocks..."), WorkspaceAction::ShowSettingsPage(SettingsSection::SharedBlocks), ) @@ -1532,7 +1557,7 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { .with_custom_action(CustomAction::ViewSharedBlocks), EditableBinding::new( "workspace:show_settings_keyboard_shortcuts_page", - BindingDescription::new("Open Settings: Keyboard Shortcuts").with_custom_description( + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_keyboard_shortcuts", "Open Settings: Keyboard Shortcuts")).with_custom_description( bindings::MAC_MENUS_CONTEXT, "Configure Keyboard Shortcuts...", ), @@ -1543,7 +1568,7 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { .with_custom_action(CustomAction::ConfigureKeybindings), EditableBinding::new( "workspace:show_settings_about_page", - BindingDescription::new("Open Settings: About") + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_about", "Open Settings: About")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "About Warp"), WorkspaceAction::ShowSettingsPage(SettingsSection::About), ) @@ -1552,7 +1577,7 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { .with_custom_action(CustomAction::ShowAboutWarp), EditableBinding::new( "workspace:show_settings_teams_page", - BindingDescription::new("Open Settings: Teams") + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_teams", "Open Settings: Teams")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Open Team Settings"), WorkspaceAction::ShowSettingsPage(SettingsSection::Teams), ) @@ -1561,14 +1586,14 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_settings_privacy_page", - BindingDescription::new("Open Settings: Privacy"), + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_privacy", "Open Settings: Privacy")), WorkspaceAction::ShowSettingsPage(SettingsSection::Privacy), ) .with_group(bindings::BindingGroup::Settings.as_str()) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_settings_warpify_page", - BindingDescription::new("Open Settings: Warpify") + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_warpify", "Open Settings: Warpify")) .with_custom_description(bindings::MAC_MENUS_CONTEXT, "Configure Warpify..."), WorkspaceAction::ShowSettingsPage(SettingsSection::Warpify), ) @@ -1576,7 +1601,7 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_ai_settings_page", - BindingDescription::new("Open Settings: AI"), + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_ai", "Open Settings: AI")), WorkspaceAction::ShowSettingsPage(SettingsSection::WarpAgent), ) .with_enabled(|| FeatureFlag::AgentMode.is_enabled()) @@ -1584,42 +1609,42 @@ fn add_open_setting_pages_as_editable_binding(app: &mut AppContext) { .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_settings_billing_and_usage_page", - BindingDescription::new("Open Settings: Billing and usage"), + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_billing_and_usage", "Open Settings: Billing and usage")), WorkspaceAction::ShowSettingsPage(SettingsSection::BillingAndUsage), ) .with_group(bindings::BindingGroup::Settings.as_str()) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_settings_code_page", - BindingDescription::new("Open Settings: Code"), + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_code", "Open Settings: Code")), WorkspaceAction::ShowSettingsPage(SettingsSection::CodeIndexing), ) .with_group(bindings::BindingGroup::Settings.as_str()) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_settings_referrals_page", - BindingDescription::new("Open Settings: Referrals"), + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_referrals", "Open Settings: Referrals")), WorkspaceAction::ShowSettingsPage(SettingsSection::Referrals), ) .with_group(bindings::BindingGroup::Settings.as_str()) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_settings_environments_page", - BindingDescription::new("Open Settings: Environments"), + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_environments", "Open Settings: Environments")), WorkspaceAction::ShowSettingsPage(SettingsSection::CloudEnvironments), ) .with_group(bindings::BindingGroup::Settings.as_str()) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:show_mcp_servers_settings_page", - BindingDescription::new("Open Settings: MCP Servers"), + BindingDescription::new(crate::menu_label("workspace.binding.open_settings_mcp_servers", "Open Settings: MCP Servers")), WorkspaceAction::ShowSettingsPage(SettingsSection::MCPServers), ) .with_group(bindings::BindingGroup::Settings.as_str()) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:open_settings_file", - "Open settings file", + crate::menu_label("workspace.binding.open_settings_file", "Open settings file"), WorkspaceAction::OpenSettingsFile, ) .with_enabled(|| FeatureFlag::SettingsFile.is_enabled() && cfg!(feature = "local_fs")) @@ -1635,39 +1660,39 @@ fn add_overflow_menu_items_as_editable_binding(app: &mut AppContext) { app.register_editable_bindings([ EditableBinding::new( "workspace:show_invite_modal", - "Invite People...", + crate::menu_label("workspace.binding.invite_people", "Invite People..."), WorkspaceAction::ShowReferralSettingsPage, ) .with_context_predicate(id!("Workspace")) .with_custom_action(CustomAction::ReferAFriend), EditableBinding::new( "workspace:link_to_slack", - "Join our Slack community (opens external link)", + crate::menu_label("workspace.binding.join_slack_community", "Join our Slack community (opens external link)"), WorkspaceAction::JoinSlack, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:link_to_user_docs", - "View user docs (opens external link)", + crate::menu_label("workspace.binding.view_user_docs", "View user docs (opens external link)"), WorkspaceAction::ViewUserDocs, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:send_feedback", - BindingDescription::new("Send feedback (opens external link)"), + BindingDescription::new(crate::menu_label("workspace.binding.send_feedback", "Send feedback (opens external link)")), WorkspaceAction::SendFeedback, ) .with_context_predicate(id!("Workspace")), #[cfg(not(target_family = "wasm"))] EditableBinding::new( "workspace:view_logs", - "View Warp logs", + crate::menu_label("workspace.binding.view_warp_logs", "View Warp logs"), WorkspaceAction::ViewLogs, ) .with_context_predicate(id!("Workspace")), EditableBinding::new( "workspace:link_to_privacy_policy", - "View privacy policy (opens external link)", + crate::menu_label("workspace.binding.view_privacy_policy", "View privacy policy (opens external link)"), WorkspaceAction::ViewPrivacyPolicy, ) .with_context_predicate(id!("Workspace")), diff --git a/app/src/workspace/view.rs b/app/src/workspace/view.rs index 021a5fe84d..43aba56b24 100644 --- a/app/src/workspace/view.rs +++ b/app/src/workspace/view.rs @@ -564,7 +564,9 @@ const TAB_BAR_PILL_WIDTH: f32 = 100.; const PILL_FONT_SIZE: f32 = 12.; // We use the word "Warp" in the Update Ready button to make it obvious that the terminal is Warp. // This can lead to free advertising when users screen-share Warp when an update is available. -const UPDATE_READY_TEXT: &str = "Update Warp"; +fn update_ready_text() -> &'static str { + crate::menu_label("workspace.update_warp", "Update Warp") +} const TAB_BAR_OVERFLOW_MENU_WIDTH: f32 = 300.; @@ -591,9 +593,13 @@ const ELLIPSE_SVG_PATH: &str = "bundled/svg/ellipse.svg"; const AI_ASSISTANT_BUTTON_ID: &str = "workspace_view:ai_assistant_button"; -const VERSION_DEPRECATION_BANNER_TEXT: &str = "Your app is out of date and some features may not work as expected. Please update immediately."; +fn version_deprecation_banner_text() -> &'static str { + crate::menu_label("workspace.version_deprecation", "Your app is out of date and some features may not work as expected. Please update immediately.") +} -const VERSION_DEPRECATION_WITHOUT_PERMISSIONS_BANNER_TEXT: &str = "Some Warp features may not work as expected without updating immediately, but Warp is unable to perform the update."; +fn version_deprecation_without_permissions_banner_text() -> &'static str { + crate::menu_label("workspace.version_deprecation_without_permissions", "Some Warp features may not work as expected without updating immediately, but Warp is unable to perform the update.") +} const ASK_AI_ASSISTANT_KEYBINDING_NAME: &str = "workspace:toggle_ai_assistant"; const TOGGLE_RESOURCE_CENTER_KEYBINDING_NAME: &str = "workspace:toggle_resource_center"; @@ -613,7 +619,9 @@ const NEW_SESSION_SIDECAR_SEARCH_BOX_HORIZONTAL_PADDING: f32 = 12.; const NEW_SESSION_SIDECAR_SEARCH_BOX_VERTICAL_PADDING: f32 = 6.; const NEW_SESSION_SIDECAR_FOOTER_HORIZONTAL_PADDING: f32 = 16.; const NEW_SESSION_SIDECAR_FOOTER_VERTICAL_PADDING: f32 = 8.; -const SESSION_CONFIG_TAB_CONFIG_CHIP_TEXT: &str = "Access your tab configs here."; +fn session_config_tab_config_chip_text() -> &'static str { + crate::menu_label("workspace.tab_configs_chip", "Access your tab configs here.") +} const SESSION_CONFIG_TAB_CONFIG_CHIP_WIDTH: f32 = 206.; const SHOW_SETTINGS_KEYBINDING_NAME: &str = "workspace:show_settings"; pub const TOGGLE_COMMAND_PALETTE_KEYBINDING_NAME: &str = "workspace:toggle_command_palette"; @@ -869,9 +877,9 @@ pub enum BannerSeverity { #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum BannerButtonVariant { /// No fill, no border, just text (and optional icon). Used for the primary - /// action in the Figma design (e.g. "Fix with Oz"). + /// action in the Figma design (e.g. crate::menu_label("workspace.fix_with_oz", "Fix with Oz")). Naked, - /// Border-only, no fill (e.g. "Open file"). + /// Border-only, no fill (e.g. crate::menu_label("workspace.open_file", "Open file")). Outlined, } @@ -881,7 +889,7 @@ struct WorkspaceBannerButtonDetails { variant: BannerButtonVariant, /// Optional leading icon shown before the label. icon: Option, - /// If set, renders an adjacent "More info" pill that dispatches this action. + /// If set, renders an adjacent crate::menu_label("workspace.more_info", "More info") pill that dispatches this action. more_info_button_action: Option, } @@ -1321,7 +1329,7 @@ impl Workspace { }, ctx, ); - editor.set_placeholder_text("Search repos", ctx); + editor.set_placeholder_text(crate::menu_label("workspace.search_repos", "Search repos"), ctx); editor }); ctx.subscribe_to_view(&editor, |me, editor_view, event, ctx| match event { @@ -1357,7 +1365,7 @@ impl Workspace { EditorView::single_line(options, ctx) }); editor.update(ctx, |editor, ctx| { - editor.set_placeholder_text("Search tabs...", ctx); + editor.set_placeholder_text(crate::menu_label("workspace.search_tabs", "Search tabs..."), ctx); }); ctx.subscribe_to_view(&editor, |me, editor_view, event, ctx| match event { EditorEvent::Edited(_) => { @@ -2539,7 +2547,7 @@ impl Workspace { .finish(); let text = Text::new_inline( - SESSION_CONFIG_TAB_CONFIG_CHIP_TEXT.to_string(), + session_config_tab_config_chip_text().to_string(), appearance.ui_font_family(), 12., ) @@ -2597,7 +2605,7 @@ impl Workspace { me.shown_staging_banner_count += 1; me.toast_stack.update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error( - "Staging API call failed. Did your IP address change?".to_string(), + crate::menu_label("workspace.staging_api_call_failed", "Staging API call failed. Did your IP address change?").to_string(), ) .with_object_id("staging_access_blocked_toast".to_string()); toast_stack.add_ephemeral_toast(toast, ctx); @@ -2684,7 +2692,7 @@ impl Workspace { let toast = DismissibleToast::error(message) .with_object_id(object_id.clone()) .with_link( - ToastLink::new("Open file".to_string()).with_onclick_action( + ToastLink::new(crate::menu_label("workspace.open_file", "Open file").to_string()).with_onclick_action( WorkspaceAction::OpenTabConfigErrorFile { path, toast_object_id: object_id, @@ -4378,7 +4386,7 @@ impl Workspace { log::error!("Failed to load conversation from server"); me.toast_stack.update(ctx, |view, ctx| { let new_toast = DismissibleToast::error( - "Failed to load conversation data.".to_string(), + crate::menu_label("workspace.failed_load_conversation_data", "Failed to load conversation data.").to_string(), ); view.add_ephemeral_toast(new_toast, ctx); }); @@ -5704,7 +5712,7 @@ impl Workspace { .unwrap_or_else(|| { let title = configuration.title().trim(); if title.is_empty() { - "Untitled pane".to_string() + crate::menu_label("workspace.untitled_pane", "Untitled pane").to_string() } else { title.to_string() } @@ -6050,7 +6058,7 @@ impl Workspace { if !FeatureFlag::ConfigurableToolbar.is_enabled() { return; } - let items = vec![MenuItemFields::new("Re-arrange toolbar items") + let items = vec![MenuItemFields::new(crate::menu_label("workspace.rearrange_toolbar_items", "Re-arrange toolbar items")) .with_on_select_action(WorkspaceAction::OpenHeaderToolbarEditor) .into_item()]; self.header_toolbar_context_menu @@ -6529,7 +6537,7 @@ impl Workspace { // 1. Agent (if AI enabled) if is_any_ai_enabled { - let mut agent_item = MenuItemFields::new("Agent") + let mut agent_item = MenuItemFields::new(crate::menu_label("workspace.agent", "Agent")) .with_on_select_action(WorkspaceAction::AddAgentTab) .with_icon(icons::Icon::LayoutAlt01); if effective_default == DefaultSessionMode::Agent { @@ -6545,7 +6553,7 @@ impl Workspace { #[cfg(target_os = "windows")] { let is_terminal_default = effective_default == DefaultSessionMode::Terminal; - let mut terminal_item = MenuItemFields::new("Terminal") + let mut terminal_item = MenuItemFields::new(crate::menu_label("workspace.terminal", "Terminal")) .with_on_select_action(WorkspaceAction::AddTerminalTab { hide_homepage: false, }) @@ -6582,7 +6590,7 @@ impl Workspace { // On other platforms, Terminal is a regular item. #[cfg(not(target_os = "windows"))] { - let mut terminal_item = MenuItemFields::new("Terminal") + let mut terminal_item = MenuItemFields::new(crate::menu_label("workspace.terminal", "Terminal")) .with_on_select_action(WorkspaceAction::AddTerminalTab { hide_homepage: false, }) @@ -6599,7 +6607,7 @@ impl Workspace { && FeatureFlag::AgentView.is_enabled() && FeatureFlag::CloudMode.is_enabled() { - let mut cloud_item = MenuItemFields::new("Cloud Agent") + let mut cloud_item = MenuItemFields::new(crate::menu_label("workspace.cloud_agent", "Cloud Agent")) .with_on_select_action(WorkspaceAction::AddAmbientAgentTab) .with_icon(icons::Icon::LayoutAlt01); if effective_default == DefaultSessionMode::CloudAgent { @@ -6610,7 +6618,7 @@ impl Workspace { // 3b. Local Docker Sandbox if FeatureFlag::LocalDockerSandbox.is_enabled() { - let mut docker_item = MenuItemFields::new("Local Docker Sandbox") + let mut docker_item = MenuItemFields::new(crate::menu_label("workspace.local_docker_sandbox", "Local Docker Sandbox")) .with_on_select_action(WorkspaceAction::AddDockerSandboxTab) .with_icon(icons::Icon::Docker); if effective_default == DefaultSessionMode::DockerSandbox { @@ -6670,14 +6678,14 @@ impl Workspace { if FeatureFlag::TabConfigs.is_enabled() { menu_items.push(MenuItem::Separator); menu_items.push( - MenuItemFields::new_submenu("New worktree config") + MenuItemFields::new_submenu(crate::menu_label("workspace.new_worktree_config", "New worktree config")) .with_icon(icons::Icon::Dataflow02) .into_item(), ); // 6. New tab config — V0: opens the TOML template. menu_items.push( - MenuItemFields::new("New tab config") + MenuItemFields::new(crate::menu_label("workspace.new_tab_config", "New tab config")) .with_on_select_action(WorkspaceAction::SelectNewSessionMenuItem( NewSessionMenuItem::CreateNewTabConfig, )) @@ -6702,7 +6710,7 @@ impl Workspace { menu_items.push(MenuItem::Separator); menu_items.push( - MenuItemFields::new("Reopen closed session") + MenuItemFields::new(crate::menu_label("workspace.reopen_closed_session", "Reopen closed session")) .with_on_select_action(WorkspaceAction::ReopenClosedSession) .with_key_shortcut_label(reopen_closed_session_shortcut_label) .with_disabled(UndoCloseStack::handle(ctx).as_ref(ctx).is_empty()) @@ -7692,13 +7700,13 @@ impl Workspace { let pane_name_target = match target { VerticalTabsPaneContextMenuTarget::ClickedPane(locator) => PaneNameMenuTarget { locator, - rename_label: "Rename pane", - reset_label: "Reset pane name", + rename_label: crate::menu_label("workspace.rename_pane", "Rename pane"), + reset_label: crate::menu_label("workspace.reset_pane_name", "Reset pane name"), }, VerticalTabsPaneContextMenuTarget::ActivePane(locator) => PaneNameMenuTarget { locator, - rename_label: "Rename active pane", - reset_label: "Reset active pane name", + rename_label: crate::menu_label("workspace.rename_active_pane", "Rename active pane"), + reset_label: crate::menu_label("workspace.reset_active_pane_name", "Reset active pane name"), }, }; let can_move_left = self.can_move_tab(tab_index, TabMovement::Left); @@ -7753,7 +7761,7 @@ impl Workspace { .into_item(), ), AutoupdateStage::UnableToUpdateToNewVersion { .. } => menu_items.push( - MenuItemFields::new("Update Warp manually") + MenuItemFields::new(crate::menu_label("workspace.update_warp_manually", "Update Warp manually")) .with_on_select_action(WorkspaceAction::DownloadNewVersion) .into_item(), ), @@ -8359,7 +8367,7 @@ impl Workspace { self.add_tab_with_pane_layout( panes_layout, Arc::new(HashMap::new()), - Some("Settings".to_owned()), + Some(crate::menu_label("workspace.settings", "Settings").to_owned()), ctx, ); } @@ -8859,7 +8867,7 @@ impl Workspace { let command_name = ChannelState::channel().cli_command_name(); let message = format!("Installed the Oz CLI globally. You can now run '{command_name}' from any terminal outside of Warp."); let toast = DismissibleToast::success(message).with_link( - ToastLink::new("Learn more".to_string()) + ToastLink::new(crate::menu_label("workspace.learn_more", "Learn more").to_string()) .with_href("https://docs.warp.dev/reference/cli".to_string()), ); view.handle_cli_command_result(result, toast, "Failed to install Oz command", ctx); @@ -9504,7 +9512,7 @@ impl Workspace { ) => { items.push( - MenuItemFields::new("Update and relaunch Warp") + MenuItemFields::new(crate::menu_label("workspace.update_and_relaunch_warp", "Update and relaunch Warp")) .with_on_select_action(WorkspaceAction::ApplyUpdate) .with_override_text_color(appearance.theme().ansi_fg_red()) .into_item(), @@ -9527,7 +9535,7 @@ impl Workspace { ) => { items.push( - MenuItemFields::new("Update Warp manually") + MenuItemFields::new(crate::menu_label("workspace.update_warp_manually", "Update Warp manually")) .with_on_select_action(WorkspaceAction::DownloadNewVersion) .with_override_text_color(appearance.theme().ansi_fg_red()) .into_item(), @@ -9538,33 +9546,33 @@ impl Workspace { } items.extend([ - MenuItemFields::new("What's new") + MenuItemFields::new(crate::menu_label("workspace.whats_new", "What's new")) .with_on_select_action(WorkspaceAction::ViewLatestChangelog) .into_item(), - MenuItemFields::new("Settings") + MenuItemFields::new(crate::menu_label("workspace.settings", "Settings")) .with_on_select_action(WorkspaceAction::ShowSettings) .into_item(), - MenuItemFields::new("Keyboard shortcuts") + MenuItemFields::new(crate::menu_label("workspace.keyboard_shortcuts", "Keyboard shortcuts")) .with_on_select_action(WorkspaceAction::ToggleKeybindingsPage) .into_item(), MenuItem::Separator, - MenuItemFields::new("Documentation") + MenuItemFields::new(crate::menu_label("workspace.documentation", "Documentation")) .with_on_select_action(WorkspaceAction::ViewUserDocs) .into_item(), - MenuItemFields::new("Feedback") + MenuItemFields::new(crate::menu_label("workspace.feedback", "Feedback")) .with_on_select_action(WorkspaceAction::SendFeedback) .into_item(), ]); #[cfg(not(target_family = "wasm"))] items.push( - MenuItemFields::new("View Warp logs") + MenuItemFields::new(crate::menu_label("workspace.view_warp_logs", "View Warp logs")) .with_on_select_action(WorkspaceAction::ViewLogs) .into_item(), ); items.extend([ - MenuItemFields::new("Slack") + MenuItemFields::new(crate::menu_label("workspace.slack", "Slack")) .with_on_select_action(WorkspaceAction::JoinSlack) .into_item(), MenuItem::Separator, @@ -9572,13 +9580,13 @@ impl Workspace { if self.auth_state.is_anonymous_or_logged_out() { items.push( - MenuItemFields::new("Sign up") + MenuItemFields::new(crate::menu_label("workspace.sign_up", "Sign up")) .with_on_select_action(WorkspaceAction::SignupAnonymousUser) .into_item(), ); } - // Check if the user is on any paid plan to determine whether to show "Billing and Usage" or "Upgrade" + // Check if the user is on any paid plan to determine whether to show "Billing and Usage" or crate::menu_label("workspace.upgrade", "Upgrade") let is_on_paid_plan = UserWorkspaces::as_ref(app) .current_workspace() .map(|workspace| workspace.billing_metadata.is_user_on_paid_plan()) @@ -9586,7 +9594,7 @@ impl Workspace { if is_on_paid_plan { items.push( - MenuItemFields::new("Billing and usage") + MenuItemFields::new(crate::menu_label("workspace.billing_and_usage", "Billing and usage")) .with_on_select_action(WorkspaceAction::ShowSettingsPage( SettingsSection::BillingAndUsage, )) @@ -9594,21 +9602,21 @@ impl Workspace { ); } else { items.push( - MenuItemFields::new("Upgrade") + MenuItemFields::new(crate::menu_label("workspace.upgrade", "Upgrade")) .with_on_select_action(WorkspaceAction::ShowUpgrade) .into_item(), ); } items.push( - MenuItemFields::new("Invite a friend") + MenuItemFields::new(crate::menu_label("workspace.invite_a_friend", "Invite a friend")) .with_on_select_action(WorkspaceAction::ShowReferralSettingsPage) .into_item(), ); if !self.auth_state.is_anonymous_or_logged_out() { items.push( - MenuItemFields::new("Log out") + MenuItemFields::new(crate::menu_label("workspace.log_out", "Log out")) .with_on_select_action(WorkspaceAction::LogOut) .into_item(), ); @@ -9742,9 +9750,9 @@ impl Workspace { let mut items = vec![]; if can_move_up { let label = if is_vertical { - "Move group up" + crate::menu_label("workspace.tab_group.move_up", "Move group up") } else { - "Move group left" + crate::menu_label("workspace.tab_group.move_left", "Move group left") }; items.push( MenuItemFields::new(label) @@ -9754,9 +9762,9 @@ impl Workspace { } if can_move_down { let label = if is_vertical { - "Move group down" + crate::menu_label("workspace.tab_group.move_down", "Move group down") } else { - "Move group right" + crate::menu_label("workspace.tab_group.move_right", "Move group right") }; items.push( MenuItemFields::new(label) @@ -9768,21 +9776,27 @@ impl Workspace { }; let close_section = { - let mut items = vec![MenuItemFields::new("Close all tabs in group") - .with_on_select_action(WorkspaceAction::CloseTabGroup(group_id)) - .into_item()]; + let mut items = vec![MenuItemFields::new(crate::menu_label( + "workspace.tab_group.close_all", + "Close all tabs in group", + )) + .with_on_select_action(WorkspaceAction::CloseTabGroup(group_id)) + .into_item()]; if has_tabs_outside { items.push( - MenuItemFields::new("Close other tabs") - .with_on_select_action(WorkspaceAction::CloseTabsOutsideGroup(group_id)) - .into_item(), + MenuItemFields::new(crate::menu_label( + "workspace.tab_group.close_others", + "Close other tabs", + )) + .with_on_select_action(WorkspaceAction::CloseTabsOutsideGroup(group_id)) + .into_item(), ); } if has_tabs_above { let label = if is_vertical { - "Close tabs above" + crate::menu_label("workspace.tab_group.close_above", "Close tabs above") } else { - "Close tabs to the left" + crate::menu_label("workspace.tab_group.close_left", "Close tabs to the left") }; items.push( MenuItemFields::new(label) @@ -9792,9 +9806,9 @@ impl Workspace { } if has_tabs_below { let label = if is_vertical { - "Close tabs below" + crate::menu_label("workspace.tab_group.close_below", "Close tabs below") } else { - "Close tabs to the right" + crate::menu_label("workspace.tab_group.close_right", "Close tabs to the right") }; items.push( MenuItemFields::new(label) @@ -9836,15 +9850,24 @@ impl Workspace { for section_items in [ pin_section, vec![ - MenuItemFields::new("Ungroup tabs") - .with_on_select_action(WorkspaceAction::UngroupTabs(group_id)) - .into_item(), - MenuItemFields::new("New tab in group") - .with_on_select_action(WorkspaceAction::NewTabInGroup(group_id)) - .into_item(), + MenuItemFields::new(crate::menu_label( + "workspace.tab_group.ungroup", + "Ungroup tabs", + )) + .with_on_select_action(WorkspaceAction::UngroupTabs(group_id)) + .into_item(), + MenuItemFields::new(crate::menu_label( + "workspace.tab_group.new_tab_in_group", + "New tab in group", + )) + .with_on_select_action(WorkspaceAction::NewTabInGroup(group_id)) + .into_item(), ], move_section, - vec![MenuItemFields::new("Rename") + vec![MenuItemFields::new(crate::menu_label( + "workspace.tab_group.rename", + "Rename", + )) .with_on_select_action(WorkspaceAction::RenameTabGroup(group_id)) .into_item()], close_section, @@ -10112,7 +10135,7 @@ impl Workspace { .with_height(NEW_SESSION_SIDECAR_SEARCH_BOX_HEIGHT) .finish() }), - Some("Search repos".to_string()), + Some(crate::menu_label("workspace.search_repos", "Search repos").to_string()), ) .with_no_interaction_on_hover() .no_highlight_on_hover() @@ -12671,7 +12694,7 @@ impl Workspace { self.add_tab_with_pane_layout( Default::default(), Arc::new(HashMap::new()), - Some("Install Update".to_owned()), + Some(crate::menu_label("workspace.install_update_tab", "Install Update").to_owned()), ctx, ); @@ -13762,7 +13785,7 @@ impl Workspace { .conversation(&conversation_id) .and_then(|c| c.title()) .map(|s| s.to_string()) - .unwrap_or_else(|| "Conversation".to_string()); + .unwrap_or_else(|| crate::menu_label("workspace.conversation", "Conversation").to_string()); let title = if source_title.chars().count() > MAX_FORK_TOAST_TITLE_LENGTH { let truncated: String = source_title @@ -14038,7 +14061,7 @@ impl Workspace { let toast = DismissibleToast::error( "Warp doesn't have permission to send desktop notifications.".to_string(), ) - .with_link(ToastLink::new("Troubleshoot notifications".to_string()).with_href(url)); + .with_link(ToastLink::new(crate::menu_label("workspace.troubleshoot_notifications", "Troubleshoot notifications").to_string()).with_href(url)); toast_stack.add_persistent_toast(toast, ctx); }); } @@ -15544,7 +15567,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Your conversation hasn't synced to the cloud yet. Try sending another message, then hand off again." + crate::menu_label("workspace.conversation_not_synced", "Your conversation hasn't synced to the cloud yet. Try sending another message, then hand off again.") .to_owned(), ), window_id, @@ -15656,7 +15679,7 @@ impl Workspace { WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Couldn't save your conversation locally. Try sending another message, then hand off again." + crate::menu_label("workspace.couldnt_save_conversation", "Couldn't save your conversation locally. Try sending another message, then hand off again.") .to_owned(), ), window_id, @@ -15998,7 +16021,7 @@ impl Workspace { if !object_found { self.toast_stack.update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error(String::from( - "Resource not found or access denied", + crate::menu_label("workspace.resource_not_found", "Resource not found or access denied"), )); toast_stack.add_ephemeral_toast(toast, ctx); }); @@ -17556,7 +17579,7 @@ impl Workspace { if !ContextFlag::CreateNewSession.is_enabled() { self.toast_stack.update(ctx, |toast_stack, ctx| { let toast = - DismissibleToast::error("Cannot open a new terminal session".to_string()); + DismissibleToast::error(crate::menu_label("workspace.cannot_open_new_terminal_session", "Cannot open a new terminal session").to_string()); toast_stack.add_ephemeral_toast(toast, ctx); }); return None; @@ -17820,7 +17843,7 @@ impl Workspace { self.toast_stack.update(ctx, |view, ctx| { view.add_ephemeral_toast( DismissibleToast::error( - "This workflow is no longer available.".to_string(), + crate::menu_label("workspace.workflow_no_longer_available", "This workflow is no longer available.").to_string(), ), ctx, ); @@ -18005,11 +18028,11 @@ impl Workspace { }, ) { new_toast = DismissibleToast::success( - "Plan synced to your Warp Drive".to_string(), + crate::menu_label("workspace.plan_synced_to_warp_drive", "Plan synced to your Warp Drive").to_string(), ) .with_object_id(object_id_clone) .with_link( - ToastLink::new("View".to_string()) + ToastLink::new(crate::menu_label("workspace.view", "View").to_string()) .with_onclick_action( WorkspaceAction::ViewObjectInWarpDrive( WarpDriveItemId::Object( @@ -18031,7 +18054,7 @@ impl Workspace { || result.operation == ObjectOperation::Update { new_toast = new_toast.with_link( - ToastLink::new("View".to_string()).with_onclick_action( + ToastLink::new(crate::menu_label("workspace.view", "View").to_string()).with_onclick_action( WorkspaceAction::ViewObjectInWarpDrive( WarpDriveItemId::Object( CloudObjectTypeAndId::Workflow(workflow.id), @@ -18044,7 +18067,7 @@ impl Workspace { if result.operation == ObjectOperation::Trash { new_toast = new_toast.with_link( - ToastLink::new("Undo".to_string()).with_onclick_action( + ToastLink::new(crate::menu_label("workspace.undo", "Undo").to_string()).with_onclick_action( WorkspaceAction::UndoTrash(cloud_object_type_and_id), ), ) @@ -19131,7 +19154,7 @@ impl Workspace { { BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, _ctx| { if let Some(conversation) = history.conversation_mut(&conversation_id) { - conversation.set_fallback_display_title("Linear Issue".to_string()); + conversation.set_fallback_display_title(crate::menu_label("workspace.linear_issue", "Linear Issue").to_string()); } }); } @@ -20127,7 +20150,7 @@ impl Workspace { icons::Icon::Grid, &self.mouse_states.agent_management_view_button, WorkspaceAction::ToggleAgentManagementView, - "Agent management panel".to_string(), + crate::menu_label("workspace.agent_management_panel", "Agent management panel").to_string(), keybinding_name_to_display_string( "workspace:toggle_agent_management_view", ctx, @@ -20157,7 +20180,7 @@ impl Workspace { if vertical_tabs_active { ( self.vertical_tabs_panel_open, - "Tabs panel", + crate::menu_label("workspace.tabs_panel", "Tabs panel"), WorkspaceAction::ToggleVerticalTabsPanel, "workspace:toggle_vertical_tabs_panel", "workspace:toggle_vertical_tabs_panel", @@ -20170,13 +20193,13 @@ impl Workspace { .copied() .unwrap_or(ToolPanelView::WarpDrive) { - ToolPanelView::ProjectExplorer => "Project explorer", - ToolPanelView::GlobalSearch { .. } => "Global search", - ToolPanelView::WarpDrive => "Warp Drive", - ToolPanelView::ConversationListView => "Agent conversations", + ToolPanelView::ProjectExplorer => crate::menu_label("workspace.project_explorer", "Project explorer"), + ToolPanelView::GlobalSearch { .. } => crate::menu_label("workspace.global_search", "Global search"), + ToolPanelView::WarpDrive => crate::menu_label("workspace.warp_drive", "Warp Drive"), + ToolPanelView::ConversationListView => crate::menu_label("workspace.agent_conversations", "Agent conversations"), } } else { - "Tools panel" + crate::menu_label("workspace.tools_panel", "Tools panel") }; ( self.active_tab_pane_group().as_ref(ctx).left_panel_open, @@ -20224,13 +20247,13 @@ impl Workspace { .copied() .unwrap_or(ToolPanelView::WarpDrive) { - ToolPanelView::ProjectExplorer => "Project explorer", - ToolPanelView::GlobalSearch { .. } => "Global search", - ToolPanelView::WarpDrive => "Warp Drive", - ToolPanelView::ConversationListView => "Agent conversations", + ToolPanelView::ProjectExplorer => crate::menu_label("workspace.project_explorer", "Project explorer"), + ToolPanelView::GlobalSearch { .. } => crate::menu_label("workspace.global_search", "Global search"), + ToolPanelView::WarpDrive => crate::menu_label("workspace.warp_drive", "Warp Drive"), + ToolPanelView::ConversationListView => crate::menu_label("workspace.agent_conversations", "Agent conversations"), } } else { - "Tools panel" + crate::menu_label("workspace.tools_panel", "Tools panel") }; SavePosition::new( @@ -20387,7 +20410,7 @@ impl Workspace { button .with_tooltip(self.render_tab_bar_icon_button_tooltip( appearance, - "Code review panel".to_string(), + crate::menu_label("workspace.code_review_panel", "Code review panel").to_string(), keybinding_name_to_display_string("workspace:toggle_right_panel", ctx), )) .build() @@ -20921,7 +20944,7 @@ impl Workspace { WorkspaceAction::ToggleNotificationMailbox { select_first: false, }, - "Notifications".to_string(), + crate::menu_label("workspace.notifications", "Notifications").to_string(), keybinding_name_to_display_string(TOGGLE_NOTIFICATION_MAILBOX_BINDING_NAME, ctx), is_inbox_active, false, @@ -21430,7 +21453,7 @@ impl Workspace { icons::Icon::Lightbulb, &self.mouse_states.resource_center_icon, WorkspaceAction::ToggleResourceCenter, - "Warp Essentials".to_string(), + crate::menu_label("workspace.warp_essentials", "Warp Essentials").to_string(), self.cached_keybindings[TOGGLE_RESOURCE_CENTER_KEYBINDING_NAME].clone(), false, false, @@ -21472,7 +21495,7 @@ impl Workspace { icons::Icon::Gear, &self.mouse_states.settings_icon, WorkspaceAction::ShowSettings, - "Settings".to_string(), + crate::menu_label("workspace.settings", "Settings").to_string(), self.cached_keybindings[SHOW_SETTINGS_KEYBINDING_NAME].clone(), false, false, @@ -21513,7 +21536,7 @@ impl Workspace { Some(hovered_styles), None, ) - .with_centered_text_label(String::from("Sign up")); + .with_centered_text_label(String::from(crate::menu_label("workspace.sign_up", "Sign up"))); Align::new( button @@ -21555,7 +21578,7 @@ impl Workspace { Some(hovered_styles), None, ) - .with_centered_text_label(String::from("Sign up")); + .with_centered_text_label(String::from(crate::menu_label("workspace.sign_up", "Sign up"))); Align::new( button @@ -21735,7 +21758,7 @@ impl Workspace { Flex::row() .with_child( Text::new_inline( - UPDATE_READY_TEXT, + update_ready_text(), appearance.ui_font_family(), PILL_FONT_SIZE, ) @@ -21928,7 +21951,7 @@ impl Workspace { AISettings::as_ref(app) .is_any_ai_enabled(app) .then(|| WorkspaceBannerButtonDetails { - text: "Fix with Oz".to_owned(), + text: crate::menu_label("workspace.fix_with_oz", "Fix with Oz").to_owned(), action: WorkspaceAction::FixSettingsWithOz { error_description: error.to_string(), }, @@ -21943,7 +21966,7 @@ impl Workspace { description, secondary_button, button: Some(WorkspaceBannerButtonDetails { - text: "Open file".to_owned(), + text: crate::menu_label("workspace.open_file", "Open file").to_owned(), action: WorkspaceAction::OpenSettingsFile, variant: BannerButtonVariant::Outlined, icon: None, @@ -21969,11 +21992,11 @@ impl Workspace { Some(WorkspaceBannerFields { banner_type: WorkspaceBanner::Reauth, severity: BannerSeverity::Warning, - heading: Some("Your login has expired.".into()), - description: "Please sign in again to restore access to cloud-based features.".into(), + heading: Some(crate::menu_label("workspace.login_expired", "Your login has expired.").into()), + description: crate::menu_label("workspace.please_sign_in_again", "Please sign in again to restore access to cloud-based features.").into(), secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Sign in".into(), + text: crate::menu_label("workspace.sign_in", "Sign in").into(), action: WorkspaceAction::Reauth, variant: BannerButtonVariant::Outlined, icon: None, @@ -21990,9 +22013,9 @@ impl Workspace { { let description = if is_incoming_version_past_current(new_version.soft_cutoff.as_deref()) { - VERSION_DEPRECATION_WITHOUT_PERMISSIONS_BANNER_TEXT.to_owned() + version_deprecation_without_permissions_banner_text().to_owned() } else { - "A new version is available but Warp is unable to perform the update." + crate::menu_label("workspace.unable_update_new_version", "A new version is available but Warp is unable to perform the update.") .to_owned() }; @@ -22003,7 +22026,7 @@ impl Workspace { description, secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Update Warp manually".to_string(), + text: crate::menu_label("workspace.update_warp_manually", "Update Warp manually").to_string(), action: WorkspaceAction::DownloadNewVersion, variant: BannerButtonVariant::Outlined, icon: None, @@ -22016,9 +22039,9 @@ impl Workspace { { let description = if is_incoming_version_past_current(new_version.soft_cutoff.as_deref()) { - VERSION_DEPRECATION_WITHOUT_PERMISSIONS_BANNER_TEXT.to_owned() + version_deprecation_without_permissions_banner_text().to_owned() } else { - "Warp was unable to launch the new installed version.".to_owned() + crate::menu_label("workspace.unable_launch_new_version", "Warp was unable to launch the new installed version.").to_owned() }; Some(WorkspaceBannerFields { @@ -22028,7 +22051,7 @@ impl Workspace { description, secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Update Warp manually".to_string(), + text: crate::menu_label("workspace.update_warp_manually", "Update Warp manually").to_string(), action: WorkspaceAction::DownloadNewVersion, variant: BannerButtonVariant::Outlined, icon: None, @@ -22043,10 +22066,10 @@ impl Workspace { banner_type: WorkspaceBanner::VersionDeprecated, severity: BannerSeverity::Error, heading: None, - description: VERSION_DEPRECATION_BANNER_TEXT.to_string(), + description: version_deprecation_banner_text().to_string(), secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Update now".to_string(), + text: crate::menu_label("workspace.update_now", "Update now").to_string(), action: WorkspaceAction::ApplyUpdate, variant: BannerButtonVariant::Outlined, icon: None, @@ -22060,11 +22083,11 @@ impl Workspace { banner_type: WorkspaceBanner::VersionDeprecated, severity: BannerSeverity::Warning, heading: None, - description: "Your app is out of date and needs to update." + description: crate::menu_label("workspace.app_needs_update", "Your app is out of date and needs to update.") .to_string(), secondary_button: None, button: Some(WorkspaceBannerButtonDetails { - text: "Restart app and update now".to_string(), + text: crate::menu_label("workspace.restart_and_update_now", "Restart app and update now").to_string(), action: WorkspaceAction::ApplyUpdate, variant: BannerButtonVariant::Outlined, icon: None, @@ -22172,7 +22195,7 @@ impl Workspace { if let Some(more_info_button_action) = more_info_button_action { let more_info_details = WorkspaceBannerButtonDetails { - text: "More info".to_owned(), + text: crate::menu_label("workspace.more_info", "More info").to_owned(), action: more_info_button_action, variant: BannerButtonVariant::Outlined, icon: None, @@ -25624,15 +25647,15 @@ impl TypedActionView for Workspace { Ok(Ok(output)) => { let stderr = String::from_utf8_lossy(&output.stderr); log::error!("sample command failed ({}): {stderr}", output.status); - "Failed to sample process (check logs)".to_string() + crate::menu_label("workspace.failed_sample_process", "Failed to sample process (check logs)").to_string() } Ok(Err(io_err)) => { log::error!("Failed to run sample command: {io_err}"); - "Failed to sample process (check logs)".to_string() + crate::menu_label("workspace.failed_sample_process", "Failed to sample process (check logs)").to_string() } Err(join_err) => { log::error!("Sample task panicked: {join_err}"); - "Failed to sample process (check logs)".to_string() + crate::menu_label("workspace.failed_sample_process", "Failed to sample process (check logs)").to_string() } }; me.toast_stack.update(ctx, |view, ctx| { @@ -25791,7 +25814,7 @@ impl TypedActionView for Workspace { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { toast_stack.add_ephemeral_toast( DismissibleToast::error( - "Failed to delete conversation. Please exit the agent view and try again.".to_string(), + crate::menu_label("workspace.failed_delete_conversation", "Failed to delete conversation. Please exit the agent view and try again.").to_string(), ), window_id, ctx, diff --git a/app/src/workspace/view/tab_grouping.rs b/app/src/workspace/view/tab_grouping.rs index 72e4eba387..886de1d3c3 100644 --- a/app/src/workspace/view/tab_grouping.rs +++ b/app/src/workspace/view/tab_grouping.rs @@ -464,14 +464,20 @@ impl Workspace { /// group" only when there's a destination group worth offering. fn tab_selection_menu_items(&self) -> Vec> { let shared_group = self.selection_shared_group(); - let mut menu_items = vec![MenuItemFields::new("Create group from tabs") + let mut menu_items = vec![MenuItemFields::new(crate::menu_label( + "workspace.tab_grouping.create_group", + "Create group from tabs", + )) .with_on_select_action(WorkspaceAction::NewTabGroupFromSelectedTabs) .into_item()]; // Only single-group selections have an unambiguous group to leave. if shared_group.is_some() { menu_items.push( - MenuItemFields::new("Remove from group") + MenuItemFields::new(crate::menu_label( + "workspace.tab_grouping.remove_from_group", + "Remove from group", + )) .with_on_select_action(WorkspaceAction::RemoveSelectedTabsFromGroup) .into_item(), ); @@ -483,7 +489,11 @@ impl Workspace { .keys() .any(|group_id| Some(*group_id) != shared_group); if has_destination_group { - menu_items.push(MenuItemFields::new_submenu(MOVE_TO_GROUP_LABEL).into_item()); + menu_items.push(MenuItemFields::new_submenu(crate::menu_label( + "workspace.tab_grouping.move_to_group", + MOVE_TO_GROUP_LABEL, + )) + .into_item()); } menu_items } @@ -700,7 +710,13 @@ impl Workspace { .tab_groups .get(&group_id) .and_then(|g| g.name.clone()) - .unwrap_or_else(|| "Untitled group".to_string()); + .unwrap_or_else(|| { + crate::menu_label( + "workspace.tab_grouping.untitled_group", + "Untitled group", + ) + .to_string() + }); let action = match tab_index { Some(tab_index) => WorkspaceAction::MoveTabToGroup { tab_index, diff --git a/crates/i18n/Cargo.toml b/crates/i18n/Cargo.toml new file mode 100644 index 0000000000..0f072c0910 --- /dev/null +++ b/crates/i18n/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "i18n" +version = "0.1.0" +edition = "2021" +authors.workspace = true +publish.workspace = true +license.workspace = true + +[dependencies] +log = { workspace = true } +serde_yaml.workspace = true +sys-locale = "0.3.2" diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs new file mode 100644 index 0000000000..233a5cbe9c --- /dev/null +++ b/crates/i18n/src/lib.rs @@ -0,0 +1,505 @@ +//! Warp i18n — YAML-based internationalization. +//! +//! Locale resolution order: +//! 1. `WARP_LANG` env var +//! 2. System locale (via `sys-locale`) +//! 3. Fallback: `en` +//! +//! ## Usage +//! +//! ```ignore +//! i18n::init_locale(); +//! let text = i18n::t!("menu.file"); // "File" or "Файл" +//! ``` + +use std::borrow::Cow; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::{OnceLock, RwLock}; + +const DEFAULT_LOCALE: &str = "en"; +const RU_LOCALE: &str = "ru"; +const ZH_CN_LOCALE: &str = "zh-CN"; +const LOCALES_DIR: &str = "bundled/locales"; + +type Locale = String; +type Key = String; +type Translations = HashMap>; + +static CURRENT_LOCALE: RwLock<&'static str> = RwLock::new(DEFAULT_LOCALE); +static TRANSLATIONS: OnceLock = OnceLock::new(); + +// ── Public API ────────────────────────────────────────────────── + +/// Detect locale from `WARP_LANG`, system locale, or default to `en`. +/// Loads translations and sets the active locale. +pub fn init_locale() { + let locale = env_locale() + .or_else(sys_locale::get_locale) + .unwrap_or_default(); + + set_locale(&locale); +} + +/// Set the active locale. +/// +/// Maps locale prefixes to supported locales: +/// - `ru*` → Russian (`ru`) +/// - `zh*` → Chinese (`zh-CN`) — NOTE: zh-CN locale file is not yet shipped; +/// the code path is forward-compatible and harmless. +/// - anything else → English (`en`) +pub fn set_locale(locale: &str) { + let locale = if locale.starts_with("ru") { + RU_LOCALE + } else if locale.starts_with("zh") { + ZH_CN_LOCALE + } else { + DEFAULT_LOCALE + }; + + if let Ok(mut current_locale) = CURRENT_LOCALE.write() { + *current_locale = locale; + } else { + log::error!("i18n: CURRENT_LOCALE lock poisoned, locale not changed"); + } +} + +/// Look up a translation key for the active locale. +/// +/// Falls back to `en`, then returns the key itself if no translation exists. +pub fn t(key: &str) -> Cow<'static, str> { + translate(current_locale(), key) + .or_else(|| translate(DEFAULT_LOCALE, key)) + .unwrap_or(Cow::Owned(key.to_string())) +} + +/// Interpolate `{key}` placeholders in a template string. +/// +/// ```ignore +/// let msg = i18n::interpolate("Hello, {name}!", &[("name", "World".into())]); +/// assert_eq!(msg, "Hello, World!"); +/// ``` +pub fn interpolate(template: &str, args: &[(&str, String)]) -> Cow<'static, str> { + let mut value = template.to_owned(); + for (key, replacement) in args { + value = value.replace(&format!("{{{key}}}"), replacement); + } + Cow::Owned(value) +} + +/// Returns the currently active locale code (e.g. `"en"`, `"ru"`). +pub fn current_locale() -> &'static str { + CURRENT_LOCALE + .read() + .map(|locale| *locale) + .unwrap_or(DEFAULT_LOCALE) +} + +// ── TranslationLookup ─────────────────────────────────────────── + +/// Result of a translation lookup. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TranslationLookup { + /// Translation found. + Found(Cow<'static, str>), + /// No translation for the given locale+key. + Missing, +} + +/// Look up a key for a specific locale, returning a `TranslationLookup`. +pub fn lookup(key: &str, locale: &str) -> TranslationLookup { + match translations().get(locale).and_then(|t| t.get(key)) { + Some(value) => TranslationLookup::Found(Cow::Owned(value.clone())), + None => TranslationLookup::Missing, + } +} + +// ── Macros ────────────────────────────────────────────────────── + +/// Look up a translation key for the active locale (1-arg form). +/// +/// Falls back to English, then returns the key as-is. +/// +/// ```ignore +/// let label = i18n::t!("menu.file"); // "File" or "Файл" +/// ``` +#[macro_export] +macro_rules! t { + ($key:expr) => { + $crate::t($key) + }; +} + +/// Look up a translation key, panicking if not found in any locale. +/// +/// Use this for keys that MUST exist in both the target locale AND the +/// English fallback. +/// +/// Reserved for security-sensitive UI per i18n spec. +/// +/// ```ignore +/// let label = i18n::t_required!("menu.file"); // panics if missing +/// ``` +#[macro_export] +macro_rules! t_required { + ($key:expr) => {{ + let key: &'static str = $key; + match $crate::lookup(key, $crate::current_locale()) { + $crate::TranslationLookup::Found(v) => v, + $crate::TranslationLookup::Missing => match $crate::lookup(key, "en") { + $crate::TranslationLookup::Found(v) => v, + $crate::TranslationLookup::Missing => { + panic!("i18n: required key '{}' not found in any locale", key); + } + }, + } + }}; +} + +// ── Internal helpers ──────────────────────────────────────────── + +fn env_locale() -> Option { + ["WARP_LANG", "LANG", "LANGUAGE", "LC_ALL", "LC_MESSAGES"] + .into_iter() + .find_map(|key| std::env::var(key).ok().filter(|value| !value.is_empty())) +} + +fn translate(locale: &str, key: &str) -> Option> { + translations() + .get(locale) + .and_then(|t| t.get(key)) + .map(|value| Cow::Borrowed(value.as_str())) +} + +fn translations() -> &'static Translations { + TRANSLATIONS.get_or_init(load_translations) +} + +// ── Platform-specific loading ─────────────────────────────────── + +#[cfg(not(target_family = "wasm"))] +fn load_translations() -> Translations { + locale_dirs() + .into_iter() + .find_map(load_dir) + .unwrap_or_default() +} + +#[cfg(not(target_family = "wasm"))] +fn locale_dirs() -> Vec { + let mut dirs = Vec::new(); + + if let Ok(path) = std::env::var("WARP_LOCALES_DIR") { + dirs.push(path.into()); + } + + if let Some(resources_dir) = bundled_resources_dir() { + dirs.push(resources_dir.join(LOCALES_DIR)); + } + + if let Some(manifest_dir) = option_env!("CARGO_MANIFEST_DIR") { + dirs.extend(resource_dirs_from_manifest(Path::new(manifest_dir))); + } + + if let Ok(cwd) = std::env::current_dir() { + dirs.push(cwd.join("resources").join(LOCALES_DIR)); + } + + dirs +} + +#[cfg(not(target_family = "wasm"))] +fn resource_dirs_from_manifest(manifest_dir: &Path) -> Vec { + manifest_dir + .ancestors() + .take(4) + .map(|dir| dir.join("resources").join(LOCALES_DIR)) + .collect() +} + +#[cfg(not(target_family = "wasm"))] +fn bundled_resources_dir() -> Option { + #[cfg(target_os = "macos")] + { + let executable = std::env::current_exe().ok()?; + let mut path = std::fs::canonicalize(executable).ok()?; + while path.pop() { + if path.extension().and_then(|extension| extension.to_str()) == Some("app") { + return Some(path.join("Contents").join("Resources")); + } + } + None + } + + #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))] + { + std::env::current_exe() + .ok() + .and_then(|executable| std::fs::canonicalize(executable).ok()) + .and_then(|executable| executable.parent().map(|parent| parent.join("resources"))) + } + + #[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "freebsd", + target_os = "windows" + )))] + { + None + } +} + +#[cfg(not(target_family = "wasm"))] +fn load_dir(path: PathBuf) -> Option { + let entries = std::fs::read_dir(path).ok()?; + let mut translations = Translations::new(); + + for entry in entries.flatten() { + let path = entry.path(); + let Some(extension) = path.extension().and_then(|extension| extension.to_str()) else { + continue; + }; + + if !matches!(extension, "yml" | "yaml") { + continue; + } + + let Ok(contents) = std::fs::read_to_string(&path) else { + continue; + }; + merge_locale_file(&contents, &mut translations); + } + + (!translations.is_empty()).then_some(translations) +} + +#[cfg(target_family = "wasm")] +fn load_translations() -> Translations { + let mut translations = Translations::new(); + merge_locale_file( + include_str!("../../../resources/bundled/locales/en.yml"), + &mut translations, + ); + merge_locale_file( + include_str!("../../../resources/bundled/locales/ru.yml"), + &mut translations, + ); + translations +} + +fn merge_locale_file(contents: &str, translations: &mut Translations) { + let Ok(value) = serde_yaml::from_str::(contents) else { + return; + }; + + let serde_yaml::Value::Mapping(locales) = value else { + return; + }; + + for (locale, values) in locales { + let Some(locale) = locale.as_str() else { + continue; + }; + + flatten_value( + "", + &values, + translations.entry(locale.to_owned()).or_default(), + ); + } +} + +fn flatten_value(prefix: &str, value: &serde_yaml::Value, translations: &mut HashMap) { + match value { + serde_yaml::Value::Mapping(values) => { + for (key, value) in values { + let Some(key) = key.as_str() else { + continue; + }; + + // Filter out _comment keys so they never appear in lookups. + if key == "_comment" { + continue; + } + + let key = if prefix.is_empty() { + key.to_owned() + } else { + format!("{prefix}.{key}") + }; + flatten_value(&key, value, translations); + } + } + serde_yaml::Value::String(value) => { + translations.insert(prefix.to_owned(), value.to_owned()); + } + _ => {} + } +} + +// ── Tests ─────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_locale_is_en() { + assert_eq!(current_locale(), "en"); + } + + #[test] + fn test_set_locale_ru() { + set_locale("ru_RU.UTF-8"); + assert_eq!(current_locale(), "ru"); + } + + #[test] + fn test_set_locale_zh() { + set_locale("zh_CN.UTF-8"); + assert_eq!(current_locale(), "zh-CN"); + } + + #[test] + fn test_set_locale_unknown_falls_back_to_en() { + set_locale("de"); + assert_eq!(current_locale(), "en"); + } + + #[test] + fn test_interpolate() { + let result = interpolate("Hello, {name}!", &[("name", "World".into())]); + assert_eq!(result, "Hello, World!"); + } + + #[test] + fn test_interpolate_multiple() { + let result = interpolate( + "{greeting}, {name}!", + &[("greeting", "Hi".into()), ("name", "Alice".into())], + ); + assert_eq!(result, "Hi, Alice!"); + } + + #[test] + fn test_lookup_missing() { + assert_eq!(lookup("nonexistent.key", "en"), TranslationLookup::Missing); + } + + #[test] + fn test_flatten_value_nested() { + let yaml = r#" +en: + menu: + file: "File" + edit: "Edit" + appearance: + opacity: "Opacity" +"#; + let mut translations = Translations::new(); + merge_locale_file(yaml, &mut translations); + let en = translations.get("en").expect("en locale should exist"); + assert_eq!(en.get("menu.file"), Some(&"File".to_string())); + assert_eq!(en.get("menu.edit"), Some(&"Edit".to_string())); + assert_eq!(en.get("appearance.opacity"), Some(&"Opacity".to_string())); + } + + #[test] + fn test_yaml_key_balance_en_equals_ru() { + // Both locale files must have the same set of keys. + // This prevents drift where a key is added to en.yml but forgotten in ru.yml. + let mut en_translations = Translations::new(); + let mut ru_translations = Translations::new(); + + merge_locale_file( + include_str!("../../../resources/bundled/locales/en.yml"), + &mut en_translations, + ); + merge_locale_file( + include_str!("../../../resources/bundled/locales/ru.yml"), + &mut ru_translations, + ); + + let en_keys: std::collections::BTreeSet<&String> = en_translations + .get("en") + .expect("en locale missing") + .keys() + .collect(); + let ru_keys: std::collections::BTreeSet<&String> = ru_translations + .get("ru") + .expect("ru locale missing") + .keys() + .collect(); + + let en_only: Vec<_> = en_keys.difference(&ru_keys).collect(); + let ru_only: Vec<_> = ru_keys.difference(&en_keys).collect(); + + assert!( + en_only.is_empty(), + "Keys in en.yml missing from ru.yml: {en_only:?}" + ); + assert!( + ru_only.is_empty(), + "Keys in ru.yml missing from en.yml: {ru_only:?}" + ); + } + + #[test] + fn test_yaml_no_key_collisions() { + let en = include_str!("../../../resources/bundled/locales/en.yml"); + let ru = include_str!("../../../resources/bundled/locales/ru.yml"); + for (raw, name) in [(en, "en.yml"), (ru, "ru.yml")] { + let value: serde_yaml::Value = serde_yaml::from_str(raw).expect("invalid YAML"); + let mut collisions = Vec::new(); + walk_collisions(&[], &value, &mut collisions); + assert!( + collisions.is_empty(), + "YAML key collision in {name}: {collisions:?}" + ); + } + } + + fn walk_collisions(path: &[&str], value: &serde_yaml::Value, out: &mut Vec) { + let serde_yaml::Value::Mapping(map) = value else { + return; + }; + let mut scalars: Vec> = Vec::new(); + let mut map_paths: Vec> = Vec::new(); + for (k, v) in map { + let Some(key) = k.as_str() else { continue }; + if key == "_comment" { + continue; + } + let mut child = path.to_vec(); + child.push(key); + if matches!(v, serde_yaml::Value::Mapping(_)) { + map_paths.push(child.clone()); + walk_collisions(&child, v, out); + } else { + scalars.push(child); + } + } + for s in &scalars { + if map_paths.contains(s) { + out.push(s.join(".")); + } + } + } + + #[test] + fn test_flatten_value_with_comment() { + let yaml = r#" +en: + _comment: "this should be skipped" + menu: + file: "File" +"#; + let mut translations = Translations::new(); + merge_locale_file(yaml, &mut translations); + let en = translations.get("en").expect("en locale should exist"); + // _comment at root level is filtered out + assert_eq!(en.get("_comment"), None); + assert_eq!(en.get("menu.file"), Some(&"File".to_string())); + } +} diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 25e8310148..4d8002aa89 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -19,6 +19,7 @@ ai.workspace = true anyhow.workspace = true cfg-if.workspace = true instant.workspace = true +i18n = { workspace = true } log.workspace = true pathfinder_color.workspace = true pathfinder_geometry.workspace = true diff --git a/crates/onboarding/src/agent_onboarding_view.rs b/crates/onboarding/src/agent_onboarding_view.rs index a002d4faf4..94ea027979 100644 --- a/crates/onboarding/src/agent_onboarding_view.rs +++ b/crates/onboarding/src/agent_onboarding_view.rs @@ -66,6 +66,8 @@ pub enum AgentOnboardingEvent { UpgradeRequested, UpgradeCopyUrlRequested, UpgradePasteTokenFromClipboardRequested, + AddApiKeyRequested, + AddCustomEndpointRequested, /// Emitted when the app regains focus (e.g. user returns from the browser). /// The parent should refresh any stale data: available models, workspace/billing metadata, etc. AppBecameActive, @@ -226,6 +228,12 @@ impl AgentOnboardingView { }; ctx.subscribe_to_view(&ai_access_slide, |_me, _view, event, ctx| match event { + AiAccessSlideEvent::AddApiKeyRequested => { + ctx.emit(AgentOnboardingEvent::AddApiKeyRequested); + } + AiAccessSlideEvent::AddCustomEndpointRequested => { + ctx.emit(AgentOnboardingEvent::AddCustomEndpointRequested); + } AiAccessSlideEvent::CopyUpgradeUrlRequested => { ctx.emit(AgentOnboardingEvent::UpgradeCopyUrlRequested); } @@ -313,6 +321,20 @@ impl AgentOnboardingView { ctx.notify(); } + /// Updates how many BYOK provider keys and custom endpoints the user has + /// configured. This drives the AI-access slide's "connected" status line and + /// gates "Next" on the bring-your-own path. + pub fn set_byok_status( + &mut self, + key_count: usize, + endpoint_count: usize, + ctx: &mut ViewContext, + ) { + self.ai_access_slide.update(ctx, |slide, ctx| { + slide.set_byok_status(key_count, endpoint_count, ctx); + }); + } + /// The current `use_vertical_tabs` value on the onboarding UI customization. /// This reflects the intention's default (agent = vertical, terminal = horizontal) /// and any change the user made on the customize slide, and is what the @@ -393,7 +415,9 @@ impl AgentOnboardingView { let cancel_button = self.no_ai_cancel_button.render( appearance, button::Params { - content: button::Content::Label("Give me AI features".into()), + content: button::Content::Label( + crate::menu_label("onboarding.agent.give_me_ai_features", "Give me AI features").into(), + ), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -408,7 +432,9 @@ impl AgentOnboardingView { let confirm_button = self.no_ai_confirm_button.render( appearance, button::Params { - content: button::Content::Label("I don't want AI".into()), + content: button::Content::Label( + crate::menu_label("onboarding.ai_access.no_ai", "I don't want AI").into(), + ), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), @@ -423,9 +449,14 @@ impl AgentOnboardingView { render_feature_optout_dialog( appearance, FeatureOptOutDialog { - title: "Are you sure you don't want AI?", - body: "Without AI, you'll still get Warp's terminal experience, but you'll miss \ - our agentic features like automatic fixes for terminal errors.", + title: crate::menu_label( + "onboarding.agent.no_ai_dialog_title", + "Are you sure you don't want AI?", + ), + body: crate::menu_label( + "onboarding.agent.no_ai_dialog_body", + "Without AI, you'll still get Warp's terminal experience, but you'll miss our agentic features like automatic fixes for terminal errors.", + ), features: &[], close_button, cancel_button, @@ -490,7 +521,10 @@ impl AgentOnboardingView { .finish(); let text = ui_builder - .span("Plan successfully activated!") + .span(crate::menu_label( + "onboarding.agent.plan_activated_toast", + "Plan successfully activated!", + )) .with_style(UiComponentStyles { font_color: Some(text_color), font_size: Some(FONT_SIZE), @@ -620,7 +654,9 @@ impl View for AgentOnboardingView { let close_button = self.close_button.render( appearance, button::Params { - content: button::Content::Label("Skip".into()), + content: button::Content::Label( + crate::menu_label("onboarding.agent.skip", "Skip").into(), + ), theme: &button::themes::Naked, options: button::Options { size: button::Size::Small, diff --git a/crates/onboarding/src/bin/main.rs b/crates/onboarding/src/bin/main.rs index 38415ad15f..ea52070b38 100644 --- a/crates/onboarding/src/bin/main.rs +++ b/crates/onboarding/src/bin/main.rs @@ -168,6 +168,8 @@ impl OnboardingMainView { | AgentOnboardingEvent::UpgradePasteTokenFromClipboardRequested | AgentOnboardingEvent::LoginFromWelcomeRequested | AgentOnboardingEvent::PrivacySettingsFromTerminalThemeSlideRequested + | AgentOnboardingEvent::AddApiKeyRequested + | AgentOnboardingEvent::AddCustomEndpointRequested | AgentOnboardingEvent::AppBecameActive => { // No-op in the standalone demo binary } diff --git a/crates/onboarding/src/callout/view.rs b/crates/onboarding/src/callout/view.rs index 44a5b3be84..2904b880c5 100644 --- a/crates/onboarding/src/callout/view.rs +++ b/crates/onboarding/src/callout/view.rs @@ -59,27 +59,38 @@ fn get_universal_input_callout_options( ) -> Option { match state { UniversalInputCalloutState::MeetInput => Some(CalloutOptions { - title: "Meet the Warp input", - text: format!( - "Your terminal input accepts both terminal commands and agent prompts and automatically detects which you're using. Use {} to lock the input to Agent mode (natural language) or Terminal mode (commands).", - keybindings.toggle_input_mode + title: crate::menu_label( + "onboarding.callout.meet_input_title", + "Meet the Warp input", ), + text: crate::menu_label( + "onboarding.callout.meet_input_text", + "Your terminal input accepts both terminal commands and agent prompts and automatically detects which you're using. Use {toggle_input_mode} to lock the input to Agent mode (natural language) or Terminal mode (commands).", + ) + .replace("{toggle_input_mode}", &keybindings.toggle_input_mode), step: StepStatus::new(0, 2), left_button: None, right_button: ButtonOptions { - text: "Next", + text: crate::menu_label("common.next", "Next"), action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, checkbox: None, }), UniversalInputCalloutState::TalkToAgent => Some(CalloutOptions { - title: "Talk to the agent", - text: "You can type in natural language to engage the agent. Submit the query below to start: What tests exist in this repo, how are they structured, and what do they cover?".to_string(), + title: crate::menu_label( + "onboarding.callout.talk_to_agent_title", + "Talk to the agent", + ), + text: crate::menu_label( + "onboarding.callout.talk_to_agent_text", + "You can type in natural language to engage the agent. Submit the query below to start: What tests exist in this repo, how are they structured, and what do they cover?", + ) + .to_string(), step: StepStatus::new(1, 2), left_button: if has_project { Some(ButtonOptions { - text: "Skip", + text: crate::menu_label("common.skip", "Skip"), action: OnboardingCalloutViewAction::SkipClicked, keystroke: Some(Keystroke::parse("delete").unwrap_or_default()), }) @@ -87,7 +98,11 @@ fn get_universal_input_callout_options( None }, right_button: ButtonOptions { - text: if has_project { "Submit" } else { "Finish" }, + text: if has_project { + crate::menu_label("common.submit", "Submit") + } else { + crate::menu_label("common.finish", "Finish") + }, action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, @@ -117,15 +132,23 @@ fn get_agent_modality_callout_options( if initial_natural_language_detection_enabled { // NL detection was already enabled - show simpler "overrides" callout without checkbox Some(CalloutOptions { - title: "Welcome to terminal mode", - text: format!( - "Run commands here, just like a regular terminal. If you type a question or task using natural language, Warp can suggest opening it in agent mode. You can always override using {}.", - keybindings.toggle_input_mode + title: crate::menu_label( + "onboarding.callout.welcome_terminal_title", + "Welcome to terminal mode", ), + text: crate::menu_label( + "onboarding.callout.terminal_mode_text", + "Run commands here, just like a regular terminal. If you type a question or task using natural language, Warp can suggest opening it in agent mode. You can always override using {toggle_input_mode}.", + ) + .replace("{toggle_input_mode}", &keybindings.toggle_input_mode), step: StepStatus::new(0, total_steps), left_button: None, right_button: ButtonOptions { - text: if is_final_step { "Finish" } else { "Next" }, + text: if is_final_step { + crate::menu_label("common.finish", "Finish") + } else { + crate::menu_label("common.next", "Next") + }, action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, @@ -134,20 +157,31 @@ fn get_agent_modality_callout_options( } else { // NL detection was disabled - show full explanation with checkbox to enable Some(CalloutOptions { - title: "You’re in terminal mode", - text: format!( - "Run commands here, just like a regular terminal. If you type a question or task using natural language, Warp can suggest opening it in agent mode. You can always override using {}.", - keybindings.toggle_input_mode + title: crate::menu_label( + "onboarding.callout.in_terminal_title", + "You’re in terminal mode", ), + text: crate::menu_label( + "onboarding.callout.terminal_mode_text", + "Run commands here, just like a regular terminal. If you type a question or task using natural language, Warp can suggest opening it in agent mode. You can always override using {toggle_input_mode}.", + ) + .replace("{toggle_input_mode}", &keybindings.toggle_input_mode), step: StepStatus::new(0, total_steps), left_button: None, right_button: ButtonOptions { - text: if is_final_step { "Finish" } else { "Next" }, + text: if is_final_step { + crate::menu_label("common.finish", "Finish") + } else { + crate::menu_label("common.next", "Next") + }, action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, checkbox: Some(CheckboxOptions { - label: "Enable Natural Language Detection", + label: crate::menu_label( + "onboarding.callout.enable_natural_language_detection", + "Enable Natural Language Detection", + ), checked: natural_language_detection_enabled, }), }) @@ -156,16 +190,26 @@ fn get_agent_modality_callout_options( AgentModalityCalloutState::AgentMode => { if has_project { Some(CalloutOptions { - title: "You're in agent mode", - text: "Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow.\n\nSubmit the query below to have the agent initialize this project, or ⊗ to clear the input and start your own!".to_string(), + title: crate::menu_label( + "onboarding.callout.in_agent_title", + "You're in agent mode", + ), + text: crate::menu_label( + "onboarding.callout.in_agent_text_with_project", + "Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow.\n\nSubmit the query below to have the agent initialize this project, or ⊗ to clear the input and start your own!", + ) + .to_string(), step: StepStatus::new(1, total_steps), left_button: Some(ButtonOptions { - text: "Skip initialization", + text: crate::menu_label( + "onboarding.callout.skip_initialization", + "Skip initialization", + ), action: OnboardingCalloutViewAction::SkipClicked, keystroke: Some(Keystroke::parse("delete").unwrap_or_default()), }), right_button: ButtonOptions { - text: "Initialize", + text: crate::menu_label("onboarding.callout.initialize", "Initialize"), action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, @@ -173,19 +217,29 @@ fn get_agent_modality_callout_options( }) } else { Some(CalloutOptions { - title: "You're in agent mode", - text: format!( - "Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow. Press {} to return to terminal mode at any point.", - keybindings.return_to_terminal_mode + title: crate::menu_label( + "onboarding.callout.in_agent_title", + "You're in agent mode", + ), + text: crate::menu_label( + "onboarding.callout.in_agent_text_no_project", + "Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow. Press {return_to_terminal_mode} to return to terminal mode at any point.", + ) + .replace( + "{return_to_terminal_mode}", + &keybindings.return_to_terminal_mode, ), step: StepStatus::new(1, total_steps), left_button: Some(ButtonOptions { - text: "Back to terminal", + text: crate::menu_label( + "onboarding.callout.back_to_terminal", + "Back to terminal", + ), action: OnboardingCalloutViewAction::BackToTerminalClicked, keystroke: Some(Keystroke::parse("escape").unwrap_or_default()), }), right_button: ButtonOptions { - text: "Finish", + text: crate::menu_label("common.finish", "Finish"), action: OnboardingCalloutViewAction::NextClicked, keystroke: Some(Keystroke::parse("enter").unwrap_or_default()), }, diff --git a/crates/onboarding/src/lib.rs b/crates/onboarding/src/lib.rs index 48c762ba0d..89acae8783 100644 --- a/crates/onboarding/src/lib.rs +++ b/crates/onboarding/src/lib.rs @@ -1,5 +1,17 @@ // Onboarding library crate +/// Looks up a translation key using the user's chosen locale. +/// Falls back to the English (en) translation, then to the provided fallback string. +pub fn menu_label(key: &str, fallback: &str) -> &'static str { + match i18n::lookup(key, i18n::current_locale()) { + i18n::TranslationLookup::Found(v) => Box::leak(v.into_owned().into_boxed_str()), + i18n::TranslationLookup::Missing => match i18n::lookup(key, "en") { + i18n::TranslationLookup::Found(v) => Box::leak(v.into_owned().into_boxed_str()), + i18n::TranslationLookup::Missing => Box::leak(fallback.to_string().into_boxed_str()), + }, + } +} + mod agent_onboarding_view; pub mod callout; mod model; diff --git a/crates/onboarding/src/model.rs b/crates/onboarding/src/model.rs index e0b1538edb..dca1eaccfb 100644 --- a/crates/onboarding/src/model.rs +++ b/crates/onboarding/src/model.rs @@ -385,7 +385,7 @@ impl OnboardingStateModel { self.set_step(OnboardingStep::Customize, ctx); } - /// "Give me AI features": abort the opt-out. The only trigger is the +/// "Give me AI features": abort the opt-out. The only trigger is the /// intention slide's "Just use the terminal", which is an explicit request /// for AI, so route onto the AI path. pub(crate) fn cancel_no_ai(&mut self, ctx: &mut ModelContext) { diff --git a/crates/onboarding/src/slides/agent_slide.rs b/crates/onboarding/src/slides/agent_slide.rs index 88600d68bd..06b1c2f3a2 100644 --- a/crates/onboarding/src/slides/agent_slide.rs +++ b/crates/onboarding/src/slides/agent_slide.rs @@ -223,7 +223,10 @@ impl AgentSlide { fn render_header(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Customize your Warp Agent") + .paragraph(crate::menu_label( + "onboarding.agent_slide.title", + "Customize your Warp Agent", + )) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -233,7 +236,10 @@ impl AgentSlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Select your Warp Agent's defaults.", + crate::menu_label( + "onboarding.agent_slide.subtitle", + "Select your Warp Agent's defaults.", + ), appearance.ui_font_family(), 16., ) @@ -319,7 +325,10 @@ impl AgentSlide { settings: &AgentDevelopmentSettings, app: &AppContext, ) -> Box { - let header = self.render_section_header("Default model", appearance); + let header = self.render_section_header( + crate::menu_label("onboarding.agent_slide.default_model", "Default model"), + appearance, + ); let expanded = self.is_model_list_expanded; let chip = self.render_collapsed_model_chip(appearance, settings, app, expanded); @@ -618,7 +627,7 @@ impl AgentSlide { }; let trailing: Box = if is_default { - make_pill("Recommended") + make_pill(crate::menu_label("common.recommended", "Recommended")) } else { Empty::new().finish() }; @@ -665,7 +674,10 @@ impl AgentSlide { } fn render_autonomy_workspace_enforced(&self, appearance: &Appearance) -> Box { - let header = self.render_section_header("Autonomy", appearance); + let header = self.render_section_header( + crate::menu_label("onboarding.agent_slide.autonomy", "Autonomy"), + appearance, + ); let theme = appearance.theme(); let background_for_text = theme.background().into_solid(); @@ -674,7 +686,14 @@ impl AgentSlide { let title_color = internal_colors::text_main(theme, background_for_text); let subtitle_color = internal_colors::text_sub(theme, background_for_text); - let title_el = Text::new("Set by Team Workspace", ui_font_family, 14.0) + let title_el = Text::new( + crate::menu_label( + "onboarding.agent_slide.set_by_workspace", + "Set by Team Workspace", + ), + ui_font_family, + 14.0, + ) .with_color(title_color) .with_style(Properties { weight: Weight::Normal, @@ -684,7 +703,10 @@ impl AgentSlide { .finish(); let subtitle_el = Text::new( - "Autonomy settings are configured as part of your team workspace.", + crate::menu_label( + "onboarding.agent_slide.autonomy_workspace_subtitle", + "Autonomy settings are configured as part of your team workspace.", + ), ui_font_family, 12.0, ) @@ -720,7 +742,10 @@ impl AgentSlide { appearance: &Appearance, settings: &AgentDevelopmentSettings, ) -> Box { - let header = self.render_section_header("Autonomy", appearance); + let header = self.render_section_header( + crate::menu_label("onboarding.agent_slide.autonomy", "Autonomy"), + appearance, + ); // The rows now take the full column width (vs. the previous three-across layout), // so they no longer need the extra height that came from cramped subtitle wrapping. @@ -732,23 +757,32 @@ impl AgentSlide { let text_main = internal_colors::text_main(theme, background_for_text); let text_sub = internal_colors::text_sub(theme, background_for_text); - let autonomy_options: [(AgentAutonomy, &str, &str, MouseStateHandle); 3] = [ + let autonomy_options: [(AgentAutonomy, &'static str, &'static str, MouseStateHandle); 3] = [ ( AgentAutonomy::Full, - "Full", - "Warp Agent runs commands, writes code, and reads files without asking.", + crate::menu_label("onboarding.agent_slide.autonomy_full_title", "Full"), + crate::menu_label( + "onboarding.agent_slide.autonomy_full_subtitle", + "Warp Agent runs commands, writes code, and reads files without asking.", + ), self.autonomy_full_mouse_state.clone(), ), ( AgentAutonomy::Partial, - "Partial", - "Warp Agent can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.", + crate::menu_label("onboarding.agent_slide.autonomy_partial_title", "Partial"), + crate::menu_label( + "onboarding.agent_slide.autonomy_partial_subtitle", + "Warp Agent can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.", + ), self.autonomy_partial_mouse_state.clone(), ), ( AgentAutonomy::None, - "None", - "Warp Agent takes no actions without your approval.", + crate::menu_label("onboarding.agent_slide.autonomy_none_title", "None"), + crate::menu_label( + "onboarding.agent_slide.autonomy_none_subtitle", + "Warp Agent takes no actions without your approval.", + ), self.autonomy_none_mouse_state.clone(), ), ]; @@ -798,7 +832,9 @@ impl AgentSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label( + crate::menu_label("common.back", "Back").into(), + ), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -813,7 +849,9 @@ impl AgentSlide { let next_button = self.next_button.render( appearance, button::Params { - content: button::Content::Label("Next".into()), + content: button::Content::Label( + crate::menu_label("common.next", "Next").into(), + ), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), diff --git a/crates/onboarding/src/slides/ai_access_slide.rs b/crates/onboarding/src/slides/ai_access_slide.rs index d1025ca6a6..183996eda9 100644 --- a/crates/onboarding/src/slides/ai_access_slide.rs +++ b/crates/onboarding/src/slides/ai_access_slide.rs @@ -38,6 +38,8 @@ pub enum AiAccessSlideAction { /// reference them directly. #[derive(Debug, Clone)] pub enum AiAccessSlideEvent { + AddApiKeyRequested, + AddCustomEndpointRequested, CopyUpgradeUrlRequested, PasteAuthTokenFromClipboardRequested, } @@ -105,7 +107,7 @@ impl AiAccessSlide { let title = appearance .ui_builder() - .paragraph("Get AI access") + .paragraph(crate::menu_label("onboarding.ai_access.choose_how", "Get AI access")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -115,7 +117,10 @@ impl AiAccessSlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Save with a recurring plan, or explore Warp's AI before committing.", + crate::menu_label( + "onboarding.ai_access.subtitle", + "Save with a recurring plan, or explore Warp's AI before committing.", + ), appearance.ui_font_family(), 16., ) @@ -215,7 +220,7 @@ impl AiAccessSlide { let label = appearance .ui_builder() - .paragraph("Subscription") + .paragraph(crate::menu_label("onboarding.ai_access.subscription", "Subscription")) .with_style(UiComponentStyles { font_size: Some(16.), font_weight: Some(Weight::Semibold), @@ -229,7 +234,7 @@ impl AiAccessSlide { let green = theme.ansi_fg_green(); let badge_text = appearance .ui_builder() - .paragraph("Best value") + .paragraph(crate::menu_label("onboarding.ai_access.best_value", "Best value")) .with_style(UiComponentStyles { font_size: Some(12.), font_weight: Some(Weight::Normal), @@ -255,8 +260,10 @@ impl AiAccessSlide { .finish(); let description = FormattedTextElement::from_str( - "Starting at $18 / mo, available with monthly or annual plans. Includes base credits, \ - frontier models, cloud agents, collaboration, and more.", + crate::menu_label( + "onboarding.ai_access.subscription_description", + "Starting at $18 / mo, available with monthly or annual plans. Includes base credits, frontier models, cloud agents, collaboration, and more.", + ), appearance.ui_font_family(), 14., ) @@ -298,7 +305,7 @@ impl AiAccessSlide { let label = appearance .ui_builder() - .paragraph("Set up later") + .paragraph(crate::menu_label("onboarding.ai_access.set_up_later_label", "Set up later")) .with_style(UiComponentStyles { font_size: Some(16.), font_weight: Some(Weight::Semibold), @@ -309,8 +316,10 @@ impl AiAccessSlide { .finish(); let description = FormattedTextElement::from_str( - "Explore Warp's built-in AI features before committing to a plan, or bring your own \ - inference.", + crate::menu_label( + "onboarding.ai_access.set_up_later_description", + "Explore Warp's built-in AI features before committing to a plan, or bring your own inference.", + ), appearance.ui_font_family(), 14., ) @@ -340,7 +349,9 @@ impl AiAccessSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label( + crate::menu_label("onboarding.ai_access.back", "Back").into(), + ), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -355,7 +366,9 @@ impl AiAccessSlide { let next_button = self.next_button.render( appearance, button::Params { - content: button::Content::Label("Next".into()), + content: button::Content::Label( + crate::menu_label("onboarding.ai_access.next", "Next").into(), + ), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), @@ -418,7 +431,11 @@ impl AiAccessSlide { let copy_url_link = ui_builder .link( - "copy the URL".into(), + crate::menu_label( + "onboarding.ai_access.auth_prompt_bar.copy_url_link", + "copy the URL", + ) + .into(), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(AiAccessSlideAction::CopyUpgradeUrlClicked); @@ -432,7 +449,11 @@ impl AiAccessSlide { let paste_token_link = ui_builder .link( - "Click here".into(), + crate::menu_label( + "onboarding.ai_access.auth_prompt_bar.paste_token_link", + "Click here", + ) + .into(), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action( @@ -452,7 +473,10 @@ impl AiAccessSlide { .with_child( Container::new( ui_builder - .span("If your browser hasn't launched, ") + .span(crate::menu_label( + "onboarding.ai_access.auth_prompt_bar.browser_not_launched_prefix", + "If your browser hasn't launched, ", + )) .with_style(text_styles) .build() .finish(), @@ -463,7 +487,10 @@ impl AiAccessSlide { .with_child(copy_url_link) .with_child( ui_builder - .span(" and open the page manually. ") + .span(crate::menu_label( + "onboarding.ai_access.auth_prompt_bar.open_manually", + " and open the page manually. ", + )) .with_style(text_styles) .build() .finish(), @@ -471,7 +498,10 @@ impl AiAccessSlide { .with_child(paste_token_link) .with_child( ui_builder - .span(" to paste your token from the browser.") + .span(crate::menu_label( + "onboarding.ai_access.auth_prompt_bar.paste_token_suffix", + " to paste your token from the browser.", + )) .with_style(text_styles) .build() .finish(), @@ -550,6 +580,19 @@ impl AiAccessSlide { }); } + /// Updates the BYOK key/endpoint counts driving the AI-access slide's + /// "connected" status line and gating "Next" on the bring-your-own path. + /// The upstream Subscription/Set-up-later flow doesn't surface these + /// counts, so the call is a no-op here but kept on the public API so the + /// app crate can keep wiring `ApiKeyManager` updates through unchanged. + pub(crate) fn set_byok_status( + &mut self, + _key_count: usize, + _endpoint_count: usize, + _ctx: &mut ViewContext, + ) { + } + /// Primary "Next" action. On the subscription path this advances when the /// user already has a plan, otherwise it launches checkout in the browser /// (the slide auto-advances once billing flips to a paying plan). The "Set diff --git a/crates/onboarding/src/slides/ai_setup_slide.rs b/crates/onboarding/src/slides/ai_setup_slide.rs index b80d4f3db3..19043c1474 100644 --- a/crates/onboarding/src/slides/ai_setup_slide.rs +++ b/crates/onboarding/src/slides/ai_setup_slide.rs @@ -24,12 +24,27 @@ use crate::model::{AiSetupChoice, OnboardingStateModel}; use crate::slides::{bottom_nav, layout, slide_content}; /// Checklist shown on the "Use Warp agent" card. -const WARP_AGENT_FEATURES: &[&str] = &[ - "Best harness for terminal tasks and agentic coding", - "Frontier models from OpenAI, Anthropic, and Google", - "Model routing across frontier and open-weight models", - "Multi-agent orchestration", -]; +/// Localized at call time because `crate::menu_label` is a runtime lookup. +fn warp_agent_features() -> [&'static str; 4] { + [ + crate::menu_label( + "onboarding.ai_setup.feature_harness", + "Best harness for terminal tasks and agentic coding", + ), + crate::menu_label( + "onboarding.ai_setup.feature_models", + "Frontier models from OpenAI, Anthropic, and Google", + ), + crate::menu_label( + "onboarding.ai_setup.feature_routing", + "Model routing across frontier and open-weight models", + ), + crate::menu_label( + "onboarding.ai_setup.feature_orchestration", + "Multi-agent orchestration", + ), + ] +} #[derive(Debug, Clone)] pub enum AiSetupSlideAction { @@ -106,7 +121,10 @@ impl AiSetupSlide { let title = appearance .ui_builder() - .paragraph("Choose your AI setup") + .paragraph(crate::menu_label( + "onboarding.ai_setup.choose_your_setup", + "Choose your AI setup", + )) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -116,7 +134,10 @@ impl AiSetupSlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Choose if you'd like to use Warp Agent or third party agents.", + crate::menu_label( + "onboarding.ai_setup.subtitle", + "Choose if you'd like to use Warp Agent or third party agents.", + ), appearance.ui_font_family(), 16., ) @@ -227,7 +248,10 @@ impl AiSetupSlide { let header_row = { let label = appearance .ui_builder() - .paragraph("Use Warp Agent") + .paragraph(crate::menu_label( + "onboarding.ai_setup.use_warp_agent", + "Use Warp Agent", + )) .with_style(UiComponentStyles { font_size: Some(16.), font_weight: Some(Weight::Semibold), @@ -241,7 +265,10 @@ impl AiSetupSlide { let green = theme.ansi_fg_green(); let badge_text = appearance .ui_builder() - .paragraph("Access more models") + .paragraph(crate::menu_label( + "onboarding.ai_setup.access_more_models", + "Access more models", + )) .with_style(UiComponentStyles { font_size: Some(12.), font_weight: Some(Weight::Normal), @@ -268,7 +295,10 @@ impl AiSetupSlide { }; let description = FormattedTextElement::from_str( - "State of the art agent harness deeply integrated into the terminal.", + crate::menu_label( + "onboarding.ai_setup.warp_agent_description", + "State of the art agent harness deeply integrated into the terminal.", + ), appearance.ui_font_family(), 14., ) @@ -289,7 +319,7 @@ impl AiSetupSlide { let mut col = Flex::column() .with_main_axis_size(MainAxisSize::Min) .with_cross_axis_alignment(CrossAxisAlignment::Start); - for &item in WARP_AGENT_FEATURES { + for item in warp_agent_features() { let icon_el = ConstrainedBox::new(Icon::Check.to_warpui_icon(check_fill).finish()) .with_width(16.) .with_height(16.) @@ -354,7 +384,10 @@ impl AiSetupSlide { let label = appearance .ui_builder() - .paragraph("Use third party agents") + .paragraph(crate::menu_label( + "onboarding.ai_setup.use_third_party", + "Use third party agents", + )) .with_style(UiComponentStyles { font_size: Some(16.), font_weight: Some(Weight::Semibold), @@ -365,7 +398,10 @@ impl AiSetupSlide { .finish(); let description = FormattedTextElement::from_str( - "Use agents like Claude Code, Codex, and Gemini.", + crate::menu_label( + "onboarding.ai_setup.third_party_description", + "Use agents like Claude Code, Codex, and Gemini.", + ), appearance.ui_font_family(), 14., ) @@ -395,7 +431,9 @@ impl AiSetupSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label( + crate::menu_label("common.back", "Back").into(), + ), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -410,7 +448,9 @@ impl AiSetupSlide { let next_button = self.next_button.render( appearance, button::Params { - content: button::Content::Label("Next".into()), + content: button::Content::Label( + crate::menu_label("common.next", "Next").into(), + ), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), diff --git a/crates/onboarding/src/slides/customize_slide.rs b/crates/onboarding/src/slides/customize_slide.rs index b8e159a9d4..310a7da05a 100644 --- a/crates/onboarding/src/slides/customize_slide.rs +++ b/crates/onboarding/src/slides/customize_slide.rs @@ -147,7 +147,7 @@ impl CustomizeUISlide { fn render_header(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Customize your Warp") + .paragraph(crate::menu_label("onboarding.customize.title", "Customize your Warp")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -157,7 +157,10 @@ impl CustomizeUISlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Tailor your features and UI to your working style.", + crate::menu_label( + "onboarding.customize.subtitle", + "Tailor your features and UI to your working style.", + ), appearance.ui_font_family(), 16., ) @@ -219,11 +222,14 @@ impl CustomizeUISlide { render_toggle_card( appearance, ToggleCardSpec { - title: "Tab styling", + title: crate::menu_label("onboarding.customize.tab_styling_title", "Tab styling"), is_expanded: is_selected, is_left_selected: ui.use_vertical_tabs, - left_label: "Vertical", - right_label: "Horizontal", + left_label: crate::menu_label("onboarding.customize.tab_vertical", "Vertical"), + right_label: crate::menu_label( + "onboarding.customize.tab_horizontal", + "Horizontal", + ), card_mouse_state: self.tab_styling_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(CustomizeSlideAction::SelectSettingCard { @@ -259,7 +265,10 @@ impl CustomizeUISlide { if ui.tools_panel_enabled(&intention) { chips.push(ChipSpec { - label: "File explorer", + label: crate::menu_label( + "onboarding.customize.tools_chip_file_explorer", + "File explorer", + ), is_enabled: ui.show_project_explorer, mouse_state: self.chip_file_explorer_mouse.clone(), on_click: Box::new(|ctx, _, _| { @@ -279,7 +288,10 @@ impl CustomizeUISlide { // Conversation history chip is only shown for the agent intention. if is_agent { chips.push(ChipSpec { - label: "Conversation history", + label: crate::menu_label( + "onboarding.customize.tools_chip_conversation_history", + "Conversation history", + ), is_enabled: ui.show_conversation_history, mouse_state: self.chip_conversation_mouse.clone(), on_click: Box::new(|ctx, _, _| { @@ -298,7 +310,10 @@ impl CustomizeUISlide { } chips.push(ChipSpec { - label: "Global file search", + label: crate::menu_label( + "onboarding.customize.tools_chip_global_search", + "Global file search", + ), is_enabled: ui.show_global_search, mouse_state: self.chip_global_search_mouse.clone(), on_click: Box::new(|ctx, _, _| { @@ -316,7 +331,10 @@ impl CustomizeUISlide { }); chips.push(ChipSpec { - label: "Warp Drive", + label: crate::menu_label( + "onboarding.customize.tools_chip_warp_drive", + "Warp Drive", + ), is_enabled: ui.show_warp_drive, mouse_state: self.chip_warp_drive_mouse.clone(), on_click: Box::new(|ctx, _, _| { @@ -337,11 +355,14 @@ impl CustomizeUISlide { render_toggle_card( appearance, ToggleCardSpec { - title: "Tools panel", + title: crate::menu_label( + "onboarding.customize.tools_panel_title", + "Tools panel", + ), is_expanded: is_selected, is_left_selected: ui.tools_panel_enabled(&intention), - left_label: "Enabled", - right_label: "Disabled", + left_label: crate::menu_label("common.enabled", "Enabled"), + right_label: crate::menu_label("common.disabled", "Disabled"), card_mouse_state: self.tools_panel_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(CustomizeSlideAction::SelectSettingCard { @@ -375,11 +396,14 @@ impl CustomizeUISlide { render_toggle_card( appearance, ToggleCardSpec { - title: "Code review", + title: crate::menu_label( + "onboarding.customize.code_review_title", + "Code review", + ), is_expanded: is_selected, is_left_selected: ui.show_code_review_button, - left_label: "Enabled", - right_label: "Disabled", + left_label: crate::menu_label("common.enabled", "Enabled"), + right_label: crate::menu_label("common.disabled", "Disabled"), card_mouse_state: self.code_review_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(CustomizeSlideAction::SelectSettingCard { @@ -409,7 +433,9 @@ impl CustomizeUISlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label( + crate::menu_label("common.back", "Back").into(), + ), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -424,7 +450,9 @@ impl CustomizeUISlide { let next_button = self.next_button.render( appearance, button::Params { - content: button::Content::Label("Next".into()), + content: button::Content::Label( + crate::menu_label("common.next", "Next").into(), + ), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), diff --git a/crates/onboarding/src/slides/intention_slide.rs b/crates/onboarding/src/slides/intention_slide.rs index 78e4b1e6e4..fa1956011c 100644 --- a/crates/onboarding/src/slides/intention_slide.rs +++ b/crates/onboarding/src/slides/intention_slide.rs @@ -83,7 +83,7 @@ impl IntentionSlide { let title = appearance .ui_builder() - .paragraph("Welcome to Warp") + .paragraph(crate::menu_label("onboarding.intention.title", "Welcome to Warp")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -93,7 +93,7 @@ impl IntentionSlide { .finish(); let subtitle = FormattedTextElement::from_str( - "How do you want to work?", + crate::menu_label("onboarding.intention.subtitle", "How do you want to work?"), appearance.ui_font_family(), 16., ) @@ -202,7 +202,10 @@ impl IntentionSlide { let header_row = { let label = appearance .ui_builder() - .paragraph("Build faster with agents") + .paragraph(crate::menu_label( + "onboarding.intention.agent_title", + "Build faster with agents", + )) .with_style(UiComponentStyles { font_size: Some(16.), font_weight: Some(Weight::Semibold), @@ -240,7 +243,10 @@ impl IntentionSlide { }; let description = FormattedTextElement::from_str( - "Get AI features to accelerate terminal and agent-driven workflows:", + crate::menu_label( + "onboarding.intention.agent_description", + "Get AI features to accelerate terminal and agent-driven workflows:", + ), appearance.ui_font_family(), 14., ) @@ -321,7 +327,10 @@ impl IntentionSlide { let label = appearance .ui_builder() - .paragraph("Just use the terminal") + .paragraph(crate::menu_label( + "onboarding.intention.terminal_title", + "Just use the terminal", + )) .with_style(UiComponentStyles { font_size: Some(16.), font_weight: Some(Weight::Semibold), @@ -334,7 +343,10 @@ impl IntentionSlide { let badge = { let badge_text = appearance .ui_builder() - .paragraph("No AI features") + .paragraph(crate::menu_label( + "onboarding.intention.terminal_badge", + "No AI features", + )) .with_style(UiComponentStyles { font_size: Some(12.), font_weight: Some(Weight::Semibold), @@ -360,7 +372,10 @@ impl IntentionSlide { .finish(); let description = FormattedTextElement::from_str( - "A modern terminal optimized for speed, context, and control without AI.", + crate::menu_label( + "onboarding.intention.terminal_description", + "A modern terminal optimized for speed, context, and control without AI.", + ), appearance.ui_font_family(), 14., ) @@ -388,7 +403,9 @@ impl IntentionSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label( + crate::menu_label("common.back", "Back").into(), + ), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -401,9 +418,9 @@ impl IntentionSlide { let new_settings_modes = FeatureFlag::OpenWarpNewSettingsModes.is_enabled(); let next_text = if !new_settings_modes && selected_index == 1 { - "Get Warping" + crate::menu_label("common.get_warping", "Get Warping") } else { - "Next" + crate::menu_label("common.next", "Next") }; let enter = Keystroke::parse("enter").unwrap_or_default(); let next_button = self.next_button.render( diff --git a/crates/onboarding/src/slides/intro_slide.rs b/crates/onboarding/src/slides/intro_slide.rs index c64778d70f..8c47b6f7a0 100644 --- a/crates/onboarding/src/slides/intro_slide.rs +++ b/crates/onboarding/src/slides/intro_slide.rs @@ -82,7 +82,10 @@ impl View for IntroSlide { let login_row = Flex::row() .with_child( ui_builder - .span("Already have an account? ") + .span(crate::menu_label( + "onboarding.intro.have_account_label", + "Already have an account? ", + )) .with_style(disclaimer_styles) .build() .finish(), @@ -90,7 +93,7 @@ impl View for IntroSlide { .with_child( ui_builder .link( - "Log in".into(), + crate::menu_label("onboarding.intro.log_in", "Log in").into(), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(IntroSlideAction::LoginClicked); @@ -151,7 +154,7 @@ impl IntroSlide { let base_color: ColorU = internal_colors::fg_overlay_4(theme).into(); let shimmer_color: ColorU = theme.foreground().into(); let title = ShimmeringTextElement::new( - "Welcome to Warp", + crate::menu_label("onboarding.intro.title", "Welcome to Warp"), appearance.ui_font_family(), 32., base_color, @@ -163,7 +166,10 @@ impl IntroSlide { let subtitle_color = internal_colors::text_sub(theme, theme.background().into_solid()); let subtitle = FormattedTextElement::from_str( - "A modern terminal with state of the art agents built in.", + crate::menu_label( + "onboarding.intro.subtitle", + "A modern terminal with state of the art agents built in.", + ), appearance.ui_font_family(), 16., ) @@ -176,7 +182,9 @@ impl IntroSlide { let get_started_button = self.get_started_button.render( appearance, button::Params { - content: button::Content::Label("Get started".into()), + content: button::Content::Label( + crate::menu_label("common.get_started", "Get started").into(), + ), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), diff --git a/crates/onboarding/src/slides/project_slide.rs b/crates/onboarding/src/slides/project_slide.rs index 16c8fae995..7087c723ff 100644 --- a/crates/onboarding/src/slides/project_slide.rs +++ b/crates/onboarding/src/slides/project_slide.rs @@ -124,7 +124,7 @@ impl ProjectSlide { fn render_header(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Open a project") + .paragraph(crate::menu_label("onboarding.project.title", "Open a project")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -135,7 +135,10 @@ impl ProjectSlide { let subtitle = appearance .ui_builder() - .paragraph("Set up a project to optimize it for coding in Warp.") + .paragraph(crate::menu_label( + "onboarding.project.subtitle", + "Set up a project to optimize it for coding in Warp.", + )) .with_style(UiComponentStyles { font_size: Some(20.), font_weight: Some(Weight::Normal), @@ -213,7 +216,10 @@ impl ProjectSlide { let folder_text = Container::new( appearance .ui_builder() - .paragraph("Open local folder") + .paragraph(crate::menu_label( + "onboarding.project.open_local_folder", + "Open local folder", + )) .with_style(UiComponentStyles { font_color: Some(text_color), ..Default::default() @@ -280,7 +286,9 @@ impl ProjectSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label( + crate::menu_label("common.back", "Back").into(), + ), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -297,15 +305,15 @@ impl ProjectSlide { let (label, keystroke, action) = match settings { ProjectOnboardingSettings::Project { .. } => ( if theme_picker_last { - "Next" + crate::menu_label("common.next", "Next") } else { - "Get Warping" + crate::menu_label("common.get_warping", "Get Warping") }, Keystroke::parse("enter").unwrap_or_default(), ProjectSlideAction::NextClicked, ), ProjectOnboardingSettings::NoProject => ( - "Skip", + crate::menu_label("common.skip", "Skip"), Keystroke::parse("cmdorctrl-enter").unwrap_or_default(), ProjectSlideAction::SkipClicked, ), @@ -408,8 +416,14 @@ impl ProjectSlide { appearance, self.initialize_projects_automatically_mouse_state.clone(), initialize_projects_automatically, - "Initialize project automatically", - "Prepares the project environment, builds an index of your code, and generates project rules—giving the agent deeper understanding and better performance.", + crate::menu_label( + "onboarding.project.initialize_title", + "Initialize project automatically", + ), + crate::menu_label( + "onboarding.project.initialize_description", + "Prepares the project environment, builds an index of your code, and generates project rules—giving the agent deeper understanding and better performance.", + ), ProjectSlideAction::ToggleInitializeProjectsAutomatically, ); diff --git a/crates/onboarding/src/slides/theme_picker_slide.rs b/crates/onboarding/src/slides/theme_picker_slide.rs index af67bd8a34..53b8a5c6c2 100644 --- a/crates/onboarding/src/slides/theme_picker_slide.rs +++ b/crates/onboarding/src/slides/theme_picker_slide.rs @@ -194,7 +194,7 @@ impl ThemePickerSlide { fn render_header_text(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Choose a theme") + .paragraph(crate::menu_label("onboarding.theme_picker.title", "Choose a theme")) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -204,7 +204,10 @@ impl ThemePickerSlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Click or use arrow keys to select, Enter to confirm.", + crate::menu_label( + "onboarding.theme_picker.subtitle", + "Click or use arrow keys to select, Enter to confirm.", + ), appearance.ui_font_family(), 16., ) @@ -261,7 +264,9 @@ impl ThemePickerSlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label( + crate::menu_label("common.back", "Back").into(), + ), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -274,9 +279,9 @@ impl ThemePickerSlide { let theme_picker_last = FeatureFlag::OpenWarpNewSettingsModes.is_enabled(); let next_label = if theme_picker_last { - "Get Warping" + crate::menu_label("common.get_warping", "Get Warping") } else { - "Next" + crate::menu_label("common.next", "Next") }; let enter = Keystroke::parse("enter").unwrap_or_default(); @@ -532,7 +537,10 @@ impl ThemePickerSlide { .finish(); let label = Text::new( - "Sync light/dark theme with OS", + crate::menu_label( + "onboarding.theme_picker.sync_with_os", + "Sync light/dark theme with OS", + ), appearance.ui_font_family(), 14.0, ) @@ -576,7 +584,10 @@ impl ThemePickerSlide { let privacy_line = Flex::row() .with_child( ui_builder - .span("If you'd like to opt out of analytics, you can adjust your ") + .span(crate::menu_label( + "onboarding.theme_picker.analytics_opt_out_prefix", + "If you'd like to opt out of analytics, you can adjust your ", + )) .with_style(disclaimer_styles) .build() .finish(), @@ -584,7 +595,11 @@ impl ThemePickerSlide { .with_child( ui_builder .link( - "Privacy Settings".into(), + crate::menu_label( + "onboarding.theme_picker.privacy_settings", + "Privacy Settings", + ) + .into(), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action( @@ -603,7 +618,10 @@ impl ThemePickerSlide { let tos_line = Flex::row() .with_child( ui_builder - .span("By continuing, you agree to Warp's ") + .span(crate::menu_label( + "onboarding.theme_picker.tos_prefix", + "By continuing, you agree to Warp's ", + )) .with_style(disclaimer_styles) .build() .finish(), @@ -611,7 +629,11 @@ impl ThemePickerSlide { .with_child( ui_builder .link( - "Terms of Service".into(), + crate::menu_label( + "onboarding.theme_picker.terms_of_service", + "Terms of Service", + ) + .into(), Some(TOS_URL.into()), None, self.tos_mouse_state.clone(), diff --git a/crates/onboarding/src/slides/third_party_slide.rs b/crates/onboarding/src/slides/third_party_slide.rs index 4a2bca5cac..96868c46af 100644 --- a/crates/onboarding/src/slides/third_party_slide.rs +++ b/crates/onboarding/src/slides/third_party_slide.rs @@ -137,7 +137,10 @@ impl ThirdPartySlide { fn render_header(&self, appearance: &Appearance) -> Box { let title = appearance .ui_builder() - .paragraph("Customize third party agents") + .paragraph(crate::menu_label( + "onboarding.third_party.title", + "Customize third party agents", + )) .with_style(UiComponentStyles { font_size: Some(36.), font_weight: Some(Weight::Medium), @@ -147,7 +150,10 @@ impl ThirdPartySlide { .finish(); let subtitle = FormattedTextElement::from_str( - "Select defaults for using agents like Claude Code, Codex, and Gemini.", + crate::menu_label( + "onboarding.third_party.subtitle", + "Select defaults for using agents like Claude Code, Codex, and Gemini.", + ), appearance.ui_font_family(), 16., ) @@ -178,11 +184,14 @@ impl ThirdPartySlide { let card = render_toggle_card( appearance, ToggleCardSpec { - title: "CLI agent toolbar", + title: crate::menu_label( + "onboarding.third_party.cli_toolbar_title", + "CLI agent toolbar", + ), is_expanded: is_selected, is_left_selected: cli_toolbar_enabled, - left_label: "Enabled", - right_label: "Disabled", + left_label: crate::menu_label("common.enabled", "Enabled"), + right_label: crate::menu_label("common.disabled", "Disabled"), card_mouse_state: self.cli_toolbar_card_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(ThirdPartySlideAction::SelectSettingCard { @@ -226,11 +235,14 @@ impl ThirdPartySlide { let card = render_toggle_card( appearance, ToggleCardSpec { - title: "Notifications", + title: crate::menu_label( + "onboarding.third_party.notifications_title", + "Notifications", + ), is_expanded: is_selected, is_left_selected: show_agent_notifications, - left_label: "Enabled", - right_label: "Disabled", + left_label: crate::menu_label("common.enabled", "Enabled"), + right_label: crate::menu_label("common.disabled", "Disabled"), card_mouse_state: self.notifications_card_mouse_state.clone(), on_expand: Box::new(|ctx, _, _| { ctx.dispatch_typed_action(ThirdPartySlideAction::SelectSettingCard { @@ -268,7 +280,9 @@ impl ThirdPartySlide { let back_button = self.back_button.render( appearance, button::Params { - content: button::Content::Label("Back".into()), + content: button::Content::Label( + crate::menu_label("common.back", "Back").into(), + ), theme: &button::themes::Naked, options: button::Options { on_click: Some(Box::new(|ctx, _app, _pos| { @@ -283,7 +297,9 @@ impl ThirdPartySlide { let next_button = self.next_button.render( appearance, button::Params { - content: button::Content::Label("Next".into()), + content: button::Content::Label( + crate::menu_label("common.next", "Next").into(), + ), theme: &button::themes::Primary, options: button::Options { keystroke: Some(enter), diff --git a/resources/bundled/locales/en.yml b/resources/bundled/locales/en.yml new file mode 100644 index 0000000000..110e184d13 --- /dev/null +++ b/resources/bundled/locales/en.yml @@ -0,0 +1,2436 @@ +en: + # ── Menu Bar ── + menu: + warp: "Warp" + file: "File" + edit: "Edit" + view: "View" + tab: "Tab" + ai: "AI" + blocks: "Blocks" + drive: "Drive" + window: "Window" + help: "Help" + + new_window: "New Window" + new_tab: "New Tab" + new_terminal_tab: "New Terminal Tab" + new_agent_tab: "New Agent Tab" + preferences: "Preferences" + privacy_policy: "Privacy Policy..." + set_default_terminal: "Set Warp as Default Terminal" + log_out: "Log out" + open_recent: "Open Recent" + + copy_on_select: "Copy on Select within the Terminal" + synchronize_inputs: "Synchronize Inputs" + + toggle_mouse_reporting: "Toggle Mouse Reporting" + toggle_scroll_reporting: "Toggle Scroll Reporting" + toggle_focus_reporting: "Toggle Focus Reporting" + compact_mode: "Compact Mode" + + show_init_block: "Show Initialization Block" + hide_init_block: "Hide Initialization Block" + show_ssh_blocks: "Show Warpified SSH Blocks" + hide_ssh_blocks: "Hide Warpified SSH Blocks" + show_in_band_command_blocks: "Show In-band Command Blocks" + hide_in_band_command_blocks: "Hide In-band Command Blocks" + export_settings_csv: "Export Default Settings as CSV to home dir" + + enable_shell_debug_mode: "Enable Shell Debug Mode (-x) for New Sessions" + disable_shell_debug_mode: "Disable Shell Debug Mode (-x) for New Sessions" + enable_in_band_generators: "Enable In-band Generators for New Sessions" + disable_in_band_generators: "Disable in-band generators for new sessions" + enable_pty_recording: "Enable PTY Recording Mode (warp.pty.recording)" + disable_pty_recording: "Disable PTY Recording Mode (warp.pty.recording)" + manually_toggle_network: "Manually Toggle Network Status" + debug: "Debug" + use_warp_prompt: "Use Warp's Prompt" + create_anonymous_user: "Create anonymous user" + send_feedback: "Send Feedback..." + warp_documentation: "Warp Documentation..." + github_issues: "GitHub Issues..." + warp_slack_community: "Warp Slack Community..." + save_new: "Save New..." + reopen_closed_session: "Reopen closed session" + launch_configurations: "Launch Configurations" + + # ── Tab Context Menu ── + tab: + rename: "Rename tab" + reset_name: "Reset tab name" + move_up: "Move Tab Up" + move_down: "Move Tab Down" + move_left: "Move Tab Left" + move_right: "Move Tab Right" + close: "Close tab" + close_other: "Close other tabs" + close_below: "Close Tabs Below" + close_right: "Close Tabs to the Right" + save_as_new_config: "Save as new config" + stop_sharing: "Stop sharing" + share_session: "Share session" + stop_sharing_all: "Stop sharing all" + copy_link: "Copy link" + copy_title: "Copy tab title" + copy_pane_title: "Copy pane title" + copy_branch: "Copy branch" + copy_working_dir: "Copy working directory" + copy_pr_link: "Copy pull request link" + default_no_color: "Default (no color)" + cloud_agent_run: "Cloud agent run" + + # ── Workspace / Toolbar ── + terminal: + share_block: + command: "Command" + command_and_output: "Command and Output" + copy: "Copy" + create_link: "Create link" + creating_block: "Creating block..." + get_embed: "Get embed" + keybinding_copy: "Copy" + link_copied: "Link copied." + output: "Output" + share_block: "Share block" + show_prompt: "Show prompt" + title_placeholder: "Title (optional)" + workspace: + settings: "Settings" + new_tab: "New Tab" + agent: "Agent" + terminal: "Terminal" + cloud_oz: "Cloud Oz" + local_docker_sandbox: "Local Docker Sandbox" + whats_new: "What's new" + keyboard_shortcuts: "Keyboard shortcuts" + tab_configs: "Tab configs" + new_worktree_config: "New worktree config" + new_tab_config: "New tab config" + reopen_closed_session: "Reopen closed session" + rearrange_toolbar_items: "Re-arrange toolbar items" + update_warp_manually: "Update Warp manually" + update_and_relaunch_warp: "Update and relaunch Warp" + documentation: "Documentation" + feedback: "Feedback" + view_warp_logs: "View Warp logs" + slack: "Slack" + sign_up: "Sign up" + billing_and_usage: "Billing and usage" + upgrade: "Upgrade" + upgrade_plan: "Upgrade plan" + invite_a_friend: "Invite a friend" + log_out: "Log out" + cloud_agent: "Cloud Agent" + current_version_is: "Current version is {version}" + install_update: "Install update ({version})" + updating_to: "Updating to ({version})" + install_update_tab: "Install Update" + reset_pane_name: "Reset pane name" + reset_active_pane_name: "Reset active pane name" + staging_api_call_failed: "Staging API call failed. Did your IP address change?" + failed_load_conversation_data: "Failed to load conversation data." + remote_control_link_copied: "Remote control link copied." + untitled_pane: "Untitled pane" + view_all: "View all" + show_less: "Show less" + no_conversations_yet: "No conversations yet" + conversations_empty_description: "Your active and past conversations with local and ambient agents will appear here." + new_conversation: "New conversation" + conversation: "Conversation" + conversations_delete_in_progress: "Conversations cannot be deleted while in progress." + conversation_cannot_be_deleted: "This conversation cannot be deleted" + delete_conversation_confirm_title: "Delete '{title}'?" + delete_conversation_confirm_title_unknown: "Delete conversation?" + delete_conversation_confirm_body: "This conversation will be permanently deleted. This action cannot be undone." + share_conversation: "Share conversation" + fork_in_new_pane: "Fork in new pane" + fork_in_new_tab: "Fork in new tab" + no_matching_conversations: "No matching conversations" + view_options: "View options" + no_tabs_open: "No tabs open" + no_tabs_match_search: "No tabs match your search." + view_as: "View as" + tab_item: "Tab item" + pane_title_as: "Pane title as" + additional_metadata: "Additional metadata" + failed_create_log_bundle: "Failed to create log bundle: {err}" + successfully_installed_oz_cli: "Successfully installed the Oz CLI! You can now run '{command_name}' from the command line." + learn_more: "Learn more" + failed_install_oz_command: "Failed to install Oz command: {error}" + successfully_uninstalled_oz_command: "Successfully uninstalled the Oz command." + failed_uninstall_oz_command: "Failed to uninstall Oz command: {error}" + notifications_permission_denied: "Warp doesn't have permission to send desktop notifications." + close_panel: "Close panel" + troubleshoot_notifications: "Troubleshoot notifications" + view_changelog: "View changelog" + view_all_cloud_runs: "View all cloud runs" + warp_updated: "Warp updated!" + command_still_running: "A command in this session is still running." + cannot_open_new_terminal_session: "Cannot open a new terminal session" + workflow_no_longer_available: "This workflow is no longer available." + plan_synced_to_warp_drive: "Plan synced to your Warp Drive" + view: "View" + undo: "Undo" + open_file: "Open file" + open: "Open: %s" + rename_pane: "Rename pane" + rename_active_pane: "Rename active pane" + rename_current_tab: "Rename the current tab" + rename_current_pane: "Rename the current pane" + tab_configs_chip: "Access your tab configs here." + add_new_repo: " + Add new repo" + failed_load_conversation: "Failed to load conversation." + failed_load_conversation_for_forking: "Failed to load conversation for forking." + conversation_forking_failed: "Conversation forking failed." + failed_prepare_handoff: "Failed to prepare handoff. Please try again." + handing_off_to_cloud: "Handing off to cloud" + no_terminal_pane_for_context: "No terminal pane open. Open a new pane to attach as context." + plan_already_in_context: "This plan is already in context." + check_latest_version: "Check out the latest version and try again." + ask_ai_description: "Ask Warp AI to explain errors, suggest commands or write scripts." + search_sessions_agents_files: "Search sessions, agents, files..." + disabled_synchronized_inputs: "Disabled all synchronized inputs." + sampling_process: "Sampling process for 3 seconds..." + failed_delete_conversation: "Failed to delete conversation. Please exit the agent view and try again." + conversation_deleted: "Conversation deleted" + update_warp: "Update Warp" + version_deprecation: "Your app is out of date and some features may not work as expected. Please update immediately." + version_deprecation_without_permissions: "Some Warp features may not work as expected without updating immediately, but Warp is unable to perform the update." + unable_update_new_version: "A new version is available but Warp is unable to perform the update." + unable_launch_new_version: "Warp was unable to launch the new installed version." + update_now: "Update now" + app_needs_update: "Your app is out of date and needs to update." + restart_and_update_now: "Restart app and update now" + search_repos: "Search repos" + search_tabs: "Search tabs..." + feedback_placeholder: "Describe what's broken, confusing, or missing..." + resource_not_found: "Resource not found or access denied" + conversation_not_synced: "Your conversation hasn't synced to the cloud yet. Try sending another message, then hand off again." + couldnt_save_conversation: "Couldn't save your conversation locally. Try sending another message, then hand off again." + failed_sample_process: "Failed to sample process (check logs)" + agent_management_panel: "Agent management panel" + tabs_panel: "Tabs panel" + project_explorer: "Project explorer" + global_search: "Global search" + warp_drive: "Warp Drive" + agent_conversations: "Agent conversations" + tools_panel: "Tools panel" + code_review_panel: "Code review panel" + notifications: "Notifications" + warp_essentials: "Warp Essentials" + settings_tooltip: "Settings" + login_expired: "Your login has expired." + please_sign_in_again: "Please sign in again to restore access to cloud-based features." + sign_in: "Sign in" + fix_with_oz: "Fix with Oz" + linear_issue: "Linear Issue" + more_info: "More info" + + # ── Workspace Conversation List ── + workspace.conversation_list: + no_conversations: "No conversations yet" + no_conversations_hint: "Your active and past conversations with local and ambient agents will appear here." + new_conversation: "New conversation" + + # ── Workspace Global Search ── + workspace.global_search: + search_in_files: "Search in files" + + # ── Workspace Bindings ── + workspace.binding: + toggle_project_explorer: "Toggle project explorer" + create_team_notebook: "Create a new team notebook" + create_personal_notebook: "Create a new personal notebook" + create_team_workflow: "Create a new team workflow" + create_personal_workflow: "Create a new personal workflow" + create_team_folder: "Create a new team folder" + create_personal_folder: "Create a new personal folder" + create_new_tab: "Create new tab" + new_terminal_tab: "New Terminal Tab" + new_agent_tab: "New Agent Tab" + new_cloud_agent_tab: "New Cloud Agent Tab" + open_left_panel: "Open Left Panel" + toggle_code_review: "Toggle code review" + toggle_vertical_tabs_panel: "Toggle vertical tabs panel" + left_panel_agent_conversations: "Left Panel: Agent conversations" + left_panel_project_explorer: "Left Panel: Project explorer" + left_panel_global_search: "Left Panel: Global search" + left_panel_warp_drive: "Left Panel: Warp Drive" + open_global_search: "Open global search" + toggle_warp_drive: "Toggle Warp Drive" + toggle_agent_conversation_list_view: "Toggle Agent conversation list view" + close_focused_panel: "Close focused panel" + toggle_command_palette: "Toggle command palette" + move_tab_left: "Move tab left" + move_tab_right: "Move tab right" + close_window: "Close Window" + close_tabs_to_the_right: "Close tabs to the right" + toggle_navigation_palette: "Toggle navigation palette" + create_team_env_vars: "Create new team environment variables" + create_personal_env_vars: "Create new personal environment variables" + create_personal_prompt: "Create a new personal prompt" + create_team_prompt: "Create a new team prompt" + open_repository: "Open repository" + open_ai_rules: "Open AI Rules" + open_mcp_servers: "Open MCP Servers" + switch_focus_left: "Switch Focus to Left Panel" + switch_focus_right: "Switch Focus to Right Panel" + import_to_personal_drive: "Import To Personal Drive" + import_to_team_drive: "Import To Team Drive" + open_theme_picker: "Open theme picker" + sample_process: "Sample Process" + dump_heap_profile: "Dump heap profile (can only be done once)" + open_tab_configs_menu: "Open tab configs menu" + increase_zoom_level: "Increase zoom level" + decrease_zoom_level: "Decrease zoom level" + reset_zoom_level: "Reset zoom level to default" + increase_font_size: "Increase font size" + decrease_font_size: "Decrease font size" + reset_font_size: "Reset font size to default" + toggle_keyboard_shortcuts: "Toggle keyboard shortcuts" + open_keybindings_editor: "Open keybindings editor" + toggle_sticky_command_header: "Toggle sticky command header" + rename_current_tab: "Rename the current tab" + rename_current_pane: "Rename the current pane" + quit_warp: "Quit Warp" + close_current_tab: "Close the current tab" + close_other_tabs: "Close other tabs" + turn_notifications_on: "Turn notifications on" + turn_notifications_off: "Turn notifications off" + launch_configuration_palette: "Launch configuration palette" + toggle_files_palette: "Toggle Files Palette" + save_new_launch_configuration_editable: "Save new launch configuration" + search_warp_drive: "Search Warp Drive" + install_update_and_relaunch: "Install update and relaunch" + check_for_updates: "Check for updates" + log_out: "Log out" + toggle_resource_center: "Toggle resource center" + export_warp_drive_objects: "Export all Warp Drive objects" + install_oz_cli: "Install Oz CLI command" + uninstall_oz_cli: "Uninstall Oz CLI command" + view_latest_changelog: "View latest changelog" + toggle_warp_ai: "Toggle Warp AI" + toggle_mouse_reporting: "Toggle Mouse Reporting" + activate_previous_pane: "Activate previous pane" + activate_next_pane: "Activate next pane" + switch_to_nth_tab: "Switch to {n} tab" + switch_to_last_tab: "Switch to last tab" + activate_previous_tab: "Activate previous tab" + activate_next_tab: "Activate next tab" + jump_to_latest_agent_task: "Jump to latest agent task" + toggle_notification_mailbox: "Toggle notification mailbox" + toggle_agent_management_view: "Toggle the agent management view" + open_settings: "Open Settings" + open_settings_account: "Open Settings: Account" + open_settings_appearance: "Open Settings: Appearance" + open_settings_features: "Open Settings: Features" + open_settings_shared_blocks: "Open Settings: Shared Blocks" + open_settings_keyboard_shortcuts: "Open Settings: Keyboard Shortcuts" + open_settings_about: "Open Settings: About" + open_settings_teams: "Open Settings: Teams" + open_settings_privacy: "Open Settings: Privacy" + open_settings_warpify: "Open Settings: Warpify" + open_settings_ai: "Open Settings: AI" + open_settings_billing_and_usage: "Open Settings: Billing and usage" + open_settings_code: "Open Settings: Code" + open_settings_referrals: "Open Settings: Referrals" + open_settings_environments: "Open Settings: Environments" + open_settings_mcp_servers: "Open Settings: MCP Servers" + invite_people: "Invite People..." + join_slack_community: "Join our Slack community (opens external link)" + view_user_docs: "View user docs (opens external link)" + send_feedback: "Send feedback (opens external link)" + view_warp_logs: "View Warp logs" + open_settings_file: "Open settings file" + switch_to_next_tab: "Switch to next tab" + switch_to_previous_tab: "Switch to previous tab" + create_new_window: "Create New Window" + new_file: "New File" + zoom_in: "Zoom In" + zoom_out: "Zoom Out" + reset_zoom: "Reset Zoom" + save_new_launch_configuration: "Save new launch configuration" + copy_access_token: "Copy access token to clipboard" + view_privacy_policy: "View privacy policy (opens external link)" + switch_to_first_tab: "Switch to 1st tab" + switch_to_second_tab: "Switch to 2nd tab" + switch_to_third_tab: "Switch to 3rd tab" + switch_to_fourth_tab: "Switch to 4th tab" + switch_to_fifth_tab: "Switch to 5th tab" + switch_to_sixth_tab: "Switch to 6th tab" + switch_to_seventh_tab: "Switch to 7th tab" + switch_to_eighth_tab: "Switch to 8th tab" + toggle_hidden_files_in_project_explorer: "Toggle hidden files in Project Explorer" + move_tab_up: "move tab up" + move_tab_down: "move tab down" + close_tabs_below: "close tabs below" + create_new_tab_group: "Create new tab group" + create_tab_group_from_active: "Create tab group from active or selected tab(s)" + remove_active_from_group: "Remove active or selected tab(s) from group" + pin_current_tab: "Pin current tab" + unpin_current_tab: "Unpin current tab" + pin_current_tab_group: "Pin current tab group" + unpin_current_tab_group: "Unpin current tab group" + + # ── Workspace Tab Group ── + workspace.tab_group: + move_up: "Move group up" + move_down: "Move group down" + move_left: "Move group left" + move_right: "Move group right" + close_all: "Close all tabs in group" + close_others: "Close other tabs" + close_above: "Close tabs above" + close_below: "Close tabs below" + close_left: "Close tabs to the left" + close_right: "Close tabs to the right" + ungroup: "Ungroup tabs" + new_tab_in_group: "New tab in group" + rename: "Rename" + + # ── Workspace Tab Grouping ── + workspace.tab_grouping: + remove_from_group: "Remove from group" + create_group: "Create group from tabs" + move_to_group: "Move to group" + untitled_group: "Untitled group" + + # ── Workspace Debug ── + workspace.debug: + dump_debug_info: "Dump debug info" + crash: "Crash the app (for testing sentry)" + log_review_comment_send_status: "[Debug] Log review comment send status for active tab" + trigger_panic: "Trigger a panic (for testing sentry-rust)" + open_view_tree_debugger: "Open view tree debugger" + view_first_time_user_experience: "[Debug] View first-time user experience" + open_build_plan_migration_modal: "[Debug] Open Build Plan Migration Modal" + reset_build_plan_migration_modal_state: "[Debug] Reset Build Plan Migration Modal State" + undismiss_aws_login_banner: "[Debug] Un-dismiss AWS login banner" + open_oz_launch_modal: "[Debug] Open Oz Launch Modal" + reset_oz_launch_modal_state: "[Debug] Reset Oz Launch Modal State" + open_openwarp_launch_modal: "[Debug] Open OpenWarp Launch Modal" + reset_openwarp_launch_modal_state: "[Debug] Reset OpenWarp Launch Modal State" + open_orchestration_launch_modal: "[Debug] Open Orchestration Launch Modal" + reset_orchestration_launch_modal_state: "[Debug] Reset Orchestration Launch Modal State" + install_opencode_warp_plugin: "[Debug] Install OpenCode Warp plugin" + use_local_opencode_warp_plugin: "[Debug] Use local OpenCode Warp plugin (testing only)" + open_session_config_modal: "[Debug] Open Session Config Modal" + start_hoa_onboarding_flow: "[Debug] Start HOA Onboarding Flow" + set_a11y_concise: "[a11y] Set concise accessibility announcements" + set_a11y_verbose: "[a11y] Set verbose accessibility announcements" + + # ── Appearance ── + appearance: + sync_with_os: "Sync with OS" + window_opacity_label: "Window Opacity:" + window_blur: "Use Window Blur (Acrylic texture)" + create_custom_theme: "Create your own custom theme" + auto_theme_description: "Automatically switch between light and dark themes when your system does." + transparency_unsupported: "Transparency is not supported with your graphics drivers." + input_mode: + pinned_to_bottom: "Pin to the bottom (Warp mode)" + pinned_to_top: "Pin to the top (Reverse mode)" + waterfall: "Start at the top (Classic mode)" + app_icon: + aurora: "Aurora" + default: "Default" + classic1: "Classic 1" + classic2: "Classic 2" + classic3: "Classic 3" + comets: "Comets" + glass_sky: "Glass Sky" + glitch: "Glitch" + cow: "Cow" + glow: "Glow" + holographic: "Holographic" + mono: "Mono" + neon: "Neon" + original: "Original" + starburst: "Starburst" + sticker: "Sticker" + warp1: "Warp 1" + thin_strokes: + never: "Never" + on_low_dpi: "On low-DPI displays" + on_high_dpi: "On high-DPI displays" + always: "Always" + enforce_minimum_contrast: + always: "Always" + only_named_colors: "Only for named colors" + never: "Never" + decoration: + always: "Always" + when_windowed: "When windowed" + on_hover: "Only on hover" + tab_close_button: + right: "Right" + left: "Left" + show_code_review_button: "Show code review button" + hide_code_review_button: "Hide code review button" + theme: + light: "Light" + dark: "Dark" + current: "Current theme" + customize_app_icon: "Customize your app icon" + app_icon_bundle_warning: "Changing the app icon requires the app to be bundled." + restart_for_app_icon: "You may need to restart Warp for MacOS to apply the preferred icon style." + open_windows_custom_size: "Open new windows with custom size" + columns: "Columns" + rows: "Rows" + graphics_transparency_warning: "The selected graphics settings may not support rendering transparent windows." + window_blur_radius: "Window Blur Radius:" + hardware_transparency_warning: "The selected hardware may not support rendering transparent windows." + input_type: + warp: "Warp" + shell_ps1: "Shell (PS1)" + input_type_label: "Input type" + input_position: "Input position" + dim_inactive_panes: "Dim inactive panes" + focus_follows_mouse: "Focus follows mouse" + compact_mode: "Compact mode" + jump_to_bottom_button: "Show Jump to Bottom of Block button" + show_block_dividers: "Show block dividers" + agent_font: "Agent font" + match_terminal: "Match terminal" + line_height: "Line height" + terminal_font: "Terminal font" + view_all_system_fonts: "View all available system fonts" + font_weight: "Font weight" + font_size_px: "Font size (px)" + notebook_font_size: "Notebook font size" + use_thin_strokes: "Use thin strokes" + enforce_minimum_contrast_label: "Enforce minimum contrast" + show_ligatures: "Show ligatures in terminal" + ligatures_performance: "Ligatures may reduce performance" + cursor_type: "Cursor type" + cursor_type_disabled_vim: "Cursor type is disabled in Vim mode" + blinking_cursor: "Blinking cursor" + tab_close_button_position: "Tab close button position" + show_tab_indicators: "Show tab indicators" + preserve_active_tab_color: "Preserve active tab color for new tabs" + vertical_tab_layout: "Use vertical tab layout" + vertical_tabs_restored: "Show vertical tabs panel in restored windows" + vertical_tabs_restored_description: "When enabled, reopening or restoring a window opens the vertical tabs panel even if it was closed when the window was last saved." + latest_prompt_as_tab_title: "Use latest user prompt as conversation title in tab names" + directory_tab_colors: "Directory tab colors" + directory_tab_colors_description: "Automatically color tabs based on the directory or repo you're working in." + default_no_color: "Default (no color)" + show_tab_bar: "Show the tab bar" + alt_screen_padding: "Use custom padding in alt-screen" + uniform_padding_px: "Uniform padding (px)" + zoom: "Zoom" + zoom_description: "Adjusts the default zoom level across all windows" + start_input_at_top: "Start Input at the Top" + pin_input_to_top: "Pin Input to the Top" + pin_input_to_bottom: "Pin Input to the Bottom" + toggle_input_mode: "Toggle Input Mode (Warp/Classic)" + zen_mode: "zen mode" + ligature_rendering: "ligature rendering" + always_show_tab_bar: "Always show tab bar" + hide_tab_bar_if_fullscreen: "Hide tab bar if fullscreen" + only_show_tab_bar_on_hover: "Only show tab bar on hover" + latest_prompt_as_tab_title_description: "Show the latest user prompt instead of the generated conversation title for Oz and third-party agent sessions in vertical tabs." + header_toolbar_layout: "Header toolbar layout" + tools_panel_visibility_consistent: "Tools panel visibility is consistent across tabs" + graphics_transparency_fix_hint: "Try changing the settings for the graphics backend or integrated GPU in Features > System." + show_code_review_button_in_tab_bar: "Show code review button in tab bar" + hide_code_review_button_in_tab_bar: "Hide code review button in tab bar" + + # ── Dialogs ── + dialog: + discard_changes: "Discard uncommitted changes?" + discard_all_changes: "Discard all changes?" + dont_save: "Don't Save" + close_pane: "Close pane?" + close_tab: "Close tab?" + close_tabs: "Close tabs?" + close_window: "Close window?" + quit_warp: "Quit Warp?" + save_changes: "Save changes?" + + # ── Provider Keys ── + provider_keys: + add_api_key: "Add API key" + description: "Use your own API keys from model providers for Warp Agent." + openai_api_key: "OpenAI API key" + anthropic_api_key: "Anthropic API key" + google_api_key: "Google API key" + cancel: "Cancel" + add_keys: "Add keys" + + # ── Free AI Removal ── + free_ai_removal: + notice: + title: "Warp is no longer providing inference on the free plan." + body: "To keep using Warp's AI features, please upgrade to a paid plan, bring your own API key or endpoint, or log in with your Grok subscription." + bonus_credits: "If you have any unused bonus credits, AI will keep working until these run out." + prompt_suggestions: + title: "How to use AI features in Warp" + body: "To use AI features in Warp, subscribe to a paid plan, add an API key (OpenAI, Anthropic, or Google), add a custom inference endpoint (OpenRouter, LiteLLM), or log in using your SuperGrok subscription." + bring_your_own_ai: "Bring your own AI" + view_pricing: "View pricing" + + # ── Auth ── + auth: + log_out_title: "Log out?" + log_out_confirm: "Log out?" + show_running_processes: "Show running processes" + paste_token_placeholder: "Enter auth token" + auth_token_placeholder: "Auth Token" + link_sso: "Link SSO" + + # ── Settings ── + settings: + execution_profile: + apply_code_diffs: "Apply code diffs:" + ask_questions: "Ask questions:" + auto: "Auto" + autosync_plans_to_warp_drive: "Auto-sync plans to Warp Drive:" + base_model: "Base model:" + call_mcp_servers: "Call MCP servers:" + call_web_tools: "Call web tools:" + command_allowlist: "Command allowlist:" + command_denylist: "Command denylist:" + computer_use_model: "Computer use:" + computer_use_permission: "Computer use:" + directory_allowlist: "Directory allowlist:" + execute_commands: "Execute commands:" + full_terminal_use: "Full terminal use:" + interact_with_running_commands: "Interact with running commands:" + mcp_allowlist: "MCP allowlist:" + mcp_denylist: "MCP denylist:" + models_section: "MODELS" + none: "None" + "off": "Off" + "on": "On" + permission_agent_decides: "Agent decides" + permission_always_allow: "Always allow" + permission_always_ask: "Always ask" + permission_ask_on_first_write: "Ask on first write" + permission_ask_unless_auto_approve: "Ask unless auto-approve" + permission_never: "Never" + permission_never_ask: "Never ask" + permission_unknown: "Unknown" + permissions_section: "PERMISSIONS" + read_files: "Read files:" + run_agents: "Run agents:" + font_description: "The monospace font used in the terminal." + reset_to_default: "Reset to default" + about: "About" + account: "Account" + ai: + add_custom_endpoint: "Add custom endpoint" + agent: + git_operations_autogen_description: "Generates a commit message based on your current changes to your git repo" + title: "AI" + appearance: + category: + blocks: "Blocks" + cursor: "Cursor" + full_screen_apps: "Full-screen Apps" + icon: "Icon" + input: "Input" + panes: "Panes" + tabs: "Tabs" + text: "Text" + themes: "Themes" + window: "Window" + default_label: "default" + label: + agent_font: "Agent font" + columns: "Columns" + compact_mode: "Compact mode" + cursor_type: "Cursor type" + font_weight: "Font weight" + input_position: "Input position" + input_type: "Input type" + line_height: "Line height" + rows: "Rows" + use_thin_strokes: "Use thin strokes" + window_blur_radius_label: "Window Blur Radius:" + window_opacity_label: "Window Opacity:" + zoom: "Zoom" + title: "Appearance" + toggle: + block_dividers: "block dividers" + compact_mode: "compact mode" + cursor_blink: "cursor blink" + tab_indicators: "tab indicators" + themes_sync_os: "themes: sync with OS" + zen_mode: "zen mode" + code: "Code" + features: + asynchronous_find: "Asynchronous find" + clipboard_osc52: "Clipboard access (OSC 52)" + clipboard_osc52_tooltip: "Controls whether programs running in the terminal can read or write your system clipboard." + code_editor_line_numbers: "Code editor line numbers:" + preserve_input_focus_on_block_selection: "Preserve input focus on block selection" + press_new_shortcut: "Press new keyboard shortcut" + title: "Features" + toggle: + agent_task_completed_notifications: "agent task completion notifications" + alias_expansion: "alias expansion" + at_context_menu_terminal: "'@' context menu in terminal mode" + autocomplete_symbols: "autocomplete quotes, parentheses, and brackets" + autosuggestion_ignore_button: "autosuggestion ignore button" + autosuggestion_keybinding_hint: "autosuggestion keybinding hint" + code_as_default_editor: "code as default editor" + focus_reporting: "focus reporting" + global_workflows_search: "global workflows in Command Search" + help_block_in_new_sessions: "help block in new sessions" + in_app_agent_notifications: "in-app agent notifications" + input_hint_text: "input hint text" + integrated_gpu: "integrated GPU rendering (low power)" + link_tooltip: "show tooltip on click on links" + long_running_notifications: "long-running command notifications" + middle_click_paste: "middle-click paste" + mouse_reporting: "mouse reporting" + needs_attention_notifications: "needs-attention notifications" + notification_sounds: "notification sounds" + outline_codebase_symbols: "codebase symbols in the '@' context menu" + preserve_input_focus_on_block_selection: "preserve input focus on block selection" + quit_warning_modal: "quit warning modal" + restore_session: "restore windows, tabs, and panes on startup" + slash_commands_terminal: "slash commands in terminal mode" + smart_select: "smart select" + ssh_reuse_control_master: "reuse existing SSH ControlMaster in the Warp SSH wrapper" + terminal_input_message_line: "terminal input message line" + vim_keybindings: "editing commands with Vim keybindings" + vim_status_bar: "Vim status bar" + vim_unnamed_register: "Vim unnamed register as system clipboard" + wayland_window_management: "Wayland for window management" + billing_and_usage: + auto_reload: "Auto reload" + buy: "Buy" + buy_more: "Buy more" + buying: "Buying…" + credit_singular: "1 credit" + credits_header: "Credits" + default_spend_limit: "$200.00" + free: "Free" + load_more: "Load more" + new_agent: "New agent" + not_set: "Not set" + overview_tab: "Overview" + plan: "Plan" + resets_at: "Resets {time}" + sign_up: "Sign up" + sort_a_to_z: "A to Z" + sort_z_to_a: "Z to A" + switch_to_business: "Switch to Business" + team_total: "Team total" + title: "Billing and usage" + usage_section: "Usage" + your_selected: "your selected" + zero_cost: "$0.00" + zero_credits: "0 credits" + keyboard_shortcuts: "Keyboard shortcuts" + scripting: + install: "Install" + install_failed: "Failed to install Warp Control command: {error}" + install_success: "Successfully installed the Warp Control CLI! You can now run '{command_name}' from the command line." + installed: "Installed" + installing: "Installing…" + title: "Scripting" + warp_control_cli_command: "Warp Control CLI command" + warp_control_cli_description: "Install the warpctrl command for scripting Warp from your terminal." + warpctrl_cli: "warpctrl CLI" + warpctrl_cli_description: "warpctrl allows for scripting Warp's UI. Use with care." + shared_blocks: "Shared blocks" + mcp_servers: "MCP Servers" + privacy: + title: "Privacy" + referrals: "Referrals" + teams: + invite_option_email: "Email" + invite_option_link: "Link" + need_more_seats: "Need more seats? " + team_full_title: "Your team is full" + team_member_count_one: "1 team member" + teammate_count_one: "1 teammate" + title: "Teams" + update_environment_form: + auth_with_github: "Auth with GitHub" + authenticate: "Authenticate" + characters_count: "{count} / {max} characters" + configure_access: "Configure access on GitHub" + create_environment: "Create environment" + delete_environment: "Delete environment" + description_label: "Description" + description_placeholder: "e.g., this environment is for all front end focused agents" + docker_image_label: "Docker image" + docker_image_label_long: "Docker image reference" + docker_image_placeholder: "e.g. python:3.11, node:20-alpine" + docker_image_placeholder_short: "e.g., node:20-alpine" + edit_environment: "Edit environment" + generating: "Generating…" + launch_agent: "Launch agent" + load_repos_error: "Couldn't load GitHub repos. You can paste repo URL(s), or retry." + load_repositories_failed: "Failed to load GitHub repositories" + loading: "Loading..." + missing_repo: "Missing a repo?" + name_label: "Name" + name_placeholder: "Environment name" + name_placeholder_orchestration: "e.g., dev-env" + no_repositories: "No repositories found" + open_image_at: "Open image at {url}" + repos_helper: "Type owner/repo and press Enter to add, or select from dropdown." + repos_label: "Repo(s)" + repos_placeholder_authed: "Enter repos (owner/repo format)" + repos_placeholder_browse: "Browse GitHub repos..." + repos_placeholder_unauthed: "Paste repo URL(s)" + save_environment: "Save environment" + setup_commands_helper: "Setup commands run independently. Each command runs from the workspace root (/workspace). If a command depends on the previous one, combine them with &&." + setup_commands_helper_short: "Press Enter or click the submit button to add each command." + setup_commands_label: "Setup command(s)" + setup_commands_placeholder: "e.g. cd my-repo && pip install -r requirements.txt" + setup_commands_placeholder_short: "e.g., node start" + share_warning: "Personal environments cannot be used with external integrations or team API keys. For the best experience, use shared environments." + share_with_team: "Share with team" + suggest_image: "Suggest image" + suggest_image_auth_required: "You need to grant access to your GitHub repos to suggest a Docker image" + suggest_image_failed: "Failed to suggest a Docker image" + suggest_image_failed_with_error: "Failed to suggest a Docker image: {error}" + suggest_image_no_match: "We couldn't find a good match. We recommend using a custom Docker image for these repos." + suggest_image_tooltip: "Warp will suggest a Docker image based on your selected repositories." + suggest_image_unknown_response: "Unknown response from suggestCloudEnvironmentImage" + warpify: "Warpify" + + description: "Configure whether Warp attempts to “Warpify” (add support for blocks, input modes, etc) certain shells. " + host_placeholder: "host (supports regex)" + command_placeholder: "command (supports regex)" + denylisted_hosts: "Denylisted hosts" + denylisted_commands: "Denylisted commands" + added_commands: "Added commands" + use_tmux_warpification: "Use Tmux Warpification" + install_ssh_extension: "Install SSH extension" + warpify_ssh_sessions: "Warpify SSH Sessions" + ssh_subtitle: "Warpify your interactive SSH sessions." + ssh: "SSH" + subshells_subtitle: "Subshells supported: bash, zsh, and fish." + subshells: "Subshells" + ssh_extension_install_mode_description: "Controls the installation behavior for Warp's SSH extension when a remote host doesn't have it installed." + tmux_warpification_description: "The tmux ssh wrapper works in many situations where the default one does not, but may require you to hit a button to warpify. Takes effect in new tabs." + ssh_session_detection: "SSH session detection for Warpification" + ssh_extension: + always_ask: "Always ask" + always_install: "Always install" + never_install: "Never install" + warp_drive: "Warp Drive" + warp_agent: "Warp Agent" + profiles: "Profiles" + mcp_servers_lower: "MCP servers" + mcp_servers_page: + title: "MCP Servers" + search_terms: "mcp servers" + search: "Search MCP Servers" + shared_by_team: "Shared by a team member" + from_another_device: "From another device" + logout_success: "Successfully logged out of {name} MCP server" + logout_success_unnamed: "Successfully logged out of MCP server" + install_modal_already_open: "Finish the current MCP install before opening another install link." + unknown_server: "Unknown MCP server '{name}'" + cannot_install_from_link: "MCP server '{name}' cannot be installed from this link." + description: "Add MCP servers to extend the Warp Agent's capabilities. MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins. Add a custom server, or use the presets to get started with popular servers. You can also find team servers that have been shared with you here." + empty_state: "Once you add a MCP server, it will be shown here." + no_search_results: "No search results found" + add: "Add" + available_to_install: "Available to install" + detected_from_config: "Detected from config file" + global_scope: "global" + update_success: "MCP server updated" + my_mcps: "My MCPs" + shared_by_warp_and_team: "Shared by Warp and {name}" + shared_by_warp_and_devices: "Shared by Warp and from other devices" + shared_from_warp: "Shared from Warp" + detected_from: "Detected from {provider}" + auto_spawn_label: "Auto-spawn servers from third-party agents" + auto_spawn_description: 'Automatically detect and spawn MCP servers from globally-scoped third-party AI agent configuration files (e.g. in your home directory). Servers detected inside a repository are never spawned automatically and must be enabled individually in the "Detected from" sections below.' + see_supported_providers: "See supported providers." + learn_more: "Learn more." + knowledge: "Knowledge" + third_party_cli_agents: "Third party CLI agents" + indexing_and_projects: "Indexing and projects" + editor_and_code_review: "Editor and Code Review" + environments: "Environments" + oz_cloud_api_keys: "Oz Cloud API Keys" + platform: + api_key_deleted: "API key deleted" + column_created: "Created" + column_expires_at: "Expires at" + column_key: "Key" + column_last_used: "Last used" + column_name: "Name" + column_scope: "Scope" + create_api_key_button: "+ Create API Key" + description_prefix: "Create and manage API keys to allow other Oz cloud agents to access your Warp account.\nFor more information, visit the " + documentation_link: "Documentation." + never: "Never" + new_api_key: "New API key" + no_api_keys: "No API Keys" + no_api_keys_description: "Create a key to manage external access to Warp" + no_api_keys_match_search: "No API keys match your search" + oz_cloud_api_keys: "Oz Cloud API Keys" + save_your_key: "Save your key" + scope_agent: "Agent" + scope_personal: "Personal" + scope_team: "Team" + search_api_keys: "Search API keys" + platform_page: + new_api_key: "New API key" + save_your_key: "Save your key" + api_key_deleted: "API key deleted" + description: "Create and manage API keys to allow other Oz cloud agents to access your Warp account.\nFor more information, visit the " + documentation: "Documentation." + column_name: "Name" + column_key: "Key" + column_scope: "Scope" + column_created: "Created" + column_last_used: "Last used" + column_expires_at: "Expires at" + never: "Never" + search_terms: "oz cloud platform api keys authentication" + no_api_keys: "No API Keys" + no_api_keys_description: "Create a key to manage external access to Warp" + create_api_key: "+ Create API Key" + personal: "Personal" + team: "Team" + agent: "Agent" + + settings.shared_blocks: + executed_on: "Executed on:" + unshare_block_title: "Unshare block" + unshare_failed: "Failed to unshare block. Please try again." + unshare_success: "Block was successfully unshared." + link_copied: "Link copied." + unshare: "Unshare" + failed_to_load: "Failed to load blocks. Please try again." + getting_blocks: "Getting blocks..." + no_blocks_yet: "You don't have any shared blocks yet." + deleting: "Deleting..." + copy_link: "Copy link" + settings.code_page: + code: "Code" + initialization_settings_header: "Initialization Settings" + codebase_indexing_label: "Codebase indexing" + codebase_index_description: "Warp can automatically index code repositories as you navigate them, helping agents quickly understand context and provide solutions. Code is never stored on the server. If a codebase is unable to be indexed, Warp can still navigate your codebase and gain insights via grep and find tool calling." + warp_indexing_ignore_description: "To exclude specific files or directories from indexing, add them to the .warpindexingignore file in your repository directory. These files will still be accessible to AI features, but they won't be included in codebase embeddings." + auto_index_feature_name: "Index new folders by default" + auto_index_description: "When set to true, Warp will automatically index code repositories as you navigate them - helping agents quickly understand context and provide targeted solutions." + indexing_disabled_admin_text: "Team admins have disabled codebase indexing." + indexing_workspace_enabled_admin_text: "Team admins have enabled codebase indexing." + indexing_disabled_global_ai_text: "AI Features must be enabled to use codebase indexing." + codebase_index_limit_reached: "You have reached the maximum number of codebase indices for your plan. Delete existing indices to auto-index new codebases." + indexing_subpage_title: "Codebase Indexing" + editor_subpage_title: "Editor and Code Review" + index_new_folder: "Index new folder" + initialized_indexed_folders: "Initialized / indexed folders" + no_folders_initialized: "No folders have been initialized yet." + indexing_label: "INDEXING" + no_index_created: "No index created" + open_project_rules: "Open project rules" + index_limit_reached: "Index limit reached" + unavailable: "Unavailable" + indexing_discovered: "Discovered {total_nodes} chunks" + indexing_syncing: "Syncing - {completed_nodes} / {total_nodes}" + indexing_syncing_generic: "Syncing..." + indexing_synced: "Synced" + indexing_too_large: "Codebase too large" + indexing_stale: "Stale" + indexing_failed: "Failed" + no_index_built: "No index built" + indexing_disabled: "Disabled" + indexing_queued: "Queued" + indexing_remote_syncing: "Indexing - {completed} / {total}" + indexing_remote_syncing_partial: "Indexing - {completed}" + indexing_remote_syncing_zero: "Indexing - 0 / {total}" + indexing_remote_generic: "Indexing..." + + settings.category: + themes: "Themes" + icon: "Icon" + window: "Window" + input: "Input" + panes: "Panes" + blocks: "Blocks" + text: "Text" + language: "Language" + cursor: "Cursor" + tabs: "Tabs" + fullscreen_apps: "Full-screen Apps" + general: "General" + session: "Session" + keys: "Keys" + text_editing: "Text Editing" + terminal_input: "Terminal Input" + terminal: "Terminal" + notifications: "Notifications" + workflows: "Workflows" + system: "System" + + settings.keybindings: + search_placeholder: 'Search by name or by keys (ex. "cmd d")' + conflict_warning: "This shortcut conflicts with other keybinds" + reset: "Default" + press_new: "Press new keyboard shortcut" + add_custom: "Add your own custom keybindings to existing actions below." + use_prefix: "Use" + reference_suffix: "to reference these keybindings in a side pane at anytime." + configure: "Configure keyboard shortcuts" + not_synced: "Keyboard shortcuts are not synced to the cloud" + command_column: "Command" + + settings.features: + left_option_meta: "Left Option key is Meta" + right_option_meta: "Right Option key is Meta" + left_alt_meta: "Left Alt key is Meta" + right_alt_meta: "Right Alt key is Meta" + configure_global_hotkey: "Configure Global Hotkey" + make_default_terminal: "Make Warp the default terminal" + pin_top: "Pin to top" + pin_bottom: "Pin to bottom" + pin_left: "Pin to left" + pin_right: "Pin to right" + new_tab_after_all: "After all tabs" + new_tab_after_current: "After current tab" + change_keybinding: "Change keybinding" + ssh_wrapper: "Warp SSH wrapper" + link_tooltip: "Show tooltip on click on links" + quit_warning_modal: "Show warning before quitting/logging out" + alias_expansion: "Expand aliases as you type" + middle_click_paste: "Middle-click to paste" + code_as_default_editor: "Use VS Code as default editor" + input_hint_text: "Show input hint text" + vim_keybindings: "Edit code and commands with Vim keybindings" + vim_unnamed_register: "Set unnamed register as system clipboard" + vim_status_bar: "Show Vim status bar" + focus_reporting: "Enable Focus Reporting" + smart_select: "Double-click smart selection" + terminal_input_message_line: "Show terminal input message line" + slash_commands_terminal: "Enable slash commands in terminal mode" + integrated_gpu: "Prefer rendering new windows with integrated GPU (low power)" + wayland_window_management: "Use Wayland for window management" + active_screen: "Active Screen" + ten_million: "10 million" + one_million: "1 million" + block_max_rows_warning: "Setting the limit above 100k lines may impact performance. Maximum rows supported is {max_rows}." + open_links_desktop: "Open links in desktop app" + open_links_desktop_tooltip: "Automatically open links in desktop app whenever possible." + restore_session: "Restore windows, tabs, and panes on startup" + wayland_no_restore: "Window positions won't be restored on Wayland. " + sticky_command_header: "Show sticky command header" + start_at_login_macos: "Start Warp at login (requires macOS 13+)" + start_at_login: "Start Warp at login" + quit_when_closed: "Quit when all windows are closed" + show_changelog: "Show changelog toast after updates" + allowed_values_1_20: "Allowed Values: 1-20" + mouse_scroll_lines: "Lines scrolled by mouse wheel interval" + mouse_scroll_tooltip: "Supports floating point values between 1 and 20." + auto_open_code_review: "Auto open code review panel" + auto_open_code_review_description: "When this setting is on, the code review panel will open on the first accepted diff of a conversation" + warp_is_default: "Warp is the default terminal" + max_block_rows: "Maximum rows in a block" + ssh_wrapper_label: "Warp SSH Wrapper" + change_takes_effect_new_sessions: "This change will take effect in new sessions" + receive_desktop_notifications: "Receive desktop notifications from Warp" + notify_agent_completes: "Notify when an agent completes a task" + notify_needs_attention: "Notify when a command or agent needs your attention to continue" + play_notification_sounds: "Play notification sounds" + in_app_agent_notifications: "Show in-app agent notifications" + toast_visible_for: "Toast notifications stay visible for" + seconds: "seconds" + confirm_close_session: "Confirm before closing shared session" + global_hotkey: "Global hotkey:" + not_supported_wayland: "Not supported on Wayland. " + autocomplete_symbols: "Autocomplete quotes, parentheses, and brackets" + error_underlining: "Error underlining for commands" + syntax_highlighting: "Syntax highlighting for commands" + completions_while_typing: "Open completions menu as you type" + command_corrections: "Suggest corrected commands" + autosuggestion_hint: "Show autosuggestion keybinding hint" + autosuggestion_ignore: "Show autosuggestion ignore button" + right_arrow_accepts: "→ accepts autosuggestions." + completions_open_typing: "Completions open as you type." + completions_unbound: "Opening the completion menu is unbound." + accept_autosuggestion: "Accept Autosuggestion" + open_completions_menu: "Open Completions Menu" + tab_key_behavior: "Tab key behavior" + ctrl_tab_behavior: "Ctrl+Tab behavior:" + mouse_reporting: "Enable Mouse Reporting" + scroll_reporting: "Enable Scroll Reporting" + audible_bell: "Use Audible Bell" + word_char_allowlist: "Characters considered part of a word" + copy_on_select: "Copy on select" + show_help_block: "Show help block in new sessions" + new_tab_placement: "New tab placement" + default_session_mode: "Default mode for new sessions" + global_workflows_search: "Show Global Workflows in Command Search (ctrl-r)" + linux_selection_clipboard: "Honor linux selection clipboard" + linux_clipboard_tooltip: "Whether the Linux primary clipboard should be supported." + changes_new_windows: "Changes will apply to new windows." + wayland_description: "Enabling this setting disables global hotkey support. When disabled, text may be blurry if your Wayland compositor is using fraction scaling (ex: 125%)." + wayland_tooltip: "Enables the use of Wayland" + restart_for_changes: "Restart Warp for changes to take effect." + default_shell: "Default shell for new sessions" + working_directory: "Working directory for new sessions" + graphics_backend: "Preferred graphics backend" + current_backend: "Current backend:" + default: "Default" + keybinding: "Keybinding" + click_to_set_hotkey: "Click to set global hotkey" + outline_codebase_symbols: "Outline codebase symbols for '@' context menu" + see_docs: "See docs." + autohides_on_focus_loss: "Autohides on loss of keyboard focus" + width_pct: "Width %" + height_pct: "Height %" + command_longer_than: "When a command takes longer than" + seconds_to_complete: "seconds to complete" + accepts_autosuggestions: "{} accepts autosuggestions." + completions_open_typing_or: "Completions open as you type (or {})." + opens_completion_menu: "{} opens completion menu." + quake_keybinding: "Quake keybinding" + quake_pin_position: "Quake pin position" + quake_pin_screen: "Quake pin screen" + at_context_menu_terminal: "Enable '@' context menu in terminal mode" + + toggle: + copy_on_select: "copy on select within the terminal" + linux_selection_clipboard: "linux selection clipboard" + scroll_reporting: "scroll reporting" + completions_while_typing: "completions while typing" + command_corrections: "command corrections" + error_underlining: "error underlining" + syntax_highlighting: "syntax highlighting" + audible_bell: "audible terminal bell" + autosuggestions: "autosuggestions" + + common: + back: "Back" + cancel: "Cancel" + clear: "Clear" + close: "Close" + copy: "Copy" + create: "Create" + delete: "Delete" + disabled: "Disabled" + dismiss: "Dismiss" + done: "Done" + edit: "Edit" + enabled: "Enabled" + finish: "Finish" + get_started: "Get started" + get_warping: "Get Warping" + learn_more: "Learn more" + next: "Next" + ok: "OK" + open: "Open" + recommended: "Recommended" + remove: "Remove" + restore: "Restore" + retry: "Retry" + save: "Save" + search: "Search" + send: "Send" + add: "Add" + skip: "Skip" + submit: "Submit" + undo: "Undo" + update: "Update" + untitled: "Untitled" + + # ── Code ── + code: + reject: "Reject" + accept_and_save: "Accept and save" + discard_this_version: "Discard this version" + overwrite: "Overwrite" + goto_line_placeholder: "Line number:Column" + previous: "Previous" + next: "Next" + select_all: "Select all" + replace_all: "Replace all" + remote_disconnected_banner: "Remote host disconnected. You will not be able to see updates and save changes." + remote_disconnected_save_failure: "Cannot save — remote session disconnected." + comment: "Comment" + add_as_context: "Add as context" + go_to_definition: "Go to definition" + find_references_action: "Find references" + file_has_saved_changes: "This file has saved changes that are not reflected here." + failed_to_load_file: "Failed to load file." + failed_to_save_file: "Failed to save file." + file_saved: "File saved." + new_file_indicator: " (new)" + save_file: "Save file" + save_file_as: "Save file as" + close_all_tabs: "Close all tabs" + close_saved_tabs: "Close saved tabs" + close_saved: "Close saved" + copy_file_path: "Copy file path" + reveal_in_finder: "Reveal in Finder" + reveal_in_explorer: "Reveal in Explorer" + reveal_in_file_manager: "Reveal in file manager" + view_markdown_preview: "View Markdown preview" + file_tree: + remote: "The Project Explorer requires access to your local workspace, which isn't supported in remote sessions." + disabled: "The Project Explorer requires access to your local workspace. Open a new session or navigate to an active session to view." + wsl: "The Project Explorer doesn't currently work in WSL." + file: "File" + folder: "Folder" + suggested_fixes: "Suggested fixes based on your last command:" + find_references: + single: "Showing 1 reference" + multiple: "Showing {total_refs} references" + loading: "Loading..." + + editor: + find: + regex_toggle: "Regex toggle" + case_sensitive: "Case sensitive search" + preserve_case: "Preserve case" + placeholder: "Find" + replace_placeholder: "Replace" + binding: + find_next: "Find the next occurrence of your search query" + find_prev: "Find the previous occurrence of your search query" + result_of: "Result {current} of {total}." + navigate_help: "Use enter and shift-enter to navigate between matches. Escape to quit." + no_results: "No results." + replace_success: "Successfully replaced match. Selected match is {index} of {total}" + replace_continue_help: "Continue pressing Enter to replace more matches, or use up/down arrows to navigate." + replace_last_success: "Successfully replaced the last match." + a11y_description_empty: "Find bar for searching text in the editor." + a11y_description_with_results: "Find bar with {count} matches found. Currently on match {current} of {total}." + a11y_help_replace: "Replace field focused. Type replacement text, press Enter to replace current match, Tab to return to find field. Use up/down arrows to navigate matches, Escape to close." + a11y_help_find: "Find field focused. Type to search text. Use Enter and Shift-Enter or up/down arrows to navigate between matches. Press Escape to close find bar." + goto_line: + enter_line_number: "Please enter a line number" + valid_line_number: "Please enter a valid line number" + valid_column_number: "Please enter a valid column number" + nav_bar: + hunk: "Hunk:" + gutter: + add_as_context: "Add diff hunk as context" + save_to_attach_context: "Save changes to attach as context." + revert_hunk: "Revert diff hunk" + save_to_revert: "Save changes to revert" + add_comment: "Add comment on line" + save_to_comment: "Save changes to add comment" + show_comment: "Show saved comment" + + # ── CLI ── + cli: + accept: "Accept" + auto_approve: "Auto-approve" + + # ── Launch Configs ── + launch_configs: + default_filename: "launch_config.yaml" + + # ── Drive ── + drive: + empty_trash: "Empty trash" + + # ── Notebook ── + notebook: + copy_to_personal: "Copy to Personal" + copy_all: "Copy All" + link_text_placeholder: "Text" + link_url_placeholder: "Link (web or file)" + copy_link: "Copy link" + edit: "Edit" + link_copied: "Link copied to clipboard" + open_folder: "Open folder" + open_file: "Open file" + open_in_warp: "Open in Warp" + + # ── Onboarding ── + onboarding: + agent_slide: + autonomy: "Autonomy" + autonomy_full_title: "Full" + autonomy_none_title: "None" + autonomy_partial_title: "Partial" + default_model: "Default model" + ai_access: + choose_how: "Get AI access" + subtitle: "Save with a recurring plan, or explore Warp's AI before committing." + subscription: "Subscription" + best_value: "Best value" + subscription_description: "Starting at $18 / mo, available with monthly or annual plans. Includes base credits, frontier models, cloud agents, collaboration, and more." + back: "Back" + no_ai: "I don't want AI" + next: "Next" + set_up_later_label: "Set up later" + set_up_later_description: "Explore Warp's built-in AI features before committing to a plan, or bring your own inference." + auth_prompt_bar: + browser_not_launched_prefix: "If your browser hasn't launched, " + copy_url_link: "copy the URL" + open_manually: " and open the page manually. " + paste_token_link: "Click here" + paste_token_suffix: " to paste your token from the browser." + ai_setup: + feature_harness: "Best harness for terminal tasks and agentic coding" + feature_models: "Frontier models from OpenAI, Anthropic, and Google" + feature_routing: "Model routing across frontier and open-weight models" + feature_orchestration: "Multi-agent orchestration" + choose_your_setup: "Choose your AI setup" + subtitle: "Choose if you'd like to use Warp Agent or third party agents." + use_warp_agent: "Use Warp Agent" + access_more_models: "Access more models" + warp_agent_description: "State of the art agent harness deeply integrated into the terminal." + use_third_party: "Use third party agents" + third_party_description: "Use agents like Claude Code, Codex, and Gemini." + back: "Back" + no_ai: "I don't want AI" + next: "Next" + agent: + skip: "Skip" + no_ai_dialog_title: "Are you sure you don't want AI?" + no_ai_dialog_body: "Without AI, you'll still get Warp's terminal experience, but you'll miss our agentic features like automatic fixes for terminal errors." + give_me_ai_features: "Give me AI features" + plan_activated_toast: "Plan successfully activated!" + + # ── AI Settings ── + callout: + initialize: "Initialize" + customize: + tab_styling_title: "Tab styling" + tab_vertical: "Vertical" + title: "Customize your Warp" + intention: + subtitle: "How do you want to work?" + title: "Welcome to Warp" + intro: + log_in: "Log in" + title: "Welcome to Warp" + project: + title: "Open a project" + theme_picker: + title: "Choose a theme" + settings.ai: + next_command_description: "Let AI suggest the next command to run based on your command history, outputs, and common workflows." + prompt_suggestions_description: "Let AI suggest natural language prompts, as inline banners in the input, based on recent commands and their outputs." + suggested_code_banners_description: "Let AI suggest code diffs and queries as inline banners in the blocklist, based on recent commands and their outputs." + natural_language_autosuggestions_description: "Let AI suggest natural language autosuggestions, based on recent commands and their outputs." + shared_block_title_generation_description: "Let AI generate a title for your shared block based on the command and output." + git_operations_autogen_description: "Let AI generate commit messages and pull request titles and descriptions." + + ai: "AI" + active_ai: "Active AI" + next_command: "Next Command" + prompt_suggestions: "Prompt Suggestions" + suggested_code_banners: "Suggested Code Banners" + natural_language_autosuggestions: "Natural Language Autosuggestions" + shared_block_title_generation: "Shared Block Title Generation" + commit_and_pr_generation: "Commit & Pull Request Generation" + voice_input: "Voice Input" + + usage: "Usage" + credits: "Credits" + models: "Models" + permissions: "Permissions" + profiles: "Profiles" + input: "Input" + voice: "Voice" + other: "Other" + agents: "Agents" + knowledge: "Knowledge" + mcp_servers: "MCP Servers" + + permission: + agent_decides: "Agent decides" + always_allow: "Always allow" + always_ask: "Always ask" + read_only: "Read only" + supervised: "Supervised" + ask_on_first_write: "Ask on first write" + allow_in_specific_directories: "Allow in specific directories" + never: "Never" + never_ask: "Never ask" + ask_unless_auto_approve: "Ask unless auto-approve" + + restricted_due_to_billing: "Restricted due to billing issue" + unlimited: "Unlimited" + + add_profile: "Add Profile" + edit_custom_endpoint: "Edit custom endpoint" + edit: "Edit" + add_custom_model: "+ Add custom model" + select_mcp_servers: "Select MCP servers" + select_coding_agent: "Select coding agent" + toolbar_layout: "Toolbar layout" + other_category: "Other" + new_tab: "New Tab" + split_pane: "Split Pane" + + show_agent_tips: "Show agent tips" + hide_agent_tips: "Hide agent tips" + show_oz_changelog: "Show Oz changelog in new agent conversation view" + hide_oz_changelog: "Hide Oz changelog in new agent conversation view" + show_use_agent_footer: 'Show "Use Agent" footer' + hide_use_agent_footer: 'Hide "Use Agent" footer' + + natural_language_detection: "Natural language detection" + autodetect_agent_prompts: "Autodetect agent prompts in terminal input" + autodetect_terminal_commands: "Autodetect terminal commands in agent input" + show_input_hint_text: "Show input hint text" + show_agent_tips_toggle: "Show agent tips" + include_agent_commands_in_history: "Include agent-executed commands in history" + natural_language_denylist: "Natural language denylist" + natural_language_denylist_description: "Commands listed here will never trigger natural language detection." + + codebase_context: "Codebase Context" + rules: "Rules" + suggested_rules: "Suggested Rules" + warp_drive_context: "Warp Drive as agent context" + manage_rules: "Manage rules" + manage_mcp_servers: "Manage MCP servers" + call_mcp_servers: "Call MCP servers" + mcp_allowlist: "MCP allowlist" + mcp_denylist: "MCP denylist" + command_denylist: "Command denylist" + command_allowlist: "Command allowlist" + directory_allowlist: "Directory allowlist" + placeholder_command_allowlist: "e.g. ls .*" + placeholder_command_denylist: "e.g. rm .*" + placeholder_directory_allowlist: "e.g. ~/code-repos/repo" + placeholder_profile_name: 'e.g. "YOLO code"' + placeholder_cli_command: "command (supports regex)" + base_model: "Base model" + apply_code_diffs: "Apply code diffs" + read_files: "Read files" + execute_commands: "Execute commands" + interact_with_running_commands: "Interact with running commands" + key_for_voice_input: "Key for Activating Voice Input" + auto_spawn_servers: "Auto-spawn servers from third-party agents" + show_model_picker_in_prompt: "Show model picker in prompt" + show_conversation_history: "Show conversation history in tools panel" + enable_builtin_feedback_skill: "Enable built-in feedback skill" + feedback_bundled_skill_description: "Let Oz use Warp's built-in skill for turning Warp product feedback into GitHub issues." + agent_thinking_display: "Agent thinking display" + preferred_layout_open_conversation: "Preferred layout when opening existing agent conversations" + + endpoint_added: "Endpoint added" + endpoint_saved: "Endpoint saved" + endpoint_removed: "Endpoint removed" + + sign_up: "Sign up" + to_use_ai_features_create_account: "To use AI features, please create an account." + remote_session_org_policy: "Your organization disallows AI when the active pane contains content from a remote session" + + commands_comma_separated: "Commands, comma separated" + this_is_limit_of_ai_credits: "This is the {duration} limit of AI credits for your account." + + resets: "Resets {time}" + + agents_description: "Set the boundaries for how your Agent operates. Choose what it can access, how much autonomy it has, and when it must ask for your approval. You can also fine-tune behavior around natural language input, codebase awareness, and more." + profiles_description: "Profiles let you define how your Agent operates — from the actions it can take and when it needs approval, to the models it uses for tasks like coding and planning. You can also scope them to individual projects." + context_window_tokens: "Context window (tokens)" + some_permissions_managed: "Some of your permissions are managed by your workspace." + + command_denylist_description: "Regular expressions to match commands that the Warp Agent should always ask permission to execute." + command_allowlist_description: "Regular expressions to match commands that can be automatically executed by the Warp Agent." + directory_allowlist_description: "Give the agent file access to certain directories." + mcp_allowlist_description: "Allow the Warp Agent to call these MCP servers." + mcp_denylist_description: "The Warp Agent will always ask for permission before calling any MCP servers on this list." + + base_model_description: "This model serves as the primary engine behind the Warp Agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization." + full_terminal_use_model_description: "The model used when the agent operates inside interactive terminal applications like database shells, debuggers, REPLs, or dev servers—reading live output and writing commands to the PTY." + computer_use_model_description: "The model used when the agent takes control of your computer to interact with graphical applications through mouse movements, clicks, and keyboard input." + context_window_description: "The base model's working memory — how many tokens of your conversation, code, and documents it can consider at once. Larger windows enable longer conversations and more coherent responses over bigger codebases, at the cost of higher latency and compute usage." + plan_auto_sync_description: "The plans this agent creates will be automatically added and synced to Warp Drive." + call_web_tools_description: "The agent may use web search when helpful for completing tasks." + upgrade_footer: "Frontier models are unavailable on free plans. Upgrade" + thinking_display_description: "Controls how reasoning/thinking traces are displayed." + + show_hint_description: 'Shows hint to use the "Full Terminal Use"-enabled agent in long running commands.' + rule_suggestions_description: "Let AI suggest rules to save based on your interactions." + warp_drive_context_description: "The Warp Agent can leverage your Warp Drive Contents to tailor responses to your personal and team developer workflows and environments. This includes any Workflows, Notebooks, and Environment Variables." + + key_for_voice_input_description: "Press and hold to activate." + + upgrade: "Upgrade" + compare_plans: "Compare plans" + contact_support: "Contact support" + + learn_more: "Learn more" + let_us_know: "Let us know" + refresh: "Refresh" + + codebase_context_description: "Allow the Warp Agent to generate an outline of your codebase that can be used for context. No code is ever stored on our servers." + mcp_description: "Add MCP servers to extend the Warp Agent's capabilities. MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins." + mcp_zero_state: "You haven't added any MCP servers yet. Once you do, you'll be able to control how much autonomy the Warp Agent has when interacting with them." + add_mcp_server: "Add a server" + + show_coding_agent_toolbar: "Show coding agent toolbar" + third_party_cli_agents: "Third party CLI agents" + auto_show_hide_rich_input: "Auto show/hide Rich Input based on agent status" + requires_warp_plugin: "Requires the Warp plugin for your coding agent" + auto_open_rich_input: "Auto open Rich Input when a coding agent session starts" + auto_dismiss_rich_input: "Auto dismiss Rich Input after prompt submission" + commands_enable_toolbar: "Commands that enable the toolbar" + add_regex_patterns_toolbar: "Add regex patterns to show the coding agent toolbar for matching commands." + + org_enforced_tooltip: "This option is enforced by your organization's settings and cannot be customized." + enable_agent_attribution: "Enable agent attribution" + agent_attribution: "Agent Attribution" + agent_attribution_description: "Oz can add attribution to commit messages and pull requests it creates" + + experimental: "Experimental" + computer_use: "Computer use" + ask_questions: "Ask questions" + full_terminal_use_model: "Full terminal use model" + computer_use_model: "Computer use model" + context_window: "Context window" + plan_auto_sync: "Plan auto-sync" + call_web_tools: "Call web tools" + section_models: "MODELS" + section_permissions: "PERMISSIONS" + computer_use_in_cloud_agents: "Computer use in Cloud Agents" + computer_use_description: "Enable computer use in cloud agent conversations started from the Warp app." + + cloud_handoff: "Cloud handoff" + cloud_handoff_header: "Cloud Handoff" + cloud_handoff_description: "Hand off local agent conversations to a cloud agent." + cloud_handoff_requires_cloud: "Cloud handoff requires cloud conversations to be enabled." + use_ampersand_handoff: "Use & to trigger handoff" + ampersand_handoff_description: "Type & as the first character to enter cloud handoff compose mode." + + openai_api_key: "OpenAI API key" + anthropic_api_key: "Anthropic API key" + google_api_key: "Google API key" + warp_credit_fallback: "Warp credit fallback" + warp_credit_fallback_description: "When enabled, agent requests may be routed to one of Warp's provided models in the event of an error. Warp will prioritize using your API keys over your Warp credits." + custom_inference: "Custom inference" + custom_endpoints: "Custom endpoints" + api_keys: "API Keys" + + aws_bedrock: "AWS Bedrock" + use_aws_bedrock_credentials: "Use AWS Bedrock credentials" + aws_bedrock_managed_by_org: "Warp loads and sends local AWS CLI credentials for Bedrock-supported models. This setting is managed by your organization." + aws_bedrock_description: "Warp loads and sends local AWS CLI credentials for Bedrock-supported models." + login_command: "Login Command" + aws_profile: "AWS Profile" + automatically_run_login_command: "Automatically run login command" + auto_login_description: "When enabled, the login command will run automatically when AWS Bedrock credentials expire." + default_model_modal: + api_key_description: "You added your own {provider_name} API key, but your default model is currently set to {current_default}, which won't work without Warp credits. Would you like to change your default model?" + custom_endpoint_description: "You added the \"{endpoint_name}\" custom endpoint, but your default model is currently set to {current_default}, which won't work without Warp credits. Would you like to change your default model?" + + # ── Custom Model Router ── + settings.custom_router: + editor: + header: "Router Editor" + new_router_title: "New Router" + name_placeholder: "My custom router" + name_placeholder_personalized: "{first_name}'s custom router" + type_complexity: "Complexity" + type_rules: "Rules" + add_rule: "+ Add rule" + name_required: "Router name is required." + complexity_field_default: "Default" + complexity_field_easy: "Easy" + complexity_field_medium: "Medium" + complexity_field_hard: "Hard" + model_required: "{field} model is required." + prompt_default_required: "A default model is required." + at_least_one_rule_required: "At least one rule with a description and model is required." + validation_error: "Validation: {err}" + serialization_error: "Serialization: {err}" + write_error: "Write error: {err}" + models_section: "Models" + default_dropdown: "Default (required)" + easy_dropdown: "Easy (required)" + medium_dropdown: "Medium (required)" + hard_dropdown: "Hard (required)" + default_model_section: "Default model" + rules_section: "Rules" + rules_help_1: "Rules are custom prompts that describe when to use a specific model. Warp intelligently matches your tasks against these rules." + rules_help_2: "Rules are matched top to bottom — rules higher in the list take precedence over those below." + router_name_section: "Router name" + router_type_section: "Router type" + complexity_based_label: "Complexity-based" + complexity_routing_explanation: " routing chooses a model based on Warp's classification of the task's difficulty." + rule_based_label: "Rule-based" + rule_routing_explanation: " routing chooses a model based on custom prompts." + rule_placeholder: "Describe when to use this model…" + rule_label: "Rule" + model_label: "Model" + view: + open_file: "Open file" + complexity_based_routing: "Complexity-based routing" + prompt_based_routing: "Prompt-based routing" + label_default: "Default:" + label_easy: "Easy:" + label_medium: "Medium:" + label_hard: "Hard:" + rules_count_one: "1 rule" + rules_count_other: "{n} rules" + routes: + complexity_requires_default: "complexity routing requires a non-empty `default` model" + prompt_requires_default: "prompt routing requires a non-empty `default` model" + default_error: "`default`: {e}" + complexity_bucket_error: "complexity bucket `{bucket}`: {e}" + prompt_rule_description_empty: "prompt rule {index}: `description` is empty" + prompt_rule_error: "prompt rule {index}: {e}" + target_empty: "target model id is empty" + target_is_auto: "target `{trimmed}` is an auto model; custom model routers must route to concrete models" + + # ── Settings Modals ── + settings.modals: + set_default_model: + not_now: "Not now" + change_default_model: "Change default model" + + # ── Billing Settings ── + settings.billing: + overage_usage_link: "View details on overage usage" + overage_toggle_admin_header: "Enable premium model usage overages" + overage_toggle_user_header_enabled: "Premium model usage overages are enabled" + overage_toggle_user_header_disabled: "Premium model usage overages are not enabled" + overage_toggle_description: "Continue using premium models beyond your plan's limits. Usage is charged in $20 increments up to your spending limit, with any remaining balance charged on your scheduled billing date." + overage_toggle_user_description: "Ask a team admin to enable overages for more AI usage." + + sort_a_to_z: "A to Z" + sort_z_to_a: "Z to A" + sort_usage_ascending: "Usage ascending" + sort_usage_descending: "Usage descending" + + auto_reload_exceed_limit: "Auto reload is disabled, as the next reload would exceed your monthly spend limit. Increase your limit to use auto reload." + auto_reload_delinquent: "Restricted due to billing issue. Update your payment method to purchase add-on credits." + restricted_billing_usage: "Auto reload is disabled due to recent failed reload. Please update your payment method and try again." + + overview_tab: "Overview" + usage_history_tab: "Usage History" + + enterprise_usage_callout_header: "Usage reporting is currently limited" + enterprise_usage_callout_body_admin_prefix: "Enterprise credit usage isn't fully available in this view yet. For the most accurate spend tracking, " + enterprise_usage_callout_body_admin_link: "visit the admin panel" + enterprise_usage_callout_body_admin_suffix: "." + enterprise_usage_callout_body_non_admin: "Enterprise credit usage isn't fully available in this view yet. Contact a team admin for detailed usage reporting." + + addon_credits_description: "Add-on credits are purchased in prepaid packages that roll over each billing cycle and expire after one year. The more you purchase, the better the per-credit rate. Once your base plan credits are used, add-on credits will be consumed." + additional_addon_credits_description_for_team: "Purchased add-on credits are shared across your team." + + cloud_agent_trial: "Cloud agent trial" + + overage_spending_limit: "Overage spending limit" + monthly_spending_limit: "Monthly spending limit" + load_more: "Load more" + + failed_update_workspace_settings: "Failed to update workspace settings" + successfully_purchased_addon_credits: "Successfully purchased add-on credits" + + not_set: "Not set" + monthly_overage_spending_limit: "Monthly overage spending limit" + sets_monthly_overage_limit: "Sets the monthly overage spending limit beyond the plan amount" + add_on_credits: "Add-on credits" + switch_to_build: "Switch to the Build plan" + upgrade_to_build: "Upgrade to the Build plan" + contact_account_executive: "Contact your Account Executive for more add-on credits." + contact_team_admin: "Contact a team admin to purchase add-on credits." + sets_monthly_limit_spent: "Sets the monthly limit spent on add-on credits" + monthly_spend_limit: "Monthly spend limit" + purchased_this_month: "Purchased this month" + auto_reload: "Auto reload" + auto_reload_description: "When enabled, auto reload will automatically purchase {amount} credits when your add-on credit balance reaches 100 credits remaining." + buying: "Buying…" + buy: "Buy" + one_time_purchase: "One-time purchase" + reloading_would_exceed: "Reloading would exceed your monthly limit. " + increase_your_limit: "Increase your limit" + total_overages: "Total overages" + usage_resets_on: "Usage resets on {date}" + credit_limit_prorated_self: "Your credit limit is prorated because you joined midway through the billing cycle." + credit_limit_prorated_other: "This credit limit is prorated because this user joined midway through the billing cycle." + usage_section: "Usage" + credits_header: "Credits" + credit_limit_description: "This is the {duration} limit of AI credits for your account." + last_30_days: "Last 30 days" + no_usage_history: "No usage history" + kick_off_agent_task: "Kick off an agent task to view usage history here." + sort_by: "Sort by" + team_total: "Team total" + manage_billing: "Manage billing" + contact_team_admin_billing: "Contact your team admin to resolve billing issues." + upgrade_to_turbo: "Upgrade to Turbo plan" + upgrade_to_lightspeed: "Upgrade to Lightspeed plan" + upgrade_plan: "Upgrade" + upgrade_to_max: "Upgrade to Max" + switch_to_business: "Switch to Business" + upgrade_to_enterprise: "Upgrade to Enterprise" + plan: "Plan" + open_admin_panel: "Open admin panel" + free: "Free" + new_agent: "New agent" + buy_more: "Buy more" + billing_and_usage: "Billing and usage" + + # ── OpenWarp Launch ── + openwarp_launch: + title: "Warp is now open-source" + description: "You, our community, can participate in building Warp using an agent-first workflow." + visit_repo: "Visit the repo" + contribute_title: "Contribute" + contribute_description: "Warp's client code is now open source. Get started by using the /feedback skill to open an issue, and follow the contribution guidelines here." + contribute_link_text: "here" + oad_title: "Open Automated Development" + oad_description: "The Warp repo is managed by an agent-first workflow powered by Oz, our cloud agent orchestration platform." + oz_link_text: "Oz" + auto_open_weights_title: "Introducing 'auto (open-weights)'" + auto_open_weights_description: "We've added a new auto model that picks the best open weight model for a task, like Kimi or MiniMax." + + # ── Privacy Settings ── + settings.privacy: + secret_redaction: "Secret redaction" + secret_redaction_description: "When this setting is enabled, Warp will scan blocks, the contents of Warp Drive objects, and Oz prompts for potential sensitive information and prevent saving or sending this data to any servers. You can customize this list via regexes." + custom_secret_redaction: "Custom secret redaction" + custom_secret_redaction_description: "Use regex to define additional secrets or data you'd like to redact. This will take effect when the next command runs. You can use the inline (?i) flag as a prefix to your regex to make it case-insensitive." + help_improve_warp: "Help improve Warp" + help_improve_warp_description: "App analytics help us make the product better for you. We may collect certain console interactions to improve Warp's AI capabilities." + help_improve_warp_description_enterprise: "App analytics help us make the product better for you. We only collect app usage metadata, never console input or output." + free_tier_analytics_note: "On the free tier, analytics must be enabled to use AI features." + manage_your_data: "Manage your data" + manage_your_data_description: "At any time, you may choose to delete your Warp account permanently. You will no longer be able to use Warp." + visit_data_management_page: "Visit data management page" + manage_privacy_settings: "Manage privacy settings" + privacy_policy_title: "Privacy Policy" + read_privacy_policy: "Read Warp's privacy policy" + add_regex_pattern: "Add regex pattern" + personal: "Personal" + enterprise: "Enterprise" + enterprise_secret_redaction_locked: "Enterprise secret redaction cannot be modified." + no_enterprise_regexes: "No enterprise regexes have been configured by your organization." + recommended: "Recommended" + add_all: "Add all" + add_regex: "Add regex" + enabled_by_organization: "Enabled by your organization." + secret_visual_redaction_mode: "Secret visual redaction mode" + secret_visual_redaction_mode_description: "Choose how secrets are visually presented in the block list while keeping them searchable. This setting only affects what you see in the block list." + zdr_enabled_description: "Your administrator has enabled zero data retention for your team. User generated content will never be collected." + managed_by_organization: "This setting is managed by your organization." + read_more_about_data_use: "Read more about Warp's use of data" + send_crash_reports: "Send crash reports" + send_crash_reports_description: "Crash reports assist with debugging and stability improvements." + store_ai_conversations_in_cloud: "Store AI conversations in the cloud" + cloud_conversation_storage_description: "Agent conversations can be shared with others and are retained when you log in on different devices. This data is only stored for product functionality, and Warp will not use it for analytics." + local_conversation_storage_description: "Agent conversations are only stored locally on your machine, are lost upon logout, and cannot be shared. Note: conversation data for ambient agents are still stored in the cloud." + network_log_console: "Network log console" + network_log_console_description: "We've built a native console that allows you to view all communications from Warp to external servers to ensure you feel comfortable that your work is always kept safe." + view_network_logging: "View network logging" + + # ── Teams Settings ── + settings.teams: + team_name: "Team name" + create_team_description: "When you create a team, you can collaborate on agent-driven development by sharing cloud agent runs, environments, automations, and artifacts. You can also create a shared knowledge store for teammates and agents alike." + leave_team: "Leave team" + delete_team: "Delete team" + create: "Create" + create_team: "Create a team" + teams: "Teams" + domains_comma_separated: "Domains, comma separated" + emails_comma_separated: "Emails, comma separated" + set: "Set" + invite: "Invite" + invalid_domains_instructions: "Some of the provided domains are invalid, or have already been added." + invite_link_toggle_instructions: "As an admin, you can choose whether to enable or disable the ability for team members to invite others by invitation link." + invite_link_domain_restrictions_instructions: "Restrict by domain — only allow users with emails at specific domains to join your team through the invite link." + email_invite_expiry_instructions: "Email invitations are valid for 7 days." + invalid_emails_instructions: "Some of the provided email addresses are invalid, already invited, or members of the team." + offline: "You are offline." + invite_team_members: "Invite team members" + by_link: "By link" + by_email: "By email" + by_discovery: "By discovery" + team_members: "Team members" + reset_links: "Reset links" + remove_domain: "Remove domain" + remove: "Remove" + approve_domains: "Approve domains" + send_email_invites: "Send email invites" + invite_link_prefix: "warp.dev/join/" + copy_link: "Copy link" + link_copied: "Link copied" + leave_team_confirmation: "Are you sure you want to leave this team?" + delete_team_confirmation: "Are you sure you want to delete this team? This action cannot be undone." + transfer_team_ownership: "Transfer team ownership?" + transfer_ownership: "Transfer ownership" + failed_to_send_invite: "Failed to send invite" + toggled_invite_links: "Toggled invite links" + failed_to_toggle_invite_links: "Failed to toggle invite links" + reset_invite_links_success: "Reset invite links" + failed_to_reset_invite_links: "Failed to reset invite links" + deleted_invite: "Deleted invite" + failed_to_delete_invite: "Failed to delete invite" + failed_to_add_domain_restriction: "Failed to add domain restriction" + failed_to_delete_domain_restriction: "Failed to delete domain restriction" + failed_to_generate_upgrade_link: "Failed to generate upgrade link. Please contact us at feedback@warp.dev" + failed_to_generate_billing_link: "Failed to generate billing link. Please contact us at feedback@warp.dev" + toggled_team_discoverability: "Toggled team discoverability" + failed_to_toggle_team_discoverability: "Failed to toggle team discoverability" + successfully_joined_team: "Successfully joined team" + failed_to_join_team: "Failed to join team" + successfully_transferred_team_ownership: "Successfully transferred team ownership" + failed_to_transfer_team_ownership: "Failed to transfer team ownership" + successfully_updated_team_member_role: "Successfully updated team member role" + failed_to_update_team_member_role: "Failed to update team member role" + error_leaving_team: "Error leaving team" + successfully_left_team: "Successfully left team" + your_new_team_name: "Your new team name" + new_team_name: "Your new team name" + seat_cap_reached: "You've reached your plan's member limit." + seat_cap_exceeded: "You've exceeded your plan's member limit. Existing team members keep their access, but you won't be able to add new members." + payment_past_due: "Payment past due" + payment_past_due_description: "Team invites have been restricted due to a past-due payment." + subscription_unpaid: "Subscription unpaid" + subscription_unpaid_description: "Team invites have been restricted due to an unpaid subscription." + contact_team_admin_to_restore: "Contact a team admin to restore access." + contact_team_admin_to_grow: "Contact a team admin to grow the team." + upgrade_to_grow_team: "Upgrade to grow your team." + contact_sales_to_grow: "Contact sales to grow your team." + update_payment_to_restore: "Update your payment information to restore access." + contact_support_to_restore: "Contact support to restore access." + upgrade: "Upgrade" + contact_sales: "Contact sales" + update_billing: "Update billing" + contact_support: "Contact support" + upgrade_to_build: "Upgrade to Build" + upgrade_to_turbo_plan: "Upgrade to Turbo plan" + upgrade_to_lightspeed_plan: "Upgrade to Lightspeed plan" + compare_plans: "Compare plans" + free_plan_usage_limits: "Free plan usage limits" + plan_usage_limits: "Plan usage limits" + shared_notebooks: "Shared Notebooks" + shared_workflows: "Shared Workflows" + want_to_grow_your_team: "Want to grow your team?" + grow_team: "Want to grow your team? " + manage_plan: "Manage plan" + manage_billing: "Manage billing" + open_admin_panel: "Open admin panel" + expired: "EXPIRED" + pending: "PENDING" + owner: "OWNER" + admin: "ADMIN" + past_due: "PAST DUE" + unpaid: "UNPAID" + failed_to_load_invite_link: "Failed to load invite link." + join_this_team: "Join this team and start collaborating on workflows, notebooks, and more." + join: "Join" + contact_admin_to_request_access: "Contact Admin to request access" + or_join_existing_team: "Or, join an existing team within your company" + allow_users_with_domain: "Allow Warp users with an @{domain} email to find and join the team." + allow_users_with_same_domain: "Allow Warp users with the same email domain as you to find and join the team." + member_limit_info: "Your plan ({plan}) has a maximum capacity of {cap} members." + additional_members_billed: "Additional members are billed at your plan's per-user rate: ${monthly:.0}/month or ${yearly:.0}/year, depending on your billing interval. {prorated}" + additional_members_billed_no_rate: "Additional members are billed at your plan's per-user rate. {prorated}" + prorated_charge_info: "You'll be charged for a portion of the team member's usage of Warp." + prorated_charge_info_admin: "Your admin will be charged for a portion of the team member's usage of Warp." + you_exceeded_member_limit: "You've exceeded your member limit" + + # ── Environments Settings ── + settings.environments: + environments: "Environments" + environments_description: "Environments define where your ambient agents run. Set one up in minutes via GitHub (recommended), Warp-assisted setup, or manual configuration." + search_environments: "Search environments..." + no_environments_match: "No environments match your search." + personal: "Personal" + no_environments_yet: "You haven't set up any environments yet." + choose_setup_method: "Choose how you'd like to set up your environment:" + quick_setup: "Quick setup" + suggested: "Suggested" + quick_setup_description: "Select the GitHub repositories you'd like to work with and we'll suggest a base image and config" + use_the_agent: "Use the agent" + use_the_agent_description: "Choose a locally set up project and we'll help you set up an environment based on it" + launch_agent: "Launch agent" + get_started: "Get started" + authorize: "Authorize" + retry: "Retry" + loading: "Loading..." + last_edited: "Last edited: {timestamp}" + last_used: "Last used: {timestamp}" + last_used_never: "Last used: never" + view_my_runs: "View my runs" + share: "Share" + edit: "Edit" + env_id: "Env ID: {id}" + image: "Image: {image}" + repos: "Repos: {repos}" + setup_commands: "Setup commands: {commands}" + shared_by_warp_and_team: "Shared by Warp and your team" + shared_by_warp_and: "Shared by Warp and {team}" + successfully_updated_environment: "Successfully updated environment" + successfully_created_environment: "Successfully created environment" + environment_deleted_successfully: "Environment deleted successfully" + successfully_shared_environment: "Successfully shared environment" + failed_to_share_environment: "Failed to share environment with team" + unable_to_create_not_logged_in: "Unable to create environment: not logged in." + unable_to_save_no_longer_exists: "Unable to save: environment no longer exists." + unable_to_share_not_on_team: "Unable to share environment: you are not currently on a team." + unable_to_share_not_synced: "Unable to share environment: environment is not yet synced." + + # ── Warp Drive Settings ── + settings.drive: + warp_drive: "Warp Drive" + sign_up: "Sign up" + to_use_warp_drive_create_account: "To use Warp Drive, please create an account." + warp_drive_description: "Warp Drive is a workspace in your terminal where you can save Workflows, Notebooks, Prompts, and Environment Variables for personal use or to share with a team." + + # ── About Settings ── + settings.about: + copyright: "Copyright 2026 Warp" + + # ── LSP ── + lsp: + open_logs: "Open logs" + restart_server: "Restart server" + stop_server: "Stop server" + start_server: "Start server" + remove_server: "Remove server" + restart_all_servers: "Restart all servers" + stop_all_servers: "Stop all servers" + start_all_servers: "Start all servers" + start_all_stopped_servers: "Start all stopped servers" + manage_servers: "Manage servers" + unknown_workspace: "unknown workspace" + stopped: "{server}: stopped" + error: "{server}: error" + lang_support_not_enabled: "Language support is not currently enabled for {name}" + lang_support_unavailable_file_type: "Language support is unavailable for this file type" + lang_server_unavailable: "Language server is unavailable for this codebase" + installing: "Installing {binary}..." + language_support_unavailable: "Language support is unavailable for {name}" + tab_config_skill_tooltip_enabled: "Open agent input with the /update-tab-config skill" + tab_config_skill_tooltip_disabled: "Enable AI to use the /update-tab-config skill" + + # ── Code Review ── + codereview: + git_dialog: + branch_label: "Branch" + button: + cancel: "Cancel" + commit: "Commit" + confirm: "Confirm" + create_pr: "Create PR" + open_pr: "Open PR" + publish: "Publish" + push: "Push" + changes_label: "Changes" + file_count_plural: "files" + file_count_singular: "file" + loading: + committing: "Committing…" + creating: "Creating…" + publishing: "Publishing…" + pushing: "Pushing…" + title: + commit: "Commit your changes" + view_changes: "View changes" + diffs_local_only: "Diffs only work for local workspaces." + diffs_git_only: "Diffs only work for git repositories." + diffs_wsl: "Diffs don't currently work in WSL." + hide_file_nav: "Hide file navigation" + show_file_nav: "Show file navigation" + cannot_discard_git_operation: "Cannot discard changes while a git operation (merge, rebase, etc.) is in progress" + no_changes_to_discard: "No changes to discard" + discard_uncommitted_title: "Discard uncommitted changes?" + discard_uncommitted_file_title: "Discard all uncommitted changes to file?" + discard_all_title: "Discard all changes?" + discard_all_file_title: "Discard all changes to file?" + discard_uncommitted_desc: "You're about to discard all local changes that haven't been committed." + discard_uncommitted_file_desc: "This will restore this file to the last committed version and discard local edits." + discard_all_desc: "You're about to discard all committed and uncommitted changes." + discard_all_file_desc: "This will restore this file to the main branch version and discard all committed and uncommitted edits." + discard_all_file_branch_desc: "This will reset this file to the {branch} branch version and discard all committed and uncommitted edits." + loading_changes: "Loading open changes..." + error_loading_diffs: "Error loading diffs" + retry: " Retry" + cannot_detect_diffs: "Cannot detect diffs for this folder" + no_open_changes: "No open changes" + track_changes_hint: "As you or the Agent make changes, you'll be able to track them here." + repo_initialized: "Repo is initialized with a {file_name} file." + comments_sent: "Comments sent to agent" + could_not_submit_comments: "Could not submit comments to the agent" + diff_removed: "Diff removed" + cannot_attach_context_terminal_running: "Cannot attach context when terminal is running" + cannot_attach_diff_input_unavailable: "Cannot attach diff while input is not available" + commit: "Commit" + no_changes_to_commit: "No changes to commit" + no_git_actions: "No git actions available" + push: "Push" + create_pr: "Create PR" + pr_number: "PR #{number}" + refreshing_pr_info: "Refreshing PR info" + publish: "Publish" + add_diff_set_context: "Add diff set as context" + show_saved_comment: "Show saved comment" + add_comment: "Add Comment" + send_to_agent: "Send to Agent" + search_diff_placeholder: "Search diff sets or branches to compare…" + discard_all: "Discard all" + stash_changes: "Stash changes" + maximize: "Maximize" + discard_changes: "Discard changes" + initialize_codebase: "Initialize codebase" + initialize_codebase_tooltip: "Enables codebase indexing and WARP.md" + open_repository: "Open repository" + open_repository_tooltip: "Navigate to a repo and initialize it for coding" + uncommitted_changes: "Uncommitted changes" + open_file: "Open file" + add_file_diff_as_context: "Add file diff as context" + copy_file_path: "Copy file path" + diff_too_large: "Diff is too large to render" + binary_file_no_diff: "Binary file - no diff available" + file_renamed_no_changes: "File renamed without changes" + new_empty_file: "New empty file" + unable_to_load_file_content: "Unable to load file content" + no_file_selected: "No file selected" + no_files_to_discard: "No files to discard" + unsaved_changes_tooltip: "This file has unsaved changes. {shortcut} to save" + dialog: + # Buttons + confirm_button: "Confirm" + cancel_button: "Cancel" + esc_tooltip: "ESC" + commit_button: "Commit" + commit_and_push: "Commit and push" + commit_and_publish: "Commit and publish" + commit_and_create_pr: "Commit and create PR" + push_button: "Push" + publish_button: "Publish" + create_pr_button: "Create PR" + # Titles + title_commit: "Commit your changes" + title_publish: "Publish branch" + title_push: "Push changes" + title_pr: "Create pull request" + # Section labels + branch_section: "Branch" + changes_section: "Changes" + message_section: "Commit message" + included_commits: "Included commits" + include_unstaged: "Include unstaged" + # Loading states + committing: "Committing…" + pushing: "Pushing…" + publishing: "Publishing…" + creating: "Creating…" + loading: "Loading…" + generating_message: "Generating commit message…" + # Placeholders + type_message: "Type a commit message" + # Tooltips + tooltip_enter_message: "Enter a commit message" + # Toast messages + toast_committed: "Changes successfully committed." + toast_committed_pushed: "Changes committed and pushed." + toast_pushed: "Changes successfully pushed." + toast_branch_published: "Branch successfully published." + toast_pr_created: "PR successfully created." + open_pr_link: "Open PR" + # Pluralization + file_singular: "file" + file_plural: "files" + # Error messages + error_no_changes: "No changes to commit." + error_git_identity: "Git identity not configured. Set user.name and user.email." + error_rejected: "Remote has new changes — pull before pushing." + error_no_remote: "No remote configured for this branch." + error_auth: "Authentication failed. Check your Git credentials." + error_network: "Network error. Check your connection." + error_repo_not_found: "Remote repository not found." + error_no_gh: "GitHub CLI (gh) not installed. See https://cli.github.com/." + error_gh_auth: "GitHub CLI not authenticated. Run `gh auth login`." + error_generic: "Git operation failed." + reviewing_open_changes: "Reviewing open changes" + reviewing_changes: "Reviewing code changes" + outdated: "Outdated" + comment: "comment" + from_github: "From GitHub" + review_comment: "Review Comment" + copy_text: "Copy text" + edit_comment: "Edit" + file_level_cannot_edit: "File-level comments currently can't be edited." + outdated_cannot_edit: "Outdated comments can't be edited." + view_in_github: "View in GitHub" + remove_comment: "Remove" + no_non_outdated_to_send: "No non-outdated comments to send" + send_to_cli: "Send diff comments to {label}" + ai_must_be_enabled: "AI must be enabled to send comments to Agent" + ai_credits_required: "Agent code review requires AI credits" + all_terminals_busy: "All terminals are busy" + send_to_agent_tooltip: "Send diff comments to Agent" + outdated_section_single: "1 comment will be omitted because it is outdated." + outdated_section_plural: "{count} comments will be omitted because they are outdated." + + # ── Terminal Input ── + terminal.input: + a11y: + label: "Command Input." + helper: "Input your shell command, press enter to execute. Press cmd-up to navigate to output of previously executed commands. Press cmd-l to re-focus command input." + placeholder: + tell_agent: "Tell the agent what to build..." + cloud_agent: "Kick off a cloud agent" + search_queries: "Search queries" + search_queries_rewind: "Search queries to rewind to" + search_conversations: "Search conversations" + search_skills: "Search skills" + search_models: "Search models" + search_profiles: "Search profiles" + search_commands: "Search commands" + search_prompts: "Search prompts" + search_indexed_repos: "Search indexed repos" + search_plans: "Search plans" + enter_prompt_for: "Enter prompt for {}..." + hint: + run_commands: "Run commands" + ai_command_search: "Type '#' for AI command suggestions" + steer_agent: "Steer the running agent" + steer_agent_classic: "Steer the running agent, or backspace to exit" + follow_up: "Ask a follow up" + follow_up_classic: "Ask a follow up, or backspace to exit" + warp_anything_0: "Warp anything e.g. Deploy my React app to Vercel and set up environment variables" + warp_anything_1: "Warp anything e.g. Help me debug why my Python tests are failing in CI" + warp_anything_2: "Warp anything e.g. Set up a new microservice with Docker and create the deployment pipeline" + warp_anything_3: "Warp anything e.g. Find and fix the memory leak in my Node.js application" + warp_anything_4: "Warp anything e.g. Create a backup script for my PostgreSQL database and schedule it" + warp_anything_5: "Warp anything e.g. Help me migrate my data from MySQL to PostgreSQL" + warp_anything_6: "Warp anything e.g. Set up monitoring and alerts for my AWS infrastructure" + warp_anything_7: "Warp anything e.g. Build a REST API for my mobile app using FastAPI" + warp_anything_8: "Warp anything e.g. Help me optimize my SQL queries that are running slowly" + warp_anything_9: "Warp anything e.g. Create a GitHub Actions workflow to automatically deploy on merge" + warp_anything_10: "Warp anything e.g. Set up Redis caching for my web application" + warp_anything_11: "Warp anything e.g. Help me troubleshoot why my Kubernetes pods keep crashing" + warp_anything_12: "Warp anything e.g. Build a data pipeline to process CSV files and load them into BigQuery" + warp_anything_13: "Warp anything e.g. Set up SSL certificates and configure HTTPS for my domain" + warp_anything_14: "Warp anything e.g. Help me refactor this legacy code to use modern design patterns" + warp_anything_15: "Warp anything e.g. Create unit tests for my authentication service" + warp_anything_16: "Warp anything e.g. Set up log aggregation with ELK stack for my distributed system" + warp_anything_17: "Warp anything e.g. Help me implement OAuth2 authentication in my Express.js app" + warp_anything_18: "Warp anything e.g. Optimize my Docker images to reduce build times and size" + warp_anything_19: "Warp anything e.g. Set up A/B testing infrastructure for my web application" + dynamic_enum: + generate: "Run the following command to generate variants:" + run: "Run command" + pending: "Command pending..." + failure: "Command failed" + no_results: "Command returned no results" + keybinding: + show_history: "Show History" + network_log: "Show Warp network log" + clear_screen: "Clear screen" + scroll_up: "Scroll terminal output up one page" + scroll_down: "Scroll terminal output down one page" + edit_prompt: "Edit Prompt" + toggle_classic_completions: "(Experimental) Toggle classic completions mode" + command_search: "Command Search" + history_search: "History Search" + open_completions: "Open completions menu" + workflows: "Workflows" + ai_command_suggestions: "Open AI Command Suggestions" + new_agent_conversation: "New agent conversation" + trigger_auto_detection: "Trigger Auto Detection" + clear_ai_context_menu: "Clear and reset AI context menu query" + toast: + no_active_conversation: "No active conversation to export" + cannot_start_conversation: "Cannot start a new conversation while agent is monitoring a command." + handoff_to_cloud: "Handoff to cloud" + handoff_to_env: "Hand off to {}" + + # ── Terminal Notifications ── + terminal.notifications: + discovery: + long_running: "Warp can notify you when long-running commands finish." + agent_task_completed: "Warp can notify you when an agent finishes responding." + needs_attention: "Warp can notify you when a command or agent needs your attention." + password_prompt: "Warp can notify you when you're prompted to enter a password." + a11y_enable_palette: "You can enable notifications through the command palette." + a11y_enable_preferences: "Make sure you have enabled access for Warp notifications in System Preferences." + error_sending: "Error sending notification" + dismissed: "We won't show this banner again, but you can always go to Settings to enable notifications." + disabled: "Notifications were turned off, but you can always go to Settings to enable notifications." + enable: "Enable" + learn_more: "Learn more" + troubleshoot: "Troubleshoot" + configure: "Configure notifications" + permissions_accepted: "Success! You are now ready to receive desktop notifications." + permissions_denied: "Warp was denied permissions to send you notifications." + permissions_error: "Something went wrong while requesting permissions." + allow_permissions: "Don't forget to 'Allow' the permissions request to finish setting up notifications." + terminal.notification: + default_title: "Notification" + finished: " finished" + failed: " failed" + finished_after: " finished after {duration}s" + failed_after: " failed after {duration}s" + latest_output: "Latest output: " + error_prefix: "Error: " + blocked: " blocked" + waiting_for_password: " is waiting for a password" + + # ── Terminal Context Menu ── + terminal.context_menu: + copy: "Copy" + copy_url: "Copy URL" + copy_path: "Copy path" + copy_command: "Copy command" + copy_commands: "Copy commands" + copy_output: "Copy output" + copy_filtered_output: "Copy filtered output" + copy_prompt: "Copy prompt" + copy_right_prompt: "Copy right prompt" + copy_working_directory: "Copy working directory" + copy_git_branch: "Copy git branch" + cut: "Cut" + paste: "Paste" + select_all: "Select all" + insert_into_input: "Insert into input" + find_within_block: "Find within block" + find_within_blocks: "Find within blocks" + scroll_to_top_of_block: "Scroll to top of block" + scroll_to_top_of_blocks: "Scroll to top of blocks" + scroll_to_bottom_of_block: "Scroll to bottom of block" + scroll_to_bottom_of_blocks: "Scroll to bottom of blocks" + save_as_workflow: "Save as workflow" + ask_warp_ai: "Ask Warp AI" + toggle_block_filter: "Toggle block filter" + toggle_bookmark: "Toggle bookmark" + rewind_to_before_here: "Rewind to before here" + clear_blocks: "Clear Blocks" + split_pane_right: "Split pane right" + split_pane_left: "Split pane left" + split_pane_down: "Split pane down" + split_pane_up: "Split pane up" + close_pane: "Close pane" + edit_cli_agent_toolbelt: "Edit CLI agent toolbelt" + edit_agent_toolbelt: "Edit agent toolbelt" + edit_prompt: "Edit prompt" + command_search: "Command search" + ai_command_search: "AI command search" + hide: "Hide" + show: "Show" + input_hint_text: "input hint text" + share_block_ellipsis: "Share block..." + share_ellipsis: "Share..." + show_in_finder: "Show in Finder" + show_containing_folder: "Show containing folder" + open_in_warp: "Open in Warp" + open_in_editor: "Open in editor" + fork_from_here_dev: "Fork from here (dev only)" + + # ── Terminal Tooltips ── + terminal.tooltip: + filter_block_output: "Filter block output" + bookmark_block: "Bookmark this block to quickly scroll to it" + lock_scrolling: "Lock scrolling at bottom of block" + jump_to_bottom: "Jump to the bottom of this block" + + # ── Terminal Block ── + terminal.block: + show_init_block: "Show initialization block" + more_info: "More info" + + # ── Terminal Zero State ── + terminal.zero_state: + new_session: "New terminal session" + start_agent_conversation: "start a new agent conversation" + start_cloud_conversation: "start a new cloud agent conversation" + cycle_past: "cycle past commands and conversations" + open_code_review: "open code review" + + # ── Terminal Shared Session ── + terminal.shared_session: + reconnecting: "Offline, trying to reconnect..." + + # ── Terminal SSH ── + terminal.ssh: + file_uploads: "File Uploads" + + # ── Terminal Shell ── + terminal.shell: + bootstrap_slow: "Seems like your shell is taking a while to start... " + incompatible_config: "Your shell configuration is incompatible with Warp... " + completions_not_working: "Seems like your completions are not working (" + completions_suffix: "). Enabling tmux warpification in " + may_resolve_issue: " may resolve this issue." + settings: "settings" + subshell: "Subshell" + + # ── Terminal A11y ── + terminal.a11y: + press_right_arrow: "Press right arrow to insert or keep editing to ignore" + command_waiting_password: "Command is waiting for a password" + + # ── Terminal Agent ── + terminal.agent: + execute_this_plan: "Execute this plan" + fork_from_last_query: "Fork from last query" + + # ── Terminal Environment ── + terminal.environment: + create_with_repo: "Create environment using the current working dir as repo" + create_without_repo: "Create environment without any repos" + + # ── Terminal Project ── + terminal.project: + setup: "Project setup" + use_file_picker: "Use file picker to select a git repository" + + # ── Terminal Skills ── + terminal.skills: + bundled_cannot_edit: "Bundled skills cannot be edited" + editing_not_supported: "Editing skills is not supported in this build" + no_skills: "No skills found" + + # ── Terminal OSC 52 (Clipboard) ── + terminal.osc52: + banner_text: "A terminal program tried to access your clipboard. This is disabled by default for security reasons." + allow_button: "Allow" + dont_show_again: "Don't show again" + write_blocked_text: "A terminal program tried to write to your clipboard. This is disabled by default for security reasons, to protect against malicious software." + read_blocked_text: "A terminal program tried to read your clipboard. This is disabled by default for security reasons, to protect against malicious software." + allow_writes: "Allow clipboard writes" + allow_reads_and_writes: "Allow clipboard reads and writes" + + # ── Agent Management ── + agent: + filter: + all: "All" + working: "Working" + done: "Done" + failed: "Failed" + status: "Status" + source: "Source" + created: "Created on" + has_artifact: "Has artifact" + harness: "Harness" + environment: "Environment" + created_by: "Created by" + none: "None" + pull_request: "Pull Request" + plan: "Plan" + screenshot: "Screenshot" + file: "File" + last_24_hours: "Last 24 hours" + past_3_days: "Past 3 days" + last_week: "Last week" + all.tooltip: "View your agent tasks plus all shared team tasks" + personal: "Personal" + personal.tooltip: "View agent tasks you created" + personal: "Personal" + get_started: "Get started" + view_agents: "View Agents" + clear_filters: "Clear filters" + clear_all: "Clear all" + search_placeholder: "Search" + new_agent: "New agent" + session_expired: "Session Expired" + no_session: "No session" + session_expired.tooltip: "Sessions expire after one week and cannot be opened." + runs: "Runs" + loading_cloud_runs: "Loading cloud runs…" + loading_agents: "Loading agents…" + no_results: "No results" + label: "Agent" + executor: "Executor" + cloud_setup: + visit_oz: "Visit Oz" + notifications: + mark_all_read: "Mark all as read" + open_conversation: "Open conversation" + no_notifications: "No notifications" + update_agent: "Update Agent" + remove_queued_prompt: "Remove queued prompt" + send_now: "Send now" + open_in_code_review: "Open in code review" + manage_rules: "Manage rules" + review_changes: "Review changes" + open_all_in_code_review: "Open all in code review" + dont_show_again: "Don't show again" + allow: "Allow" + refine: "Refine" + take_over: "Take over" + take_control: "Take control" + type_your_answer_placeholder: "Type your answer and press Enter" + footer: + file_explorer: "File explorer" + file_explorer_tooltip: "Open file explorer" + rich_input: "Rich Input" + rich_input_tooltip: "Open Rich Input" + hide_rich_input: "Hide Rich Input" + hide_rich_input_tooltip: "Hide Rich Input" + coding_agent_settings_tooltip: "Open coding agent settings" + enable_notifications: "Enable notifications" + install_plugin_tooltip: "Install the Warp plugin to enable rich agent notifications within Warp" + notifications_setup_instructions: "Notifications setup instructions" + plugin_install_instructions_tooltip: "View instructions to install the Warp plugin" + update_warp_plugin: "Update Warp plugin" + plugin_update_available_tooltip: "A new version of the Warp plugin is available" + plugin_update_instructions: "Plugin update instructions" + plugin_update_instructions_tooltip: "View instructions to update the Warp plugin" + remote_control: "/remote-control" + stop_sharing: "Stop sharing" + stop_sharing_tooltip: "Stop sharing" + context_window_tooltip: "Context window usage" + rewind: "Rewind" + rewind_tooltip: "Rewind to before this block" + refresh_aws_credentials: "Refresh AWS Credentials" + configure: "Configure" + add_rule: "Add rule" + edit_rule: "Edit rule" + continue_locally: "Continue locally" + continue_locally_tooltip: "Fork this conversation locally" + view_in_oz: "View in Oz" + view_in_oz_tooltip: "View this run in the Oz web app" + initialize_project: "Initialize Project" + delete_rule: "Delete rule" + delete_profile: "Delete profile" + profile_editor: "Profile Editor" + edit_profile: "Edit Profile" + name_label: "Name" + default_profile_name_locked: "Default profile name cannot be changed." + profile_default: "Profile default" + creator_unknown: "Unknown" + copied_branch_name: "Copied branch name" + metadata: + source: "Source: {name}" + harness: "Harness: {name}" + run_time: "Run time: {value}" + credits_used: "Credits used: {usage}" + + # ── Resource Center ── + resource_center: + toggle_panel: "To toggle this panel" + mark_all_read: "Mark all as read" + + # ── Agent Message Bar ── + agent.message_bar: + starting_shell: "Starting shell..." + + # ── Agent Output ── + agent.output: + always_allow_file_access: "Always allow file access for this repo" + grep_for: "Grep for " + search_files: "Search for files that match " + + # ── Agent Search Codebase ── + agent.search_codebase: + no_results: "No results found" + + # ── Agent Usage ── + agent.usage: + credits_total: "Credits spent (total)" + credits_spent: "Credits spent" + tool_calls: "Tool calls" + context_window: "Context window used" + files_changed: "Files changed" + diffs_applied: "Diffs applied" + commands_executed: "Commands executed" + time_to_first_token: "Time to first token" + total_response_time: "Total agent response time" + + # ── Agent Zero State ── + agent.zero_state: + send_prompt: "Send a prompt below to start a new conversation" + new_conversation: "New Oz agent conversation" diff --git a/resources/bundled/locales/ru.yml b/resources/bundled/locales/ru.yml new file mode 100644 index 0000000000..9cf61be20c --- /dev/null +++ b/resources/bundled/locales/ru.yml @@ -0,0 +1,2436 @@ +ru: + # ── Меню ── + menu: + warp: "Warp" + file: "Файл" + edit: "Правка" + view: "Вид" + tab: "Вкладка" + ai: "ИИ" + blocks: "Блоки" + drive: "Диск" + window: "Окно" + help: "Справка" + + new_window: "Новое окно" + new_tab: "Новая вкладка" + new_terminal_tab: "Новая вкладка терминала" + new_agent_tab: "Новая вкладка Agent" + preferences: "Настройки" + privacy_policy: "Политика конфиденциальности..." + set_default_terminal: "Сделать Warp терминалом по умолчанию" + log_out: "Выйти" + open_recent: "Недавние" + + copy_on_select: "Копировать при выделении в терминале" + synchronize_inputs: "Синхронизировать ввод" + + toggle_mouse_reporting: "Переключить отчёты мыши" + toggle_scroll_reporting: "Переключить отчёты прокрутки" + toggle_focus_reporting: "Переключить отчёты фокуса" + compact_mode: "Компактный режим" + + show_init_block: "Показать блок инициализации" + hide_init_block: "Скрыть блок инициализации" + show_ssh_blocks: "Показать Warpified SSH блоки" + hide_ssh_blocks: "Скрыть Warpified SSH блоки" + show_in_band_command_blocks: "Показать встроенные командные блоки" + hide_in_band_command_blocks: "Скрыть встроенные командные блоки" + export_settings_csv: "Экспорт настроек по умолчанию в CSV" + + enable_shell_debug_mode: "Включить режим отладки Shell (-x) для новых сессий" + disable_shell_debug_mode: "Отключить режим отладки Shell (-x) для новых сессий" + enable_in_band_generators: "Включить встроенные генераторы для новых сессий" + disable_in_band_generators: "Отключить встроенные генераторы для новых сессий" + enable_pty_recording: "Включить режим записи PTY" + disable_pty_recording: "Отключить режим записи PTY" + manually_toggle_network: "Переключить состояние сети вручную" + debug: "Отладка" + use_warp_prompt: "Использовать приглашение Warp" + create_anonymous_user: "Создать анонимного пользователя" + send_feedback: "Отправить отзыв..." + warp_documentation: "Документация Warp..." + github_issues: "Задачи на GitHub..." + warp_slack_community: "Сообщество Warp в Slack..." + save_new: "Сохранить как новое..." + reopen_closed_session: "Открыть закрытую сессию" + launch_configurations: "Конфигурации запуска" + + # ── Контекстное меню вкладки ── + tab: + rename: "Переименовать вкладку" + reset_name: "Сбросить имя вкладки" + move_up: "Переместить вверх" + move_down: "Переместить вниз" + move_left: "Переместить влево" + move_right: "Переместить вправо" + close: "Закрыть вкладку" + close_other: "Закрыть другие вкладки" + close_below: "Закрыть вкладки ниже" + close_right: "Закрыть вкладки справа" + save_as_new_config: "Сохранить как новую конфигурацию" + stop_sharing: "Остановить общий доступ" + share_session: "Поделиться сессией" + stop_sharing_all: "Остановить весь общий доступ" + copy_link: "Копировать ссылку" + copy_title: "Копировать заголовок вкладки" + copy_pane_title: "Копировать заголовок панели" + copy_branch: "Копировать ветку" + copy_working_dir: "Копировать рабочую директорию" + copy_pr_link: "Копировать ссылку Pull Request" + default_no_color: "По умолчанию (без цвета)" + cloud_agent_run: "Облачный запуск Agent" + + # ── Рабочая область / Панель инструментов ── + terminal: + share_block: + command: "Команда" + command_and_output: "Команда и вывод" + copy: "Копировать" + create_link: "Создать ссылку" + creating_block: "Создание блока..." + get_embed: "Получить код для вставки" + keybinding_copy: "Копировать" + link_copied: "Ссылка скопирована." + output: "Вывод" + share_block: "Поделиться блоком" + show_prompt: "Показать приглашение" + title_placeholder: "Название (необязательно)" + workspace: + settings: "Настройки" + new_tab: "Новая вкладка" + agent: "Агент" + terminal: "Терминал" + cloud_oz: "Облачный Oz" + local_docker_sandbox: "Локальная песочница Docker" + whats_new: "Что нового" + keyboard_shortcuts: "Горячие клавиши" + tab_configs: "Конфигурации вкладок" + new_worktree_config: "Новая конфигурация рабочего дерева" + new_tab_config: "Новая конфигурация вкладки" + reopen_closed_session: "Открыть закрытую сессию" + rearrange_toolbar_items: "Переставить элементы панели" + update_warp_manually: "Обновить Warp вручную" + update_and_relaunch_warp: "Обновить и перезапустить Warp" + documentation: "Документация" + feedback: "Обратная связь" + view_warp_logs: "Посмотреть логи Warp" + slack: "Slack" + sign_up: "Регистрация" + billing_and_usage: "Биллинг и использование" + upgrade: "Улучшить" + upgrade_plan: "Улучшить план" + invite_a_friend: "Пригласить друга" + log_out: "Выйти" + cloud_agent: "Облачный Agent" + current_version_is: "Текущая версия: {version}" + install_update: "Установить обновление ({version})" + updating_to: "Обновление до ({version})" + install_update_tab: "Установить обновление" + reset_pane_name: "Сбросить имя панели" + reset_active_pane_name: "Сбросить имя активной панели" + staging_api_call_failed: "Ошибка вызова Staging API. Возможно, изменился IP-адрес?" + failed_load_conversation_data: "Не удалось загрузить данные разговора." + remote_control_link_copied: "Ссылка удалённого управления скопирована." + untitled_pane: "Безымянная панель" + view_all: "Показать все" + show_less: "Показать меньше" + no_conversations_yet: "Пока нет разговоров" + conversations_empty_description: "Ваши активные и прошлые разговоры с локальными и ambient агентами появятся здесь." + new_conversation: "Новый разговор" + conversation: "Разговор" + conversations_delete_in_progress: "Нельзя удалить разговоры во время выполнения." + conversation_cannot_be_deleted: "Этот разговор нельзя удалить" + delete_conversation_confirm_title: "Удалить '{title}'?" + delete_conversation_confirm_title_unknown: "Удалить разговор?" + delete_conversation_confirm_body: "Этот разговор будет удалён навсегда. Это действие нельзя отменить." + share_conversation: "Поделиться разговором" + fork_in_new_pane: "Форкнуть в новой панели" + fork_in_new_tab: "Форкнуть в новой вкладке" + no_matching_conversations: "Нет подходящих разговоров" + view_options: "Параметры просмотра" + no_tabs_open: "Нет открытых вкладок" + no_tabs_match_search: "Нет вкладок, соответствующих поиску." + view_as: "Просмотр как" + tab_item: "Элемент вкладки" + pane_title_as: "Заголовок панели как" + additional_metadata: "Дополнительные метаданные" + failed_create_log_bundle: "Не удалось создать пакет логов: {err}" + successfully_installed_oz_cli: "Oz CLI успешно установлен! Теперь можно запускать '{command_name}' из командной строки." + learn_more: "Узнать больше" + failed_install_oz_command: "Не удалось установить команду Oz: {error}" + successfully_uninstalled_oz_command: "Команда Oz успешно удалена." + failed_uninstall_oz_command: "Не удалось удалить команду Oz: {error}" + notifications_permission_denied: "У Warp нет разрешения на отправку уведомлений." + close_panel: "Закрыть панель" + troubleshoot_notifications: "Диагностика уведомлений" + view_changelog: "Посмотреть список изменений" + view_all_cloud_runs: "Посмотреть все облачные запуски" + warp_updated: "Warp обновлён!" + command_still_running: "Команда в этой сессии всё ещё выполняется." + cannot_open_new_terminal_session: "Не удалось открыть новую терминальную сессию" + workflow_no_longer_available: "Этот рабочий процесс больше не доступен." + plan_synced_to_warp_drive: "План синхронизирован с Warp Drive" + view: "Просмотр" + undo: "Отменить" + open_file: "Открыть файл" + open: "Открыть: %s" + rename_pane: "Переименовать панель" + rename_active_pane: "Переименовать активную панель" + rename_current_tab: "Переименовать текущую вкладку" + rename_current_pane: "Переименовать текущую панель" + tab_configs_chip: "Доступ к конфигурациям вкладок здесь." + add_new_repo: " + Добавить репозиторий" + failed_load_conversation: "Не удалось загрузить разговор." + failed_load_conversation_for_forking: "Не удалось загрузить разговор для форка." + conversation_forking_failed: "Форк разговора не удался." + failed_prepare_handoff: "Не удалось подготовить передачу. Попробуйте снова." + handing_off_to_cloud: "Передача в облако" + no_terminal_pane_for_context: "Нет открытой терминальной панели. Откройте новую панель для прикрепления контекста." + plan_already_in_context: "Этот план уже в контексте." + check_latest_version: "Проверьте последнюю версию и попробуйте снова." + ask_ai_description: "Спросите Warp AI объяснить ошибки, предложить команды или написать скрипты." + search_sessions_agents_files: "Поиск сессий, агентов, файлов..." + disabled_synchronized_inputs: "Все синхронизированные вводы отключены." + sampling_process: "Сэмплирование процесса 3 секунды..." + failed_delete_conversation: "Не удалось удалить разговор. Выйдите из вида агента и попробуйте снова." + conversation_deleted: "Разговор удалён" + update_warp: "Обновить Warp" + version_deprecation: "Ваше приложение устарело, некоторые функции могут работать некорректно. Пожалуйста, обновитесь немедленно." + version_deprecation_without_permissions: "Некоторые функции Warp могут работать некорректно без немедленного обновления, но Warp не может выполнить обновление." + unable_update_new_version: "Доступна новая версия, но Warp не может выполнить обновление." + unable_launch_new_version: "Warp не смог запустить новую установленную версию." + update_now: "Обновить сейчас" + app_needs_update: "Приложение устарело и требует обновления." + restart_and_update_now: "Перезапустить и обновить сейчас" + search_repos: "Поиск репозиториев" + search_tabs: "Поиск вкладок..." + feedback_placeholder: "Опишите, что сломано, непонятно или отсутствует..." + resource_not_found: "Ресурс не найден или доступ запрещён" + conversation_not_synced: "Ваш разговор ещё не синхронизирован с облаком. Отправьте ещё одно сообщение и попробуйте снова." + couldnt_save_conversation: "Не удалось сохранить разговор локально. Отправьте ещё одно сообщение и попробуйте снова." + failed_sample_process: "Не удалось сэмплировать процесс (проверьте логи)" + agent_management_panel: "Панель управления агентами" + tabs_panel: "Панель вкладок" + project_explorer: "Проводник проекта" + global_search: "Глобальный поиск" + warp_drive: "Warp Drive" + agent_conversations: "Разговоры агентов" + tools_panel: "Панель инструментов" + code_review_panel: "Панель код-ревью" + notifications: "Уведомления" + warp_essentials: "Warp Essentials" + settings_tooltip: "Настройки" + login_expired: "Срок входа истёк." + please_sign_in_again: "Пожалуйста, войдите снова для восстановления доступа к облачным функциям." + sign_in: "Войти" + fix_with_oz: "Исправить с Oz" + linear_issue: "Задача Linear" + more_info: "Подробнее" + + # ── Workspace Conversation List ── + workspace.conversation_list: + no_conversations: "Пока нет бесед" + no_conversations_hint: "Ваши активные и прошлые беседы с локальными и облачными агентами будут отображаться здесь." + new_conversation: "Новая беседа" + + # ── Workspace Global Search ── + workspace.global_search: + search_in_files: "Поиск в файлах" + + # ── Привязки рабочей области ── + workspace.binding: + toggle_project_explorer: "Переключить обозреватель проекта" + create_team_notebook: "Создать новую командную записную книжку" + create_personal_notebook: "Создать новую личную записную книжку" + create_team_workflow: "Создать новый командный рабочий процесс" + create_personal_workflow: "Создать новый личный рабочий процесс" + create_team_folder: "Создать новую командную папку" + create_personal_folder: "Создать новую личную папку" + create_new_tab: "Создать новую вкладку" + new_terminal_tab: "Новая вкладка терминала" + new_agent_tab: "Новая вкладка Agent" + new_cloud_agent_tab: "Новая вкладка облачного Agent" + open_left_panel: "Открыть левую панель" + toggle_code_review: "Переключить Code Review" + toggle_vertical_tabs_panel: "Переключить вертикальную панель вкладок" + left_panel_agent_conversations: "Левая панель: Разговоры Agent" + left_panel_project_explorer: "Левая панель: Обозреватель проекта" + left_panel_global_search: "Левая панель: Глобальный поиск" + left_panel_warp_drive: "Левая панель: Warp Drive" + open_global_search: "Открыть глобальный поиск" + toggle_warp_drive: "Переключить Warp Drive" + toggle_agent_conversation_list_view: "Переключить список разговоров Agent" + close_focused_panel: "Закрыть сфокусированную панель" + toggle_command_palette: "Переключить командную палитру" + move_tab_left: "Переместить вкладку влево" + move_tab_right: "Переместить вкладку вправо" + close_window: "Закрыть окно" + close_tabs_to_the_right: "Закрыть вкладки справа" + toggle_navigation_palette: "Переключить навигационную палитру" + create_team_env_vars: "Создать новые командные переменные окружения" + create_personal_env_vars: "Создать новые личные переменные окружения" + create_personal_prompt: "Создать новый личный промпт" + create_team_prompt: "Создать новый командный промпт" + open_repository: "Открыть репозиторий" + open_ai_rules: "Открыть правила AI" + open_mcp_servers: "Открыть серверы MCP" + switch_focus_left: "Переключить фокус на левую панель" + switch_focus_right: "Переключить фокус на правую панель" + import_to_personal_drive: "Импортировать в личный Drive" + import_to_team_drive: "Импортировать в командный Drive" + open_theme_picker: "Открыть выбор темы" + sample_process: "Сэмплировать процесс" + dump_heap_profile: "Дамп профиля кучи (только один раз)" + open_tab_configs_menu: "Открыть меню конфигураций вкладок" + increase_zoom_level: "Увеличить масштаб" + decrease_zoom_level: "Уменьшить масштаб" + reset_zoom_level: "Сбросить масштаб по умолчанию" + increase_font_size: "Увеличить размер шрифта" + decrease_font_size: "Уменьшить размер шрифта" + reset_font_size: "Сбросить размер шрифта по умолчанию" + toggle_keyboard_shortcuts: "Переключить горячие клавиши" + open_keybindings_editor: "Открыть редактор горячих клавиш" + toggle_sticky_command_header: "Переключить закреплённый заголовок команды" + rename_current_tab: "Переименовать текущую вкладку" + rename_current_pane: "Переименовать текущую панель" + quit_warp: "Выйти из Warp" + close_current_tab: "Закрыть текущую вкладку" + close_other_tabs: "Закрыть другие вкладки" + turn_notifications_on: "Включить уведомления" + turn_notifications_off: "Выключить уведомления" + launch_configuration_palette: "Палитра конфигураций запуска" + toggle_files_palette: "Переключить палитру файлов" + save_new_launch_configuration_editable: "Сохранить новую конфигурацию запуска" + search_warp_drive: "Поиск в Warp Drive" + install_update_and_relaunch: "Установить обновление и перезапустить" + check_for_updates: "Проверить обновления" + log_out: "Выйти" + toggle_resource_center: "Переключить центр ресурсов" + export_warp_drive_objects: "Экспортировать все объекты Warp Drive" + install_oz_cli: "Установить команду Oz CLI" + uninstall_oz_cli: "Удалить команду Oz CLI" + view_latest_changelog: "Посмотреть последний список изменений" + toggle_warp_ai: "Переключить Warp AI" + toggle_mouse_reporting: "Переключить отчёты мыши" + activate_previous_pane: "Активировать предыдущую панель" + activate_next_pane: "Активировать следующую панель" + switch_to_nth_tab: "Переключиться на вкладку {n}" + switch_to_last_tab: "Переключиться на последнюю вкладку" + activate_previous_tab: "Активировать предыдущую вкладку" + activate_next_tab: "Активировать следующую вкладку" + jump_to_latest_agent_task: "Перейти к последней задаче Agent" + toggle_notification_mailbox: "Переключить почтовый ящик уведомлений" + toggle_agent_management_view: "Переключить вид управления Agent" + open_settings: "Открыть настройки" + open_settings_account: "Открыть настройки: Аккаунт" + open_settings_appearance: "Открыть настройки: Внешний вид" + open_settings_features: "Открыть настройки: Функции" + open_settings_shared_blocks: "Открыть настройки: Общие блоки" + open_settings_keyboard_shortcuts: "Открыть настройки: Горячие клавиши" + open_settings_about: "Открыть настройки: О программе" + open_settings_teams: "Открыть настройки: Команды" + open_settings_privacy: "Открыть настройки: Конфиденциальность" + open_settings_warpify: "Открыть настройки: Warpify" + open_settings_ai: "Открыть настройки: AI" + open_settings_billing_and_usage: "Открыть настройки: Биллинг и использование" + open_settings_code: "Открыть настройки: Код" + open_settings_referrals: "Открыть настройки: Рефералы" + open_settings_environments: "Открыть настройки: Окружения" + open_settings_mcp_servers: "Открыть настройки: Серверы MCP" + invite_people: "Пригласить людей..." + join_slack_community: "Присоединиться к сообществу Slack (внешняя ссылка)" + view_user_docs: "Посмотреть документацию (внешняя ссылка)" + send_feedback: "Отправить отзыв (внешняя ссылка)" + view_warp_logs: "Посмотреть логи Warp" + open_settings_file: "Открыть файл настроек" + switch_to_next_tab: "Переключиться на следующую вкладку" + switch_to_previous_tab: "Переключиться на предыдущую вкладку" + create_new_window: "Создать новое окно" + new_file: "Новый файл" + zoom_in: "Увеличить" + zoom_out: "Уменьшить" + reset_zoom: "Сбросить масштаб" + save_new_launch_configuration: "Сохранить новую конфигурацию запуска" + copy_access_token: "Копировать токен доступа в буфер" + view_privacy_policy: "Посмотреть политику конфиденциальности (внешняя ссылка)" + switch_to_first_tab: "Переключиться на 1-ю вкладку" + switch_to_second_tab: "Переключиться на 2-ю вкладку" + switch_to_third_tab: "Переключиться на 3-ю вкладку" + switch_to_fourth_tab: "Переключиться на 4-ю вкладку" + switch_to_fifth_tab: "Переключиться на 5-ю вкладку" + switch_to_sixth_tab: "Переключиться на 6-ю вкладку" + switch_to_seventh_tab: "Переключиться на 7-ю вкладку" + switch_to_eighth_tab: "Переключиться на 8-ю вкладку" + toggle_hidden_files_in_project_explorer: "Показать/скрыть скрытые файлы в обозревателе проекта" + move_tab_up: "переместить вкладку вверх" + move_tab_down: "переместить вкладку вниз" + close_tabs_below: "закрыть вкладки ниже" + create_new_tab_group: "Создать новую группу вкладок" + create_tab_group_from_active: "Создать группу из активной или выбранных вкладок" + remove_active_from_group: "Удалить активную или выбранные вкладки из группы" + pin_current_tab: "Закрепить текущую вкладку" + unpin_current_tab: "Открепить текущую вкладку" + pin_current_tab_group: "Закрепить текущую группу вкладок" + unpin_current_tab_group: "Открепить текущую группу вкладок" + + # ── Группа вкладок ── + workspace.tab_group: + move_up: "Переместить группу вверх" + move_down: "Переместить группу вниз" + move_left: "Переместить группу влево" + move_right: "Переместить группу вправо" + close_all: "Закрыть все вкладки в группе" + close_others: "Закрыть остальные вкладки" + close_above: "Закрыть вкладки выше" + close_below: "Закрыть вкладки ниже" + close_left: "Закрыть вкладки слева" + close_right: "Закрыть вкладки справа" + ungroup: "Разгруппировать вкладки" + new_tab_in_group: "Новая вкладка в группе" + rename: "Переименовать" + + # ── Группировка вкладок ── + workspace.tab_grouping: + remove_from_group: "Удалить из группы" + create_group: "Создать группу из вкладок" + move_to_group: "Переместить в группу" + untitled_group: "Группа без названия" + + # ── Отладка рабочей области ── + workspace.debug: + dump_debug_info: "Дамп отладочной информации" + crash: "Уронить приложение (для тестирования sentry)" + log_review_comment_send_status: "[Debug] Логировать статус отправки комментария ревью" + trigger_panic: "Вызвать панику (для тестирования sentry-rust)" + open_view_tree_debugger: "Открыть отладчик дерева видов" + view_first_time_user_experience: "[Debug] Посмотреть опыт первого использования" + open_build_plan_migration_modal: "[Debug] Открыть модальное окно миграции плана сборки" + reset_build_plan_migration_modal_state: "[Debug] Сбросить состояние модального окна миграции" + undismiss_aws_login_banner: "[Debug] Вернуть баннер входа AWS" + open_oz_launch_modal: "[Debug] Открыть модальное окно запуска Oz" + reset_oz_launch_modal_state: "[Debug] Сбросить состояние модального окна запуска Oz" + open_openwarp_launch_modal: "[Debug] Открыть модальное окно запуска OpenWarp" + reset_openwarp_launch_modal_state: "[Debug] Сбросить состояние модального окна запуска OpenWarp" + open_orchestration_launch_modal: "[Debug] Открыть модальное окно запуска оркестрации" + reset_orchestration_launch_modal_state: "[Debug] Сбросить состояние модального окна запуска оркестрации" + install_opencode_warp_plugin: "[Debug] Установить плагин OpenCode Warp" + use_local_opencode_warp_plugin: "[Debug] Использовать локальный плагин OpenCode Warp (только тестирование)" + open_session_config_modal: "[Debug] Открыть модальное окно конфигурации сессии" + start_hoa_onboarding_flow: "[Debug] Запустить процесс онбординга HOA" + set_a11y_concise: "[a11y] Краткие объявления доступности" + set_a11y_verbose: "[a11y] Подробные объявления доступности" + + # ── Внешний вид ── + appearance: + sync_with_os: "Как в системе" + window_opacity_label: "Прозрачность окна:" + window_blur: "Размытие окна (эффект Acrylic)" + create_custom_theme: "Создать свою тему" + auto_theme_description: "Автоматически переключать светлую и тёмную тему при смене системной." + transparency_unsupported: "Прозрачность не поддерживается вашими графическими драйверами." + input_mode: + pinned_to_bottom: "Прикрепить снизу (режим Warp)" + pinned_to_top: "Прикрепить сверху (обратный режим)" + waterfall: "Начинать сверху (классический режим)" + app_icon: + aurora: "Аврора" + default: "По умолчанию" + classic1: "Классическая 1" + classic2: "Классическая 2" + classic3: "Классическая 3" + comets: "Кометы" + glass_sky: "Стеклянное небо" + glitch: "Глитч" + cow: "Корова" + glow: "Свечение" + holographic: "Голографическая" + mono: "Моно" + neon: "Неон" + original: "Оригинальная" + starburst: "Звёздная вспышка" + sticker: "Стикер" + warp1: "Warp 1" + thin_strokes: + never: "Никогда" + on_low_dpi: "На экранах с низким DPI" + on_high_dpi: "На экранах с высоким DPI" + always: "Всегда" + enforce_minimum_contrast: + always: "Всегда" + only_named_colors: "Только для именованных цветов" + never: "Никогда" + decoration: + always: "Всегда" + when_windowed: "В оконном режиме" + on_hover: "Только при наведении" + tab_close_button: + right: "Справа" + left: "Слева" + show_code_review_button: "Показать кнопку code review" + hide_code_review_button: "Скрыть кнопку code review" + theme: + light: "Светлая" + dark: "Тёмная" + current: "Текущая тема" + customize_app_icon: "Настроить иконку приложения" + app_icon_bundle_warning: "Для смены иконки требуется сборка приложения." + restart_for_app_icon: "Возможно, потребуется перезапустить Warp, чтобы macOS применил новый стиль иконки." + open_windows_custom_size: "Открывать новые окна с заданным размером" + columns: "Столбцы" + rows: "Строки" + graphics_transparency_warning: "Выбранные настройки графики могут не поддерживать прозрачность окон." + window_blur_radius: "Радиус размытия окна:" + hardware_transparency_warning: "Выбранное оборудование может не поддерживать прозрачность окон." + input_type: + warp: "Warp" + shell_ps1: "Shell (PS1)" + input_type_label: "Тип ввода" + input_position: "Позиция ввода" + dim_inactive_panes: "Затемнять неактивные панели" + focus_follows_mouse: "Фокус следует за мышью" + compact_mode: "Компактный режим" + jump_to_bottom_button: "Показать кнопку перехода вниз блока" + show_block_dividers: "Показать разделители блоков" + agent_font: "Шрифт агента" + match_terminal: "Как в терминале" + line_height: "Высота строки" + terminal_font: "Шрифт терминала" + view_all_system_fonts: "Показать все системные шрифты" + font_weight: "Насыщенность шрифта" + font_size_px: "Размер шрифта (px)" + notebook_font_size: "Размер шрифта блокнота" + use_thin_strokes: "Использовать тонкие линии" + enforce_minimum_contrast_label: "Обеспечить минимальный контраст" + show_ligatures: "Показать лигатуры в терминале" + ligatures_performance: "Лигатуры могут снизить производительность" + cursor_type: "Тип курсора" + cursor_type_disabled_vim: "Тип курсора отключён в режиме Vim" + blinking_cursor: "Мигающий курсор" + tab_close_button_position: "Позиция кнопки закрытия вкладки" + show_tab_indicators: "Показать индикаторы вкладок" + preserve_active_tab_color: "Сохранять цвет активной вкладки для новых" + vertical_tab_layout: "Использовать вертикальный макет вкладок" + vertical_tabs_restored: "Показывать вертикальную панель вкладок в восстановленных окнах" + vertical_tabs_restored_description: "При включении, повторное открытие или восстановление окна открывает вертикальную панель вкладок, даже если она была закрыта при последнем сохранении окна." + latest_prompt_as_tab_title: "Использовать последний запрос пользователя как заголовок вкладки" + directory_tab_colors: "Цвета вкладок по директориям" + directory_tab_colors_description: "Автоматически окрашивать вкладки в зависимости от директории или репозитория." + default_no_color: "По умолчанию (без цвета)" + show_tab_bar: "Показать панель вкладок" + alt_screen_padding: "Использовать пользовательские отступы в alt-режиме" + uniform_padding_px: "Равномерный отступ (px)" + zoom: "Масштаб" + zoom_description: "Настраивает уровень масштабирования по умолчанию для всех окон" + start_input_at_top: "Начинать ввод сверху" + pin_input_to_top: "Закрепить ввод сверху" + pin_input_to_bottom: "Закрепить ввод снизу" + toggle_input_mode: "Переключить режим ввода (Warp/Classic)" + zen_mode: "режим дзен" + ligature_rendering: "отрисовка лигатур" + always_show_tab_bar: "Всегда показывать панель вкладок" + hide_tab_bar_if_fullscreen: "Скрывать панель вкладок в полноэкранном режиме" + only_show_tab_bar_on_hover: "Показывать панель вкладок только при наведении" + latest_prompt_as_tab_title_description: "Показывать последний запрос пользователя вместо сгенерированного заголовка для сессий Oz и сторонних агентов в вертикальных вкладках." + header_toolbar_layout: "Макет панели инструментов заголовка" + tools_panel_visibility_consistent: "Видимость панели инструментов одинакова для всех вкладок" + graphics_transparency_fix_hint: "Попробуйте изменить настройки графического бэкенда или интегрированного GPU в разделе Функции > Система." + show_code_review_button_in_tab_bar: "Показать кнопку code review на панели вкладок" + hide_code_review_button_in_tab_bar: "Скрыть кнопку code review на панели вкладок" + + # ── Диалоги ── + dialog: + discard_changes: "Отменить несохранённые изменения?" + discard_all_changes: "Отменить все изменения?" + dont_save: "Не сохранять" + close_pane: "Закрыть панель?" + close_tab: "Закрыть вкладку?" + close_tabs: "Закрыть вкладки?" + close_window: "Закрыть окно?" + quit_warp: "Выйти из Warp?" + save_changes: "Сохранить изменения?" + + # ── Provider Keys ── + provider_keys: + add_api_key: "Добавить API-ключ" + description: "Используйте свои API-ключи от провайдеров моделей для Warp Agent." + openai_api_key: "API-ключ OpenAI" + anthropic_api_key: "API-ключ Anthropic" + google_api_key: "API-ключ Google" + cancel: "Отмена" + add_keys: "Добавить ключи" + + # ── Free AI Removal ── + free_ai_removal: + notice: + title: "Warp больше не предоставляет инференс на бесплатном плане." + body: "Чтобы продолжить использовать AI-функции Warp, перейдите на платный план, добавьте свой API-ключ или эндпоинт, либо войдите с подпиской Grok." + bonus_credits: "Если у вас есть неиспользованные бонусные кредиты, AI продолжит работать, пока они не закончатся." + prompt_suggestions: + title: "Как использовать AI-функции в Warp" + body: "Чтобы использовать AI-функции в Warp, оформите платную подписку, добавьте API-ключ (OpenAI, Anthropic или Google), добавьте свой эндпоинт инференса (OpenRouter, LiteLLM) или войдите с подпиской SuperGrok." + bring_your_own_ai: "Свой AI" + view_pricing: "Тарифы" + + # ── Аутентификация ── + auth: + log_out_title: "Выйти?" + log_out_confirm: "Выйти?" + show_running_processes: "Показать запущенные процессы" + paste_token_placeholder: "Введите токен" + auth_token_placeholder: "Токен аутентификации" + link_sso: "Привязать SSO" + + # ── Настройки ── + settings: + execution_profile: + apply_code_diffs: "Применять изменения кода:" + ask_questions: "Задавать вопросы:" + auto: "Авто" + autosync_plans_to_warp_drive: "Автосинхронизация планов с Warp Drive:" + base_model: "Базовая модель:" + call_mcp_servers: "Вызывать MCP-серверы:" + call_web_tools: "Вызывать веб-инструменты:" + command_allowlist: "Белый список команд:" + command_denylist: "Чёрный список команд:" + computer_use_model: "Управление компьютером:" + computer_use_permission: "Управление компьютером:" + directory_allowlist: "Белый список директорий:" + execute_commands: "Выполнение команд:" + full_terminal_use: "Полный доступ к терминалу:" + interact_with_running_commands: "Взаимодействие с запущенными командами:" + mcp_allowlist: "Белый список MCP:" + mcp_denylist: "Чёрный список MCP:" + models_section: "МОДЕЛИ" + none: "Нет" + "off": "Выкл." + "on": "Вкл." + permission_agent_decides: "Решает агент" + permission_always_allow: "Всегда разрешать" + permission_always_ask: "Всегда спрашивать" + permission_ask_on_first_write: "Спрашивать при первой записи" + permission_ask_unless_auto_approve: "Спрашивать, кроме авто-одобрения" + permission_never: "Никогда" + permission_never_ask: "Никогда не спрашивать" + permission_unknown: "Неизвестно" + permissions_section: "РАЗРЕШЕНИЯ" + read_files: "Чтение файлов:" + run_agents: "Запуск агентов:" + font_description: "Моноширинный шрифт, используемый в терминале." + reset_to_default: "Сбросить по умолчанию" + about: "О программе" + account: "Аккаунт" + ai: + add_custom_endpoint: "Добавить свой endpoint" + agent: + git_operations_autogen_description: "Генерирует сообщение коммита на основе текущих изменений в вашем git-репозитории" + title: "ИИ" + appearance: + category: + blocks: "Блоки" + cursor: "Курсор" + full_screen_apps: "Полноэкранные приложения" + icon: "Значок" + input: "Ввод" + panes: "Панели" + tabs: "Вкладки" + text: "Текст" + themes: "Темы" + window: "Окно" + default_label: "по умолчанию" + label: + agent_font: "Шрифт агента" + columns: "Столбцы" + compact_mode: "Компактный режим" + cursor_type: "Тип курсора" + font_weight: "Насыщенность шрифта" + input_position: "Позиция ввода" + input_type: "Тип ввода" + line_height: "Высота строки" + rows: "Строки" + use_thin_strokes: "Использовать тонкие штрихи" + window_blur_radius_label: "Радиус размытия окна:" + window_opacity_label: "Прозрачность окна:" + zoom: "Масштаб" + title: "Внешний вид" + toggle: + block_dividers: "разделители блоков" + compact_mode: "компактный режим" + cursor_blink: "мигание курсора" + tab_indicators: "индикаторы вкладок" + themes_sync_os: "темы: синхронизация с ОС" + zen_mode: "режим дзен" + code: "Код" + features: + asynchronous_find: "Асинхронный поиск" + clipboard_osc52: "Доступ к буферу обмена (OSC 52)" + clipboard_osc52_tooltip: "Управляет тем, могут ли программы, запущенные в терминале, читать или записывать ваш системный буфер обмена." + code_editor_line_numbers: "Номера строк в редакторе кода:" + preserve_input_focus_on_block_selection: "Сохранять фокус ввода при выделении блока" + press_new_shortcut: "Нажмите новую комбинацию клавиш" + title: "Функции" + toggle: + agent_task_completed_notifications: "уведомления о завершении задач агентом" + alias_expansion: "раскрытие алиасов" + at_context_menu_terminal: "контекстное меню '@' в режиме терминала" + autocomplete_symbols: "автодополнение кавычек, скобок" + autosuggestion_ignore_button: "кнопка игнорирования автопредложений" + autosuggestion_keybinding_hint: "подсказка горячей клавиши автопредложений" + code_as_default_editor: "редактор кода по умолчанию" + focus_reporting: "отчёты фокуса" + global_workflows_search: "глобальные workflows в поиске команд" + help_block_in_new_sessions: "справочный блок в новых сессиях" + in_app_agent_notifications: "уведомления агента в приложении" + input_hint_text: "текст подсказки ввода" + integrated_gpu: "встроенный GPU-рендеринг (энергосбережение)" + link_tooltip: "показывать подсказку при клике по ссылкам" + long_running_notifications: "уведомления о долго работающих командах" + middle_click_paste: "вставка средней кнопкой мыши" + mouse_reporting: "отчёты мыши" + needs_attention_notifications: "уведомления о запросе внимания" + notification_sounds: "звуки уведомлений" + outline_codebase_symbols: "символы кодовой базы в контекстном меню '@'" + preserve_input_focus_on_block_selection: "сохранять фокус ввода при выделении блока" + quit_warning_modal: "предупреждение при выходе" + restore_session: "восстанавливать окна, вкладки и панели при запуске" + slash_commands_terminal: "слеш-команды в режиме терминала" + smart_select: "умное выделение" + ssh_reuse_control_master: "переиспользовать существующий SSH ControlMaster в обёртке Warp SSH" + terminal_input_message_line: "строка сообщения ввода в терминале" + vim_keybindings: "редактирование командами Vim" + vim_status_bar: "строка состояния Vim" + vim_unnamed_register: "безымянный регистр Vim как системный буфер обмена" + wayland_window_management: "Wayland для управления окнами" + billing_and_usage: + auto_reload: "Автопополнение" + buy: "Купить" + buy_more: "Купить ещё" + buying: "Покупка…" + credit_singular: "1 кредит" + credits_header: "Кредиты" + default_spend_limit: "$200,00" + free: "Бесплатно" + load_more: "Загрузить ещё" + new_agent: "Новый агент" + not_set: "Не задано" + overview_tab: "Обзор" + plan: "Тариф" + resets_at: "Сброс в {time}" + sign_up: "Зарегистрироваться" + sort_a_to_z: "От А до Я" + sort_z_to_a: "Я → А" + switch_to_business: "Переключиться на Business" + team_total: "Всего по команде" + title: "Биллинг и использование" + usage_section: "Использование" + your_selected: "выбрано" + zero_cost: "$0,00" + zero_credits: "0 кредитов" + keyboard_shortcuts: "Горячие клавиши" + scripting: + install: "Установить" + install_failed: "Не удалось установить команду Warp Control: {error}" + install_success: "Warp Control CLI успешно установлен! Теперь можно запускать '{command_name}' из командной строки." + installed: "Установлено" + installing: "Установка…" + title: "Скриптинг" + warp_control_cli_command: "Команда Warp Control CLI" + warp_control_cli_description: "Установите команду warpctrl для управления Warp из терминала с помощью скриптов." + warpctrl_cli: "warpctrl CLI" + warpctrl_cli_description: "warpctrl позволяет управлять интерфейсом Warp из скриптов. Используйте с осторожностью." + shared_blocks: "Общие блоки" + mcp_servers: "Серверы MCP" + privacy: + title: "Конфиденциальность" + referrals: "Рефералы" + teams: + invite_option_email: "Email" + invite_option_link: "Ссылка" + need_more_seats: "Нужно больше мест? " + team_full_title: "Ваша команда заполнена" + team_member_count_one: "1 участник команды" + teammate_count_one: "1 коллега" + title: "Команды" + update_environment_form: + auth_with_github: "Войти через GitHub" + authenticate: "Авторизоваться" + characters_count: "{count} / {max} символов" + configure_access: "Настроить доступ на GitHub" + create_environment: "Создать окружение" + delete_environment: "Удалить окружение" + description_label: "Описание" + description_placeholder: "например, это окружение для всех агентов, работающих с фронтендом" + docker_image_label: "Docker-образ" + docker_image_label_long: "Ссылка на Docker-образ" + docker_image_placeholder: "например, python:3.11, node:20-alpine" + docker_image_placeholder_short: "например, node:20-alpine" + edit_environment: "Изменить окружение" + generating: "Генерация…" + launch_agent: "Запустить агента" + load_repos_error: "Не удалось загрузить репозитории GitHub. Вы можете вставить URL-адрес(а) репозиториев или повторить попытку." + load_repositories_failed: "Не удалось загрузить репозитории GitHub" + loading: "Загрузка..." + missing_repo: "Не хватает репозитория?" + name_label: "Название" + name_placeholder: "Название окружения" + name_placeholder_orchestration: "например, dev-env" + no_repositories: "Репозитории не найдены" + open_image_at: "Открыть образ по адресу {url}" + repos_helper: "Введите owner/repo и нажмите Enter, чтобы добавить, или выберите из выпадающего списка." + repos_label: "Репозитории" + repos_placeholder_authed: "Введите репозитории (формат owner/repo)" + repos_placeholder_browse: "Найти репозитории GitHub..." + repos_placeholder_unauthed: "Вставьте URL-адреса репозиториев" + save_environment: "Сохранить окружение" + setup_commands_helper: "Команды настройки выполняются независимо. Каждая команда запускается из корня рабочей области (/workspace). Если команда зависит от предыдущей, объедините их с помощью &&." + setup_commands_helper_short: "Нажмите Enter или кнопку отправки, чтобы добавить каждую команду." + setup_commands_label: "Команды настройки" + setup_commands_placeholder: "например, cd my-repo && pip install -r requirements.txt" + setup_commands_placeholder_short: "например, node start" + share_warning: "Персональные окружения нельзя использовать с внешними интеграциями или командными API-ключами. Для лучшего опыта используйте общие окружения." + share_with_team: "Поделиться с командой" + suggest_image: "Подобрать образ" + suggest_image_auth_required: "Для подбора Docker-образа необходимо предоставить доступ к репозиториям GitHub" + suggest_image_failed: "Не удалось подобрать Docker-образ" + suggest_image_failed_with_error: "Не удалось подобрать Docker-образ: {error}" + suggest_image_no_match: "Не удалось найти подходящий образ. Рекомендуем использовать собственный Docker-образ для этих репозиториев." + suggest_image_tooltip: "Warp подберёт Docker-образ на основе выбранных репозиториев." + suggest_image_unknown_response: "Неизвестный ответ от suggestCloudEnvironmentImage" + warpify: "Warpify" + + description: "Настройте, должен ли Warp пытаться «Warpify» (добавлять поддержку блоков, режимов ввода и т.д.) определённые оболочки. " + host_placeholder: "хост (поддерживает regex)" + command_placeholder: "команда (поддерживает regex)" + denylisted_hosts: "Запрещённые хосты" + denylisted_commands: "Запрещённые команды" + added_commands: "Добавленные команды" + use_tmux_warpification: "Использовать Tmux Warpification" + install_ssh_extension: "Установить расширение SSH" + warpify_ssh_sessions: "Warpify SSH-сессий" + ssh_subtitle: "Warpify ваших интерактивных SSH-сессий." + ssh: "SSH" + subshells_subtitle: "Поддерживаемые подоболочки: bash, zsh и fish." + subshells: "Подоболочки" + ssh_extension_install_mode_description: "Управляет поведением установки расширения Warp SSH, когда на удалённом хосте оно не установлено." + tmux_warpification_description: "Обёртка tmux ssh работает во многих ситуациях, где стандартная не работает, но может потребовать нажатия кнопки для warpify. Вступает в силу в новых вкладках." + ssh_session_detection: "Определение SSH-сессий для Warpification" + ssh_extension: + always_ask: "Всегда спрашивать" + always_install: "Всегда устанавливать" + never_install: "Никогда не устанавливать" + warp_drive: "Warp Drive" + warp_agent: "Warp Агент" + profiles: "Профили" + mcp_servers_lower: "Серверы MCP" + mcp_servers_page: + title: "Серверы MCP" + search_terms: "mcp servers" + search: "Поиск серверов MCP" + shared_by_team: "Поделился участник команды" + from_another_device: "С другого устройства" + logout_success: "Успешный выход из MCP-сервера {name}" + logout_success_unnamed: "Успешный выход из MCP-сервера" + install_modal_already_open: "Завершите текущую установку MCP перед открытием другой ссылки установки." + unknown_server: "Неизвестный MCP-сервер '{name}'" + cannot_install_from_link: "MCP-сервер '{name}' не может быть установлен по этой ссылке." + description: "Добавьте MCP-серверы, чтобы расширить возможности Warp Agent. MCP-серверы предоставляют агентам доступ к источникам данных и инструментам через стандартизированный интерфейс, по сути работая как плагины. Добавьте собственный сервер или используйте предустановки, чтобы начать работу с популярными серверами. Здесь также отображаются командные серверы, которыми с вами поделились." + empty_state: "После добавления MCP-сервер будет отображаться здесь." + no_search_results: "Результатов поиска не найдено" + add: "Добавить" + available_to_install: "Доступен для установки" + detected_from_config: "Обнаружен в файле конфигурации" + global_scope: "глобальный" + update_success: "MCP-сервер обновлён" + my_mcps: "Мои MCP" + shared_by_warp_and_team: "Предоставлено Warp и {name}" + shared_by_warp_and_devices: "Предоставлено Warp и с других устройств" + shared_from_warp: "Предоставлено Warp" + detected_from: "Обнаружен в {provider}" + auto_spawn_label: "Автозапуск серверов от сторонних агентов" + auto_spawn_description: "Автоматически обнаруживать и запускать MCP-серверы из глобальных конфигурационных файлов сторонних ИИ-агентов (например, в вашей домашней директории). Серверы, обнаруженные внутри репозитория, никогда не запускаются автоматически и должны быть включены индивидуально в разделах «Обнаружен в» ниже." + see_supported_providers: "См. поддерживаемых провайдеров." + learn_more: "Подробнее." + knowledge: "База знаний" + third_party_cli_agents: "Сторонние CLI-агенты" + indexing_and_projects: "Индексация и проекты" + editor_and_code_review: "Редактор и Code Review" + environments: "Окружения" + oz_cloud_api_keys: "Ключи API Oz Cloud" + platform: + api_key_deleted: "Ключ API удалён" + column_created: "Создан" + column_expires_at: "Истекает" + column_key: "Ключ" + column_last_used: "Последнее использование" + column_name: "Название" + column_scope: "Область" + create_api_key_button: "+ Создать ключ API" + description_prefix: "Создавайте и управляйте ключами API, чтобы другие агенты Oz Cloud могли получить доступ к вашему аккаунту Warp.\nДля получения дополнительной информации посетите " + documentation_link: "Документацию." + never: "Никогда" + new_api_key: "Новый ключ API" + no_api_keys: "Нет ключей API" + no_api_keys_description: "Создайте ключ для управления внешним доступом к Warp" + no_api_keys_match_search: "Нет ключей API, соответствующих поиску" + oz_cloud_api_keys: "Ключи API Oz Cloud" + save_your_key: "Сохраните ваш ключ" + scope_agent: "Агент" + scope_personal: "Личный" + scope_team: "Командный" + search_api_keys: "Поиск по ключам API" + platform_page: + new_api_key: "Новый ключ API" + save_your_key: "Сохраните ваш ключ" + api_key_deleted: "Ключ API удалён" + description: "Создавайте и управляйте ключами API, чтобы другие агенты Oz Cloud могли получить доступ к вашему аккаунту Warp.\nДля получения дополнительной информации посетите " + documentation: "Документацию." + column_name: "Название" + column_key: "Ключ" + column_scope: "Область" + column_created: "Создан" + column_last_used: "Последнее использование" + column_expires_at: "Истекает" + never: "Никогда" + search_terms: "oz cloud platform api keys authentication" + no_api_keys: "Нет ключей API" + no_api_keys_description: "Создайте ключ для управления внешним доступом к Warp" + create_api_key: "+ Создать ключ API" + personal: "Личный" + team: "Командный" + agent: "Агент" + + settings.shared_blocks: + executed_on: "Выполнено:" + unshare_block_title: "Отменить доступ к блоку" + unshare_failed: "Не удалось отозвать доступ к блоку. Пожалуйста, попробуйте снова." + unshare_success: "Блок успешно отозван из общего доступа." + link_copied: "Ссылка скопирована." + unshare: "Отменить доступ" + failed_to_load: "Не удалось загрузить блоки. Пожалуйста, попробуйте снова." + getting_blocks: "Получение блоков..." + no_blocks_yet: "У вас пока нет общих блоков." + deleting: "Удаление..." + copy_link: "Копировать ссылку" + settings.code_page: + code: "Код" + initialization_settings_header: "Настройки инициализации" + codebase_indexing_label: "Индексация кодовой базы" + codebase_index_description: "Warp может автоматически индексировать репозитории кода по мере навигации по ним, помогая агентам быстро понимать контекст и предлагать решения. Код никогда не хранится на сервере. Если кодовую базу невозможно проиндексировать, Warp всё равно может ориентироваться в ней и получать информацию с помощью grep и вызовов инструмента find." + warp_indexing_ignore_description: "Чтобы исключить определённые файлы или директории из индексации, добавьте их в файл .warpindexingignore в директории вашего репозитория. Эти файлы по-прежнему будут доступны для функций ИИ, но не будут включены в эмбеддинги кодовой базы." + auto_index_feature_name: "Индексировать новые папки по умолчанию" + auto_index_description: "При включении Warp будет автоматически индексировать репозитории кода по мере навигации по ним — помогая агентам быстро понимать контекст и предлагать целевые решения." + indexing_disabled_admin_text: "Администраторы команды отключили индексацию кодовой базы." + indexing_workspace_enabled_admin_text: "Администраторы команды включили индексацию кодовой базы." + indexing_disabled_global_ai_text: "Для использования индексации кодовой базы необходимо включить функции ИИ." + codebase_index_limit_reached: "Вы достигли максимального количества индексов кодовой базы для вашего тарифа. Удалите существующие индексы, чтобы автоматически индексировать новые кодовые базы." + indexing_subpage_title: "Индексация кодовой базы" + editor_subpage_title: "Редактор и Code Review" + index_new_folder: "Индексировать новую папку" + initialized_indexed_folders: "Инициализированные / проиндексированные папки" + no_folders_initialized: "Папки ещё не были инициализированы." + indexing_label: "ИНДЕКСАЦИЯ" + no_index_created: "Индекс не создан" + open_project_rules: "Открыть правила проекта" + index_limit_reached: "Достигнут лимит индексов" + unavailable: "Недоступно" + indexing_discovered: "Обнаружено {total_nodes} фрагментов" + indexing_syncing: "Синхронизация - {completed_nodes} / {total_nodes}" + indexing_syncing_generic: "Синхронизация..." + indexing_synced: "Синхронизировано" + indexing_too_large: "Кодовая база слишком велика" + indexing_stale: "Устарело" + indexing_failed: "Ошибка" + no_index_built: "Индекс не построен" + indexing_disabled: "Отключено" + indexing_queued: "В очереди" + indexing_remote_syncing: "Индексация - {completed} / {total}" + indexing_remote_syncing_partial: "Индексация - {completed}" + indexing_remote_syncing_zero: "Индексация - 0 / {total}" + indexing_remote_generic: "Индексация..." + + settings.category: + themes: "Темы" + icon: "Значок" + window: "Окно" + input: "Ввод" + panes: "Панели" + blocks: "Блоки" + text: "Текст" + language: "Язык" + cursor: "Курсор" + tabs: "Вкладки" + fullscreen_apps: "Полноэкранные приложения" + general: "Общие" + session: "Сессия" + keys: "Клавиши" + text_editing: "Редактирование" + terminal_input: "Ввод в терминале" + terminal: "Терминал" + notifications: "Уведомления" + workflows: "Рабочие процессы" + system: "Система" + + settings.keybindings: + search_placeholder: 'Поиск по имени или клавишам (например, "cmd d")' + conflict_warning: "Это сочетание конфликтует с другими комбинациями" + reset: "По умолчанию" + press_new: "Нажмите новую комбинацию клавиш" + add_custom: "Добавьте свои комбинации клавиш к действиям ниже." + use_prefix: "Используйте" + reference_suffix: "чтобы посмотреть эти комбинации на боковой панели в любое время." + configure: "Настройка горячих клавиш" + not_synced: "Горячие клавиши не синхронизируются с облаком" + command_column: "Команда" + + settings.features: + left_option_meta: "Левая Option — Meta" + right_option_meta: "Правая Option — Meta" + left_alt_meta: "Левый Alt — Meta" + right_alt_meta: "Правый Alt — Meta" + configure_global_hotkey: "Настроить глобальную горячую клавишу" + make_default_terminal: "Сделать Warp терминалом по умолчанию" + pin_top: "Закрепить сверху" + pin_bottom: "Закрепить снизу" + pin_left: "Закрепить слева" + pin_right: "Закрепить справа" + new_tab_after_all: "После всех вкладок" + new_tab_after_current: "После текущей вкладки" + change_keybinding: "Изменить комбинацию" + ssh_wrapper: "Warp SSH обёртка" + link_tooltip: "Показывать подсказку при клике на ссылки" + quit_warning_modal: "Показывать предупреждение перед выходом" + alias_expansion: "Раскрывать алиасы при вводе" + middle_click_paste: "Вставка по средней кнопке мыши" + code_as_default_editor: "Использовать VS Code как редактор по умолчанию" + input_hint_text: "Показывать подсказки ввода" + vim_keybindings: "Редактировать код и команды с раскладкой Vim" + vim_unnamed_register: "Использовать безымянный регистр как системный буфер обмена" + vim_status_bar: "Показать строку состояния Vim" + focus_reporting: "Включить Focus Reporting" + smart_select: "Двойной клик для умного выделения" + terminal_input_message_line: "Показать строку сообщений ввода терминала" + slash_commands_terminal: "Включить слеш-команды в режиме терминала" + integrated_gpu: "Предпочитать рендеринг на интегрированном GPU (низкое энергопотребление)" + wayland_window_management: "Использовать Wayland для управления окнами" + active_screen: "Активный экран" + ten_million: "10 миллионов" + one_million: "1 миллион" + block_max_rows_warning: "Установка лимита выше 100 тыс. строк может повлиять на производительность. Максимально поддерживается {max_rows} строк." + open_links_desktop: "Открывать ссылки в десктопном приложении" + open_links_desktop_tooltip: "Автоматически открывать ссылки в десктопном приложении, когда возможно." + restore_session: "Восстанавливать окна, вкладки и панели при запуске" + wayland_no_restore: "Позиции окон не будут восстановлены в Wayland. " + sticky_command_header: "Показать закреплённый заголовок команд" + start_at_login_macos: "Запускать Warp при входе (требуется macOS 13+)" + start_at_login: "Запускать Warp при входе" + quit_when_closed: "Выходить при закрытии всех окон" + show_changelog: "Показывать уведомление о изменениях после обновлений" + allowed_values_1_20: "Допустимые значения: 1-20" + mouse_scroll_lines: "Строк, прокручиваемых колёсиком мыши" + mouse_scroll_tooltip: "Поддерживает дробные значения от 1 до 20." + auto_open_code_review: "Автооткрытие панели code review" + auto_open_code_review_description: "Когда эта настройка включена, панель code review откроется при первом принятом диффе в разговоре" + warp_is_default: "Warp является терминалом по умолчанию" + max_block_rows: "Максимальное количество строк в блоке" + ssh_wrapper_label: "Warp SSH обёртка" + change_takes_effect_new_sessions: "Изменения вступят в силу в новых сессиях" + receive_desktop_notifications: "Получать уведомления на рабочий стол от Warp" + notify_agent_completes: "Уведомлять, когда агент завершает задачу" + notify_needs_attention: "Уведомлять, когда команда или агент требует внимания" + play_notification_sounds: "Проигрывать звуки уведомлений" + in_app_agent_notifications: "Показывать встроенные уведомления агента" + toast_visible_for: "Всплывающие уведомления видны в течение" + seconds: "секунд" + confirm_close_session: "Подтверждать перед закрытием общей сессии" + global_hotkey: "Глобальная горячая клавиша:" + not_supported_wayland: "Не поддерживается в Wayland. " + autocomplete_symbols: "Автодополнение кавычек, скобок" + error_underlining: "Подчёркивание ошибок в командах" + syntax_highlighting: "Подсветка синтаксиса команд" + completions_while_typing: "Открывать меню автодополнения при вводе" + command_corrections: "Предлагать исправленные команды" + autosuggestion_hint: "Показывать подсказку автопредложений" + autosuggestion_ignore: "Показывать кнопку игнорирования автопредложений" + right_arrow_accepts: "→ принимает автопредложения." + completions_open_typing: "Автодополнение открывается при вводе." + completions_unbound: "Открытие меню автодополнения не назначено." + accept_autosuggestion: "Принять автопредложение" + open_completions_menu: "Открыть меню автодополнения" + tab_key_behavior: "Поведение клавиши Tab" + ctrl_tab_behavior: "Поведение Ctrl+Tab:" + mouse_reporting: "Включить Mouse Reporting" + scroll_reporting: "Включить Scroll Reporting" + audible_bell: "Использовать звуковой сигнал" + word_char_allowlist: "Символы, считающиеся частью слова" + copy_on_select: "Копировать при выделении" + show_help_block: "Показать справочный блок в новых сессиях" + new_tab_placement: "Размещение новых вкладок" + default_session_mode: "Режим по умолчанию для новых сессий" + global_workflows_search: "Показывать глобальные Workflows в поиске команд (ctrl-r)" + linux_selection_clipboard: "Использовать Linux selection clipboard" + linux_clipboard_tooltip: "Поддерживать ли первичный буфер обмена Linux." + changes_new_windows: "Изменения применятся к новым окнам." + wayland_description: "Включение этой настройки отключает поддержку глобальной горячей клавиши. При отключении текст может быть размыт, если ваш Wayland композитор использует дробное масштабирование (например, 125%)." + wayland_tooltip: "Включает использование Wayland" + restart_for_changes: "Перезапустите Warp для применения изменений." + default_shell: "Оболочка по умолчанию для новых сессий" + working_directory: "Рабочая директория для новых сессий" + graphics_backend: "Предпочитаемый графический бэкенд" + current_backend: "Текущий бэкенд:" + default: "По умолчанию" + keybinding: "Комбинация клавиш" + click_to_set_hotkey: "Нажмите для установки глобальной горячей клавиши" + outline_codebase_symbols: "Показывать символы кодовой базы в контекстном меню '@'" + see_docs: "См. документацию." + autohides_on_focus_loss: "Автоматически скрывать при потере фокуса клавиатуры" + width_pct: "Ширина %" + height_pct: "Высота %" + command_longer_than: "Когда команда выполняется дольше" + seconds_to_complete: "секунд" + accepts_autosuggestions: "{} принимает автопредложения." + completions_open_typing_or: "Автодополнение открывается при вводе (или {})." + opens_completion_menu: "{} открывает меню автодополнения." + quake_keybinding: "Комбинация Quake" + quake_pin_position: "Позиция закрепления Quake" + quake_pin_screen: "Экран закрепления Quake" + at_context_menu_terminal: "Включить контекстное меню '@' в режиме терминала" + + toggle: + copy_on_select: "копировать при выделении в терминале" + linux_selection_clipboard: "буфер обмена linux по выделению" + scroll_reporting: "отчёты прокрутки" + completions_while_typing: "автодополнение при вводе" + command_corrections: "исправление команд" + error_underlining: "подчёркивание ошибок" + syntax_highlighting: "подсветка синтаксиса" + audible_bell: "звуковой сигнал терминала" + autosuggestions: "автопредложения" + + common: + back: "Назад" + cancel: "Отмена" + clear: "Очистить" + close: "Закрыть" + copy: "Копировать" + create: "Создать" + delete: "Удалить" + disabled: "Отключено" + dismiss: "Закрыть" + done: "Готово" + edit: "Редактировать" + enabled: "Включено" + finish: "Завершить" + get_started: "Начать" + get_warping: "Начать работу" + learn_more: "Узнать больше" + next: "Далее" + ok: "ОК" + open: "Открыть" + recommended: "Рекомендуется" + remove: "Удалить" + restore: "Восстановить" + retry: "Повторить" + save: "Сохранить" + search: "Поиск" + send: "Отправить" + add: "Добавить" + skip: "Пропустить" + submit: "Отправить" + undo: "Отменить" + update: "Обновить" + untitled: "Без названия" + + # ── Код ── + code: + reject: "Отклонить" + accept_and_save: "Принять и сохранить" + discard_this_version: "Отменить эту версию" + overwrite: "Перезаписать" + goto_line_placeholder: "Номер строки:Колонка" + previous: "Предыдущий" + next: "Следующий" + select_all: "Выбрать всё" + replace_all: "Заменить всё" + remote_disconnected_banner: "Удалённый хост отключён. Вы не сможете видеть обновления и сохранять изменения." + remote_disconnected_save_failure: "Не удалось сохранить — удалённая сессия отключена." + comment: "Комментарий" + add_as_context: "Добавить как контекст" + go_to_definition: "Перейти к определению" + find_references_action: "Найти ссылки" + file_has_saved_changes: "Этот файл имеет сохранённые изменения, которые здесь не отражены." + failed_to_load_file: "Не удалось загрузить файл." + failed_to_save_file: "Не удалось сохранить файл." + file_saved: "Файл сохранён." + new_file_indicator: " (новый)" + save_file: "Сохранить файл" + save_file_as: "Сохранить файл как" + close_all_tabs: "Закрыть все вкладки" + close_saved_tabs: "Закрыть сохранённые вкладки" + close_saved: "Закрыть сохранённые" + copy_file_path: "Копировать путь к файлу" + reveal_in_finder: "Показать в Finder" + reveal_in_explorer: "Показать в проводнике" + reveal_in_file_manager: "Показать в файловом менеджере" + view_markdown_preview: "Открыть предпросмотр Markdown" + file_tree: + remote: "Проводник проектов требует доступа к локальному рабочему пространству, что не поддерживается в удалённых сессиях." + disabled: "Проводник проектов требует доступа к локальному рабочему пространству. Откройте новую сессию или перейдите к активной сессии." + wsl: "Проводник проектов пока не работает в WSL." + file: "Файл" + folder: "Папка" + suggested_fixes: "Предлагаемые исправления на основе вашей последней команды:" + find_references: + single: "Показана 1 ссылка" + multiple: "Показано {total_refs} ссылок" + loading: "Загрузка..." + + editor: + find: + regex_toggle: "Переключить regex" + case_sensitive: "Поиск с учётом регистра" + preserve_case: "Сохранять регистр" + placeholder: "Найти" + replace_placeholder: "Заменить" + binding: + find_next: "Найти следующее вхождение поискового запроса" + find_prev: "Найти предыдущее вхождение поискового запроса" + result_of: "Результат {current} из {total}." + navigate_help: "Используйте Enter и Shift+Enter для навигации между совпадениями. Escape для выхода." + no_results: "Нет результатов." + replace_success: "Совпадение заменено. Выбрано совпадение {index} из {total}" + replace_continue_help: "Продолжайте нажимать Enter для замены следующих совпадений или используйте стрелки для навигации." + replace_last_success: "Последнее совпадение заменено." + a11y_description_empty: "Панель поиска текста в редакторе." + a11y_description_with_results: "Панель поиска: найдено {count} совпадений. Текущее совпадение {current} из {total}." + a11y_help_replace: "Поле замены в фокусе. Введите текст замены, нажмите Enter для замены текущего совпадения, Tab для возврата к полю поиска. Используйте стрелки для навигации, Escape для закрытия." + a11y_help_find: "Поле поиска в фокусе. Введите текст для поиска. Используйте Enter и Shift+Enter или стрелки для навигации между совпадениями. Нажмите Escape для закрытия." + goto_line: + enter_line_number: "Введите номер строки" + valid_line_number: "Введите корректный номер строки" + valid_column_number: "Введите корректный номер колонки" + nav_bar: + hunk: "Ханк:" + gutter: + add_as_context: "Добавить ханк диффа как контекст" + save_to_attach_context: "Сохраните изменения, чтобы прикрепить как контекст." + revert_hunk: "Отменить ханк диффа" + save_to_revert: "Сохраните изменения для отмены" + add_comment: "Добавить комментарий к строке" + save_to_comment: "Сохраните изменения, чтобы добавить комментарий" + show_comment: "Показать сохранённый комментарий" + + # ── CLI ── + cli: + accept: "Принять" + auto_approve: "Авто-одобрение" + + # ── Конфигурации запуска ── + launch_configs: + default_filename: "launch_config.yaml" + + # ── Диск ── + drive: + empty_trash: "Очистить корзину" + + # ── Блокнот ── + notebook: + copy_to_personal: "Копировать в личное" + copy_all: "Копировать всё" + link_text_placeholder: "Текст" + link_url_placeholder: "Ссылка (веб или файл)" + copy_link: "Копировать ссылку" + edit: "Редактировать" + link_copied: "Ссылка скопирована в буфер обмена" + open_folder: "Открыть папку" + open_file: "Открыть файл" + open_in_warp: "Открыть в Warp" + + # ── Onboarding ── + onboarding: + agent_slide: + autonomy: "Автономность" + autonomy_full_title: "Полная" + autonomy_none_title: "Без автономности" + autonomy_partial_title: "Частичная" + default_model: "Модель по умолчанию" + ai_access: + choose_how: "Получите доступ к ИИ" + subtitle: "Экономьте с регулярным планом или изучите возможности ИИ Warp, прежде чем оформлять подписку." + subscription: "Подписка" + best_value: "Лучшая цена" + subscription_description: "От $18 / мес, доступно с месячной или годовой подпиской. Включает базовые кредиты, фронтир-модели, облачных агентов, коллаборацию и многое другое." + back: "Назад" + no_ai: "Мне не нужен AI" + next: "Далее" + set_up_later_label: "Настроить позже" + set_up_later_description: "Изучите встроенные возможности ИИ Warp, прежде чем оформлять подписку, или используйте собственный инференс." + auth_prompt_bar: + browser_not_launched_prefix: "Если браузер не открылся, " + copy_url_link: "скопируйте URL" + open_manually: " и откройте страницу вручную. " + paste_token_link: "Нажмите здесь" + paste_token_suffix: ", чтобы вставить токен из браузера." + ai_setup: + feature_harness: "Лучший харнесс для терминальных задач и агентного кодинга" + feature_models: "Фронтир-модели от OpenAI, Anthropic и Google" + feature_routing: "Маршрутизация между фронтир и open-weight моделями" + feature_orchestration: "Мульти-агентная оркестрация" + choose_your_setup: "Выберите настройку AI" + subtitle: "Выберите, хотите ли вы использовать Warp Agent или сторонних агентов." + use_warp_agent: "Использовать Warp Agent" + access_more_models: "Доступ к большему числу моделей" + warp_agent_description: "Современный агентный харнесс, глубоко интегрированный в терминал." + use_third_party: "Использовать сторонних агентов" + third_party_description: "Используйте агентов, таких как Claude Code, Codex и Gemini." + back: "Назад" + no_ai: "Мне не нужен AI" + next: "Далее" + agent: + skip: "Пропустить" + no_ai_dialog_title: "Вы уверены, что не хотите ИИ?" + no_ai_dialog_body: "Без ИИ вы по-прежнему получите терминальный опыт Warp, но лишитесь агентных возможностей, например автоматического исправления ошибок терминала." + give_me_ai_features: "Дайте мне функции ИИ" + plan_activated_toast: "План успешно активирован!" + + # ── Настройки AI ── + callout: + initialize: "Инициализировать" + customize: + tab_styling_title: "Стиль вкладок" + tab_vertical: "Вертикально" + title: "Настройте Warp под себя" + intention: + subtitle: "Как вы хотите работать?" + title: "Добро пожаловать в Warp" + intro: + log_in: "Войти" + title: "Добро пожаловать в Warp" + project: + title: "Открыть проект" + theme_picker: + title: "Выберите тему" + settings.ai: + next_command_description: "Позволяет AI предлагать следующую команду на основе истории команд, выводов и популярных рабочих процессов." + prompt_suggestions_description: "Позволяет AI предлагать подсказки на естественном языке в виде баннеров в поле ввода на основе последних команд и их выводов." + suggested_code_banners_description: "Позволяет AI предлагать диффы кода и запросы в виде баннеров в списке блоков на основе последних команд и их выводов." + natural_language_autosuggestions_description: "Позволяет AI предлагать автоподсказки на естественном языке на основе последних команд и их выводов." + shared_block_title_generation_description: "Позволяет AI генерировать заголовок для общего блока на основе команды и вывода." + git_operations_autogen_description: "Позволяет AI генерировать сообщения коммитов и заголовки и описания pull request'ов." + + ai: "AI" + active_ai: "Активный AI" + next_command: "Следующая команда" + prompt_suggestions: "Подсказки" + suggested_code_banners: "Баннеры с кодом" + natural_language_autosuggestions: "Автоподсказки на естественном языке" + shared_block_title_generation: "Генерация заголовков блоков" + commit_and_pr_generation: "Генерация коммитов и PR" + voice_input: "Голосовой ввод" + + usage: "Использование" + credits: "Кредиты" + models: "Модели" + permissions: "Разрешения" + profiles: "Профили" + input: "Ввод" + voice: "Голос" + other: "Другое" + agents: "Агенты" + knowledge: "Знания" + mcp_servers: "MCP-серверы" + + permission: + agent_decides: "Решает агент" + always_allow: "Всегда разрешать" + always_ask: "Всегда спрашивать" + read_only: "Только чтение" + supervised: "Под наблюдением" + ask_on_first_write: "Спрашивать при первой записи" + allow_in_specific_directories: "Разрешить в указанных папках" + never: "Никогда" + never_ask: "Никогда не спрашивать" + ask_unless_auto_approve: "Спрашивать, кроме авто-одобрения" + + restricted_due_to_billing: "Ограничено из-за проблем с оплатой" + unlimited: "Безлимитно" + + add_profile: "Добавить профиль" + edit_custom_endpoint: "Изменить свой endpoint" + edit: "Изменить" + add_custom_model: "+ Добавить свою модель" + select_mcp_servers: "Выбрать MCP-серверы" + select_coding_agent: "Выбрать агента кодирования" + toolbar_layout: "Макет панели инструментов" + other_category: "Другое" + new_tab: "Новая вкладка" + split_pane: "Разделённая панель" + + show_agent_tips: "Показывать подсказки агента" + hide_agent_tips: "Скрывать подсказки агента" + show_oz_changelog: "Показывать изменения Oz в новом представлении агента" + hide_oz_changelog: "Скрывать изменения Oz в новом представлении агента" + show_use_agent_footer: 'Показывать футер "Использовать агента"' + hide_use_agent_footer: 'Скрывать футер "Использовать агента"' + + natural_language_detection: "Обнаружение естественного языка" + autodetect_agent_prompts: "Автоопределение агентских подсказок в вводе терминала" + autodetect_terminal_commands: "Автоопределение терминальных команд в вводе агента" + show_input_hint_text: "Показывать текст подсказки ввода" + show_agent_tips_toggle: "Показывать подсказки агента" + include_agent_commands_in_history: "Включать команды агента в историю" + natural_language_denylist: "Чёрный список естественного языка" + natural_language_denylist_description: "Команды из этого списка никогда не будут вызывать обнаружение естественного языка." + + codebase_context: "Контекст кодовой базы" + rules: "Правила" + suggested_rules: "Предложенные правила" + warp_drive_context: "Warp Drive как контекст агента" + manage_rules: "Управление правилами" + manage_mcp_servers: "Управление MCP-серверами" + call_mcp_servers: "Вызов MCP-серверов" + mcp_allowlist: "Белый список MCP" + mcp_denylist: "Чёрный список MCP" + command_denylist: "Чёрный список команд" + command_allowlist: "Белый список команд" + directory_allowlist: "Белый список директорий" + placeholder_command_allowlist: "напр. ls .*" + placeholder_command_denylist: "напр. rm .*" + placeholder_directory_allowlist: "напр. ~/code-repos/repo" + placeholder_profile_name: 'напр. "YOLO code"' + placeholder_cli_command: "команда (поддерживает regex)" + base_model: "Базовая модель" + apply_code_diffs: "Применять диффы кода" + read_files: "Чтение файлов" + execute_commands: "Выполнение команд" + interact_with_running_commands: "Взаимодействие с запущенными командами" + key_for_voice_input: "Клавиша активации голосового ввода" + auto_spawn_servers: "Автозапуск серверов от сторонних агентов" + show_model_picker_in_prompt: "Показывать выбор модели в подсказке" + show_conversation_history: "Показывать историю разговоров в панели инструментов" + enable_builtin_feedback_skill: "Включить встроенный навык обратной связи" + feedback_bundled_skill_description: "Позволяет Oz использовать встроенный навык Warp для превращения отзывов о продукте в задачи на GitHub." + agent_thinking_display: "Отображение размышлений агента" + preferred_layout_open_conversation: "Предпочитаемый макет при открытии существующих разговоров" + + endpoint_added: "Endpoint добавлен" + endpoint_saved: "Endpoint сохранён" + endpoint_removed: "Endpoint удалён" + + sign_up: "Зарегистрироваться" + to_use_ai_features_create_account: "Чтобы использовать функции AI, создайте аккаунт." + remote_session_org_policy: "Ваша организация запрещает AI, когда активная панель содержит контент из удалённой сессии" + + commands_comma_separated: "Команды, через запятую" + this_is_limit_of_ai_credits: "Это {duration} лимит AI кредитов для вашего аккаунта." + + resets: "Сброс {time}" + + agents_description: "Установите границы того, как работает ваш Агент. Выберите, к чему он имеет доступ, насколько он автономен и когда он должен запрашивать ваше разрешение. Вы также можете настроить поведение в отношении ввода на естественном языке, осведомлённости о кодовой базе и многого другого." + profiles_description: "Профили позволяют вам определить, как работает ваш Агент — от действий, которые он может выполнять и когда ему нужно разрешение, до моделей, которые он использует для задач, таких как кодирование и планирование. Вы также можете ограничить их отдельными проектами." + context_window_tokens: "Контекстное окно (токенов)" + some_permissions_managed: "Некоторые из ваших разрешений управляются вашим рабочим пространством." + + command_denylist_description: "Регулярные выражения для команд, при выполнении которых Агент Warp должен всегда запрашивать разрешение." + command_allowlist_description: "Регулярные выражения для команд, которые могут автоматически выполняться Агентом Warp." + directory_allowlist_description: "Предоставить агенту доступ к файлам в определённых папках." + mcp_allowlist_description: "Разрешить Агенту Warp вызывать эти MCP-серверы." + mcp_denylist_description: "Агент Warp всегда будет запрашивать разрешение перед вызовом любых MCP-серверов из этого списка." + + base_model_description: "Эта модель служит основным движком Агента Warp. Она обеспечивает большинство взаимодействий и вызывает другие модели для задач, таких как планирование или генерация кода, когда это необходимо. Warp может автоматически переключаться на альтернативные модели в зависимости от доступности модели или для вспомогательных задач, таких как суммирование разговоров." + full_terminal_use_model_description: "Модель, используемая, когда агент работает в интерактивных терминальных приложениях, таких как оболочки баз данных, отладчики, REPL или серверы разработки — читая живой вывод и записывая команды в PTY." + computer_use_model_description: "Модель, используемая, когда агент берет на себя управление вашим компьютером для взаимодействия с графическими приложениями через движения мыши, клики и ввод с клавиатуры." + context_window_description: "Рабочая память базовой модели — сколько токенов вашего разговора, кода и документов она может учитывать одновременно. Большие окна обеспечивают более длинные разговоры и более связные ответы на больших кодовых базах ценой более высокой задержки и использования вычислений." + plan_auto_sync_description: "Планы, создаваемые этим агентом, будут автоматически добавлены и синхронизированы с Warp Drive." + call_web_tools_description: "Агент может использовать веб-поиск, когда это полезно для выполнения задач." + upgrade_footer: "Продвинутые модели недоступны на бесплатных тарифах. Улучшить" + thinking_display_description: "Управляет отображением следов размышлений/рассуждений." + + show_hint_description: 'Показывает подсказку использовать агента с "Full Terminal Use" в длительных командах.' + rule_suggestions_description: "Позволяет AI предлагать правила для сохранения на основе ваших взаимодействий." + warp_drive_context_description: "Агент Warp может использовать содержимое вашего Warp Drive для адаптации ответов к вашим личным и командным рабочим процессам и средам разработки. Это включает любые Workflows, Notebooks и переменные окружения." + + key_for_voice_input_description: "Нажмите и удерживайте для активации." + + upgrade: "Улучшить" + compare_plans: "Сравнить планы" + contact_support: "Связаться с поддержкой" + + learn_more: "Узнать больше" + let_us_know: "Сообщите нам" + refresh: "Обновить" + + codebase_context_description: "Позволяет Агенту Warp генерировать структуру вашей кодовой базы, которая может использоваться для контекста. Никакой код не хранится на наших серверах." + mcp_description: "Добавьте MCP-серверы для расширения возможностей Агента Warp. MCP-серверы предоставляют источники данных или инструменты агентам через стандартизированный интерфейс, действуя как плагины." + mcp_zero_state: "Вы ещё не добавили ни одного MCP-сервера. После добавления вы сможете контролировать, насколько автономным будет Агент Warp при взаимодействии с ними." + add_mcp_server: "Добавить сервер" + + show_coding_agent_toolbar: "Показывать панель инструментов агента кодирования" + third_party_cli_agents: "Сторонние CLI-агенты" + auto_show_hide_rich_input: "Автоматически показывать/скрывать Rich Input в зависимости от статуса агента" + requires_warp_plugin: "Требуется плагин Warp для вашего агента кодирования" + auto_open_rich_input: "Автоматически открывать Rich Input при запуске сессии агента кодирования" + auto_dismiss_rich_input: "Автоматически закрывать Rich Input после отправки запроса" + commands_enable_toolbar: "Команды, включающие панель инструментов" + add_regex_patterns_toolbar: "Добавьте regex-шаблоны для отображения панели инструментов агента кодирования для соответствующих команд." + + org_enforced_tooltip: "Этот параметр задан настройками вашей организации и не может быть изменён." + enable_agent_attribution: "Включить атрибуцию агента" + agent_attribution: "Атрибуция агента" + agent_attribution_description: "Oz может добавлять атрибуцию к сообщениям коммитов и создаваемым pull request'ам" + + experimental: "Экспериментально" + computer_use: "Управление компьютером" + ask_questions: "Задавать вопросы" + full_terminal_use_model: "Модель для работы в терминале" + computer_use_model: "Модель для управления компьютером" + context_window: "Контекстное окно" + plan_auto_sync: "Автосинхронизация планов" + call_web_tools: "Веб-инструменты" + section_models: "МОДЕЛИ" + section_permissions: "РАЗРЕШЕНИЯ" + computer_use_in_cloud_agents: "Computer Use в облачных агентах" + computer_use_description: "Включить computer use в разговорах облачного агента, запущенных из приложения Warp." + + cloud_handoff: "Передача в облако" + cloud_handoff_header: "Передача в облако" + cloud_handoff_description: "Передавать локальные разговоры агента облачному агенту." + cloud_handoff_requires_cloud: "Для передачи в облако необходимо включить облачные разговоры." + use_ampersand_handoff: "Использовать & для запуска передачи" + ampersand_handoff_description: "Введите & как первый символ для входа в режим передачи в облако." + + openai_api_key: "API-ключ OpenAI" + anthropic_api_key: "API-ключ Anthropic" + google_api_key: "API-ключ Google" + warp_credit_fallback: "Запасной вариант с кредитами Warp" + warp_credit_fallback_description: "При включении запросы агента могут быть направлены на одну из моделей Warp в случае ошибки. Warp будет приоритетно использовать ваши API-ключи, а не кредиты Warp." + custom_inference: "Свой inference" + custom_endpoints: "Свои endpoint'ы" + api_keys: "API-ключи" + + aws_bedrock: "AWS Bedrock" + use_aws_bedrock_credentials: "Использовать учётные данные AWS Bedrock" + aws_bedrock_managed_by_org: "Warp загружает и отправляет локальные учётные данные AWS CLI для моделей, поддерживаемых Bedrock. Этот параметр управляется вашей организацией." + aws_bedrock_description: "Warp загружает и отправляет локальные учётные данные AWS CLI для моделей, поддерживаемых Bedrock." + login_command: "Команда входа" + aws_profile: "Профиль AWS" + automatically_run_login_command: "Автоматически выполнять команду входа" + auto_login_description: "При включении команда входа будет выполняться автоматически при истечении учётных данных AWS Bedrock." + default_model_modal: + api_key_description: "Вы добавили свой API-ключ {provider_name}, но ваша модель по умолчанию сейчас — {current_default}, которая не будет работать без кредитов Warp. Изменить модель по умолчанию?" + custom_endpoint_description: "Вы добавили кастомный эндпоинт «{endpoint_name}», но ваша модель по умолчанию сейчас — {current_default}, которая не будет работать без кредитов Warp. Изменить модель по умолчанию?" + + # ── Кастомный маршрутизатор моделей ── + settings.custom_router: + editor: + header: "Редактор маршрутизатора" + new_router_title: "Новый маршрутизатор" + name_placeholder: "Мой маршрутизатор" + name_placeholder_personalized: "Маршрутизатор {first_name}" + type_complexity: "Сложность" + type_rules: "Правила" + add_rule: "+ Добавить правило" + name_required: "Необходимо указать имя маршрутизатора." + complexity_field_default: "По умолчанию" + complexity_field_easy: "Лёгкая" + complexity_field_medium: "Средняя" + complexity_field_hard: "Сложная" + model_required: "Необходимо выбрать модель для «{field}»." + prompt_default_required: "Необходимо выбрать модель по умолчанию." + at_least_one_rule_required: "Необходимо указать хотя бы одно правило с описанием и моделью." + validation_error: "Ошибка проверки: {err}" + serialization_error: "Ошибка сериализации: {err}" + write_error: "Ошибка записи: {err}" + models_section: "Модели" + default_dropdown: "По умолчанию (обязательно)" + easy_dropdown: "Лёгкая (обязательно)" + medium_dropdown: "Средняя (обязательно)" + hard_dropdown: "Сложная (обязательно)" + default_model_section: "Модель по умолчанию" + rules_section: "Правила" + rules_help_1: "Правила — это пользовательские подсказки, описывающие, когда использовать конкретную модель. Warp интеллектуально сопоставляет ваши задачи с этими правилами." + rules_help_2: "Правила сопоставляются сверху вниз — правила выше в списке имеют приоритет над правилами ниже." + router_name_section: "Имя маршрутизатора" + router_type_section: "Тип маршрутизатора" + complexity_based_label: "На основе сложности" + complexity_routing_explanation: " — маршрутизация выбирает модель на основе классификации сложности задачи в Warp." + rule_based_label: "На основе правил" + rule_routing_explanation: " — маршрутизация выбирает модель на основе пользовательских подсказок." + rule_placeholder: "Опишите, когда использовать эту модель…" + rule_label: "Правило" + model_label: "Модель" + view: + open_file: "Открыть файл" + complexity_based_routing: "Маршрутизация по сложности" + prompt_based_routing: "Маршрутизация по промпту" + label_default: "По умолчанию:" + label_easy: "Лёгкие:" + label_medium: "Средние:" + label_hard: "Сложные:" + rules_count_one: "1 правило" + rules_count_other: "{n} правил" + routes: + complexity_requires_default: "complexity routing requires a non-empty `default` model" + prompt_requires_default: "prompt routing requires a non-empty `default` model" + default_error: "`default`: {e}" + complexity_bucket_error: "complexity bucket `{bucket}`: {e}" + prompt_rule_description_empty: "prompt rule {index}: `description` is empty" + prompt_rule_error: "prompt rule {index}: {e}" + target_empty: "target model id is empty" + target_is_auto: "target `{trimmed}` is an auto model; custom model routers must route to concrete models" + + # ── Модальные окна настроек ── + settings.modals: + set_default_model: + not_now: "Не сейчас" + change_default_model: "Изменить модель по умолчанию" + + # ── Настройки биллинга ── + settings.billing: + overage_usage_link: "Подробнее о перерасходе" + overage_toggle_admin_header: "Включить перерасход для премиум-моделей" + overage_toggle_user_header_enabled: "Перерасход для премиум-моделей включён" + overage_toggle_user_header_disabled: "Перерасход для премиум-моделей отключён" + overage_toggle_description: "Продолжайте использовать премиум-модели сверх лимитов вашего плана. Использование оплачивается с шагом $20 до вашего лимита расходов, остаток списывается в дату планового платежа." + overage_toggle_user_description: "Попросите администратора команды включить перерасход для большего использования AI." + + sort_a_to_z: "От A до Z" + sort_z_to_a: "От Z до A" + sort_usage_ascending: "По использованию (возр.)" + sort_usage_descending: "По использованию (убыв.)" + + auto_reload_exceed_limit: "Автопополнение отключено, так как следующее пополнение превысит ваш месячный лимит расходов. Увеличьте лимит для использования автопополнения." + auto_reload_delinquent: "Ограничено из-за проблем с оплатой. Обновите способ оплаты для покупки дополнительных кредитов." + restricted_billing_usage: "Автопополнение отключено из-за недавней неудачной попытки пополнения. Пожалуйста, обновите способ оплаты и попробуйте снова." + + overview_tab: "Обзор" + usage_history_tab: "История использования" + + enterprise_usage_callout_header: "Отчётность по использованию временно ограничена" + enterprise_usage_callout_body_admin_prefix: "Использование корпоративных кредитов пока не полностью доступно в этом представлении. Для наиболее точного отслеживания расходов " + enterprise_usage_callout_body_admin_link: "посетите панель администратора" + enterprise_usage_callout_body_admin_suffix: "." + enterprise_usage_callout_body_non_admin: "Использование корпоративных кредитов пока не полностью доступно в этом представлении. Обратитесь к администратору команды для подробной отчётности." + + addon_credits_description: "Дополнительные кредиты приобретаются предоплаченными пакетами, которые переносятся на следующий расчётный цикл и истекают через год. Чем больше покупаете, тем выгоднее цена за кредит. После использования кредитов базового плана будут расходоваться дополнительные кредиты." + additional_addon_credits_description_for_team: "Приобретённые дополнительные кредиты распределяются между членами команды." + + cloud_agent_trial: "Пробный период облачного агента" + + overage_spending_limit: "Лимит расходов на перерасход" + monthly_spending_limit: "Месячный лимит расходов" + load_more: "Загрузить ещё" + + failed_update_workspace_settings: "Не удалось обновить настройки рабочего пространства" + successfully_purchased_addon_credits: "Дополнительные кредиты успешно приобретены" + + not_set: "Не задано" + monthly_overage_spending_limit: "Месячный лимит расходов на перерасход" + sets_monthly_overage_limit: "Устанавливает месячный лимит расходов на перерасход сверх суммы плана" + add_on_credits: "Дополнительные кредиты" + switch_to_build: "Перейти на план Build" + upgrade_to_build: "Улучшить до плана Build" + contact_account_executive: "Свяжитесь с вашим аккаунт-менеджером для получения дополнительных кредитов." + contact_team_admin: "Обратитесь к администратору команды для покупки дополнительных кредитов." + sets_monthly_limit_spent: "Устанавливает месячный лимит расходов на дополнительные кредиты" + monthly_spend_limit: "Месячный лимит расходов" + purchased_this_month: "Приобретено в этом месяце" + auto_reload: "Автопополнение" + auto_reload_description: "При включении автопополнение будет автоматически приобретать {amount} кредитов, когда баланс дополнительных кредитов достигнет 100 оставшихся кредитов." + buying: "Покупка…" + buy: "Купить" + one_time_purchase: "Разовая покупка" + reloading_would_exceed: "Пополнение превысит ваш месячный лимит. " + increase_your_limit: "Увеличьте лимит" + total_overages: "Общий перерасход" + usage_resets_on: "Использование сбрасывается {date}" + credit_limit_prorated_self: "Ваш кредитный лимит пропорционален, потому что вы присоединились в середине расчётного цикла." + credit_limit_prorated_other: "Этот кредитный лимит пропорционален, потому что этот пользователь присоединился в середине расчётного цикла." + usage_section: "Использование" + credits_header: "Кредиты" + credit_limit_description: "Это {duration} лимит AI кредитов для вашего аккаунта." + last_30_days: "Последние 30 дней" + no_usage_history: "Нет истории использования" + kick_off_agent_task: "Запустите задачу агента, чтобы увидеть историю использования." + sort_by: "Сортировать" + team_total: "Всего по команде" + manage_billing: "Управление оплатой" + contact_team_admin_billing: "Обратитесь к администратору команды для решения проблем с оплатой." + upgrade_to_turbo: "Улучшить до плана Turbo" + upgrade_to_lightspeed: "Улучшить до плана Lightspeed" + upgrade_plan: "Улучшить" + upgrade_to_max: "Улучшить до Max" + switch_to_business: "Перейти на Business" + upgrade_to_enterprise: "Улучшить до Enterprise" + plan: "План" + open_admin_panel: "Открыть панель администратора" + free: "Бесплатный" + new_agent: "Новый агент" + buy_more: "Купить ещё" + billing_and_usage: "Биллинг и использование" + + # ── Запуск OpenWarp ── + openwarp_launch: + title: "Warp теперь с открытым исходным кодом" + description: "Вы, наше сообщество, можете участвовать в разработке Warp, используя agent-first подход." + visit_repo: "Посетить репозиторий" + contribute_title: "Внести вклад" + contribute_description: "Клиентский код Warp теперь открыт. Начните с использования навыка /feedback, чтобы создать задачу, и следуйте руководству по внесению вклада здесь." + contribute_link_text: "здесь" + oad_title: "Открытая автоматизированная разработка" + oad_description: "Репозиторий Warp управляется agent-first подходом на базе Oz, нашей облачной платформы оркестрации агентов." + oz_link_text: "Oz" + auto_open_weights_title: "Представляем 'auto (open-weights)'" + auto_open_weights_description: "Мы добавили новую авто-модель, которая выбирает лучшую открытую модель для задачи, например Kimi или MiniMax." + + # ── Настройки конфиденциальности ── + settings.privacy: + secret_redaction: "Скрытие секретов" + secret_redaction_description: "При включении этой настройки Warp будет сканировать блоки, содержимое объектов Warp Drive и промпты Oz на наличие потенциально чувствительной информации и предотвращать сохранение или отправку этих данных на серверы. Вы можете настроить этот список с помощью регулярных выражений." + custom_secret_redaction: "Пользовательское скрытие секретов" + custom_secret_redaction_description: "Используйте regex для определения дополнительных секретов или данных, которые вы хотите скрыть. Изменения вступят в силу при следующем запуске команды. Вы можете использовать флаг (?i) в начале regex, чтобы сделать его нечувствительным к регистру." + help_improve_warp: "Помочь улучшить Warp" + help_improve_warp_description: "Аналитика приложения помогает нам сделать продукт лучше для вас. Мы можем собирать определённые взаимодействия с консолью для улучшения возможностей AI Warp." + help_improve_warp_description_enterprise: "Аналитика приложения помогает нам сделать продукт лучше для вас. Мы собираем только метаданные использования приложения, никогда не собираем ввод или вывод консоли." + free_tier_analytics_note: "На бесплатном тарифе аналитика должна быть включена для использования функций AI." + manage_your_data: "Управление вашими данными" + manage_your_data_description: "В любое время вы можете безвозвратно удалить свой аккаунт Warp. Вы больше не сможете использовать Warp." + visit_data_management_page: "Посетить страницу управления данными" + manage_privacy_settings: "Управление настройками конфиденциальности" + privacy_policy_title: "Политика конфиденциальности" + read_privacy_policy: "Прочитать политику конфиденциальности Warp" + add_regex_pattern: "Добавить regex-шаблон" + personal: "Личное" + enterprise: "Корпоративное" + enterprise_secret_redaction_locked: "Корпоративное скрытие секретов нельзя изменить." + no_enterprise_regexes: "Ваша организация не настроила корпоративные регулярные выражения." + recommended: "Рекомендованные" + add_all: "Добавить все" + add_regex: "Добавить regex" + enabled_by_organization: "Включено вашей организацией." + secret_visual_redaction_mode: "Режим визуального скрытия секретов" + secret_visual_redaction_mode_description: "Выберите, как секреты визуально отображаются в списке блоков, сохраняя возможность поиска. Эта настройка влияет только на то, что вы видите в списке блоков." + zdr_enabled_description: "Ваш администратор включил нулевое хранение данных для вашей команды. Пользовательский контент никогда не будет собираться." + managed_by_organization: "Эта настройка управляется вашей организацией." + read_more_about_data_use: "Узнать больше об использовании данных Warp" + send_crash_reports: "Отправлять отчёты о сбоях" + send_crash_reports_description: "Отчёты о сбоях помогают в отладке и повышении стабильности." + store_ai_conversations_in_cloud: "Хранить разговоры AI в облаке" + cloud_conversation_storage_description: "Разговоры агента могут быть доступны другим и сохраняются при входе на разных устройствах. Эти данные хранятся только для функциональности продукта, и Warp не будет использовать их для аналитики." + local_conversation_storage_description: "Разговоры агента хранятся только локально на вашем компьютере, теряются при выходе из системы и не могут быть переданы другим. Примечание: данные разговоров фоновых агентов по-прежнему хранятся в облаке." + network_log_console: "Консоль сетевого журнала" + network_log_console_description: "Мы создали встроенную консоль, которая позволяет просматривать все коммуникации от Warp к внешним серверам, чтобы вы могли быть уверены, что ваша работа всегда в безопасности." + view_network_logging: "Просмотр сетевого журнала" + + # ── Настройки команд ── + settings.teams: + team_name: "Название команды" + create_team_description: "Создав команду, вы можете совместно работать над разработкой с помощью агентов, обмениваясь запусками облачных агентов, окружениями, автоматизациями и артефактами. Вы также можете создать общую базу знаний для коллег и агентов." + leave_team: "Покинуть команду" + delete_team: "Удалить команду" + create: "Создать" + create_team: "Создать команду" + teams: "Команды" + domains_comma_separated: "Домены через запятую" + emails_comma_separated: "Email через запятую" + set: "Установить" + invite: "Пригласить" + invalid_domains_instructions: "Некоторые из указанных доменов недействительны или уже добавлены." + invite_link_toggle_instructions: "Как администратор, вы можете включить или отключить возможность для участников команды приглашать других по ссылке-приглашению." + invite_link_domain_restrictions_instructions: "Ограничение по домену — разрешить присоединяться к команде по ссылке-приглашению только пользователям с email в определённых доменах." + email_invite_expiry_instructions: "Приглашения по email действительны в течение 7 дней." + invalid_emails_instructions: "Некоторые из указанных email-адресов недействительны, уже приглашены или являются участниками команды." + offline: "Вы не в сети." + invite_team_members: "Пригласить участников" + by_link: "По ссылке" + by_email: "По email" + by_discovery: "По обнаружению" + team_members: "Участники команды" + reset_links: "Сбросить ссылки" + remove_domain: "Удалить домен" + remove: "Удалить" + approve_domains: "Подтвердить домены" + send_email_invites: "Отправить приглашения" + invite_link_prefix: "warp.dev/join/" + copy_link: "Копировать ссылку" + link_copied: "Ссылка скопирована" + leave_team_confirmation: "Вы уверены, что хотите покинуть эту команду?" + delete_team_confirmation: "Вы уверены, что хотите удалить эту команду? Это действие нельзя отменить." + transfer_team_ownership: "Передать владение командой?" + transfer_ownership: "Передать владение" + failed_to_send_invite: "Не удалось отправить приглашение" + toggled_invite_links: "Ссылки-приглашения переключены" + failed_to_toggle_invite_links: "Не удалось переключить ссылки-приглашения" + reset_invite_links_success: "Ссылки-приглашения сброшены" + failed_to_reset_invite_links: "Не удалось сбросить ссылки-приглашения" + deleted_invite: "Приглашение удалено" + failed_to_delete_invite: "Не удалось удалить приглашение" + failed_to_add_domain_restriction: "Не удалось добавить ограничение домена" + failed_to_delete_domain_restriction: "Не удалось удалить ограничение домена" + failed_to_generate_upgrade_link: "Не удалось создать ссылку для улучшения. Свяжитесь с нами по адресу feedback@warp.dev" + failed_to_generate_billing_link: "Не удалось создать ссылку для биллинга. Свяжитесь с нами по адресу feedback@warp.dev" + toggled_team_discoverability: "Обнаружение команды переключено" + failed_to_toggle_team_discoverability: "Не удалось переключить обнаружение команды" + successfully_joined_team: "Успешно присоединились к команде" + failed_to_join_team: "Не удалось присоединиться к команде" + successfully_transferred_team_ownership: "Владение командой успешно передано" + failed_to_transfer_team_ownership: "Не удалось передать владение командой" + successfully_updated_team_member_role: "Роль участника команды успешно обновлена" + failed_to_update_team_member_role: "Не удалось обновить роль участника команды" + error_leaving_team: "Ошибка при выходе из команды" + successfully_left_team: "Вы успешно покинули команду" + your_new_team_name: "Название вашей новой команды" + new_team_name: "Название вашей новой команды" + seat_cap_reached: "Вы достигли лимита участников вашего плана." + seat_cap_exceeded: "Вы превысили лимит участников вашего плана. Существующие участники сохраняют доступ, но вы не сможете добавлять новых." + payment_past_due: "Платёж просрочен" + payment_past_due_description: "Приглашения в команду ограничены из-за просроченного платежа." + subscription_unpaid: "Подписка не оплачена" + subscription_unpaid_description: "Приглашения в команду ограничены из-за неоплаченной подписки." + contact_team_admin_to_restore: "Обратитесь к администратору команды для восстановления доступа." + contact_team_admin_to_grow: "Обратитесь к администратору команды для расширения." + upgrade_to_grow_team: "Улучшите план, чтобы расширить команду." + contact_sales_to_grow: "Свяжитесь с отделом продаж для расширения команды." + update_payment_to_restore: "Обновите платёжную информацию для восстановления доступа." + contact_support_to_restore: "Свяжитесь с поддержкой для восстановления доступа." + upgrade: "Улучшить" + contact_sales: "Связаться с продажами" + update_billing: "Обновить оплату" + contact_support: "Связаться с поддержкой" + upgrade_to_build: "Улучшить до Build" + upgrade_to_turbo_plan: "Улучшить до Turbo" + upgrade_to_lightspeed_plan: "Улучшить до Lightspeed" + compare_plans: "Сравнить планы" + free_plan_usage_limits: "Лимиты бесплатного плана" + plan_usage_limits: "Лимиты плана" + shared_notebooks: "Общие записные книжки" + shared_workflows: "Общие рабочие процессы" + want_to_grow_your_team: "Хотите расширить команду?" + grow_team: "Хотите расширить команду? " + manage_plan: "Управление планом" + manage_billing: "Управление оплатой" + open_admin_panel: "Открыть панель администратора" + expired: "ИСТЁК" + pending: "ОЖИДАЕТ" + owner: "ВЛАДЕЛЕЦ" + admin: "АДМИН" + past_due: "ПРОСРОЧЕН" + unpaid: "НЕ ОПЛАЧЕН" + failed_to_load_invite_link: "Не удалось загрузить ссылку-приглашение." + join_this_team: "Присоединитесь к этой команде и начните совместную работу над рабочими процессами, записными книжками и многим другим." + join: "Присоединиться" + contact_admin_to_request_access: "Свяжитесь с администратором для запроса доступа" + or_join_existing_team: "Или присоединитесь к существующей команде в вашей компании" + allow_users_with_domain: "Разрешить пользователям Warp с email @{domain} находить команду и присоединяться." + allow_users_with_same_domain: "Разрешить пользователям Warp с тем же доменом email, что и у вас, находить команду и присоединяться." + member_limit_info: "Ваш план ({plan}) имеет максимальную вместимость {cap} участников." + additional_members_billed: "Дополнительные участники оплачиваются по ставке вашего плана за пользователя: ${monthly:.0}/мес или ${yearly:.0}/год, в зависимости от вашего расчётного интервала. {prorated}" + additional_members_billed_no_rate: "Дополнительные участники оплачиваются по ставке вашего плана за пользователя. {prorated}" + prorated_charge_info: "С вас будет взиматься плата за часть использования Warp участником команды." + prorated_charge_info_admin: "С вашего администратора будет взиматься плата за часть использования Warp участником команды." + you_exceeded_member_limit: "Вы превысили лимит участников" + + # ── Настройки окружений ── + settings.environments: + environments: "Окружения" + environments_description: "Окружения определяют, где запускаются ваши фоновые агенты. Настройте за несколько минут через GitHub (рекомендуется), ассистированную настройку Warp или ручную конфигурацию." + search_environments: "Поиск окружений..." + no_environments_match: "Нет окружений, соответствующих поиску." + personal: "Личное" + no_environments_yet: "У вас пока нет настроенных окружений." + choose_setup_method: "Выберите способ настройки окружения:" + quick_setup: "Быстрая настройка" + suggested: "Рекомендуется" + quick_setup_description: "Выберите репозитории GitHub, с которыми вы хотите работать, и мы предложим базовый образ и конфигурацию" + use_the_agent: "Использовать агента" + use_the_agent_description: "Выберите локально настроенный проект, и мы поможем вам настроить окружение на его основе" + launch_agent: "Запустить агента" + get_started: "Начать" + authorize: "Авторизоваться" + retry: "Повторить" + loading: "Загрузка..." + last_edited: "Последнее изменение: {timestamp}" + last_used: "Последнее использование: {timestamp}" + last_used_never: "Последнее использование: никогда" + view_my_runs: "Мои запуски" + share: "Поделиться" + edit: "Редактировать" + env_id: "ID окружения: {id}" + image: "Образ: {image}" + repos: "Репозитории: {repos}" + setup_commands: "Команды настройки: {commands}" + shared_by_warp_and_team: "Предоставлено Warp и вашей командой" + shared_by_warp_and: "Предоставлено Warp и {team}" + successfully_updated_environment: "Окружение успешно обновлено" + successfully_created_environment: "Окружение успешно создано" + environment_deleted_successfully: "Окружение успешно удалено" + successfully_shared_environment: "Окружение успешно опубликовано" + failed_to_share_environment: "Не удалось опубликовать окружение для команды" + unable_to_create_not_logged_in: "Не удалось создать окружение: вы не вошли в систему." + unable_to_save_no_longer_exists: "Не удалось сохранить: окружение больше не существует." + unable_to_share_not_on_team: "Не удалось опубликовать окружение: вы не состоите в команде." + unable_to_share_not_synced: "Не удалось опубликовать окружение: окружение ещё не синхронизировано." + + # ── Настройки Warp Drive ── + settings.drive: + warp_drive: "Warp Drive" + sign_up: "Регистрация" + to_use_warp_drive_create_account: "Чтобы использовать Warp Drive, создайте аккаунт." + warp_drive_description: "Warp Drive — это рабочее пространство в вашем терминале, где вы можете сохранять Workflows, Notebooks, Prompts и переменные окружения для личного использования или для обмена с командой." + + # ── Настройки «О программе» ── + settings.about: + copyright: "© Warp, 2026" + + # ── LSP ── + lsp: + open_logs: "Открыть логи" + restart_server: "Перезапустить сервер" + stop_server: "Остановить сервер" + start_server: "Запустить сервер" + remove_server: "Удалить сервер" + restart_all_servers: "Перезапустить все серверы" + stop_all_servers: "Остановить все серверы" + start_all_servers: "Запустить все серверы" + start_all_stopped_servers: "Запустить все остановленные серверы" + manage_servers: "Управление серверами" + unknown_workspace: "неизвестное рабочее пространство" + stopped: "{server}: остановлен" + error: "{server}: ошибка" + lang_support_not_enabled: "Языковая поддержка в настоящее время не включена для {name}" + lang_support_unavailable_file_type: "Языковая поддержка недоступна для этого типа файлов" + lang_server_unavailable: "Языковой сервер недоступен для этой кодовой базы" + installing: "Установка {binary}..." + language_support_unavailable: "Языковая поддержка недоступна для {name}" + tab_config_skill_tooltip_enabled: "Открыть ввод агента с навыком /update-tab-config" + tab_config_skill_tooltip_disabled: "Включите AI для использования навыка /update-tab-config" + + # ── Code Review ── + codereview: + git_dialog: + branch_label: "Ветка" + button: + cancel: "Отмена" + commit: "Коммит" + confirm: "Подтвердить" + create_pr: "Создать PR" + open_pr: "Открыть PR" + publish: "Опубликовать" + push: "Запушить" + changes_label: "Изменения" + file_count_plural: "файлов" + file_count_singular: "файл" + loading: + committing: "Коммит…" + creating: "Создание…" + publishing: "Публикация…" + pushing: "Пуш…" + title: + commit: "Закоммитить изменения" + view_changes: "Посмотреть изменения" + diffs_local_only: "Диффы работают только для локальных рабочих пространств." + diffs_git_only: "Диффы работают только для git-репозиториев." + diffs_wsl: "Диффы пока не работают в WSL." + hide_file_nav: "Скрыть навигацию по файлам" + show_file_nav: "Показать навигацию по файлам" + cannot_discard_git_operation: "Нельзя отменить изменения во время git-операции (merge, rebase и т.д.)" + no_changes_to_discard: "Нет изменений для отмены" + discard_uncommitted_title: "Отменить незакоммиченные изменения?" + discard_uncommitted_file_title: "Отменить все незакоммиченные изменения в файле?" + discard_all_title: "Отменить все изменения?" + discard_all_file_title: "Отменить все изменения в файле?" + discard_uncommitted_desc: "Вы собираетесь отменить все локальные изменения, которые не были закоммичены." + discard_uncommitted_file_desc: "Это восстановит файл до последней закоммиченной версии и отменит локальные правки." + discard_all_desc: "Вы собираетесь отменить все закоммиченные и незакоммиченные изменения." + discard_all_file_desc: "Это восстановит файл до версии основной ветки и отменит все закоммиченные и незакоммиченные правки." + discard_all_file_branch_desc: "Это сбросит файл до версии ветки {branch} и отменит все закоммиченные и незакоммиченные правки." + loading_changes: "Загрузка открытых изменений..." + error_loading_diffs: "Ошибка загрузки диффов" + retry: " Повторить" + cannot_detect_diffs: "Не удаётся обнаружить диффы для этой папки" + no_open_changes: "Нет открытых изменений" + track_changes_hint: "Когда вы или Агент вносите изменения, вы сможете отслеживать их здесь." + repo_initialized: "Репозиторий инициализирован с файлом {file_name}." + comments_sent: "Комментарии отправлены агенту" + could_not_submit_comments: "Не удалось отправить комментарии агенту" + diff_removed: "Дифф удалён" + cannot_attach_context_terminal_running: "Нельзя прикрепить контекст во время работы терминала" + cannot_attach_diff_input_unavailable: "Нельзя прикрепить дифф, когда ввод недоступен" + commit: "Коммит" + no_changes_to_commit: "Нет изменений для коммита" + no_git_actions: "Нет доступных git-действий" + push: "Запушить" + create_pr: "Создать PR" + pr_number: "PR №{number}" + refreshing_pr_info: "Обновление информации о PR" + publish: "Опубликовать" + add_diff_set_context: "Добавить набор диффов как контекст" + show_saved_comment: "Показать сохранённый комментарий" + add_comment: "Добавить комментарий" + send_to_agent: "Отправить агенту" + search_diff_placeholder: "Поиск наборов диффов или веток для сравнения…" + discard_all: "Отменить все" + stash_changes: "Спрятать изменения (stash)" + maximize: "Развернуть" + discard_changes: "Отменить изменения" + initialize_codebase: "Инициализировать кодовую базу" + initialize_codebase_tooltip: "Включает индексацию кодовой базы и WARP.md" + open_repository: "Открыть репозиторий" + open_repository_tooltip: "Перейти к репозиторию и инициализировать его для разработки" + uncommitted_changes: "Незакоммиченные изменения" + open_file: "Открыть файл" + add_file_diff_as_context: "Добавить дифф файла как контекст" + copy_file_path: "Копировать путь к файлу" + diff_too_large: "Дифф слишком большой для отображения" + binary_file_no_diff: "Бинарный файл — дифф недоступен" + file_renamed_no_changes: "Файл переименован без изменений" + new_empty_file: "Новый пустой файл" + unable_to_load_file_content: "Не удалось загрузить содержимое файла" + no_file_selected: "Файл не выбран" + no_files_to_discard: "Нет файлов для отмены" + unsaved_changes_tooltip: "Этот файл имеет несохранённые изменения. {shortcut} для сохранения" + dialog: + # Buttons + confirm_button: "Подтвердить" + cancel_button: "Отмена" + esc_tooltip: "ESC" + commit_button: "Коммит" + commit_and_push: "Коммит и пуш" + commit_and_publish: "Коммит и публикация" + commit_and_create_pr: "Коммит и создать PR" + push_button: "Запушить" + publish_button: "Опубликовать" + create_pr_button: "Создать PR" + # Titles + title_commit: "Закоммитить изменения" + title_publish: "Опубликовать ветку" + title_push: "Запушить изменения" + title_pr: "Создать пулл-реквест" + # Section labels + branch_section: "Ветка" + changes_section: "Изменения" + message_section: "Сообщение коммита" + included_commits: "Включённые коммиты" + include_unstaged: "Включить неиндексированные" + # Loading states + committing: "Коммит…" + pushing: "Пуш…" + publishing: "Публикация…" + creating: "Создание…" + loading: "Загрузка…" + generating_message: "Генерация сообщения коммита…" + # Placeholders + type_message: "Введите сообщение коммита" + # Tooltips + tooltip_enter_message: "Введите сообщение коммита" + # Toast messages + toast_committed: "Изменения успешно закоммичены." + toast_committed_pushed: "Изменения закоммичены и запушены." + toast_pushed: "Изменения успешно запушены." + toast_branch_published: "Ветка успешно опубликована." + toast_pr_created: "PR успешно создан." + open_pr_link: "Открыть PR" + # Pluralization + file_singular: "файл" + file_plural: "файлов" + # Error messages + error_no_changes: "Нет изменений для коммита." + error_git_identity: "Git-идентификация не настроена. Установите user.name и user.email." + error_rejected: "На удалённом сервере новые изменения — сделайте pull перед push." + error_no_remote: "Для этой ветки не настроен удалённый репозиторий." + error_auth: "Ошибка аутентификации. Проверьте Git-учётные данные." + error_network: "Сетевая ошибка. Проверьте подключение." + error_repo_not_found: "Удалённый репозиторий не найден." + error_no_gh: "GitHub CLI (gh) не установлен. См. https://cli.github.com/." + error_gh_auth: "GitHub CLI не аутентифицирован. Выполните `gh auth login`." + error_generic: "Ошибка Git-операции." + reviewing_open_changes: "Просмотр открытых изменений" + reviewing_changes: "Просмотр изменений кода" + outdated: "Устаревший" + comment: "комментарий" + from_github: "Из GitHub" + review_comment: "Комментарий ревью" + copy_text: "Копировать текст" + edit_comment: "Редактировать" + file_level_cannot_edit: "Комментарии уровня файла пока нельзя редактировать." + outdated_cannot_edit: "Устаревшие комментарии нельзя редактировать." + view_in_github: "Посмотреть в GitHub" + remove_comment: "Удалить" + no_non_outdated_to_send: "Нет не устаревших комментариев для отправки" + send_to_cli: "Отправить дифф-комментарии в {label}" + ai_must_be_enabled: "AI должен быть включён для отправки комментариев агенту" + ai_credits_required: "Для ревью кода агентом требуются AI-кредиты" + all_terminals_busy: "Все терминалы заняты" + send_to_agent_tooltip: "Отправить дифф-комментарии агенту" + outdated_section_single: "1 комментарий будет пропущен, так как он устарел." + outdated_section_plural: "{count} комментариев будет пропущено, так как они устарели." + + # ── Ввод терминала ── + terminal.input: + a11y: + label: "Ввод команды." + helper: "Введите команду оболочки, нажмите Enter для выполнения. Нажмите Cmd-Up для навигации к выводу предыдущих команд. Нажмите Cmd-L для перефокусировки ввода." + placeholder: + tell_agent: "Опишите, что должен создать агент..." + cloud_agent: "Запустить облачного агента" + search_queries: "Поиск запросов" + search_queries_rewind: "Поиск запросов для возврата" + search_conversations: "Поиск разговоров" + search_skills: "Поиск навыков" + search_models: "Поиск моделей" + search_profiles: "Поиск профилей" + search_commands: "Поиск команд" + search_prompts: "Поиск подсказок" + search_indexed_repos: "Поиск индексированных репозиториев" + search_plans: "Поиск планов" + enter_prompt_for: "Введите запрос для {}..." + hint: + run_commands: "Выполнение команд" + ai_command_search: "Введите '#' для предложений AI-команд" + steer_agent: "Управление запущенным агентом" + steer_agent_classic: "Управление запущенным агентом или Backspace для выхода" + follow_up: "Задать уточняющий вопрос" + follow_up_classic: "Задать уточняющий вопрос или Backspace для выхода" + warp_anything_0: "Warp что угодно, напр. Разверни моё React-приложение на Vercel и настрой переменные окружения" + warp_anything_1: "Warp что угодно, напр. Помоги отладить, почему мои Python-тесты падают в CI" + warp_anything_2: "Warp что угодно, напр. Настрой новый микросервис с Docker и создай пайплайн развёртывания" + warp_anything_3: "Warp что угодно, напр. Найди и исправь утечку памяти в моём Node.js-приложении" + warp_anything_4: "Warp что угодно, напр. Создай скрипт резервного копирования для PostgreSQL и запланируй его" + warp_anything_5: "Warp что угодно, напр. Помоги мигрировать данные из MySQL в PostgreSQL" + warp_anything_6: "Warp что угодно, напр. Настрой мониторинг и оповещения для моей AWS-инфраструктуры" + warp_anything_7: "Warp что угодно, напр. Построй REST API для мобильного приложения с помощью FastAPI" + warp_anything_8: "Warp что угодно, напр. Помоги оптимизировать медленные SQL-запросы" + warp_anything_9: "Warp что угодно, напр. Создай рабочий процесс GitHub Actions для автоматического деплоя при merge" + warp_anything_10: "Warp что угодно, напр. Настрой кэширование Redis для веб-приложения" + warp_anything_11: "Warp что угодно, напр. Помоги выяснить, почему падают поды Kubernetes" + warp_anything_12: "Warp что угодно, напр. Построй пайплайн данных для обработки CSV и загрузки в BigQuery" + warp_anything_13: "Warp что угодно, напр. Настрой SSL-сертификаты и HTTPS для домена" + warp_anything_14: "Warp что угодно, напр. Помоги отрефакторить устаревший код с современными паттернами" + warp_anything_15: "Warp что угодно, напр. Создай модульные тесты для сервиса аутентификации" + warp_anything_16: "Warp что угодно, напр. Настрой агрегацию логов с ELK-стеком для распределённой системы" + warp_anything_17: "Warp что угодно, напр. Помоги реализовать OAuth2-аутентификацию в Express.js" + warp_anything_18: "Warp что угодно, напр. Оптимизируй Docker-образы для уменьшения времени сборки и размера" + warp_anything_19: "Warp что угодно, напр. Настрой инфраструктуру A/B-тестирования для веб-приложения" + dynamic_enum: + generate: "Выполните следующую команду для генерации вариантов:" + run: "Выполнить команду" + pending: "Команда выполняется..." + failure: "Команда завершилась с ошибкой" + no_results: "Команда не вернула результатов" + keybinding: + show_history: "Показать историю" + network_log: "Показать сетевой журнал Warp" + clear_screen: "Очистить экран" + scroll_up: "Прокрутить вывод терминала на страницу вверх" + scroll_down: "Прокрутить вывод терминала на страницу вниз" + edit_prompt: "Редактировать приглашение" + toggle_classic_completions: "(Экспериментально) Переключить классический режим автодополнения" + command_search: "Поиск команд" + history_search: "Поиск по истории" + open_completions: "Открыть меню автодополнения" + workflows: "Рабочие процессы" + ai_command_suggestions: "Открыть предложения AI-команд" + new_agent_conversation: "Новый разговор с агентом" + trigger_auto_detection: "Включить автоопределение" + clear_ai_context_menu: "Очистить и сбросить контекстное меню AI" + toast: + no_active_conversation: "Нет активного разговора для экспорта" + cannot_start_conversation: "Нельзя начать новый разговор, пока агент отслеживает команду." + handoff_to_cloud: "Передать в облако" + handoff_to_env: "Передать в {}" + + # ── Уведомления терминала ── + terminal.notifications: + discovery: + long_running: "Warp может уведомить вас о завершении длительных команд." + agent_task_completed: "Warp может уведомить вас об окончании ответа агента." + needs_attention: "Warp может уведомить вас, когда команде или агенту требуется внимание." + password_prompt: "Warp может уведомить вас о запросе пароля." + a11y_enable_palette: "Вы можете включить уведомления через палитру команд." + a11y_enable_preferences: "Убедитесь, что вы предоставили доступ для уведомлений Warp в системных настройках." + error_sending: "Ошибка отправки уведомления" + dismissed: "Мы больше не покажем этот баннер, но вы всегда можете включить уведомления в Настройках." + disabled: "Уведомления были отключены, но вы всегда можете включить их в Настройках." + enable: "Включить" + learn_more: "Узнать больше" + troubleshoot: "Диагностика" + configure: "Настроить уведомления" + permissions_accepted: "Готово! Теперь вы можете получать уведомления на рабочий стол." + permissions_denied: "Warp было отказано в разрешении отправлять уведомления." + permissions_error: "Что-то пошло не так при запросе разрешений." + allow_permissions: "Не забудьте 'Разрешить' запрос разрешений для завершения настройки уведомлений." + terminal.notification: + default_title: "Уведомление" + finished: " завершена" + failed: " провалена" + finished_after: " завершена через {duration}с" + failed_after: " провалена через {duration}с" + latest_output: "Последний вывод: " + error_prefix: "Ошибка: " + blocked: " заблокирована" + waiting_for_password: " ожидает ввода пароля" + + # ── Контекстное меню терминала ── + terminal.context_menu: + copy: "Копировать" + copy_url: "Копировать URL" + copy_path: "Копировать путь" + copy_command: "Копировать команду" + copy_commands: "Копировать команды" + copy_output: "Копировать вывод" + copy_filtered_output: "Копировать отфильтрованный вывод" + copy_prompt: "Копировать приглашение" + copy_right_prompt: "Копировать правое приглашение" + copy_working_directory: "Копировать рабочую директорию" + copy_git_branch: "Копировать ветку git" + cut: "Вырезать" + paste: "Вставить" + select_all: "Выделить всё" + insert_into_input: "Вставить в поле ввода" + find_within_block: "Искать в блоке" + find_within_blocks: "Искать в блоках" + scroll_to_top_of_block: "Прокрутить к началу блока" + scroll_to_top_of_blocks: "Прокрутить к началу блоков" + scroll_to_bottom_of_block: "Прокрутить к концу блока" + scroll_to_bottom_of_blocks: "Прокрутить к концу блоков" + save_as_workflow: "Сохранить как рабочий процесс" + ask_warp_ai: "Спросить Warp AI" + toggle_block_filter: "Переключить фильтр блока" + toggle_bookmark: "Переключить закладку" + rewind_to_before_here: "Откатить до этого места" + clear_blocks: "Очистить блоки" + split_pane_right: "Разделить панель вправо" + split_pane_left: "Разделить панель влево" + split_pane_down: "Разделить панель вниз" + split_pane_up: "Разделить панель вверх" + close_pane: "Закрыть панель" + edit_cli_agent_toolbelt: "Редактировать инструменты CLI-агента" + edit_agent_toolbelt: "Редактировать инструменты агента" + edit_prompt: "Редактировать приглашение" + command_search: "Поиск команд" + ai_command_search: "AI-поиск команд" + hide: "Скрыть" + show: "Показать" + input_hint_text: "текст подсказки ввода" + share_block_ellipsis: "Поделиться блоком..." + share_ellipsis: "Поделиться..." + show_in_finder: "Показать в Finder" + show_containing_folder: "Показать содержащую папку" + open_in_warp: "Открыть в Warp" + open_in_editor: "Открыть в редакторе" + fork_from_here_dev: "Форкнуть отсюда (для разработчиков)" + + # ── Подсказки терминала ── + terminal.tooltip: + filter_block_output: "Фильтровать вывод блока" + bookmark_block: "Добавить этот блок в закладки для быстрого перехода" + lock_scrolling: "Зафиксировать прокрутку внизу блока" + jump_to_bottom: "Перейти к концу этого блока" + + # ── Блок терминала ── + terminal.block: + show_init_block: "Показать блок инициализации" + more_info: "Подробнее" + + # ── Terminal Zero State ── + terminal.zero_state: + new_session: "Новая сессия терминала" + start_agent_conversation: "начать новую беседу с агентом" + start_cloud_conversation: "начать новую беседу с облачным агентом" + cycle_past: "листать прошлые команды и беседы" + open_code_review: "открыть code review" + + # ── Terminal Shared Session ── + terminal.shared_session: + reconnecting: "Нет сети, переподключение..." + + # ── Terminal SSH ── + terminal.ssh: + file_uploads: "Загрузки файлов" + + # ── Оболочка терминала ── + terminal.shell: + bootstrap_slow: "Похоже, ваша оболочка долго запускается... " + incompatible_config: "Ваша конфигурация оболочки несовместима с Warp... " + completions_not_working: "Похоже, ваши автодополнения не работают (" + completions_suffix: "). Включение tmux warpification в " + may_resolve_issue: " может решить эту проблему." + settings: "настройках" + subshell: "Подоболочка" + + # ── Доступность терминала ── + terminal.a11y: + press_right_arrow: "Нажмите стрелку вправо для вставки или продолжайте редактировать для игнорирования" + command_waiting_password: "Команда ожидает ввода пароля" + + # ── Агент терминала ── + terminal.agent: + execute_this_plan: "Выполнить этот план" + fork_from_last_query: "Форкнуть с последнего запроса" + + # ── Окружение терминала ── + terminal.environment: + create_with_repo: "Создать окружение, используя текущую рабочую директорию как репо" + create_without_repo: "Создать окружение без репозиториев" + + # ── Проект терминала ── + terminal.project: + setup: "Настройка проекта" + use_file_picker: "Используйте выбор файла для указания git-репозитория" + + # ── Навыки терминала ── + terminal.skills: + bundled_cannot_edit: "Встроенные навыки нельзя редактировать" + editing_not_supported: "Редактирование навыков не поддерживается в этой сборке" + no_skills: "Навыки не найдены" + + # ── Терминал OSC 52 (буфер обмена) ── + terminal.osc52: + banner_text: "Терминальная программа попыталась получить доступ к буферу обмена. По умолчанию это отключено из соображений безопасности." + allow_button: "Разрешить" + dont_show_again: "Больше не показывать" + write_blocked_text: "Терминальная программа попыталась записать данные в буфер обмена. По умолчанию это отключено из соображений безопасности, для защиты от вредоносного ПО." + read_blocked_text: "Терминальная программа попыталась прочитать буфер обмена. По умолчанию это отключено из соображений безопасности, для защиты от вредоносного ПО." + allow_writes: "Разрешить запись в буфер обмена" + allow_reads_and_writes: "Разрешить чтение и запись буфера обмена" + + # ── Управление агентами ── + agent: + filter: + all: "Все" + working: "В работе" + done: "Готово" + failed: "С ошибкой" + status: "Статус" + source: "Источник" + created: "Создано" + has_artifact: "Есть артефакт" + harness: "Оснастка" + environment: "Окружение" + created_by: "Создатель" + none: "Нет" + pull_request: "Пул-реквест" + plan: "План" + screenshot: "Скриншот" + file: "Файл" + last_24_hours: "Последние 24 часа" + past_3_days: "Последние 3 дня" + last_week: "Последняя неделя" + all.tooltip: "Просмотр ваших задач агентов и всех общих задач команды" + personal: "Личные" + personal.tooltip: "Просмотр задач агентов, созданных вами" + personal: "Личные" + get_started: "Начать" + view_agents: "Посмотреть агентов" + clear_filters: "Сбросить фильтры" + clear_all: "Сбросить всё" + search_placeholder: "Поиск" + new_agent: "Новый агент" + session_expired: "Сессия истекла" + no_session: "Нет доступной сессии" + session_expired.tooltip: "Сессии истекают через неделю и не могут быть открыты." + runs: "Запуски" + loading_cloud_runs: "Загрузка облачных запусков агентов" + loading_agents: "Загрузка агентов..." + no_results: "Нет результатов по вашим фильтрам" + label: "Агент" + executor: "Исполнитель" + cloud_setup: + visit_oz: "Посетить Oz" + notifications: + mark_all_read: "Отметить все как прочитанные" + open_conversation: "Открыть беседу" + no_notifications: "Нет уведомлений" + update_agent: "Обновить агента" + remove_queued_prompt: "Удалить отложенный запрос" + send_now: "Отправить сейчас" + open_in_code_review: "Открыть в код-ревью" + manage_rules: "Управление правилами" + review_changes: "Просмотр изменений" + open_all_in_code_review: "Открыть всё в код-ревью" + dont_show_again: "Больше не показывать" + allow: "Разрешить" + refine: "Уточнить" + take_over: "Взять на себя" + take_control: "Взять управление" + type_your_answer_placeholder: "Введите ответ и нажмите Enter" + footer: + file_explorer: "Проводник файлов" + file_explorer_tooltip: "Открыть проводник файлов" + rich_input: "Расширенный ввод" + rich_input_tooltip: "Открыть расширенный ввод" + hide_rich_input: "Скрыть расширенный ввод" + hide_rich_input_tooltip: "Скрыть расширенный ввод" + coding_agent_settings_tooltip: "Открыть настройки агента" + enable_notifications: "Включить уведомления" + install_plugin_tooltip: "Установите плагин Warp для расширенных уведомлений агента" + notifications_setup_instructions: "Инструкции по настройке уведомлений" + plugin_install_instructions_tooltip: "Просмотр инструкций по установке плагина Warp" + update_warp_plugin: "Обновить плагин Warp" + plugin_update_available_tooltip: "Доступна новая версия плагина Warp" + plugin_update_instructions: "Инструкции по обновлению плагина" + plugin_update_instructions_tooltip: "Просмотр инструкций по обновлению плагина Warp" + remote_control: "/remote-control" + stop_sharing: "Остановить показ" + stop_sharing_tooltip: "Остановить показ" + context_window_tooltip: "Использование контекстного окна" + rewind: "Откатить" + rewind_tooltip: "Откатить до этого блока" + refresh_aws_credentials: "Обновить учётные данные AWS" + configure: "Настроить" + add_rule: "Добавить правило" + edit_rule: "Редактировать правило" + continue_locally: "Продолжить локально" + continue_locally_tooltip: "Форкнуть этот разговор локально" + view_in_oz: "Посмотреть в Oz" + view_in_oz_tooltip: "Посмотреть этот запуск в веб-приложении Oz" + initialize_project: "Инициализировать проект" + delete_rule: "Удалить правило" + delete_profile: "Удалить профиль" + profile_editor: "Редактор профиля" + edit_profile: "Редактировать профиль" + name_label: "Название" + default_profile_name_locked: "Название профиля по умолчанию нельзя изменить." + profile_default: "Профиль по умолчанию" + creator_unknown: "Неизвестно" + copied_branch_name: "Название ветки скопировано" + metadata: + source: "Источник: {name}" + harness: "Оснастка: {name}" + run_time: "Время выполнения: {value}" + credits_used: "Потрачено кредитов: {usage}" + + # ── Resource Center ── + resource_center: + toggle_panel: "Переключить эту панель" + mark_all_read: "Отметить всё как прочитанное" + + # ── Agent Message Bar ── + agent.message_bar: + starting_shell: "Запуск оболочки..." + + # ── Agent Output ── + agent.output: + always_allow_file_access: "Всегда разрешать доступ к файлам для этого репо" + grep_for: "Поиск grep: " + search_files: "Поиск файлов по шаблону: " + + # ── Agent Search Codebase ── + agent.search_codebase: + no_results: "Результатов не найдено" + + # ── Agent Usage ── + agent.usage: + credits_total: "Кредитов потрачено (всего)" + credits_spent: "Кредитов потрачено" + tool_calls: "Вызовов инструментов" + context_window: "Использовано контекстного окна" + files_changed: "Файлов изменено" + diffs_applied: "Диффов применено" + commands_executed: "Команд выполнено" + time_to_first_token: "Время до первого токена" + total_response_time: "Общее время ответа агента" + + # ── Agent Zero State ── + agent.zero_state: + send_prompt: "Отправьте запрос ниже, чтобы начать новую беседу" + new_conversation: "Новая беседа с Oz-агентом"