Skip to content

Instantly share code, notes, and snippets.

@MaximBazarov
Created May 4, 2018 14:56
Show Gist options
  • Save MaximBazarov/9ee0d4c868e933bb1b9a7264ee2e411c to your computer and use it in GitHub Desktop.
Save MaximBazarov/9ee0d4c868e933bb1b9a7264ee2e411c to your computer and use it in GitHub Desktop.
#!/usr/bin/perl
# HOW TO USE:
# script suppose to run as one of the build phases in a Xcode project
# (For example like this: ${SOURCE_ROOT}/scripts/check_strings.pl ${PRODUCT_NAME})
# HOW IT WORKS
# script parse the Localizable file for the specific target (get target name from parameter ${PRODUCT_NAME})
# and .m files in the project (except KDSLocalization.m)
# script generates xcode error messages during a build process
# WHAT SCRIPT TESTS:
# - duplicates in the Localizable file
# - Unused keys in the Localizable file
# - Missed translations (key used in code but doesn't exist in a Localizable file)
# - Format of the I18N command - must be I18N(@"something")
# ADDITIONAL FEATURES
# - script ignores lines staring with "//"
# - script supports plurals (__one, __many)
# - script supports two method names, I18N and I18N_PluralString
# Preparations...
our $project_path_global = $ENV{'PROJECT_DIR'};
die_with_error("Wrong PROJECT_DIR: \"$project_path_global\"") unless -d $project_path_global;
$target_name = "";
$country_code = "en";
# $supported_langs = "en", "cs", "da", "pl", "pt", "pt-BR", "ru", "sv", "de", "zh", "es", "fr", "it", "nl"
our $localizable_file_global = $project_path_global . "/" .$target_name ."/Supporting Files/en.lproj/Localizable.strings";
die_with_error("Wrong localizable file : \"$localizable_file_global\"") unless -e $localizable_file_global;
# Start comparing strings !
compare_strings($project_path_global, $localizable_file_global);
check_wrong_brand_names($localizable_file_global, $target_name);
exit;
# -------------------------------------- main sub
sub compare_strings
{
my ($project_path, $localizable_file) = @_;
# 1. parse strings file and find duplicates in strings file
my %keys_from_strings = %{parse_localizable_file($localizable_file)};
my $count = keys %keys_from_strings;
print "Found $count keys in $localizable_file\n";
# 2. parse code (all .m files in the project dir) and find missed translations
my %keys_from_code = %{parse_source_files($project_path, \%keys_from_strings)};
# 3. find unused keys
my %unused_keys = %keys_from_strings; # take all keys
foreach $key (keys %keys_from_code) {
delete $unused_keys{$key} if exists $keys_from_code{$key}; #remove keys we use in the code from hash
}
foreach $key (keys %unused_keys) { #every key left in the hash - is unused
error($localizable_file_global, $unused_keys{$key}, "Unused key: \"$key\""); # !!!
}
}
# ------------------------------------ Parse source files (*.m) and report missing translation keys
# ------------------------------------ returns hash with keys used in code
sub parse_source_files
{
my $path = @_[0];
my %localized_strings = %{@_[1]};
my %keys_in_code = ();
@files_in_project = `find $path -iname "*.m" -o -iname "*.swift"`;
@m_files = (@files_in_project, @files_in_submodules);
print "Found .m files: " . scalar(@m_files), "in $path folder (including ../submodules) \n";
foreach $file (@m_files) {
chomp($file);
open FILE, "$file";
my $line_number = 0;
while($line= <FILE> ){
$line_number++;
next if $line =~ /^\/\//; # skip lines started with "//"
check_code_conventions($file, $line_number, $line);
#find all I18N(@"bla-bla") or I18N_PluralString(bla-bla);
# while ($line =~ /(I18N|I18N_PluralString)\((.+?)\)/g) {
while ($line =~ /(NSLocalizedString|I18n.pluralString)\((.+?)\)/g) {
$method_name = $1;
$inside_brackets = $2;
if ($method_name eq "I18n.PluralString") {
if ($inside_brackets =~ /^\@?\"(.*?)\"/) {
$i18n_key = $1;
check_missing_translation($file, $line_number, $i18n_key, \%localized_strings);
check_missing_translation($file, $line_number, $i18n_key."__one", \%localized_strings);
check_missing_translation($file, $line_number, $i18n_key."__many", \%localized_strings);
# Add keys to the list of keys found in code
$keys_in_code{$i18n_key} = $line_number;
$keys_in_code{$i18n_key."__one"} = $line_number;
$keys_in_code{$i18n_key."__many"} = $line_number;
}
}
else { # else - method name is I18N
if ($inside_brackets =~ /^\@?\"(.*?)\"$/) {
# correct I18N format
$i18n_key = $1;
check_missing_translation($file, $line_number, $i18n_key, \%localized_strings);
# Add key to the list of keys found in code
$keys_in_code{$i18n_key} = $line_number;
}
else {
# wrong I18N format
error($file, $line_number, "Wrong I18N format ! Must be I18N(@\"something\")."); # !!!
}
}
}
}
close FILE;
}
return \%keys_in_code;
}
sub check_missing_translation
{
my $file = @_[0];
my $line_number = @_[1];
my $i18n_key = @_[2];
my %localized_strings = %{@_[3]};
if (!$localized_strings{$i18n_key}) {
error($file, $line_number, "Missing translation for key \"$i18n_key\""); # !!!
}
}
# ------------------------------------ parse apple strings file (e.g. Localizable.strings) and report duplicates
# get: one parameter - file name
# returns: hash with keys and values
sub parse_localizable_file
{
$filename = shift;
die_with_error("file doesn't exist \"$filename\"") unless -e $filename;
open FILE, $filename or die_with_error ("Couldn't open file: $filename");
my %keys_in_strings = ();
my $line_number = 0;
while ($line= <FILE>) { # using perl variable $_
$line_number++;
next if $line =~ /^\/\//; # skip lines started with "//"
# parce key-value
if ($line =~ /\"([^\"]*?)\"[^\"]*?\"([^\"]*?)\"/ ) {
($key, $value) = ($1, $2);
if ($keys_in_strings{$key}) {
error($filename, $line_number, "key already defined \"$key\""); # !!!
}
$keys_in_strings{$key} = $line_number;
}
}
close FILE;
return \%keys_in_strings;
}
# BACKUP: it works: ## find . -iname \*.m -exec egrep -i "I18N\(.*\)" {} \; -print |wc -l
# BACKUP: convert to the right encoding using system util (don't need anymore with POEditor utf-8)
# BACKUP: system("iconv -f UTF-16LE -t UTF-8 $filename > $filename.utf8");
# BACKUP: $filename_utf8 = "$filename.utf8";
# ---------------------------- Helper
sub warning
{
my ($filename, $line_number, $message) = @_;
print "$filename:$line_number: warning: $message\n";
}
sub error
{
my ($filename, $line_number, $message) = @_;
print "$filename:$line_number: error: $message\n";
}
# Dies with Xcode Error Message
sub die_with_error
{
$message = shift;
error("","", "SCRIPT FATAL ERROR: $message");
die;
}
# print hash for debug
sub print_hash
{
my %hash = %{@_[0]};
my $count = keys %hash;
print "number of keys in hash = $count\n";
print "======================= \n";
foreach $key (keys %hash) {
print "[" . $i++ . "]: ($key) = ($hash{$key})\n";
}
print "======================= \n";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment