I've done a few (started my career doing VB6 -> .NET porting). Working Effectively with Legacy Code was the first book I read. Email's in my profile

Team just 'finished' a migration of a 10 year old codebase from Java -> Kotlin (ended up with 100k SLOC). Part-time this took around 1.5 years, which was faster than I expected. Seeing benefits already but it'll take years to see if it was worth the opportunity cost. Also in the middle of a second migration of some aspects of the codebase to Rust/Svelte. Massive and permanent immediate benefits when this goes through.

- Have well-defined rationale, scope, and buy-in.

- Determine how you'll approach bad code that nobody's touched for a long time

- Testing guideline: ensure you 'could' test the code in the 'before' (typically about 15% coverage). If you can't test the 'after', you've probably picked the wrong language

- CI is table-stakes.

- Define the level of interop between old and new code that you want.

- If you can avoid a 'big bang', do so. The burden of a 'big test' beforehand and 'everything is now unstable' afterwards isn't an easy situation to solve (still working on this). Nobody wants to be in the position of 'you did nothing and everything's worse'

- Ensure you keep git blame working between old and new. git bisect is an ideal

- Set up linting immediately for the new project

- Optional: I went down the route of adding AST-based linting for a couple common errors/issues. Tempted to add some more. Didn't get a massive return from this but on balance it was worthwhile, this may be different for other projects.

- Convert and refactor separately (use your judgment on small refactors). When converting, add language-based annotations or standardised comments to areas which need love (tests, important for refactoring, or bad style). You don't want to be in a situation where you've both introduced bugs from the conversion, then introduced bugs from a refactoring in the same commit. Only add 'issue tracker tasks' for major opportunities or structural fixes.

- With the above annotations, you have a quantitative metric of 'first we convert to 100%', then 'get the annotation count to 0'. It keeps focus, and gives microtasks to handle.

- Having 'porting helper functions' is useful: you're going to join strings 100 times, have a helper that works the same way and has similar syntax to the source language. You can use your IDE to inline that function to something more idiomatic when you're done. Type aliases are awesome.

- Avoid lava layers[0]. After a conversion is 'complete', you should be consistent in your use of programming language(s). Aim to split the project if this isn't the case. Also gives a compile time benefit to only use one toolchain

- Consider isolating toolchains within dependencies: I split out a Java -> Rust interop library into a separate dependency: this massively helped onboard new developers as they only needed one download to get the 'core' of the project running.

- You spent X years getting to a stable process with your current code, accept that it'll take time and effort to get to the same level with your new language

- Automate the boring parts (depending on the size of your project): this can both be more fun, and save time with the conversion

[0] http://mikehadlow.blogspot.com/2014/12/the-lava-layer-anti-p...

When I was a King and a Mason -- a Master proven and skilled --
I cleared me ground for a Palace such as a King should build.
I decreed and dug down to my levels. Presently, under the silt,
I came on the wreck of a Palace such as a King had built.

There was no worth in the fashion -- there was no wit in the plan --
Hither and thither, aimless, the ruined footings ran --
Masonry, brute, mishandled, but carven on every stone:
"After me cometh a Builder. Tell him, I too have known."

Swift to my use in my trenches, where my well-planned ground-works grew,
I tumbled his quoins and his ashlars, and cut and reset them anew.
Lime I milled of his marbles; burned it, slacked it, and spread;
Taking and leaving at pleasure the gifts of the humble dead.

Yet I despised not nor gloried; yet, as we wrenched them apart,
I read in the razed foundations the heart of that builder's heart.
As he had risen and pleaded, so did I understand
The form of the dream he had followed in the face of the thing he had planned.

* * * * *

When I was a King and a Mason -- in the open noon of my pride,
They sent me a Word from the Darkness. They whispered and called me aside.
They said -- "The end is forbidden." They said -- "Thy use is fulfilled.
"Thy Palace shall stand as that other's -- the spoil of a King who shall build."

I called my men from my trenches, my quarries, my wharves, and my sheers.
All I had wrought I abandoned to the faith of the faithless years.
Only I cut on the timber -- only I carved on the stone:
"After me cometh a Builder. Tell him, I too have known!"