Encouraging Code Policies Using Git Hooks
A practical exploration of using Git hooks to enforce code policies, demonstrated through a pre-commit hook that validates translation file consistency in a React i18n project.
I've been working on a project that uses react-i18next to translate my project to different languages. I'd run into an issue where some of the keys in my English translation file weren't matching those in the other translation files.
Now there are multiple solutions available but because this is a hobby project where I am exploring new things, I wanted to explore a bit more of Git beyond what I would use in my general day-to-day. This mis-match of keys gave me the opportunity to finally explore Git Hooks and implement a pre-commit hook that would let me know that I've messed up my translations.
Project Structure and Problem
As stated before, I have a React project that utilizes react-i18next to handle the translations. My translation
files are small JSON files and only contain key text to translate. These files are located in the src/__translations__
directory.
For the purpose of this example, I have:
- English translation json —
en.json - Spanish translation json —
es.json - Initialization script —
initTranslation.ts
en.json
{
"translation": {
"Home": "Home",
"Loading": "Loading"
}
} es.json:
{
"translation": {
"Home": "Casa",
"Loading": "Espera"
}
} initTranslation.ts
import i18n from "i18next";
import {initReactI18next} from "react-i18next";
import * as en from "./en.json";
import * as es from "./es.json";
type translation = {
translation: {
[key: string]: string
}
}
export const initializeTranslation = () => {
i18n
.use(initReactI18next)
.init({
resources: {
en: en as translation,
es: es as translation,
},
fallbackLng: "en",
interpolation: {
escapeValue: false
}
});
} Most of this is just boilerplate from the react-i18next documentation for getting started.
In my App.tsx file, I call the initializeTranslation and I can run the following to
change the language during runtime:
initializeTranslation();
const App = () => {
const {i18n} = useTranslation();
useEffect(() = > {
i18n.changeLanguage("es"); // Change to "en" for English
}, []);
...
} Python Script to Check for Missing Keys
Now if I add a key to the en.json and forget it in the es.json, it's not a big
deal. The translation will fallback to English and it will still render correctly, just in the incorrect
language.
Translation is always something I've seen as something that I don't want to have to do later down the track and if I have the appropriate framework in place, it's a lot easier to fix those small mistakes then implement it entirely and ensure I'm covering it in all the appropriate places. It's not something I'd do by myself usually but I'm also learning languages and this is a good way to assist in that and pick up some slang for webpages that I wouldn't cover in DuoLingo.
If I miss a key, it's not the end of the world basically. I just want to stay on top of it.
Let's say I add a key for "Sign In" to my en.json and forget to add it. I have tests to
cover this but I also want to make sure I have something pre-commit just to say "Hey, you forgot a key
in one of the language files".
Before we explore hooks, I want to write a Python script that does this:
#!/usr/bin/env python3
import os
import json
def check_translation_files(directory):
translation_files = []
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith(".json"):
translation_files.append(os.path.join(root, file))
translations = []
for file in translation_files:
with open(file) as f:
data = json.load(f)
translations.append(data.get("translation", {}))
is_match = True
not_matching = []
for i in range(len(translations) - 1):
if translations[i].keys() != translations[i + 1].keys():
not_matching = list(
set(translations[i].keys()) - set(translations[i + 1].keys()))
is_match = False
if (is_match):
print("✅ All translation files have matching keys")
else:
print("❌ WARNING: Translation files do not have matching keys. Missing keys:",
str(not_matching))
check_translation_files("src/__translations__")
All this does is open the __translations__ directory, picking up all the .json
files. It will compare the english one to the other and then output:
- ✅ All translation files have matching keys — If all keys are present
- ❌ WARNING: Translation files do not have matching keys. Missing: […{list of missing keys}] — If keys do not match
If I run this now, with the above files, it will output that all the keys are present.
It's important to note that the directory that it searches in will be the root directory of the project.
Adding a pre-commit hook
Now that I have a working script, I'm going to copy the code to a file named pre-commit and
store it under the .git/hooks/ directory. This file will have no extension.
That's all there is to it. It will know it's a Python script from the shebang at the top of the file and
every time I run git commit ... it will let me know if I have the same keys in all my
translation files.
Problems
- This works fine for me as a solo developer but working on a team would mean I have to have the developer run some pre-configuration before they start. I don't see this as a bad thing as you could just add a script and some addition to the project README.
- It doesn't enforce any code conventions and just says "Hey, you missed something that might not be too crucial"
Overall, it was a fun, quick exploration into using Git hooks for the first time.