From 53bb18e88b98eadfc27d4fb88d16a1e6a0015808 Mon Sep 17 00:00:00 2001 From: "Vincent A. Cicirello" Date: Thu, 7 Sep 2023 15:46:14 -0400 Subject: [PATCH] Refactored to extract locales from Python dictionaries to JSON files (#221) --- .gitattributes | 1 + CHANGELOG.md | 3 +- README.md | 58 +- src/ColorUtil.py | 91 +- src/PieChart.py | 30 +- src/StatConfig.py | 1335 ++---------------------------- src/Statistician.py | 253 +++--- src/StatsImageGenerator.py | 168 ++-- src/TextLength.py | 31 +- src/UserStatistician.py | 63 +- src/locales/bn.json | 47 ++ src/locales/de.json | 47 ++ src/locales/en.json | 47 ++ src/locales/es.json | 47 ++ src/locales/fr.json | 47 ++ src/locales/hi.json | 47 ++ src/locales/hu.json | 47 ++ src/locales/id.json | 47 ++ src/locales/it.json | 47 ++ src/locales/ja.json | 47 ++ src/locales/ko.json | 47 ++ src/locales/lt.json | 47 ++ src/locales/nl.json | 47 ++ src/locales/no.json | 47 ++ src/locales/or.json | 47 ++ src/locales/pl.json | 47 ++ src/locales/pt.json | 47 ++ src/locales/ro.json | 47 ++ src/locales/ru.json | 47 ++ src/locales/sat.json | 47 ++ src/locales/sr.json | 47 ++ src/locales/th.json | 47 ++ src/locales/tr.json | 47 ++ src/locales/uk.json | 47 ++ tests/tests.py | 54 +- util/refactor-locales-to-json.py | 41 + 36 files changed, 1704 insertions(+), 1552 deletions(-) create mode 100644 src/locales/bn.json create mode 100644 src/locales/de.json create mode 100644 src/locales/en.json create mode 100644 src/locales/es.json create mode 100644 src/locales/fr.json create mode 100644 src/locales/hi.json create mode 100644 src/locales/hu.json create mode 100644 src/locales/id.json create mode 100644 src/locales/it.json create mode 100644 src/locales/ja.json create mode 100644 src/locales/ko.json create mode 100644 src/locales/lt.json create mode 100644 src/locales/nl.json create mode 100644 src/locales/no.json create mode 100644 src/locales/or.json create mode 100644 src/locales/pl.json create mode 100644 src/locales/pt.json create mode 100644 src/locales/ro.json create mode 100644 src/locales/ru.json create mode 100644 src/locales/sat.json create mode 100644 src/locales/sr.json create mode 100644 src/locales/th.json create mode 100644 src/locales/tr.json create mode 100644 src/locales/uk.json create mode 100644 util/refactor-locales-to-json.py diff --git a/.gitattributes b/.gitattributes index dd7fd92c..1c2e378a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ *.graphql linguist-detectable +*.json linguist-detectable quickstart/*.yml linguist-detectable diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ee4ffc..16cd42a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - 2023-09-06 +## [Unreleased] - 2023-09-07 ### Added @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed ### Fixed +* Refactored everything locale related to extract definitions of locales from Python dictionaries to JSON files to ease the process of contributing additional language translations. ### Dependencies diff --git a/README.md b/README.md index 55ca7f7b..cef1c1a0 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,9 @@ The remainder of the documentation is organized into the following sections: * [Blog Posts](#blog-posts): A selection of blog posts about the GitHub Action. * [Support the Project](#support-the-project): Ways that you can support the project. * [Contributors](#contributors): Information for potential contributors. + * [Contributing a Translation](#contributing-a-translation): Detailed instructions + for contributing a language translation for a new locale, the most common type + of contribution to this project. ## Quickstart See the [Quickstart](https://github.com/cicirello/user-statistician/blob/main/quickstart) directory @@ -827,9 +830,9 @@ action will use the default of "en". The following locales are currently support | tr | Turkish | | uk | Ukrainian | -If you are interested in contributing a new locale, only the -[src/StatConfig.py](https://github.com/cicirello/user-statistician/blob/main/src/StatConfig.py) file must be updated. See the comments -within that file for guidance in contributing a locale. +If you are interested in contributing a new locale, see the +section [Contributing a Translation](#contributing-a-translation) +for detailed instructions for doing so. ### `fail-on-error` @@ -989,7 +992,54 @@ If you would like to contribute to the project, please start by reading our [Contributing Guidelines](https://github.com/cicirello/.github/blob/main/CONTRIBUTING.md) and our [Code of Conduct](https://github.com/cicirello/.github/blob/main/CODE_OF_CONDUCT.md). After reading the contributing guidelines and code of conduct, fork the repository, create -a feature branch in your fork, make your changes, and submit a pull request. +a feature branch in your fork, make your changes, and submit a pull request. Please note that +every pull request must be associated with an issue that it addresses. If there is a change +that you would like to propose that doesn't already have an issue, please submit an issue +first. + +### Contributing a Translation + +The most common type of contribution to this project has been language translations. Here is +step-by-step guidance on how to contribute a translation. +* Is there an [open issue](https://github.com/cicirello/user-statistician/issues?q=is%3Aopen) + for the language you want to contribute? If so, and if nobody is assigned to it, comment on + the issue expressing interest, and I will assign you to it (e.g., so we don't have multiple + people working on the same thing). If there isn't an open issue for it, then submit an issue + for a feature request for the language. Then comment on your new issue indicating if you are + interested in contributing the translation (e.g., so I know the difference between just a + request for a language vs a request with offer to contribute it). I'll then assign you to it. +* Fork the repository, and create a branch in your fork for your proposed translation. +* Find the [ISO 639-1 two-character language + code](https://www.loc.gov/standards/iso639-2/php/English_list.php) for the language. Some + languages have both a two-character code and a three-character code in the table at that link. + In those cases, we are going with the two-character code. But there are some languages that + only have a three-character ISO-639-2 code, and in those cases we'll use the three-character + code. For example, one of the currently supported languages is Santali, whose code is `sat`. +* In the [src/StatConfig.py](src/StatConfig.py) file, look for the set `supportedLocales` and + add a string for the locale code alphabetically. +* Next, within the directory [src/locales](src/locales), create a JSON file named with the locale + code (in all lowercase) and file extension `.json`. For example, the English version is in + `en.json`. You might consider starting with a copy of the file for a different language that + you also know, and then editing the various strings. +* Don't change any of the key fields, since those are used internally. +* Translate the `"titleTemplate"`, the first mapping you see in each of the JSON files. The `{0}` + that you see in a title template string is a placeholder for where the user's name will go. The + `"titleTemplate"` is required to have that someplace, or else the test cases will fail. +* Translate the `"categoryLabels"`, which are the headings of each of the sections of the SVG, as + well as the column headings within those sections. The categories that don't have columns should + just have `null` for the column headings as you will see in any of the existing JSON files. +* Translate the `"statLabels"`, which are the labels for each individual statistic on the SVG. +* Run the unit tests locally. The test cases will verify that there are strings associated with + all required keys for all locale codes, although they obviously won't verify that the translations + are correct. To run the unit tests locally, from the root of the repository, + execute: `python -B -m unittest tests/tests.py`. Note that for the tests to include your locale, + as well as for the Action itself to include your locale, the code for it must be in the + `supportedLocales` set within [src/StatConfig.py](src/StatConfig.py). See the earlier step on this. +* Despite what the pull request template indicates, you do not need to update the README to document + the new locale code. We'll do that later at the time a new release is made that includes your new + locale. The reason not to document it at the time of your PR is that all changes to the README from + the default branch go live immediately on GitHub's Marketplace, which may confuse users into + believing the new locale is available before it actually is. ## License diff --git a/src/ColorUtil.py b/src/ColorUtil.py index ecdcae25..d93df6e5 100644 --- a/src/ColorUtil.py +++ b/src/ColorUtil.py @@ -1,6 +1,6 @@ # user-statistician: Github action for generating a user stats card # -# Copyright (c) 2021 Vincent A Cicirello +# Copyright (c) 2021-2023 Vincent A Cicirello # https://www.cicirello.org/ # # MIT License @@ -24,24 +24,25 @@ # SOFTWARE. # -def isValidColor(color) : +def isValidColor(color): """Checks if a color is a valid color. Keyword arguments: - color - The color to check, either in hex or as a named color or as an rgba(). + color - The color to check, either in hex or as + a named color or as an rgba(). """ validHexDigits = set("0123456789abcdefABCDEF") color = color.strip() - if color.startswith("#") : - if len(color) != 4 and len(color) != 7 : + if color.startswith("#"): + if len(color) != 4 and len(color) != 7: return False return all(c in validHexDigits for c in color[1:]) - elif color.startswith("rgba(") : + elif color.startswith("rgba("): return strToRGBA(color) != None - else : + else: return color in _namedColors -def strToRGBA(color) : +def strToRGBA(color): """Converts a str specifying rgba color to r, g, b, and a channels. Returns (r, g, b, a) is valid and otherwise returns None. @@ -49,38 +50,30 @@ def strToRGBA(color) : Keyword arguments: color - a str of the form: rgba(r,g,b,a). """ - if color.startswith("rgba(") : + if color.startswith("rgba("): last = color.find(")") - if last > 5 : + if last > 5: rgba = color[5:last].split(",") - if len(rgba) == 4 : - try : + if len(rgba) == 4: + try: r = int(rgba[0]) g = int(rgba[1]) b = int(rgba[2]) a = float(rgba[3]) - except ValueError : + except ValueError: return None - if r >= 256 : - r = 255 - if g >= 256 : - g = 255 - if b >= 256 : - b = 255 - if r < 0 : - r = 0 - if g < 0 : - g = 0 - if b < 0 : - b = 0 - if a < 0.0 : - a = 0.0 - if a > 1.0 : - a = 1.0 + r = min(r, 255) + g = min(g, 255) + b = min(b, 255) + r = max(r, 0) + g = max(g, 0) + b = max(b, 0) + a = max(a, 0.0) + a = min(a, 1.0) return r, g, b, a return None -def highContrastingColor(color) : +def highContrastingColor(color): """Computes a highly contrasting color. Specifically, maximizes the contrast ratio. Contrast ratio is (L1 + 0.05) / (L2 + 0.05), where L1 and L2 are the luminances @@ -88,17 +81,17 @@ def highContrastingColor(color) : if color is not a valid hex color or named color. Keyword arguments: - color - The color to contrast with, either in hex or as a named color. + color - The color to contrast with, in hex or as a named color. """ L = luminance(color) - if L == None : + if L == None: return None - if (L + 0.05) / 0.05 >= 1.05 / (L + 0.05) : + if (L + 0.05) / 0.05 >= 1.05 / (L + 0.05): return "#000000" # black - else : + else: return "#ffffff" # white -def contrastRatio(c1, c2) : +def contrastRatio(c1, c2): """Computes contrast ratio of a pair of colors. Returns the contrast ratio provided both colors are valid, and otherwise returns None. @@ -109,13 +102,13 @@ def contrastRatio(c1, c2) : """ L1 = luminance(c1) L2 = luminance(c2) - if L1 == None or L2 == None : + if L1 == None or L2 == None: return None - if L1 < L2 : + if L1 < L2: L1, L2 = L2, L1 return (L1 + 0.05) / (L2 + 0.05) -def luminance(color) : +def luminance(color): """Calculates the luminance of a color. Returns None if color is not a valid hex color or named color. @@ -123,22 +116,22 @@ def luminance(color) : color - The color, either in hex or as a named color. """ color = color.strip() - if color.startswith("rgba(") : + if color.startswith("rgba("): rgba = strToRGBA(color) - if rgba != None : + if rgba != None: r, g, b, a = rgba - else : + else: return None - else : - if not color.startswith("#") : - if color not in _namedColors : + else: + if not color.startswith("#"): + if color not in _namedColors: return None color = _namedColors[color] - if len(color) == 4 : + if len(color) == 4: r = color[1] + color[1] g = color[2] + color[2] b = color[3] + color[3] - elif len(color) == 7 : + elif len(color) == 7: r = color[1:3] g = color[3:5] b = color[5:7] @@ -152,14 +145,14 @@ def luminance(color) : b = _sRGBtoLin(b / 255) return 0.2126 * r + 0.7152 * g + 0.0722 * b -def _sRGBtoLin(c) : +def _sRGBtoLin(c): """Transformation from https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_Colors_and_Luminance Keyword arguments: c - A color channel (i.e., r, g, or b) """ - if c <= 0.04045 : + if c <= 0.04045: return c / 12.92 else : return ((c + 0.055)/1.055) ** 2.4 @@ -319,4 +312,4 @@ def _sRGBtoLin(c) : "whitesmoke" : "#f5f5f5", "yellow" : "#ffff00", "yellowgreen" : "#9acd32" - } +} diff --git a/src/PieChart.py b/src/PieChart.py index 0e82e048..931d1a68 100644 --- a/src/PieChart.py +++ b/src/PieChart.py @@ -1,7 +1,7 @@ # # user-statistician: Github action for generating a user stats card # -# Copyright (c) 2021 Vincent A Cicirello +# Copyright (c) 2021-2023 Vincent A Cicirello # https://www.cicirello.org/ # # MIT License @@ -32,7 +32,7 @@ _circleTemplate = '' _animationTemplate = '' -def svgPieChart(wedges, radius, animate, speed, includeSVGHeader=False) : +def svgPieChart(wedges, radius, animate, speed, includeSVGHeader=False): """Generates an SVG of a pie chart. The intention is to include as part of a larger SVG (e.g., it does not insert xmlns into the opening svg tag). If wedges list is empty, it retrurns None. @@ -42,20 +42,22 @@ def svgPieChart(wedges, radius, animate, speed, includeSVGHeader=False) : containing fields color and percentage. radius - the radius, in pixels for the pie chart. animate - Pass True to animate the pie chart. - speed - If animate is True, then this input is the number of seconds for one full rotation. + speed - If animate is True, then this input is the number of + seconds for one full rotation. """ - if includeSVGHeader : + if includeSVGHeader: components = [_headerTemplate.format(str(2*radius))] - else : + else: components = [] - if len(wedges) == 0 : + if len(wedges) == 0: return None - elif len(wedges) == 1 : - components.append(_circleTemplate.format(wedges[0]["color"], str(radius))) - else : + elif len(wedges) == 1: + components.append( + _circleTemplate.format(wedges[0]["color"], str(radius))) + else: startPercentage = 0 - for w in wedges : + for w in wedges: endPercentage = startPercentage + w["percentage"] w["start"] = startPercentage * 2 * math.pi w["end"] = endPercentage * 2 * math.pi @@ -65,10 +67,10 @@ def svgPieChart(wedges, radius, animate, speed, includeSVGHeader=False) : # (i.e., last edge should complete a full circle). wedges[-1]["end"] = 2 * math.pi - if animate : + if animate: components.append("") - for w in wedges : + for w in wedges: components.append( _pathTemplate.format( w["color"], @@ -82,7 +84,7 @@ def svgPieChart(wedges, radius, animate, speed, includeSVGHeader=False) : ) ) - if animate : + if animate: components.append( _animationTemplate.format( radius, @@ -91,6 +93,6 @@ def svgPieChart(wedges, radius, animate, speed, includeSVGHeader=False) : ) components.append("") - if includeSVGHeader : + if includeSVGHeader: components.append("") return "".join(components) diff --git a/src/StatConfig.py b/src/StatConfig.py index 517e1a9c..61b677ec 100644 --- a/src/StatConfig.py +++ b/src/StatConfig.py @@ -1,7 +1,7 @@ # # user-statistician: Github action for generating a user stats card # -# Copyright (c) 2021-2022 Vincent A Cicirello +# Copyright (c) 2021-2023 Vincent A Cicirello # https://www.cicirello.org/ # # MIT License @@ -25,14 +25,41 @@ # SOFTWARE. # +import json -# ADDITIONAL LICENSE NOTES -# -# GitHub's Octicons: -# The paths defining the icons used in the action -# are derived from GitHub's Octicons (https://github.com/primer/octicons), -# and are copyright (c) GitHub, Inc, and licensed by GitHub under -# the MIT license. +# The locale keys are ISO 639-1 two-character language codes +# (see: https://www.loc.gov/standards/iso639-2/php/English_list.php). +# If you are contributing a new locale, please add code in alphabetical +# order below. +supportedLocales = { + "bn", + "de", + "en", + "es", + "fr", + "hi", + "hu", + "id", + "it", + "ja", + "ko", + "lt", + "nl", + "no", + "or", + "pl", + "pt", + "ro", + "ru", + "sat", + "sr", + "th", + "tr", + "uk", +} + +# The default order for the categories of stats on the SVG +categoryOrder = ["general", "repositories", "contributions", "languages"] # Mapping from category key to list of stats keys in the # order they should appear. @@ -66,1261 +93,45 @@ "languages" : [] } -# The default order for the categories of stats on the SVG -categoryOrder = ["general", "repositories", "contributions", "languages"] - - -# Steps to Contributing a New Locale: -# (0) Check if there are any open issues or pull requests -# related to the locale that you want to add. Begin by opening -# an issue indicating the locale that you want to add. Perhaps -# mention in the issue that you are planning to work on it, so -# we know the difference between a simple request with nobody to -# work on it vs a volunteer. Once you've submitted the issue, -# fork the repo, and create a branch for your feature. -# (1) Add a string for the 2-character code to the set -# supportedLocales. -# (2) In the Python dictionary, categoryLabels, create a -# mapping corresponding to that new 2-character string. -# You might start by copying and pasting the entirety of -# the entry for "en". Make sure you keep the dictionary keys -# as they are, and only translate the values. -# (3) In the Python dictionary, titleTemplates, add a template for -# the default title by adding a mapping from the 2-character -# string for the new locale to a format string (see the comments -# where titleTemplates is declared. -# (4) In the Python dictionary, statLabels, each key "label" maps to -# a Python dictionary with the locale code as key. Add a corresponding -# key value pair for the new locale. -# (5) The existing test cases will verify that all of the above -# has been done for each 2 character locale code in the -# supportedLocales set. So no new test cases should be necessary when -# adding a locale, but existing tests must pass. -# (6) If you contribute translations for a new locale, -# or if you correct any errors in one, then please -# credit yourself here by either adding a list below for -# the relevant locale if it is new, or adding your -# GitHub user id to the list below if you contributed a bug -# fix to an existing one. -# -# Locale Contributors: -# en: cicirello -# it: ziriuz84 -# de: pje3110 -# pt: andrefpoliveira -# hi: Anik-Bardhan -# fr: thomasbnt -# ru: JayBee007 -# es: alanverdugo -# pl: Jibendu007 -# tr: Rutujabadve -# ja: a-ayush19 -# bn: ponickkhan -# ko: 5d-jh -# lt: mantasio -# uk: therrance -# ro: donheshanthaka -# nl: lovelacecoding -# th: Slowlife01 -# no: rubjo -# hu: jpacsai -# sat: Prasanta-Hembram -# sr: keen003 -# or: Prasanta-Hembram - -# The locale keys are ISO 639-1 two-character language codes -# (see: https://www.loc.gov/standards/iso639-2/php/English_list.php). -supportedLocales = { "en", "it", "de", "pt", "id", "hi", "fr", "ru", "es", "pl", "tr", "ja", "bn", "ko", "lt", "uk", "ro", "nl", "th", "no", "hu", "sat", "sr", "or"} - - -# Dictionary of header rows for categories of statistics -categoryLabels = { - - "en" : { - "general" : { - "heading" : "General Stats and Info", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repositories", - "column-one" : "Non-Forks", - "column-two" : "All" - }, - "contributions" : { - "heading" : "Contributions", - "column-one" : "Past Year", - "column-two" : "Total" - }, - "languages" : { - "heading" : "Language Distribution in Public Repositories", - "column-one" : None, - "column-two" : None - } - }, - - "it" : { - "general" : { - "heading" : "Statistiche Generali e Informazioni", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repository", - "column-one" : "Non-Fork", - "column-two" : "Tutti" - }, - "contributions" : { - "heading" : "Contributi", - "column-one" : "Anno Scorso", - "column-two" : "Totale" - }, - "languages" : { - "heading" : "Distribuzione del Linguaggio nei Repository Pubblici", - "column-one" : None, - "column-two" : None - } - }, - - "de" : { - "general" : { - "heading" : "Allgemeine Statistiken und Informationen", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repositories", - "column-one" : "Non-Forks", - "column-two" : "Alle" - }, - "contributions" : { - "heading" : "Beiträge", - "column-one" : "Letztes Jahr", - "column-two" : "Gesamt" - }, - "languages" : { - "heading" : "Verteilung der Sprachen in Öffentlichen Repositories", - "column-one" : None, - "column-two" : None - } - }, - - "pt" : { - "general" : { - "heading" : "Estatísticas Gerais e Informações", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repositórios", - "column-one" : "Sem Forks", - "column-two" : "Todos" - }, - "contributions" : { - "heading" : "Contribuições", - "column-one" : "Último ano", - "column-two" : "Total" - }, - "languages" : { - "heading" : "Distribuição de Linguagens em Repositórios Públicos", - "column-one" : None, - "column-two" : None - } - }, - - "id" : { - "general" : { - "heading" : "Info dan Status Umum", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repositori", - "column-one" : "Non Fork", - "column-two" : "Semua" - }, - "contributions" : { - "heading" : "Kontribusi", - "column-one" : "Tahun Lalu", - "column-two" : "Total" - }, - "languages" : { - "heading" : "Distribusi Bahasa dalam Repositori Publik", - "column-one" : None, - "column-two" : None - } - }, - - "hi" : { - "general" : { - "heading" : "साधारण सांख्यिकी और सूचना", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "भंडार", - "column-one" : "गैर-फोर्क", - "column-two" : "सभी" - }, - "contributions" : { - "heading" : "योगदान", - "column-one" : "पिछला वर्ष", - "column-two" : "कुल" - }, - "languages" : { - "heading" : "सार्वजनिक भंडारों में भाषा वितरण", - "column-one" : None, - "column-two" : None - } - }, - - "fr" : { - "general" : { - "heading" : "Statistiques Générales et Info", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Dépôts", - "column-one" : "Non clonés", - "column-two" : "Tout" - }, - "contributions" : { - "heading" : "Contributions", - "column-one" : "Dernière année", - "column-two" : "Total" - }, - "languages" : { - "heading" : "Répartition des langages dans les dépôts publiques", - "column-one" : None, - "column-two" : None - } - }, - - "ru" : { - "general" : { - "heading" : "Общая статистика и информация", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Статистика репозиториев", - "column-one" : "Без форков", - "column-two" : "Все" - }, - "contributions" : { - "heading" : "Работа в репозиториях", - "column-one" : "За последний год", - "column-two" : "За все время" - }, - "languages" : { - "heading" : "Использование языков в общедоступных репозиториях", - "column-one" : None, - "column-two" : None - } - }, - - "es" : { - "general" : { - "heading" : "Estadísticas generales e información", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repositorios", - "column-one" : "No bifurcados", - "column-two" : "Todos" - }, - "contributions" : { - "heading" : "Contribuciones", - "column-one" : "Año pasado", - "column-two" : "Total" - }, - "languages" : { - "heading" : "Distribución de lenguajes en repositorios públicos", - "column-one" : None, - "column-two" : None - } - }, - - "pl" : { - "general" : { - "heading" : "Ogólne statystyki i informacje", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repozytoria", - "column-one" : "Non-Forks", - "column-two" : "Wszystkie" - }, - "contributions" : { - "heading" : "Kontrybucje", - "column-one" : "Ostatni rok", - "column-two" : "Wszystkie" - }, - "languages" : { - "heading" : "Rozkład języków w Repozytoriach Publicznych", - "column-one" : None, - "column-two" : None - } - }, - - "tr" : { - "general" : { - "heading" : "Genel İstatistikler ve Bilgiler", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Depolar", - "column-one" : "Çatalsız", - "column-two" : "Tüm" - }, - "contributions" : { - "heading" : "Katkılar", - "column-one" : "Geçen sene", - "column-two" : "Total" - }, - "languages" : { - "heading" : "Genel Depolarda Dil Dağılımı", - "column-one" : None, - "column-two" : None - } - }, - - "ja" : { - "general" : { - "heading" : "一般的な統計と情報", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "リポジトリ", - "column-one" : "非フォーク", - "column-two" : "全て" - }, - "contributions" : { - "heading" : "貢献", - "column-one" : "昨年", - "column-two" : "合計" - }, - "languages" : { - "heading" : "公開リポジトリでの言語配布", - "column-one" : None, - "column-two" : None - } - }, - - "bn" : { - "general" : { - "heading" : "সাধারণ পরিসংখ্যান এবং তথ্য", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "সংগ্রহস্থল", - "column-one" : "অ-কাঁটা", - "column-two" : "সব" - }, - "contributions" : { - "heading" : "অবদানসমূহ", - "column-one" : "বিগত বছর", - "column-two" : "মোট" - }, - "languages" : { - "heading" : "প্রকাশ্য ভান্ডারে ভাষা বিতরণ", - "column-one" : None, - "column-two" : None - } - }, - - "ko" : { - "general" : { - "heading" : "통계 및 정보", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "저장소", - "column-one" : "직접 만든(Non-Forks)", - "column-two" : "모두" - }, - "contributions" : { - "heading" : "기여", - "column-one" : "지난해", - "column-two" : "총" - }, - "languages" : { - "heading" : "공개 저장소 사용 언어 분포", - "column-one" : None, - "column-two" : None - } - }, - - "lt" : { - "general" : { - "heading" : "Bendra statistika ir informacija", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repozitorijos", - "column-one" : "Neklonuotos", - "column-two" : "Visos" - }, - "contributions" : { - "heading" : "Įnašai", - "column-one" : "Praeitais metais", - "column-two" : "Viso" - }, - "languages" : { - "heading" : "Kalbu pasiskirstymas viešosiose repozitorijose", - "column-one" : None, - "column-two" : None - } - }, - - "uk": { - "general": { - "heading": "Загальна статистика та інформація", - "column-one": None, - "column-two": None - }, - "repositories": { - "heading": "Репозиторіїв", - "column-one": "Без форків", - "column-two": "Всі" - }, - "contributions": { - "heading": "Внески", - "column-one": "За останній рік", - "column-two": "Всього" - }, - "languages": { - "heading": "Використання мов у загальнодоступних репозиторіях", - "column-one": None, - "column-two": None - } - }, - - "ro" : { - "general" : { - "heading" : "Statistici generale și informații", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Depozitele", - "column-one" : "Non-bifurcatii", - "column-two" : "Toate" - }, - "contributions" : { - "heading" : "Contribuții", - "column-one" : "Anul trecut", - "column-two" : "Total" - }, - "languages" : { - "heading" : "Distribuția limbii în arhivele publice", - "column-one" : None, - "column-two" : None - } - }, - - "nl" : { - "general" : { - "heading" : "Algemene statistieken en info", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repositories", - "column-one" : "Non-Forks", - "column-two" : "Alles" - }, - "contributions" : { - "heading" : "Bijdragen", - "column-one" : "Dit jaar", - "column-two" : "Totaal" - }, - "languages" : { - "heading" : "Talen distributies in Publieke Repositories", - "column-one" : None, - "column-two" : None - } - }, - - "th" : { - "general" : { - "heading" : "สถิติและข้อมูลทั่วไป", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repositories", - "column-one" : "ที่ไม่ใช่ Fork", - "column-two" : "รวมทั้งหมด" - }, - "contributions" : { - "heading" : "Contributions", - "column-one" : "ปีที่แล้ว", - "column-two" : "รวมทั้งหมด" - }, - "languages" : { - "heading" : "ภาษาที่ใช้ใน Repo สาธารณะ", - "column-one" : None, - "column-two" : None - } - }, - - "no" : { - "general" : { - "heading" : "Generell statistikk og info", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Kodebaser", - "column-one" : "Ikke-forgreninger", - "column-two" : "Alle" - }, - "contributions" : { - "heading" : "Bidrag", - "column-one" : "Forrige år", - "column-two" : "Totalt" - }, - "languages" : { - "heading" : "Språkdistribusjon i offentlige kodebaser", - "column-one" : None, - "column-two" : None - } - }, - "hu" : { - "general" : { - "heading" : "Általános statisztika és információ", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repository-k", - "column-one" : "Non-Fork-ok", - "column-two" : "Mind" - }, - "contributions" : { - "heading" : "Kontribúciók", - "column-one" : "Elmúlt év", - "column-two" : "Összesen" - }, - "languages" : { - "heading" : "Nyelvek eloszlása nyilvános repository-kban", - "column-one" : None, - "column-two" : None - } - }, - - "sat" : { - "general" : { - "heading" : "ᱥᱟᱫᱷᱟᱨᱚᱬ ᱵᱟᱛᱟᱣ ᱟᱨ ᱵᱤᱵᱨᱚᱬ", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "ᱜᱩᱫᱟᱢ", - "column-one" : "ᱵᱤᱱ ᱯᱷᱚᱨᱠ ᱠᱚ", - "column-two" : "ᱢᱩᱴ" - }, - "contributions" : { - "heading" : "ᱮᱱᱮᱢᱤᱭᱟᱹᱠᱚ", - "column-one" : "ᱪᱟᱞᱟᱣᱮᱱ ᱥᱮᱨᱢᱟᱸ", - "column-two" : "ᱢᱩᱴ" - }, - "languages" : { - "heading" : "ᱥᱟᱱᱟᱢ ᱜᱩᱫᱟᱢ ᱨᱮ ᱯᱟᱹᱨᱥᱤ ᱠᱚᱣᱟᱜ ᱯᱟᱥᱱᱟᱣ", - "column-one" : None, - "column-two" : None - } - }, - - "sr" : { - "general" : { - "heading" : "Opšta statistika i informacije", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "Repozitoriji", - "column-one" : "Ne-forkovani", - "column-two" : "Svi" - }, - "contributions" : { - "heading" : "Doprinosi", - "column-one" : "Prošla godina", - "column-two" : "Ukupno" - }, - "languages" : { - "heading" : "Zastupljenost jezika u javnim repozitorijima", - "column-one" : None, - "column-two" : None - } - }, - "or" : { - "general" : { - "heading" : "ସାଧାରଣ ପରିସଂଖ୍ୟାନ ଏବଂ ସୂଚନା", - "column-one" : None, - "column-two" : None - }, - "repositories" : { - "heading" : "ସଂଗ୍ରହାଳୟ", - "column-one" : "ଅଣ-ଫର୍କସ୍", - "column-two" : "ସମସ୍ତ" - }, - "contributions" : { - "heading" : "ଅବଦାନ", - "column-one" : "ବିଗତ ବର୍ଷ", - "column-two" : "ମୋଟ" - }, - "languages" : { - "heading" : "ସର୍ବସାଧାରଣ ସଂଗ୍ରହାଳୟରେ ଭାଷା ବଣ୍ଟନ", - "column-one" : None, - "column-two" : None - } - } -} - -# Dictionary of default title templates. -# {0} corresponds to repository owner's name. -titleTemplates = { - "en" : "{0}'s GitHub Activity", - "it" : "Attività GitHub di {0}", - "de" : "{0}s GitHub Aktivität", - "pt" : "Atividade de {0} no GitHub", - "id" : "Aktivitas Github {0}", - "hi" : "{0} की गिटहब गतिविधि", - "fr" : "Activité GitHub de {0}", - "ru" : "Активность пользователя {0} на гитхабе", - "es" : "Actividad en GitHub de {0}", - "pl" : "Aktywność {0} na GitHubie", - "tr" : "{0}'in GitHub Etkinliği", - "ja" : "{0}のgithubアクティビティ", - "bn" : "{0} এর গিটহাব কার্যকলাপ", - "ko" : "{0}의 GitHub 활동", - "lt" : "{0} aktyvumas GitHub", - "uk": "{0} активностей на GitHub", - "ro": "Activitatea GitHub a lui {0}", - "nl": "{0}'s GitHub activiteiten", - "th": "กิจกรรมของ {0} บน GitHub", - "no": "{0}s GitHub-aktivitet", - "hu" : "{0} GitHub aktivitása", - "sat" : "{0}ᱟᱜ ᱜᱤᱴᱦᱚᱵᱽ ᱠᱟᱹᱢᱤᱦᱚᱨᱟᱠᱚ", - # Serbian is the same as Russian in this regard - # but I decided to format it as: - # "Firstname Lastname - GitHub activity" - # similar could be applied to Russian and related languages - "sr" : "{0} - Aktivnost na Githabu", - "or" : "{0}ର GitHub କାର୍ଯ୍ୟକଳାପ", -} - -# Dictionary of icon paths and labels for the supported statistics. -statLabels = { - - "joined" : { - "icon" : '', - "label" : { - "en" : "Year Joined", - "it" : "Anno di Iscrizione", - "de" : "Beitrittsdatum", - "pt" : "Ano de Inscrição", - "id" : "Tahun Bergabung", - "hi" : "युक्त होने का वर्ष", - "fr" : "Année d'adhésion", - "ru" : "Год регистрации на гитхабе", - "es" : "Año de ingreso", - "pl" : "Rok Dołączenia", - "tr" : "Katıldığı Yıl", - "ja" : "入社年", - "bn" : "যোগদানের বছর", - "ko" : "가입 년도", - "lt" : "Prisijungimo metai", - "uk": "Рік приєднання", - "ro": "An alăturat", - "nl": "Jaar van aanmelding", - "th": "ปีที่เข้าร่วม", - "no" : "Ble med i år", - "hu": "Csatlakozás éve", - "sat": "ᱥᱮᱞᱮᱫ ᱥᱮᱨᱢᱟᱸ", - "sr" : "Godina pristupa", - "or" : "ବର୍ଷ ଯୋଗଦାନ", - } - }, - - "featured" : { - "icon" : '', - "label" : { - "en" : "Featured Repo", - "it" : "Repo in Primo Piano", - "de" : "Vorgestelltes Repo", - "pt" : "Repositório em Primeiro Plano", - "id" : "Repositori Unggulan", - "hi" : "विशेष रुप से प्रदर्शित भंडार", - "fr" : "Dépôt en vedette", - "ru" : "Избранное репо", - "es" : "Repositorio destacado", - "pl" : "Polecane repozytorium", - "tr" : "Öne Çıkan Repo", - "ja" : "注目のリポジトリ", - "bn" : "বৈশিষ্ট্যযুক্ত রেপো", - "ko" : "추천 저장소", - "lt" : "Siūloma repozitorija", - "uk": "Вибрані ререпозиторії", - "ro": "Repo recomandate", - "th": "Repo ที่โดดเด่น", - "nl" : "Uitgelichte repository", - "no" : "Framhevet kodebase", - "hu": "Kiemelt repo", - "sat": "ᱵᱤᱥᱮᱥ ᱜᱩᱫᱟᱢ", - "sr" : "Izabrani repozitorij", - "or" : "ବୈଶିଷ୍ଟ୍ୟ ରେପୋ", - } - }, - - "mostStarred" : { - "icon" : '', - "label" : { - "en" : "Most Starred Repo", - "it" : "Repo con più Stelle", - "de" : "Meistmarkiertes Repo", - "pt" : "Repositório com mais estrelas", - "id" : "Repositori dengan Bintang Terbanyak", - "hi" : "सर्वाधिक तारांकित भंडार", - "fr" : "Dépôt le plus étoilé", - "ru" : "Самое популярное репо", - "es" : "Repositorio con más estrellas", - "pl" : "Repozytoria z największą ilością gwiazdek", - "tr" : "En Çok Yıldızlı Repo", - "ja" : "最もスター付きのリポジトリ", - "bn" : "সর্বাধিক তারকা প্রাপ্ত রেপো", - "ko" : "Star를 가장 많이 받은 저장소", - "lt" : "Labiausiai pažymėta repozitorija", - "uk": "Найпопулярніший репозиторій", - "ro": "Cel mai marcat Repo", - "th": "Repo ที่ติดดาวมากที่สุด", - "nl" : "Repository met meeste sterren", - "no" : "Kodebase med flest stjerner", - "hu" : "Legtöbbet csillagozott repo", - "sat": "ᱡᱷᱚᱛᱚ ᱠᱷᱚᱱ ᱰᱷᱮᱨ ᱪᱤᱱᱦᱟᱹ ᱦᱟᱜ ᱜᱩᱫᱟᱹᱢ", - "sr" : "Najviše zvezdica na repou", - "or" : "ସର୍ବାଧିକ ତାରକା ରେପୋ", - } - }, - - "mostForked" : { - "icon" : '', - "label" : { - "en" : "Most Forked Repo", - "it" : "Repo con più Fork", - "de" : "Meistgeforktes Repo", - "pt" : "Repositório mais bifurcado", - "id" : "Repositori dengan Fork Terbanyak", - "hi" : "सर्वाधिक फोर्क भंडार", - "fr" : "Dépôt le plus cloné", - "ru" : "Самое клонированное репо", - "es" : "Repositorio más bifurcado", - "pl" : "Najczęściej Forkowane Repozytoria", - "tr" : "En Çatallı Repo", - "ja" : "最もフォークされたリポジトリ", - "bn" : "সর্বাধিক ফর্কড রেপো", - "ko" : "Fork가 가장 많이된 저장소", - "lt" : "Labiausiai klonuota repozitorija", - "uk": "Найбільш клонований репозиторій", - "ro": "Repo cel mai bifurcat", - "th": "Repo ที่มีการ Fork มากที่สุด", - "nl" : "Repository met meeste forks", - "no" : "Kodebase med flest forgreninger", - "hu" : "Legtöbbet fork-olt repo", - "sat" : "ᱡᱟᱹᱥᱛᱤ ᱱᱚᱠᱚᱞ ᱠᱟᱱ ᱜᱚᱫᱟᱢ", - "sr" : "Najviše forkovan repo", - "or" : "ଅଧିକାଂଶ ଫୋର୍କଡ୍ ରେପୋ", - } - }, - - "followers" : { - "icon" : '', - "label" : { - "en" : "Followers", - "it" : "Seguaci", - "de" : "Follower", - "pt" : "Seguidores", - "id" : "Pengikut", - "hi" : "समर्थक", - "fr" : "Abonnés", - "ru" : "Подписчики", - "es" : "Seguidores", - "pl" : "Obserwujący", - "tr" : "Takipçiler", - "ja" : "フォロワー", - "bn" : "অনুসারী", - "ko" : "팔로워", - "lt" : "Sekėjai", - "uk": "Підписники", - "ro": "Urmaritori", - "th": "ผู้ติดตาม", - "nl" : "Volgers", - "no" : "Følgere", - "hu" : "Követői", - "sat" : "ᱯᱟᱧᱡᱟ ᱠᱩᱜ", - "sr" : "Pratilaca", - "or" : "ଅନୁସରଣକାରୀ", - } - }, +_locale_directory = "/locales/" - "following" : { - "icon" : '', - "label" : { - "en" : "Following", - "it" : "Seguendo", - "de" : "Folgt", - "pt" : "A seguir", - "id" : "Mengikuti", - "hi" : "अनुगामी", - "fr" : "Abonnements", - "ru" : "Подписан", - "es" : "Siguiendo", - "pl" : "Obserwowani", - "tr" : "Takip etmek", - "ja" : "続く", - "bn" : "অনুসরণ করছে", - "ko" : "팔로잉", - "lt" : "Sekama", - "uk": "Підписки", - "ro": "Ca urmare a", - "th": "กำลังติดตาม", - "nl" : "Volgend", - "no" : "Følger", - "hu" : "Követi", - "sat" : "ᱯᱟᱧᱡᱟ ᱮᱫᱟᱢ", - "sr" : "Prati", - "or" : "ନିମ୍ନଲିଖିତ", - } - }, +def loadLocale(locale) : + """Loads the specified locale. - "sponsors" : { - "icon" : '', - "label" : { - "en" : "Sponsors", - "it" : "Sponsors", - "de" : "Sponsoren", - "pt" : "Patrocinado", - "id" : "Sponsor", - "hi" : "प्रायोजक", - "fr" : "Sponsors", - "ru" : "Спонсоры", - "es" : "Patrocinadores", - "pl" : "Sponsorzy", - "tr" : "Sponsorlar", - "ja" : "スポンサー", - "bn" : "পৃষ্ঠপোষক", - "ko" : "후원받은", - "lt" : "Remėjai", - "uk": "Спонсори", - "ro": "Sponsori", - "th": "ผู้สนับสนุน", - "nl" : "Sponsoren", - "no" : "Sponsorer", - "hu" : "Szponzorok", - "sat" : "ᱨᱚᱠᱚᱢᱤᱭᱟᱹ", - "sr" : "Sponzori", - "or" : "ପ୍ରଯୋଜକ", - } - }, + Keyword arguments: + locale - The locale code to load. + """ + with open(_locale_directory + locale + ".json", "r", encoding="utf8") as f: + return json.load(f) - "sponsoring" : { - "icon" : '', - "label" : { - "en" : "Sponsoring", - "it" : "Sponsorizza", - "de" : "Sponsoring", - "pt" : "A patrocinar", - "id" : "Mensponsori", - "hi" : "प्रायोजन", - "fr" : "Sponsorise", - "ru" : "Спонсирует", - "es" : "Patrocinando", - "pl" : "Sponsoring", - "tr" : "Sponsorluk", - "ja" : "主催", - "bn" : "পৃষ্ঠপোষকতা", - "ko" : "후원하는", - "lt" : "Remiama", - "uk": "Спонсорство", - "ro": "Sponsorizare", - "th": "กำลังสนับสนุน", - "nl" : "Gesponsord", - "no" : "Sponser", - "hu" : "Szponzorál", - "sat" : "ᱨᱚᱠᱚᱢᱚᱜ ᱠᱟᱱᱟ", - "sr" : "Sponzoriše", - "or" : "ପ୍ରାୟୋଜକ", - } - }, - - "public" : { - "icon" : '', - "label" : { - "en" : "My Repositories", - "it" : "Repository di Proprietà", - "de" : "Eigene Repositories", - "pt" : "Repositórios Possuídos", - "id" : "Repositori yang Dimiliki", - "hi" : "अपना भंडार", - "fr" : "Dépôts possédés", - "ru" : "Собственные репозитории", - "es" : "Repositorios propios", - "pl" : "Posiadane Repozytoria", - "tr" : "Sahip Olunan Depolar", - "ja" : "所有リポジトリ", - "bn" : "ভাণ্ডার মালিকানাধীন", - "ko" : "보유한 저장소", - "lt" : "Priklausančios repozitorijos", - "uk": "Власні репозиторії", - "ro": "Arhivele mele", - "th": "Repo ทั้งหมดของฉัน", - "nl" : "Mijn Repositories", - "no" : "Mine kodebaser", - "hu" : "Saját repository-k", - "sat" : "ᱤᱧᱟᱜ ᱜᱩᱫᱟᱢ ᱠᱚ", - "sr" : "Lični repozitoriji", - "or" : "ମୋର ସଂଗ୍ରହାଳୟ", - } - }, - - "starredBy" : { - "icon" : '', - "label" : { - "en" : "Starred By", - "it" : "Stellato Da", - "de" : "Markiert Von", - "pt" : "Com Estrela De", - "id" : "Diberikan bintang oleh", - "hi" : "किसके द्वारा तारांकित", - "fr" : "Étoilé par", - "ru" : "Добавили в избранное", - "es" : "Con estrella por", - "pl" : "Polubione przez", - "tr" : "Tarafından yıldız", - "ja" : "主演", - "bn" : "তারকা প্রদান করেছে", - "ko" : "받은 Star", - "lt" : "Pažymėta", - "uk": "Відмітили", - "ro": "Înscris de", - "th": "ติดดาวทั้งหมด", - "nl" : "Ster gegeven door", - "no" : "Stjernemerket av", - "hu" : "Csillagozta", - "sat" : "ᱪᱤᱱᱦᱟᱹᱤᱭᱟᱹ", - "sr" : "Dodeljenih zvezdica", - "or" : "ଷ୍ଟାର୍ ହୋଇଥିବା", - } - }, - - "forkedBy" : { - "icon" : '', - "label" : { - "en" : "Forked By", - "it" : "Forkato Da", - "de" : "Geforkt Von", - "pt" : "Bifurcado Por", - "id" : "Di-fork oleh", - "hi" : "किसके द्वारा फोर्क किया गया", - "fr" : "Cloné par", - "ru" : "Клонирован", - "es" : "Bifurcado por", - "pl" : "Sforkowane przez", - "tr" : "Tarafından çatallandı", - "ja" : "によるフォーク", - "bn" : "ফোর্ক করেছে", - "ko" : "Fork된 횟수", - "lt" : "Klonuota", - "uk": "Клонували", - "ro": "Bifurcat de", - "th": "มีการ Fork ทั้งหมด", - "nl" : "Geforkt door", - "no" : "Forgrenet av", - "hu" : "Forkolta", - "sat" : "ᱱᱚᱠᱚᱞᱤᱭᱟᱹ", - "sr" : "Broj forkovanja", - "or" : "ଦ୍ୱାରା କଣ୍ଟା ହୋଇଛି", - } - }, - - "watchedBy" : { - "icon" : '', - "label" : { - "en" : "Watched By", - "it" : "Seguito Da", - "de" : "Verfolgt Von", - "pt" : "Visto Por", - "id" : "Dilihat oleh", - "hi" : "किसके द्वारा देखा गया", - "fr" : "Regardé par", - "ru" : "Наблюдатели", - "es" : "Visto por", - "pl" : "Obserwowane przez", - "tr" : "İzleyen", - "ja" : "によって見られた", - "bn" : "দেখেছেন", - "ko" : "Watch된 횟수", - "lt" : "Stebima", - "uk": "Підписники", - "ro": "Vizionat de", - "th": "ผู้ติดตาม", - "nl" : "Gevolgd door", - "no" : "Overvåket av", - "hu" : "Figyeli", - "sat" : "ᱛᱤᱱᱹᱜ ᱠᱚ ᱧᱮᱞ ᱠᱟᱫᱟ", - "sr" : "Pregledi", - "or" : "ଦେଖିଲା", - } - }, - - "templates" : { - "icon" : '', - "label" : { - "en" : "Templates", - "it" : "Modelli", - "de" : "Vorlagen", - "pt" : "Modelos", - "id" : "Template", - "hi" : "आकार पट्ट", - "fr" : "Modèles", - "ru" : "Шаблоны", - "es" : "Plantillas", - "pl" : "Szablony", - "tr" : "Sablonlar", - "ja" : "レンプレート", - "bn" : "টেমপ্লেট সমুহ", - "ko" : "템플릿", - "lt" : "Šablonai", - "uk": "Шаблони", - "ro": "Șabloane", - "th": "เทมเพลตแม่แบบ", - "nl" : "Sjablonen", - "no" : "Maler", - "hu" : "Sablonok", - "sat" : "ᱪᱷᱟᱸᱪᱠᱚ", - "sr" : "Šabloni", - "or" : "ଟେମ୍ପଲେଟ୍", - } - }, - - "archived" : { - "icon" : '', - "label" : { - "en" : "Archived", - "it" : "Archiviato", - "de" : "Archiviert", - "pt" : "Arquivados", - "id" : "Diarsipkan", - "hi" : "संग्रहीत", - "fr" : "Archivé", - "ru" : "Заархивировано", - "es" : "Archivado", - "pl" : "Zarchiwizowane", - "tr" : "Arşivlenmiş", - "ja" : "記録", - "bn" : "সংরক্ষণাগারভুক্ত", - "ko" : "보관 처리된(Archived) 저장소", - "lt" : "Archyvuota", - "uk": "Заархівовано", - "ro": "Arhivat", - "th": "เก็บถาวร", - "nl" : "Gearchiveerd", - "no" : "Arkivert", - "hu" : "Archiválva", - "sat" : "ᱜᱟᱵᱟᱱᱮᱱᱟ", - "sr" : "Arhive", - "or" : "ସଂଗୃହିତ", - } - }, - - "commits" : { - "icon" : '', - "label" : { - "en" : "Commits", - "it" : "Commits", - "de" : "Commits", - "pt" : "Commits", - "id" : "Commits", - "hi" : "प्रतिबद्ध", - "fr" : "Commits", - "ru" : "Коммиты", - "es" : "Commits", - "pl" : "Commity", - "tr" : "Taahhütler", - "ja" : "専念", - "bn" : "কমিট করে", - "ko" : "커밋", - "lt" : "Commits", - "uk": "Комміти", - "ro": "Commits", - "th": "คอมมิท", - "nl" : "Commits", - "no" : "Commits", - "hu" : "Commitok", - "sat" : "ᱰᱟᱞᱟᱣᱠᱚ", - "sr" : "Komiti", - "or" : "ପ୍ରତିବଦ୍ଧତା", - } - }, - - "issues" : { - "icon" : '', - "label" : { - "en" : "Issues", - "it" : "Problemi", - "de" : "Issues", - "pt" : "Problemas", - "id" : "Isu", - "hi" : "मुद्दे", - "fr" : "Issues", - "ru" : "Проблемы", - "es" : "Problemas", - "pl" : "Problemy", - "tr" : "Sorunlar", - "ja" : "問題", - "bn" : "ইস্যু", - "ko" : "이슈", - "lt" : "Problemos", - "uk": "Проблеми", - "ro": "Probleme", - "th": "ปัญหา", - "nl" : "Problemen", - "no" : "Saker", - "hu" : "Issue-k", - "sat" : "ᱯᱚᱞᱚᱡᱽᱠᱚ", - "sr" : "Problemi", - "or" : "ସମସ୍ୟାଗୁଡିକ", - } - }, - - "prs" : { - "icon" : '', - "label" : { - "en" : "Pull Requests", - "it" : "Richieste di Pull", - "de" : "Pull Requests", - "pt" : "Pull Requests", - "id" : "Pull Requests", - "hi" : "अनुरोध", - "fr" : "Pull Requests", - "ru" : "Пулл реквесты", - "es" : "Pull Requests", - "pl" : "Pull Requesty", - "tr" : "Çekme İstekleri", - "ja" : "プルリクエスト", - "bn" : "অনুরোধগুলি টানুন", - "ko" : "풀 리퀘스트", - "lt" : "Pull Prašymai", - "uk": "Пулл реквести", - "ro": "Solicitări de tragere", - "th": "Pull Requests", - "nl" : "Pull Requests", - "no" : "Pull Requests", - "hu" : "Pull request-ek", - "sat" : "ᱚᱨ ᱱᱮᱦᱚᱨᱠᱚ", - "sr" : "Pul zahtevi", - "or" : "ଅନୁରୋଧ ଟାଣନ୍ତୁ", - } - }, - - "reviews" : { - "icon" : '', - "label" : { - "en" : "Pull Request Reviews", - "it" : "Revisioni di Richieste di Pull", - "de" : "Überprüfungen von Pull Requests", - "pt" : "Avaliação de Pull Requests", - "id" : "Ulasan Pull Request", - "hi" : "अनुरोध समीक्षा", - "fr" : "Révision de Pull Request", - "ru" : "Ревью пулл реквестов", - "es" : "Revisiones de Pull Requests", - "pl" : "Recenzje Pull Requestów", - "tr" : "İstek İncelemelerini Çekin", - "ja" : "プルリクエストレビュー", - "bn" : "অনুরোধ টানার পর্যালোচনাগুলি", - "ko" : "리뷰", - "lt" : "Pull prašymų peržiūros", - "uk": "Огляди пулл реквестів", - "ro": "Recenzii Pull Request", - "th": "รีวิว Pull Request", - "nl" : "Pull Request Recensies", - "no" : "Pull Request-vurderinger", - "hu" : "Pull request review-k", - "sat" : "ᱚᱨ ᱱᱮᱦᱚᱨ ᱧᱮᱞᱯᱚᱨᱚᱠᱷ ᱠᱚ", - "sr" : "Revizije pul zahteva", - "or" : "ଅନୁରୋଧ ସମୀକ୍ଷାଗୁଡିକ ଟାଣନ୍ତୁ", - } - }, - - "contribTo" : { - "icon" : '', - "totalIsLowerBound" : False, - "label" : { - "en" : "Contributed To", - "it" : "Contribuito A", - "de" : "Beigetragen Zu", - "pt" : "Contribuiu Para", - "id" : "Berkontribusi Ke", - "hi" : "योगदान", - "fr" : "Contribué à", - "ru" : "Участие в", - "es" : "Contribuido a", - "pl" : "Kontrybuował Do", - "tr" : "Katkıda Bulunanlar", - "ja" : "に貢献しました", - "bn" : "অবদান", - "ko" : "기여 횟수", - "lt" : "Prisidėjo prie", - "uk": "Участь в", - "ro": "Contribuit la", - "th": "มีการช่วยไปแล้ว", - "nl" : "Bijgedragen aan", - "no" : "Bidro til", - "hu" : "Kontribútolt", - "sat" : "ᱮᱱᱮᱢ", - "sr" : "Doprinosi", - "or" : "ଯୋଗଦାନ", - } - }, - - "private" : { - "icon" : '', - "label" : { - "en" : "Private Contributions", - "it" : "Contributi Privati", - "de" : "Private Beiträge", - "pt" : "Contribuições Privadas", - "id" : "Kontribusi Pribadi", - "hi" : "गुप्त योगदान", - "fr" : "Contributions privées", - "ru" : "Приватные изменения", - "es" : "Contribuciones privadas", - "pl" : "Prywatne Kontrybucje", - "tr" : "Özel Katkılar", - "ja" : "個人的な貢献", - "bn" : "ব্যক্তিগত অবদান", - "ko" : "비공개", - "lt" : "Privatūs įnašai", - "uk": "Приватна участь", - "ro": "Contribuții private", - "th": "Contributions ส่วนตัว", - "nl": "Prive Bijdragen", - "no" : "Private bidrag", - "hu" : "Privát kontribúciók", - "sat" : "ᱱᱤᱡᱚᱨᱟᱜ ᱩᱠᱩ ᱮᱱᱮᱢᱠᱚ", - "sr" : "Privatni doprinosi", - "or" : "ବ୍ୟକ୍ତିଗତ ଅବଦାନ", - } - } +# ADDITIONAL LICENSE NOTES +# +# GitHub's Octicons: +# The paths defining the icons used in the action, as specified in the +# Python dictionary icons below, are derived from GitHub's Octicons +# (https://github.com/primer/octicons), and are copyright (c) GitHub, Inc, +# and licensed by GitHub under the MIT license. + +# Dictionary of icon paths for the supported statistics. +icons = { + "joined" : '', + "featured" : '', + "mostStarred" : '', + "mostForked" : '', + "followers" : '', + "following" : '', + "sponsors" : '', + "sponsoring" : '', + "public" : '', + "starredBy" : '', + "forkedBy" : '', + "watchedBy" : '', + "templates" : '', + "archived" : '', + "commits" : '', + "issues" : '', + "prs" : '', + "reviews" : '', + "contribTo" : '', + "private" : '', } diff --git a/src/Statistician.py b/src/Statistician.py index 43254f97..559d79c9 100644 --- a/src/Statistician.py +++ b/src/Statistician.py @@ -29,21 +29,21 @@ import subprocess import os -def set_outputs(names_values) : +def set_outputs(names_values): """Sets the GitHub Action outputs. Keyword arguments: names_values - Dictionary of output names with values """ - if "GITHUB_OUTPUT" in os.environ : - with open(os.environ["GITHUB_OUTPUT"], "a") as f : - for name, value in names_values.items() : + if "GITHUB_OUTPUT" in os.environ: + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + for name, value in names_values.items(): print("{0}={1}".format(name, value), file=f) - else : # Fall-back to deprecated set-output for self-hosted runners - for name, value in names_values.items() : + else: # Fall-back to deprecated set-output for self-hosted runners + for name, value in names_values.items(): print("::set-output name={0}::{1}".format(name, value)) -class Statistician : +class Statistician: """The Statistician class executes GitHub GraphQl queries, and parses the query results. """ @@ -62,7 +62,13 @@ class Statistician : '_featuredRepo' ] - def __init__(self, fail, autoLanguages, maxLanguages, languageRepoExclusions, featuredRepo) : + def __init__( + self, + fail, + autoLanguages, + maxLanguages, + languageRepoExclusions, + featuredRepo): """The initializer executes the queries and parses the results. Upon completion of the intitializer, the user statistics will be available. @@ -112,24 +118,24 @@ def __init__(self, fail, autoLanguages, maxLanguages, languageRepoExclusions, fe ) ) - def getStatsByKey(self, key) : + def getStatsByKey(self, key): """Gets a category of stats by key. Keyword arguments: key - A category key. """ - if key == "general" : + if key == "general": return self._user - elif key == "repositories" : + elif key == "repositories": return self._repo - elif key == "contributions" : + elif key == "contributions": return self._contrib - elif key == "languages" : + elif key == "languages": return self._languages - else : + else: return None # passed an invalid key - def loadQuery(self, queryFilepath, failOnError=True) : + def loadQuery(self, queryFilepath, failOnError=True): """Loads a graphql query. Keyword arguments: @@ -138,7 +144,7 @@ def loadQuery(self, queryFilepath, failOnError=True) : query; and if False, this action will quietly exit with no error code. In either case, an error message will be logged to the console. """ - try : + try: with open(queryFilepath, 'r') as file: return file.read() except IOError: @@ -146,7 +152,7 @@ def loadQuery(self, queryFilepath, failOnError=True) : set_outputs({"exit-code" : 1}) exit(1 if failOnError else 0) - def parseStats(self, basicStats, repoStats, watchingStats, reposContributedToStats) : + def parseStats(self, basicStats, repoStats, watchingStats, reposContributedToStats): """Parses the user statistics. Keyword arguments: @@ -162,14 +168,16 @@ def parseStats(self, basicStats, repoStats, watchingStats, reposContributedToSta # The name field is nullable, so use the login id if # user's public name is null. - if self._name == None : + if self._name == None: self._name = self._login # Extract most recent year data from query results pastYearData = basicStats["data"]["user"]["contributionsCollection"] # Extract repositories contributes to (owned by others) in past year - pastYearData["repositoriesContributedTo"] = basicStats["data"]["user"]["repositoriesContributedTo"]["totalCount"] + pastYearData[ + "repositoriesContributedTo"] = basicStats[ + "data"]["user"]["repositoriesContributedTo"]["totalCount"] # Extract list of contribution years self._contributionYears = pastYearData["contributionYears"] @@ -178,16 +186,20 @@ def parseStats(self, basicStats, repoStats, watchingStats, reposContributedToSta # Extract followed and following counts self._user = {} - self._user["followers"] = [ basicStats["data"]["user"]["followers"]["totalCount"] ] - self._user["following"] = [ basicStats["data"]["user"]["following"]["totalCount"] ] + self._user["followers"] = [ + basicStats["data"]["user"]["followers"]["totalCount"] ] + self._user["following"] = [ + basicStats["data"]["user"]["following"]["totalCount"] ] self._user["joined"] = [ min(self._contributionYears) ] # Extract sponsors and sponsoring counts - self._user["sponsors"] = [ basicStats["data"]["user"]["sponsorshipsAsMaintainer"]["totalCount"] ] - self._user["sponsoring"] = [ basicStats["data"]["user"]["sponsorshipsAsSponsor"]["totalCount"] ] + self._user["sponsors"] = [ + basicStats["data"]["user"]["sponsorshipsAsMaintainer"]["totalCount"] ] + self._user["sponsoring"] = [ + basicStats["data"]["user"]["sponsorshipsAsSponsor"]["totalCount"] ] # - if self._featuredRepo != None : + if self._featuredRepo != None: self._user["featured"] = [ self._featuredRepo ] # Extract all time counts of issues and pull requests @@ -197,7 +209,8 @@ def parseStats(self, basicStats, repoStats, watchingStats, reposContributedToSta # Reorganize for simplicity repoStats = list(map(lambda x : x["data"]["user"]["repositories"], repoStats)) watchingStats = list(map(lambda x : x["data"]["user"]["watching"], watchingStats)) - reposContributedToStats = list(map(lambda x : x["data"]["user"]["topRepositories"], reposContributedToStats)) + reposContributedToStats = list( + map(lambda x : x["data"]["user"]["topRepositories"], reposContributedToStats)) # This is the count of owned repos, including all public, # but may or may not include all private depending upon token used to authenticate. @@ -208,7 +221,10 @@ def parseStats(self, basicStats, repoStats, watchingStats, reposContributedToSta # or combination of queries to actually compute this other than for the most recent # year's data. Keeping the query in, but changing to leave that stat blank in # the SVG. - repositoriesContributedTo = sum(1 for page in reposContributedToStats if page["nodes"] != None for repo in page["nodes"] if repo["owner"]["login"] != self._login) + repositoriesContributedTo = sum( + 1 for page in reposContributedToStats if page[ + "nodes"] != None for repo in page[ + "nodes"] if repo["owner"]["login"] != self._login) self._contrib = { "commits" : [pastYearData["totalCommitContributions"], 0], @@ -222,60 +238,104 @@ def parseStats(self, basicStats, repoStats, watchingStats, reposContributedToSta } # The "nodes" field is nullable so make sure the user owns at least 1 repo. - if repoStats[0]["totalCount"] > 0 : - # Note that the explicit checks of, if page["nodes"] != None, are precautionary - # since the above check of totalCount should be sufficient to protect against a null list of repos. + if repoStats[0]["totalCount"] > 0: + # Note that the explicit checks of, if page["nodes"] != None, are + # precautionary since the above check of totalCount should be sufficient + # to protect against a null list of repos. # Count stargazers, forks of my repos, and watchers excluding me - stargazers = sum(repo["stargazerCount"] for page in repoStats if page["nodes"] != None for repo in page["nodes"] if not repo["isPrivate"] and not repo["isFork"]) - forksOfMyRepos = sum(repo["forkCount"] for page in repoStats if page["nodes"] != None for repo in page["nodes"] if not repo["isPrivate"] and not repo["isFork"]) - stargazersAll = sum(repo["stargazerCount"] for page in repoStats if page["nodes"] != None for repo in page["nodes"] if not repo["isPrivate"]) - forksOfMyReposAll = sum(repo["forkCount"] for page in repoStats if page["nodes"] != None for repo in page["nodes"] if not repo["isPrivate"]) + stargazers = sum( + repo["stargazerCount"] for page in repoStats if page[ + "nodes"] != None for repo in page[ + "nodes"] if not repo["isPrivate"] and not repo["isFork"]) + forksOfMyRepos = sum( + repo["forkCount"] for page in repoStats if page[ + "nodes"] != None for repo in page[ + "nodes"] if not repo["isPrivate"] and not repo["isFork"]) + stargazersAll = sum( + repo["stargazerCount"] for page in repoStats if page[ + "nodes"] != None for repo in page[ + "nodes"] if not repo["isPrivate"]) + forksOfMyReposAll = sum( + repo["forkCount"] for page in repoStats if page[ + "nodes"] != None for repo in page[ + "nodes"] if not repo["isPrivate"]) # Find repos with most stars and most forks - try : - mostStars = max( (repo for page in repoStats if page["nodes"] != None for repo in page["nodes"] if not repo["isPrivate"] and not repo["isFork"]), key=lambda x : x["stargazerCount"])["name"] + try: + mostStars = max( + (repo for page in repoStats if page[ + "nodes"] != None for repo in page[ + "nodes"] if not repo["isPrivate"] and not repo["isFork"]), + key=lambda x : x["stargazerCount"])["name"] self._user["mostStarred"] = [ mostStars ] except ValueError: pass - try : - mostForks = max( (repo for page in repoStats if page["nodes"] != None for repo in page["nodes"] if not repo["isPrivate"] and not repo["isFork"]), key=lambda x : x["forkCount"])["name"] + try: + mostForks = max( + (repo for page in repoStats if page[ + "nodes"] != None for repo in page["nodes"] if not repo[ + "isPrivate"] and not repo["isFork"]), + key=lambda x : x["forkCount"])["name"] self._user["mostForked"] = [ mostForks ] except ValueError: pass # Compute number of watchers excluding cases where user is watching their own repos. - watchers = sum(repo["watchers"]["totalCount"] for page in repoStats if page["nodes"] != None for repo in page["nodes"] if not repo["isPrivate"]) + watchers = sum( + repo["watchers"]["totalCount"] for page in repoStats if page[ + "nodes"] != None for repo in page["nodes"] if not repo["isPrivate"]) watchers -= watchingStats[0]["totalCount"] - if watchingStats[0]["totalCount"] > 0 : - watchingMyOwnNonForks = sum(1 for page in watchingStats if page["nodes"] != None for repo in page["nodes"] if not repo["isFork"]) - else : + if watchingStats[0]["totalCount"] > 0: + watchingMyOwnNonForks = sum( + 1 for page in watchingStats if page[ + "nodes"] != None for repo in page["nodes"] if not repo["isFork"]) + else: watchingMyOwnNonForks = 0 - watchersNonForks = sum(repo["watchers"]["totalCount"] for page in repoStats if page["nodes"] != None for repo in page["nodes"] if not repo["isPrivate"] and not repo["isFork"]) + watchersNonForks = sum( + repo["watchers"]["totalCount"] for page in repoStats if page[ + "nodes"] != None for repo in page["nodes"] if not repo[ + "isPrivate"] and not repo["isFork"]) watchersNonForks -= watchingMyOwnNonForks - # Count of private repos (which is not accurate since depends on token used to authenticate query, + # Count of private repos (not accurate since depends on token used to authenticate query, # however, all those here are included in count of owned repos. - privateCount = sum(1 for page in repoStats if page["nodes"] != None for repo in page["nodes"] if repo["isPrivate"]) + privateCount = sum( + 1 for page in repoStats if page[ + "nodes"] != None for repo in page["nodes"] if repo["isPrivate"]) publicAll = ownedRepositories - privateCount # Counts of archived repos - publicNonForksArchivedCount = sum(1 for page in repoStats if page["nodes"] != None for repo in page["nodes"] if repo["isArchived"] and not repo["isPrivate"] and not repo["isFork"]) - publicArchivedCount = sum(1 for page in repoStats if page["nodes"] != None for repo in page["nodes"] if repo["isArchived"] and not repo["isPrivate"]) + publicNonForksArchivedCount = sum( + 1 for page in repoStats if page[ + "nodes"] != None for repo in page["nodes"] if repo[ + "isArchived"] and not repo["isPrivate"] and not repo["isFork"]) + publicArchivedCount = sum( + 1 for page in repoStats if page[ + "nodes"] != None for repo in page["nodes"] if repo[ + "isArchived"] and not repo["isPrivate"]) # Counts of template repos - publicNonForksTemplatesCount = sum(1 for page in repoStats if page["nodes"] != None for repo in page["nodes"] if repo["isTemplate"] and not repo["isPrivate"] and not repo["isFork"]) - publicTemplatesCount = sum(1 for page in repoStats if page["nodes"] != None for repo in page["nodes"] if repo["isTemplate"] and not repo["isPrivate"]) + publicNonForksTemplatesCount = sum( + 1 for page in repoStats if page[ + "nodes"] != None for repo in page["nodes"] if repo[ + "isTemplate"] and not repo["isPrivate"] and not repo["isFork"]) + publicTemplatesCount = sum( + 1 for page in repoStats if page[ + "nodes"] != None for repo in page["nodes"] if repo[ + "isTemplate"] and not repo["isPrivate"]) # Count of public non forks owned by user - publicNonForksCount = ownedRepositories - sum(1 for page in repoStats if page["nodes"] != None for repo in page["nodes"] if repo["isPrivate"] or repo["isFork"]) + publicNonForksCount = ownedRepositories - sum( + 1 for page in repoStats if page["nodes"] != None for repo in page[ + "nodes"] if repo["isPrivate"] or repo["isFork"]) # Compute language distribution totalSize, languageData = self.summarizeLanguageStats(repoStats) - else : + else: # if no owned repos then set all repo related stats to 0 stargazers = 0 forksOfMyRepos = 0 @@ -300,11 +360,11 @@ def parseStats(self, basicStats, repoStats, watchingStats, reposContributedToSta "watchedBy" : [watchersNonForks, watchers], "archived" : [publicNonForksArchivedCount, publicArchivedCount], "templates" : [publicNonForksTemplatesCount, publicTemplatesCount] - } + } self._languages = self.organizeLanguageStats(totalSize, languageData) - def organizeLanguageStats(self, totalSize, languageData) : + def organizeLanguageStats(self, totalSize, languageData): """Computes a list of languages and percentages in decreasing order by percentage. @@ -312,29 +372,29 @@ def organizeLanguageStats(self, totalSize, languageData) : totalSize - total size of all code with language detection data languageData - the summarized language totals, colors, etc """ - if totalSize == 0 : + if totalSize == 0: return { "totalSize" : 0, "languages" : [] } - else : + else: languages = [ (name, data) for name, data in languageData.items() ] languages.sort(key = lambda L : L[1]["size"], reverse=True) if self._autoLanguages : - for i, L in enumerate(languages) : - if L[1]["percentage"] < 0.01 : + for i, L in enumerate(languages): + if L[1]["percentage"] < 0.01: self._maxLanguages = i break - if len(languages) > self._maxLanguages : + if len(languages) > self._maxLanguages: self.combineLanguages(languages, self._maxLanguages, totalSize) self.checkColors(languages) return { "totalSize" : totalSize, "languages" : languages } - def combineLanguages(self, languages, maxLanguages, totalSize) : + def combineLanguages(self, languages, maxLanguages, totalSize): """Combines lowest percentage languages into an Other. Keyword arguments: languages - Sorted list of languages (sorted by size). maxLanguages - The maximum number of languages to keep as is. """ - if len(languages) > self._maxLanguages : + if len(languages) > self._maxLanguages: combinedSize = sum(L[1]["size"] for L in languages[maxLanguages:]) languages[maxLanguages] = ( "Other", @@ -345,7 +405,7 @@ def combineLanguages(self, languages, maxLanguages, totalSize) : ) del languages[maxLanguages+1:] - def checkColors(self, languages) : + def checkColors(self, languages): """Make sure all languages have colors, and assign shades of gray to those that don't. @@ -356,12 +416,12 @@ def checkColors(self, languages) : # In such cases, we alternate between these two shades of gray. colorsForLanguagesWithoutColors = [ "#959da5", "#d1d5da" ] index = 0 - for L in languages : - if L[1]["color"] == None : + for L in languages: + if L[1]["color"] == None: L[1]["color"] = colorsForLanguagesWithoutColors[index] index = (index + 1) % 2 - def summarizeLanguageStats(self, repoStats) : + def summarizeLanguageStats(self, repoStats): """Summarizes the language distibution of the user's owned repositories. Keyword arguments: @@ -369,50 +429,56 @@ def summarizeLanguageStats(self, repoStats) : """ totalSize = 0 languageData = {} - for page in repoStats : - if page["nodes"] != None : - for repo in page["nodes"] : - if not repo["isPrivate"] and not repo["isFork"] and (repo["name"].lower() not in self._languageRepoExclusions) : + for page in repoStats: + if page["nodes"] != None: + for repo in page["nodes"]: + if (not repo["isPrivate"] and not repo["isFork"] and + (repo["name"].lower() not in self._languageRepoExclusions)): totalSize += repo["languages"]["totalSize"] if repo["languages"]["edges"] != None : - for L in repo["languages"]["edges"] : + for L in repo["languages"]["edges"]: name = L["node"]["name"] - if name in languageData : + if name in languageData: languageData[name]["size"] += L["size"] - else : + else: languageData[name] = { "color" : L["node"]["color"], "size" : L["size"] } - for L in languageData : + for L in languageData: languageData[L]["percentage"] = languageData[L]["size"] / totalSize return totalSize, languageData - def createPriorYearStatsQuery(self, yearList, oneYearContribTemplate) : + def createPriorYearStatsQuery(self, yearList, oneYearContribTemplate): """Generates the query for prior year stats. Keyword arguments: - yearList - a list of the years when the user had contributions, obtained by one of the other queries. - oneYearContribTemplate - a string template of the part of a query for one year + yearList - a list of the years when the user had contributions, + obtained by one of the other queries. + oneYearContribTemplate - a string template of the part of a query + for one year. """ query = "query($owner: String!) {\n user(login: $owner) {\n" - for y in yearList : + for y in yearList: query += oneYearContribTemplate.format(y) query += " }\n}\n" return query - def parsePriorYearStats(self, queryResults) : + def parsePriorYearStats(self, queryResults): """Parses one year of commits, PR reviews, and restricted contributions. Keyword arguments: queryResults - The results of the query. """ queryResults = queryResults["data"]["user"] - self._contrib["commits"][1] = sum(stats["totalCommitContributions"] for k, stats in queryResults.items()) - self._contrib["reviews"][1] = sum(stats["totalPullRequestReviewContributions"] for k, stats in queryResults.items()) - self._contrib["private"][1] = sum(stats["restrictedContributionsCount"] for k, stats in queryResults.items()) + self._contrib["commits"][1] = sum( + stats["totalCommitContributions"] for k, stats in queryResults.items()) + self._contrib["reviews"][1] = sum( + stats["totalPullRequestReviewContributions"] for k, stats in queryResults.items()) + self._contrib["private"][1] = sum( + stats["restrictedContributionsCount"] for k, stats in queryResults.items()) - def executeQuery(self, query, needsPagination=False, failOnError=True) : + def executeQuery(self, query, needsPagination=False, failOnError=True): """Executes a GitHub GraphQl query using the GitHub CLI (gh). Keyword arguments: @@ -422,9 +488,9 @@ def executeQuery(self, query, needsPagination=False, failOnError=True) : query; and if False, this action will quietly exit with no error code. In either case, an error message will be logged to the console. """ - if "GITHUB_REPOSITORY_OWNER" in os.environ : + if "GITHUB_REPOSITORY_OWNER" in os.environ: owner = os.environ["GITHUB_REPOSITORY_OWNER"] - else : + else: print("Error (7): Could not determine the repository owner.") set_outputs({"exit-code" : 7}) exit(7 if failOnError else 0) @@ -434,7 +500,7 @@ def executeQuery(self, query, needsPagination=False, failOnError=True) : '--cache', '1h', '-f', 'query=' + query ] - if needsPagination : + if needsPagination: arguments.insert(5, '--paginate') result = subprocess.run( arguments, @@ -442,42 +508,43 @@ def executeQuery(self, query, needsPagination=False, failOnError=True) : universal_newlines=True ).stdout.strip() numPages = result.count('"data"') - if numPages == 0 : + if numPages == 0: # Check if any error details result = json.loads(result) if len(result) > 0 else "" - if "errors" in result : + if "errors" in result: print("Error (2): GitHub api Query failed with error:") print(result["errors"]) code = 2 - else : + else: print("Error (3): Something unexpected occurred during GitHub API query.") code = 3 set_outputs({"exit-code" : code}) exit(code if failOnError else 0) - elif needsPagination : - if (numPages > 1) : + elif needsPagination: + if (numPages > 1): result = result.replace('}{"data"', '},{"data"') result = "[" + result + "]" result = json.loads(result) failed = False errorMessage = None - if (not needsPagination) and (("data" not in result) or result["data"] == None) : + if ((not needsPagination) and + (("data" not in result) or result["data"] == None)): failed = True - if "errors" in result : + if "errors" in result: errorMessage = result["errors"] elif needsPagination and ("data" not in result[0] or result[0]["data"] == None): failed = True if "errors" in result[0] : errorMessage = result[0]["errors"] - if failed : + if failed: print("Error (6): No data returned.") - if errorMessage != None : + if errorMessage != None: print(errorMessage) set_outputs({"exit-code" : 6}) exit(6 if failOnError else 0) return result - def ghDisableInteractivePrompts(self) : + def ghDisableInteractivePrompts(self): """Disable gh's interactive prompts. This is probably unnecessary, as all of our testing so far, the queries run fine and don't produce any prompts. Disabling as a precaution in case some unexpected condition occurs diff --git a/src/StatsImageGenerator.py b/src/StatsImageGenerator.py index 61c16bdd..d68c9255 100644 --- a/src/StatsImageGenerator.py +++ b/src/StatsImageGenerator.py @@ -1,7 +1,7 @@ # # user-statistician: Github action for generating a user stats card # -# Copyright (c) 2021-2022 Vincent A Cicirello +# Copyright (c) 2021-2023 Vincent A Cicirello # https://www.cicirello.org/ # # MIT License @@ -25,14 +25,14 @@ # SOFTWARE. # -from StatConfig import * +from StatConfig import statsByCategory, loadLocale, icons from PieChart import svgPieChart from Colors import iconTemplates from ColorUtil import highContrastingColor from TextLength import calculateTextLength, calculateTextLength110Weighted import math -class StatsImageGenerator : +class StatsImageGenerator: """Generates an svg image from the collected stats.""" headerTemplate = '' @@ -91,6 +91,7 @@ class StatsImageGenerator : '_lineHeight', '_margin', '_locale', + '_labels', '_radius', '_titleSize', '_pieRadius', @@ -118,7 +119,7 @@ def __init__(self, width, customTitle, includeTitle, - exclude) : + exclude): """Initializes the StatsImageGenerator. Keyword arguments: @@ -127,12 +128,16 @@ def __init__(self, locale - The 2-character locale code. radius - The border radius. titleSize - The font size for the title. - categories - List of category keys in order they should appear on card. - animateLanguageChart - Boolean controlling whether to animate the language pie chart. - animationSpeed - An integer duration for one full rotation of language pie chart. - width - The minimum width of the SVG, but will autosize larger as needed. - customTitle - If not None, this is used as the title, otherwise title is formed - from user's name. + categories - List of category keys in order they should + appear on card. + animateLanguageChart - Boolean controlling whether to animate + the language pie chart. + animationSpeed - An integer duration for one full rotation + of language pie chart. + width - The minimum width of the SVG, but will autosize larger + as needed. + customTitle - If not None, this is used as the title, otherwise + title is formed from user's name. includeTitle - If True inserts a title. exclude - A set of keys to exclude. """ @@ -140,12 +145,13 @@ def __init__(self, self._colors = colors self._highContrast = highContrastingColor(self._colors["bg"]) self._locale = locale + self._labels = loadLocale(self._locale) self._radius = radius self._titleSize = titleSize - if customTitle != None : + if customTitle != None: self._title = customTitle - else : - self._title = titleTemplates[self._locale].format(self._stats._name) + else: + self._title = self._labels["titleTemplate"].format(self._stats._name) self._includeTitle = includeTitle self._topIconSize = 25 self._categoryOrder = categories @@ -161,37 +167,40 @@ def __init__(self, self._firstColX = (self._width // 2) self._secondColX = self._firstColX + (self._width // 4) self._lineHeight = 21 - self._pieRadius = (((self._width // 2 - 2*self._margin) // self._lineHeight * self._lineHeight) - (self._lineHeight - 16)) // 2 + self._pieRadius = ( + ((self._width // 2 - 2*self._margin) // self._lineHeight * self._lineHeight) - ( + self._lineHeight - 16)) // 2 self._rows = [ StatsImageGenerator.headerTemplate, StatsImageGenerator.backgroundTemplate, StatsImageGenerator.fontGroup ] - def calculateMinimumFeasibleWidth(self) : + def calculateMinimumFeasibleWidth(self): """Calculates the minimum feasible width for the SVG based on the lengths of the labels of the stats that are to be included, the category headings, and the title (if any), factoring in the chosen locale. """ length = 0 - if self._includeTitle : - length = calculateTextLength(self._title, self._titleSize, True, 600) + 2 * self._margin - if "title-icon" in self._colors : + if self._includeTitle: + length = calculateTextLength( + self._title, self._titleSize, True, 600) + 2 * self._margin + if "title-icon" in self._colors: length += 2 * (self._topIconSize + self._margin) - for category in self._categoryOrder : - if category not in self._exclude : - if category == "languages" : + for category in self._categoryOrder: + if category not in self._exclude: + if category == "languages": languageData = self._stats.getStatsByKey(category) - if languageData["totalSize"] > 0 : + if languageData["totalSize"] > 0: headingRowLength = calculateTextLength( - categoryLabels[self._locale][category]["heading"], + self._labels["categoryLabels"][category]["heading"], 14, True, 600) headingRowLength += 2 * self._margin length = max(length, headingRowLength) - for lang in languageData["languages"] : + for lang in languageData["languages"]: langStr = StatsImageGenerator.languageStringTemplate.format( lang[0], 100 * lang[1]["percentage"] @@ -206,23 +215,23 @@ def calculateMinimumFeasibleWidth(self) : length, (langRowLength + 25 + (2 * self._margin)) * 2 ) - else : + else: keys = self.filterKeys( self._stats.getStatsByKey(category), statsByCategory[category] ) - if len(keys) > 0 : - headerRow = categoryLabels[self._locale][category] + if len(keys) > 0: + headerRow = self._labels["categoryLabels"][category] headingRowLength = calculateTextLength( headerRow["heading"], 14, True, 600) headingRowLength += 2 * self._margin - if headerRow["column-one"] != None : + if headerRow["column-one"] != None: headingRowLength *= 2 length = max(length, headingRowLength) - if headerRow["column-one"] != None : + if headerRow["column-one"] != None: length = max( length, 4*(self._margin + calculateTextLength( @@ -231,7 +240,7 @@ def calculateMinimumFeasibleWidth(self) : True, 600)) ) - if headerRow["column-two"] != None : + if headerRow["column-two"] != None: length = max( length, 4*(self._margin + calculateTextLength( @@ -241,9 +250,9 @@ def calculateMinimumFeasibleWidth(self) : 600)) ) data = self._stats.getStatsByKey(category) - for k in keys : + for k in keys: labelLength = calculateTextLength( - statLabels[k]["label"][self._locale], + self._labels["statLabels"][k], 14, True, 600) @@ -251,7 +260,7 @@ def calculateMinimumFeasibleWidth(self) : length, (labelLength + 25 + (2 * self._margin)) * 2 ) - if len(data[k]) == 1 and not self.isInt(data[k][0]) : + if len(data[k]) == 1 and not self.isInt(data[k][0]): dataLength = calculateTextLength( data[k][0], 14, @@ -263,20 +272,20 @@ def calculateMinimumFeasibleWidth(self) : ) return math.ceil(length) - def generateImage(self) : + def generateImage(self): """Generates and returns the image.""" self.insertTitle() - for category in self._categoryOrder : - if category not in self._exclude : - if category == "languages" : + for category in self._categoryOrder: + if category not in self._exclude: + if category == "languages": self.insertLanguagesChart( self._stats.getStatsByKey(category), - categoryLabels[self._locale][category]["heading"] + self._labels["categoryLabels"][category]["heading"] ) - else : + else: self.insertGroup( self._stats.getStatsByKey(category), - categoryLabels[self._locale][category], + self._labels["categoryLabels"][category], self.filterKeys( self._stats.getStatsByKey(category), statsByCategory[category] @@ -285,16 +294,20 @@ def generateImage(self) : self.finalizeImageData() return "".join(self._rows).replace("\n", "") - def filterKeys(self, data, keys) : + def filterKeys(self, data, keys): """Returns a list of the keys that have non-zero data and which are not excluded. Keyword arguments: data - The data (either contrib or repo data) keys - The list of keys relevant for the table. """ - return [ k for k in keys if (k not in self._exclude) and (k in data) and ((not self.isInt(data[k][0])) or data[k][0] > 0 or (len(data[k]) > 1 and data[k][1] > 0)) ] + return [ + k for k in keys if ( + (k not in self._exclude) and (k in data) and ( + (not self.isInt(data[k][0]) + ) or data[k][0] > 0 or (len(data[k]) > 1 and data[k][1] > 0))) ] - def isInt(self, value) : + def isInt(self, value): """Checks if a value is an int. Keyword arguments: @@ -306,9 +319,9 @@ def isInt(self, value) : return False return True - def insertTitle(self) : + def insertTitle(self): """Generates, formats, and inserts title.""" - if self._includeTitle : + if self._includeTitle: scale = round(0.75 * self._titleSize / 110, 3) titleTextLength = round(calculateTextLength110Weighted(self._title, 600)) self._rows.append( @@ -321,7 +334,7 @@ def insertTitle(self) : titleTextLength ) ) - if "title-icon" in self._colors : + if "title-icon" in self._colors: icon = iconTemplates[self._colors["title-icon"]] self._rows.append( icon.format( @@ -341,7 +354,7 @@ def insertTitle(self) : ) self._height += 39 - def insertGroup(self, data, headerRow, keys) : + def insertGroup(self, data, headerRow, keys): """Generates the portion of the image for a group (i.e., the repositories section or the contributions section). If there are no keys with data, then this does nothing (excludes @@ -349,19 +362,23 @@ def insertGroup(self, data, headerRow, keys) : Keyword arguments: data - A dictionary with the data. - headerRow - A dictionary with the header row text. Pass None for no table header. + headerRow - A dictionary with the header row text. Pass None for + no table header. keys - A list of keys in the order they should appear. """ - if len(keys) > 0 : + if len(keys) > 0: scale = round(0.75 * 14 / 110, 3) self._height += self._lineHeight - self._rows.append(StatsImageGenerator.groupHeaderTemplate.format(self._height, self._colors["text"])) - if headerRow != None : - if headerRow["column-one"] == None : + self._rows.append( + StatsImageGenerator.groupHeaderTemplate.format( + self._height, + self._colors["text"])) + if headerRow != None: + if headerRow["column-one"] == None: template = StatsImageGenerator.tableHeaderTemplateNoColumns - elif headerRow["column-two"] == None : + elif headerRow["column-two"] == None: template = StatsImageGenerator.tableHeaderTemplateOneColumn - else : + else: template = StatsImageGenerator.tableHeaderTemplate self._rows.append(template.format( "{0:.3f}".format(scale), @@ -376,18 +393,17 @@ def insertGroup(self, data, headerRow, keys) : round(calculateTextLength110Weighted(headerRow["column-two"], 600)) )) offset = self._lineHeight - else : + else: offset = 0 - for k in keys : - template = StatsImageGenerator.tableEntryTemplate if len(data[k]) > 1 else StatsImageGenerator.tableEntryTemplateOneColumn - label = statLabels[k]["label"][self._locale] + for k in keys: + template = StatsImageGenerator.tableEntryTemplate if ( + len(data[k]) > 1) else StatsImageGenerator.tableEntryTemplateOneColumn + label = self._labels["statLabels"][k] data1 = str(self.formatCount(data[k][0])) data2 = str(self.formatCount(data[k][1])) if len(data[k]) > 1 else "" - if "totalIsLowerBound" in statLabels[k] and statLabels[k]["totalIsLowerBound"] : - data2 = "≥" + data2 self._rows.append(template.format( str(offset), - statLabels[k]["icon"].format(self._colors["icons"]), + icons[k].format(self._colors["icons"]), "{0:.3f}".format(scale), str(round(12.5/scale)), label, @@ -404,7 +420,7 @@ def insertGroup(self, data, headerRow, keys) : self._rows.append("") self._height += offset - def insertLanguagesChart(self, languageData, categoryHeading) : + def insertLanguagesChart(self, languageData, categoryHeading): """Generates and returns the SVG section for the language distribution summary and pie chart. @@ -412,7 +428,7 @@ def insertLanguagesChart(self, languageData, categoryHeading) : languageData - The language stats data categoryHeading - The heading for the section """ - if languageData["totalSize"] > 0 : + if languageData["totalSize"] > 0: scale = round(0.75 * 14 / 110, 3) self._height += self._lineHeight self._rows.append( @@ -452,8 +468,8 @@ def insertLanguagesChart(self, languageData, categoryHeading) : ) diameter = self._pieRadius * 2 numRowsToLeft = round(diameter / self._lineHeight) - for i, L in enumerate(languageData["languages"]) : - if i < numRowsToLeft : + for i, L in enumerate(languageData["languages"]): + if i < numRowsToLeft: lang = StatsImageGenerator.languageStringTemplate.format( L[0], 100 * L[1]["percentage"] @@ -471,15 +487,15 @@ def insertLanguagesChart(self, languageData, categoryHeading) : ) ) offset += self._lineHeight - else : + else: break - for j in range(numRowsToLeft, len(languageData["languages"]), 2) : + for j in range(numRowsToLeft, len(languageData["languages"]), 2): L = languageData["languages"][j] lang = StatsImageGenerator.languageStringTemplate.format( L[0], 100 * L[1]["percentage"] ) - if j+1 < len(languageData["languages"]) : + if j+1 < len(languageData["languages"]): L2 = languageData["languages"][j+1] lang2 = StatsImageGenerator.languageStringTemplate.format( L2[0], @@ -503,7 +519,7 @@ def insertLanguagesChart(self, languageData, categoryHeading) : ) ) offset += self._lineHeight - else : + else: self._rows.append( StatsImageGenerator.languageEntryTemplate.format( str(offset), @@ -518,26 +534,26 @@ def insertLanguagesChart(self, languageData, categoryHeading) : ) offset += self._lineHeight self._rows.append("") - if diameter + self._lineHeight + self._lineHeight - self._margin - 1 <= offset : + if diameter + self._lineHeight + self._lineHeight - self._margin - 1 <= offset: self._height += offset - else : + else: self._height += diameter + self._lineHeight + self._lineHeight - self._margin - 1 - def formatCount(self, count) : + def formatCount(self, count): """Formats the count. Keyword arguments: count - The count to format. """ - if (not self.isInt(count)) or count < 100000 : + if (not self.isInt(count)) or count < 100000: return count - elif count < 1000000 : + elif count < 1000000: return "{0:.1f}K".format(count // 100 * 100 / 1000) - else : + else: # can such a real user exist? return "{0:.1f}M".format(count // 100000 * 100000 / 1000000) - def finalizeImageData(self) : + def finalizeImageData(self): """Inserts the height into the svg opening tag and the rect for the background. Also inserts the border and background colors into the rect for the background. Must be called after generating the rest of the image since we won't know diff --git a/src/TextLength.py b/src/TextLength.py index df336060..dccf223a 100644 --- a/src/TextLength.py +++ b/src/TextLength.py @@ -1,7 +1,7 @@ # # user-statistician: Github action for generating a user stats card # -# Copyright (c) 2021 Vincent A Cicirello +# Copyright (c) 2021-2023 Vincent A Cicirello # https://www.cicirello.org/ # # MIT License @@ -25,7 +25,7 @@ # SOFTWARE. # -def calculateTextLength(s, size, pixels, fontWeight) : +def calculateTextLength(s, size, pixels, fontWeight): """Calculates the length of a string in DejaVu Sans for a specified font size. @@ -33,40 +33,45 @@ def calculateTextLength(s, size, pixels, fontWeight) : s - The string. size - The font size. pixels - If True, the size is in px, otherwise it is in pt. - fontWeight - The weight of the font (e.g., 400 for normal, 600 for bold, etc) + fontWeight - The weight of the font (e.g., 400 for normal, + 600 for bold, etc) """ - if pixels : + if pixels: size *= 0.75 weightMultiplier = 1 - if fontWeight != 400 : + if fontWeight != 400: weightMultiplier = fontWeight / 400 return weightMultiplier * size * calculateTextLength110(s) / 110 -def calculateTextLength110Weighted(s, fontWeight) : +def calculateTextLength110Weighted(s, fontWeight): """Calculates the length of a string in DejaVu Sans 110pt font, factoring in font weight. Keyword arguments: s - The string. - fontWeight - The weight of the font (e.g., 400 for normal, 600 for bold, etc) + fontWeight - The weight of the font (e.g., 400 for normal, + 600 for bold, etc) """ weightMultiplier = 1 - if fontWeight != 400 : + if fontWeight != 400: weightMultiplier = fontWeight / 400 return weightMultiplier * calculateTextLength110(s) -def calculateTextLength110(s) : +def calculateTextLength110(s): """Calculates the length of a string in DejaVu Sans 110pt font. Keyword arguments: s - The string. """ - if s==None or len(s) == 0 : + if s==None or len(s) == 0: return 0 - total = sum(defaultWidths["character-lengths"][c] if c in defaultWidths["character-lengths"] else defaultWidths["mean-character-length"] for c in s) - for i in range(1,len(s)) : + total = sum( + defaultWidths["character-lengths"][c] if ( + c in defaultWidths["character-lengths"] + ) else defaultWidths["mean-character-length"] for c in s) + for i in range(1,len(s)): pair = s[i-1:i+1] - if pair in defaultWidths["kerning-pairs"] : + if pair in defaultWidths["kerning-pairs"]: total -= defaultWidths["kerning-pairs"][pair] return total diff --git a/src/UserStatistician.py b/src/UserStatistician.py index f4682d0d..dba9dff5 100755 --- a/src/UserStatistician.py +++ b/src/UserStatistician.py @@ -2,7 +2,7 @@ # # user-statistician: Github action for generating a user stats card # -# Copyright (c) 2021-2022 Vincent A Cicirello +# Copyright (c) 2021-2023 Vincent A Cicirello # https://www.cicirello.org/ # # MIT License @@ -34,7 +34,7 @@ import os import subprocess -def writeImageToFile(filename, image, failOnError) : +def writeImageToFile(filename, image, failOnError): """Writes the image to a file, creating any missing directories from the path. @@ -52,7 +52,7 @@ def writeImageToFile(filename, image, failOnError) : os.umask(0) # Create the directory if it doesn't exist. directoryName = os.path.dirname(filename) - if len(directoryName) > 0 : + if len(directoryName) > 0: os.makedirs(directoryName, exist_ok=True, mode=0o777) try: # Write the image to a file @@ -64,7 +64,7 @@ def writeImageToFile(filename, image, failOnError) : set_outputs({"exit-code" : 4}) exit(4 if failOnError else 0) -def executeCommand(arguments) : +def executeCommand(arguments): """Execute a subprocess and return result and exit code. Keyword arguments: @@ -77,7 +77,7 @@ def executeCommand(arguments) : ) return result.stdout.strip(), result.returncode -def commitAndPush(filename, name, login, failOnError) : +def commitAndPush(filename, name, login, failOnError): """Commits and pushes the image. Keyword arguments: @@ -86,44 +86,48 @@ def commitAndPush(filename, name, login, failOnError) : login - The user's login id. """ # Make sure this isn't being run during a pull-request. - result = executeCommand(["git", "symbolic-ref", "-q", "HEAD"]) - if result[1] == 0 : + result = executeCommand( + ["git", "symbolic-ref", "-q", "HEAD"]) + if result[1] == 0: # Check if the image changed - result = executeCommand(["git", "status", "--porcelain", filename]) - if len(result[0]) > 0 : + result = executeCommand( + ["git", "status", "--porcelain", filename]) + if len(result[0]) > 0: # Commit and push - executeCommand(["git", "config", "--global", "user.name", name]) - executeCommand(["git", "config", "--global", - "user.email", login + '@users.noreply.github.com']) + executeCommand( + ["git", "config", "--global", "user.name", name]) + executeCommand( + ["git", "config", "--global", "user.email", + login + '@users.noreply.github.com']) executeCommand(["git", "add", filename]) executeCommand(["git", "commit", "-m", "Automated change by https://github.com/cicirello/user-statistician", filename]) r = executeCommand(["git", "push"]) - if r[1] != 0 : + if r[1] != 0: print("Error (5): push failed.") set_outputs({"exit-code" : 5}) exit(5 if failOnError else 0) -if __name__ == "__main__" : +if __name__ == "__main__": imageFilenameWithPath = sys.argv[1].strip() includeTitle = sys.argv[2].strip().lower() == "true" customTitle = sys.argv[3].strip() - if len(customTitle) == 0 or not includeTitle : + if len(customTitle) == 0 or not includeTitle: customTitle = None colors = sys.argv[4].strip().replace(",", " ").split() - if len(colors) == 1 and colors[0] in colorMapping : + if len(colors) == 1 and colors[0] in colorMapping: # get theme colors colors = colorMapping[colors[0]] - elif len(colors) < 4 : + elif len(colors) < 4: # default to light theme if invalid number of colors passed colors = colorMapping["light"] - else : + else: colors = { "title-icon" : "github", "bg" : colors[0], @@ -140,7 +144,7 @@ def commitAndPush(filename, name, login, failOnError) : commit = sys.argv[7].strip().lower() == "true" locale = sys.argv[8].strip().lower() - if locale not in supportedLocales : + if locale not in supportedLocales: locale = "en" radius = int(sys.argv[9]) @@ -158,9 +162,9 @@ def commitAndPush(filename, name, login, failOnError) : maxLanguages = sys.argv[12].strip().lower() autoLanguages = maxLanguages == "auto" - if not autoLanguages : + if not autoLanguages: maxLanguages = int(maxLanguages) - else : + else: maxLanguages = 1000 # doesn't really matter, but should be an int categories = sys.argv[13].strip().replace(",", " ").lower().split() @@ -169,10 +173,11 @@ def commitAndPush(filename, name, login, failOnError) : if len(categories) == 0 : categories = categoryOrder - languageRepoExclusions = set(sys.argv[14].strip().replace(",", " ").lower().split()) + languageRepoExclusions = set( + sys.argv[14].strip().replace(",", " ").lower().split()) featuredRepo = sys.argv[15].strip() - if len(featuredRepo) == 0 : + if len(featuredRepo) == 0: featuredRepo = None animateLanguageChart = sys.argv[16].strip().lower() == "true" @@ -181,9 +186,9 @@ def commitAndPush(filename, name, login, failOnError) : width = int(sys.argv[18].strip()) topIcon = sys.argv[19].strip().lower() - if topIcon == "none" : + if topIcon == "none": colors.pop("title-icon", None) - elif topIcon != "default" and topIcon in iconTemplates : + elif topIcon != "default" and topIcon in iconTemplates: colors["title-icon"] = topIcon stats = Statistician( @@ -210,8 +215,12 @@ def commitAndPush(filename, name, login, failOnError) : image = generator.generateImage() writeImageToFile(imageFilenameWithPath, image, failOnError) - if commit : - commitAndPush(imageFilenameWithPath, "github-actions", "41898282+github-actions[bot]", failOnError) + if commit: + commitAndPush( + imageFilenameWithPath, + "github-actions", + "41898282+github-actions[bot]", + failOnError) set_outputs({"exit-code" : 0}) diff --git a/src/locales/bn.json b/src/locales/bn.json new file mode 100644 index 00000000..a077507f --- /dev/null +++ b/src/locales/bn.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0} এর গিটহাব কার্যকলাপ", + "categoryLabels": { + "general": { + "heading": "সাধারণ পরিসংখ্যান এবং তথ্য", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "সংগ্রহস্থল", + "column-one": "অ-কাঁটা", + "column-two": "সব" + }, + "contributions": { + "heading": "অবদানসমূহ", + "column-one": "বিগত বছর", + "column-two": "মোট" + }, + "languages": { + "heading": "প্রকাশ্য ভান্ডারে ভাষা বিতরণ", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "যোগদানের বছর", + "featured": "বৈশিষ্ট্যযুক্ত রেপো", + "mostStarred": "সর্বাধিক তারকা প্রাপ্ত রেপো", + "mostForked": "সর্বাধিক ফর্কড রেপো", + "followers": "অনুসারী", + "following": "অনুসরণ করছে", + "sponsors": "পৃষ্ঠপোষক", + "sponsoring": "পৃষ্ঠপোষকতা", + "public": "ভাণ্ডার মালিকানাধীন", + "starredBy": "তারকা প্রদান করেছে", + "forkedBy": "ফোর্ক করেছে", + "watchedBy": "দেখেছেন", + "templates": "টেমপ্লেট সমুহ", + "archived": "সংরক্ষণাগারভুক্ত", + "commits": "কমিট করে", + "issues": "ইস্যু", + "prs": "অনুরোধগুলি টানুন", + "reviews": "অনুরোধ টানার পর্যালোচনাগুলি", + "contribTo": "অবদান", + "private": "ব্যক্তিগত অবদান" + } +} \ No newline at end of file diff --git a/src/locales/de.json b/src/locales/de.json new file mode 100644 index 00000000..5a9eaeae --- /dev/null +++ b/src/locales/de.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0}s GitHub Aktivität", + "categoryLabels": { + "general": { + "heading": "Allgemeine Statistiken und Informationen", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repositories", + "column-one": "Non-Forks", + "column-two": "Alle" + }, + "contributions": { + "heading": "Beiträge", + "column-one": "Letztes Jahr", + "column-two": "Gesamt" + }, + "languages": { + "heading": "Verteilung der Sprachen in Öffentlichen Repositories", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Beitrittsdatum", + "featured": "Vorgestelltes Repo", + "mostStarred": "Meistmarkiertes Repo", + "mostForked": "Meistgeforktes Repo", + "followers": "Follower", + "following": "Folgt", + "sponsors": "Sponsoren", + "sponsoring": "Sponsoring", + "public": "Eigene Repositories", + "starredBy": "Markiert Von", + "forkedBy": "Geforkt Von", + "watchedBy": "Verfolgt Von", + "templates": "Vorlagen", + "archived": "Archiviert", + "commits": "Commits", + "issues": "Issues", + "prs": "Pull Requests", + "reviews": "Überprüfungen von Pull Requests", + "contribTo": "Beigetragen Zu", + "private": "Private Beiträge" + } +} \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 00000000..cab9efcd --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0}'s GitHub Activity", + "categoryLabels": { + "general": { + "heading": "General Stats and Info", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repositories", + "column-one": "Non-Forks", + "column-two": "All" + }, + "contributions": { + "heading": "Contributions", + "column-one": "Past Year", + "column-two": "Total" + }, + "languages": { + "heading": "Language Distribution in Public Repositories", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Year Joined", + "featured": "Featured Repo", + "mostStarred": "Most Starred Repo", + "mostForked": "Most Forked Repo", + "followers": "Followers", + "following": "Following", + "sponsors": "Sponsors", + "sponsoring": "Sponsoring", + "public": "My Repositories", + "starredBy": "Starred By", + "forkedBy": "Forked By", + "watchedBy": "Watched By", + "templates": "Templates", + "archived": "Archived", + "commits": "Commits", + "issues": "Issues", + "prs": "Pull Requests", + "reviews": "Pull Request Reviews", + "contribTo": "Contributed To", + "private": "Private Contributions" + } +} \ No newline at end of file diff --git a/src/locales/es.json b/src/locales/es.json new file mode 100644 index 00000000..a1c14ad5 --- /dev/null +++ b/src/locales/es.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "Actividad en GitHub de {0}", + "categoryLabels": { + "general": { + "heading": "Estadísticas generales e información", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repositorios", + "column-one": "No bifurcados", + "column-two": "Todos" + }, + "contributions": { + "heading": "Contribuciones", + "column-one": "Año pasado", + "column-two": "Total" + }, + "languages": { + "heading": "Distribución de lenguajes en repositorios públicos", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Año de ingreso", + "featured": "Repositorio destacado", + "mostStarred": "Repositorio con más estrellas", + "mostForked": "Repositorio más bifurcado", + "followers": "Seguidores", + "following": "Siguiendo", + "sponsors": "Patrocinadores", + "sponsoring": "Patrocinando", + "public": "Repositorios propios", + "starredBy": "Con estrella por", + "forkedBy": "Bifurcado por", + "watchedBy": "Visto por", + "templates": "Plantillas", + "archived": "Archivado", + "commits": "Commits", + "issues": "Problemas", + "prs": "Pull Requests", + "reviews": "Revisiones de Pull Requests", + "contribTo": "Contribuido a", + "private": "Contribuciones privadas" + } +} \ No newline at end of file diff --git a/src/locales/fr.json b/src/locales/fr.json new file mode 100644 index 00000000..13f66211 --- /dev/null +++ b/src/locales/fr.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "Activité GitHub de {0}", + "categoryLabels": { + "general": { + "heading": "Statistiques Générales et Info", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Dépôts", + "column-one": "Non clonés", + "column-two": "Tout" + }, + "contributions": { + "heading": "Contributions", + "column-one": "Dernière année", + "column-two": "Total" + }, + "languages": { + "heading": "Répartition des langages dans les dépôts publiques", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Année d'adhésion", + "featured": "Dépôt en vedette", + "mostStarred": "Dépôt le plus étoilé", + "mostForked": "Dépôt le plus cloné", + "followers": "Abonnés", + "following": "Abonnements", + "sponsors": "Sponsors", + "sponsoring": "Sponsorise", + "public": "Dépôts possédés", + "starredBy": "Étoilé par", + "forkedBy": "Cloné par", + "watchedBy": "Regardé par", + "templates": "Modèles", + "archived": "Archivé", + "commits": "Commits", + "issues": "Issues", + "prs": "Pull Requests", + "reviews": "Révision de Pull Request", + "contribTo": "Contribué à", + "private": "Contributions privées" + } +} \ No newline at end of file diff --git a/src/locales/hi.json b/src/locales/hi.json new file mode 100644 index 00000000..87e98bf9 --- /dev/null +++ b/src/locales/hi.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0} की गिटहब गतिविधि", + "categoryLabels": { + "general": { + "heading": "साधारण सांख्यिकी और सूचना", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "भंडार", + "column-one": "गैर-फोर्क", + "column-two": "सभी" + }, + "contributions": { + "heading": "योगदान", + "column-one": "पिछला वर्ष", + "column-two": "कुल" + }, + "languages": { + "heading": "सार्वजनिक भंडारों में भाषा वितरण", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "युक्त होने का वर्ष", + "featured": "विशेष रुप से प्रदर्शित भंडार", + "mostStarred": "सर्वाधिक तारांकित भंडार", + "mostForked": "सर्वाधिक फोर्क भंडार", + "followers": "समर्थक", + "following": "अनुगामी", + "sponsors": "प्रायोजक", + "sponsoring": "प्रायोजन", + "public": "अपना भंडार", + "starredBy": "किसके द्वारा तारांकित", + "forkedBy": "किसके द्वारा फोर्क किया गया", + "watchedBy": "किसके द्वारा देखा गया", + "templates": "आकार पट्ट", + "archived": "संग्रहीत", + "commits": "प्रतिबद्ध", + "issues": "मुद्दे", + "prs": "अनुरोध", + "reviews": "अनुरोध समीक्षा", + "contribTo": "योगदान", + "private": "गुप्त योगदान" + } +} \ No newline at end of file diff --git a/src/locales/hu.json b/src/locales/hu.json new file mode 100644 index 00000000..c6bcd379 --- /dev/null +++ b/src/locales/hu.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0} GitHub aktivitása", + "categoryLabels": { + "general": { + "heading": "Általános statisztika és információ", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repository-k", + "column-one": "Non-Fork-ok", + "column-two": "Mind" + }, + "contributions": { + "heading": "Kontribúciók", + "column-one": "Elmúlt év", + "column-two": "Összesen" + }, + "languages": { + "heading": "Nyelvek eloszlása nyilvános repository-kban", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Csatlakozás éve", + "featured": "Kiemelt repo", + "mostStarred": "Legtöbbet csillagozott repo", + "mostForked": "Legtöbbet fork-olt repo", + "followers": "Követői", + "following": "Követi", + "sponsors": "Szponzorok", + "sponsoring": "Szponzorál", + "public": "Saját repository-k", + "starredBy": "Csillagozta", + "forkedBy": "Forkolta", + "watchedBy": "Figyeli", + "templates": "Sablonok", + "archived": "Archiválva", + "commits": "Commitok", + "issues": "Issue-k", + "prs": "Pull request-ek", + "reviews": "Pull request review-k", + "contribTo": "Kontribútolt", + "private": "Privát kontribúciók" + } +} \ No newline at end of file diff --git a/src/locales/id.json b/src/locales/id.json new file mode 100644 index 00000000..8dba37d6 --- /dev/null +++ b/src/locales/id.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "Aktivitas Github {0}", + "categoryLabels": { + "general": { + "heading": "Info dan Status Umum", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repositori", + "column-one": "Non Fork", + "column-two": "Semua" + }, + "contributions": { + "heading": "Kontribusi", + "column-one": "Tahun Lalu", + "column-two": "Total" + }, + "languages": { + "heading": "Distribusi Bahasa dalam Repositori Publik", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Tahun Bergabung", + "featured": "Repositori Unggulan", + "mostStarred": "Repositori dengan Bintang Terbanyak", + "mostForked": "Repositori dengan Fork Terbanyak", + "followers": "Pengikut", + "following": "Mengikuti", + "sponsors": "Sponsor", + "sponsoring": "Mensponsori", + "public": "Repositori yang Dimiliki", + "starredBy": "Diberikan bintang oleh", + "forkedBy": "Di-fork oleh", + "watchedBy": "Dilihat oleh", + "templates": "Template", + "archived": "Diarsipkan", + "commits": "Commits", + "issues": "Isu", + "prs": "Pull Requests", + "reviews": "Ulasan Pull Request", + "contribTo": "Berkontribusi Ke", + "private": "Kontribusi Pribadi" + } +} \ No newline at end of file diff --git a/src/locales/it.json b/src/locales/it.json new file mode 100644 index 00000000..da722f2d --- /dev/null +++ b/src/locales/it.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "Attività GitHub di {0}", + "categoryLabels": { + "general": { + "heading": "Statistiche Generali e Informazioni", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repository", + "column-one": "Non-Fork", + "column-two": "Tutti" + }, + "contributions": { + "heading": "Contributi", + "column-one": "Anno Scorso", + "column-two": "Totale" + }, + "languages": { + "heading": "Distribuzione del Linguaggio nei Repository Pubblici", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Anno di Iscrizione", + "featured": "Repo in Primo Piano", + "mostStarred": "Repo con più Stelle", + "mostForked": "Repo con più Fork", + "followers": "Seguaci", + "following": "Seguendo", + "sponsors": "Sponsors", + "sponsoring": "Sponsorizza", + "public": "Repository di Proprietà", + "starredBy": "Stellato Da", + "forkedBy": "Forkato Da", + "watchedBy": "Seguito Da", + "templates": "Modelli", + "archived": "Archiviato", + "commits": "Commits", + "issues": "Problemi", + "prs": "Richieste di Pull", + "reviews": "Revisioni di Richieste di Pull", + "contribTo": "Contribuito A", + "private": "Contributi Privati" + } +} \ No newline at end of file diff --git a/src/locales/ja.json b/src/locales/ja.json new file mode 100644 index 00000000..8d1e7b70 --- /dev/null +++ b/src/locales/ja.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0}のgithubアクティビティ", + "categoryLabels": { + "general": { + "heading": "一般的な統計と情報", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "リポジトリ", + "column-one": "非フォーク", + "column-two": "全て" + }, + "contributions": { + "heading": "貢献", + "column-one": "昨年", + "column-two": "合計" + }, + "languages": { + "heading": "公開リポジトリでの言語配布", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "入社年", + "featured": "注目のリポジトリ", + "mostStarred": "最もスター付きのリポジトリ", + "mostForked": "最もフォークされたリポジトリ", + "followers": "フォロワー", + "following": "続く", + "sponsors": "スポンサー", + "sponsoring": "主催", + "public": "所有リポジトリ", + "starredBy": "主演", + "forkedBy": "によるフォーク", + "watchedBy": "によって見られた", + "templates": "レンプレート", + "archived": "記録", + "commits": "専念", + "issues": "問題", + "prs": "プルリクエスト", + "reviews": "プルリクエストレビュー", + "contribTo": "に貢献しました", + "private": "個人的な貢献" + } +} \ No newline at end of file diff --git a/src/locales/ko.json b/src/locales/ko.json new file mode 100644 index 00000000..d0c3ee4e --- /dev/null +++ b/src/locales/ko.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0}의 GitHub 활동", + "categoryLabels": { + "general": { + "heading": "통계 및 정보", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "저장소", + "column-one": "직접 만든(Non-Forks)", + "column-two": "모두" + }, + "contributions": { + "heading": "기여", + "column-one": "지난해", + "column-two": "총" + }, + "languages": { + "heading": "공개 저장소 사용 언어 분포", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "가입 년도", + "featured": "추천 저장소", + "mostStarred": "Star를 가장 많이 받은 저장소", + "mostForked": "Fork가 가장 많이된 저장소", + "followers": "팔로워", + "following": "팔로잉", + "sponsors": "후원받은", + "sponsoring": "후원하는", + "public": "보유한 저장소", + "starredBy": "받은 Star", + "forkedBy": "Fork된 횟수", + "watchedBy": "Watch된 횟수", + "templates": "템플릿", + "archived": "보관 처리된(Archived) 저장소", + "commits": "커밋", + "issues": "이슈", + "prs": "풀 리퀘스트", + "reviews": "리뷰", + "contribTo": "기여 횟수", + "private": "비공개" + } +} \ No newline at end of file diff --git a/src/locales/lt.json b/src/locales/lt.json new file mode 100644 index 00000000..056555db --- /dev/null +++ b/src/locales/lt.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0} aktyvumas GitHub", + "categoryLabels": { + "general": { + "heading": "Bendra statistika ir informacija", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repozitorijos", + "column-one": "Neklonuotos", + "column-two": "Visos" + }, + "contributions": { + "heading": "Įnašai", + "column-one": "Praeitais metais", + "column-two": "Viso" + }, + "languages": { + "heading": "Kalbu pasiskirstymas viešosiose repozitorijose", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Prisijungimo metai", + "featured": "Siūloma repozitorija", + "mostStarred": "Labiausiai pažymėta repozitorija", + "mostForked": "Labiausiai klonuota repozitorija", + "followers": "Sekėjai", + "following": "Sekama", + "sponsors": "Remėjai", + "sponsoring": "Remiama", + "public": "Priklausančios repozitorijos", + "starredBy": "Pažymėta", + "forkedBy": "Klonuota", + "watchedBy": "Stebima", + "templates": "Šablonai", + "archived": "Archyvuota", + "commits": "Commits", + "issues": "Problemos", + "prs": "Pull Prašymai", + "reviews": "Pull prašymų peržiūros", + "contribTo": "Prisidėjo prie", + "private": "Privatūs įnašai" + } +} \ No newline at end of file diff --git a/src/locales/nl.json b/src/locales/nl.json new file mode 100644 index 00000000..d936d7cf --- /dev/null +++ b/src/locales/nl.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0}'s GitHub activiteiten", + "categoryLabels": { + "general": { + "heading": "Algemene statistieken en info", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repositories", + "column-one": "Non-Forks", + "column-two": "Alles" + }, + "contributions": { + "heading": "Bijdragen", + "column-one": "Dit jaar", + "column-two": "Totaal" + }, + "languages": { + "heading": "Talen distributies in Publieke Repositories", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Jaar van aanmelding", + "featured": "Uitgelichte repository", + "mostStarred": "Repository met meeste sterren", + "mostForked": "Repository met meeste forks", + "followers": "Volgers", + "following": "Volgend", + "sponsors": "Sponsoren", + "sponsoring": "Gesponsord", + "public": "Mijn Repositories", + "starredBy": "Ster gegeven door", + "forkedBy": "Geforkt door", + "watchedBy": "Gevolgd door", + "templates": "Sjablonen", + "archived": "Gearchiveerd", + "commits": "Commits", + "issues": "Problemen", + "prs": "Pull Requests", + "reviews": "Pull Request Recensies", + "contribTo": "Bijgedragen aan", + "private": "Prive Bijdragen" + } +} \ No newline at end of file diff --git a/src/locales/no.json b/src/locales/no.json new file mode 100644 index 00000000..2605874d --- /dev/null +++ b/src/locales/no.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0}s GitHub-aktivitet", + "categoryLabels": { + "general": { + "heading": "Generell statistikk og info", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Kodebaser", + "column-one": "Ikke-forgreninger", + "column-two": "Alle" + }, + "contributions": { + "heading": "Bidrag", + "column-one": "Forrige år", + "column-two": "Totalt" + }, + "languages": { + "heading": "Språkdistribusjon i offentlige kodebaser", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Ble med i år", + "featured": "Framhevet kodebase", + "mostStarred": "Kodebase med flest stjerner", + "mostForked": "Kodebase med flest forgreninger", + "followers": "Følgere", + "following": "Følger", + "sponsors": "Sponsorer", + "sponsoring": "Sponser", + "public": "Mine kodebaser", + "starredBy": "Stjernemerket av", + "forkedBy": "Forgrenet av", + "watchedBy": "Overvåket av", + "templates": "Maler", + "archived": "Arkivert", + "commits": "Commits", + "issues": "Saker", + "prs": "Pull Requests", + "reviews": "Pull Request-vurderinger", + "contribTo": "Bidro til", + "private": "Private bidrag" + } +} \ No newline at end of file diff --git a/src/locales/or.json b/src/locales/or.json new file mode 100644 index 00000000..b80a3f19 --- /dev/null +++ b/src/locales/or.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0}ର GitHub କାର୍ଯ୍ୟକଳାପ", + "categoryLabels": { + "general": { + "heading": "ସାଧାରଣ ପରିସଂଖ୍ୟାନ ଏବଂ ସୂଚନା", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "ସଂଗ୍ରହାଳୟ", + "column-one": "ଅଣ-ଫର୍କସ୍", + "column-two": "ସମସ୍ତ" + }, + "contributions": { + "heading": "ଅବଦାନ", + "column-one": "ବିଗତ ବର୍ଷ", + "column-two": "ମୋଟ" + }, + "languages": { + "heading": "ସର୍ବସାଧାରଣ ସଂଗ୍ରହାଳୟରେ ଭାଷା ବଣ୍ଟନ", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "ବର୍ଷ ଯୋଗଦାନ", + "featured": "ବୈଶିଷ୍ଟ୍ୟ ରେପୋ", + "mostStarred": "ସର୍ବାଧିକ ତାରକା ରେପୋ", + "mostForked": "ଅଧିକାଂଶ ଫୋର୍କଡ୍ ରେପୋ", + "followers": "ଅନୁସରଣକାରୀ", + "following": "ନିମ୍ନଲିଖିତ", + "sponsors": "ପ୍ରଯୋଜକ", + "sponsoring": "ପ୍ରାୟୋଜକ", + "public": "ମୋର ସଂଗ୍ରହାଳୟ", + "starredBy": "ଷ୍ଟାର୍ ହୋଇଥିବା", + "forkedBy": "ଦ୍ୱାରା କଣ୍ଟା ହୋଇଛି", + "watchedBy": "ଦେଖିଲା", + "templates": "ଟେମ୍ପଲେଟ୍", + "archived": "ସଂଗୃହିତ", + "commits": "ପ୍ରତିବଦ୍ଧତା", + "issues": "ସମସ୍ୟାଗୁଡିକ", + "prs": "ଅନୁରୋଧ ଟାଣନ୍ତୁ", + "reviews": "ଅନୁରୋଧ ସମୀକ୍ଷାଗୁଡିକ ଟାଣନ୍ତୁ", + "contribTo": "ଯୋଗଦାନ", + "private": "ବ୍ୟକ୍ତିଗତ ଅବଦାନ" + } +} \ No newline at end of file diff --git a/src/locales/pl.json b/src/locales/pl.json new file mode 100644 index 00000000..e914062d --- /dev/null +++ b/src/locales/pl.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "Aktywność {0} na GitHubie", + "categoryLabels": { + "general": { + "heading": "Ogólne statystyki i informacje", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repozytoria", + "column-one": "Non-Forks", + "column-two": "Wszystkie" + }, + "contributions": { + "heading": "Kontrybucje", + "column-one": "Ostatni rok", + "column-two": "Wszystkie" + }, + "languages": { + "heading": "Rozkład języków w Repozytoriach Publicznych", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Rok Dołączenia", + "featured": "Polecane repozytorium", + "mostStarred": "Repozytoria z największą ilością gwiazdek", + "mostForked": "Najczęściej Forkowane Repozytoria", + "followers": "Obserwujący", + "following": "Obserwowani", + "sponsors": "Sponsorzy", + "sponsoring": "Sponsoring", + "public": "Posiadane Repozytoria", + "starredBy": "Polubione przez", + "forkedBy": "Sforkowane przez", + "watchedBy": "Obserwowane przez", + "templates": "Szablony", + "archived": "Zarchiwizowane", + "commits": "Commity", + "issues": "Problemy", + "prs": "Pull Requesty", + "reviews": "Recenzje Pull Requestów", + "contribTo": "Kontrybuował Do", + "private": "Prywatne Kontrybucje" + } +} \ No newline at end of file diff --git a/src/locales/pt.json b/src/locales/pt.json new file mode 100644 index 00000000..f1118380 --- /dev/null +++ b/src/locales/pt.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "Atividade de {0} no GitHub", + "categoryLabels": { + "general": { + "heading": "Estatísticas Gerais e Informações", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repositórios", + "column-one": "Sem Forks", + "column-two": "Todos" + }, + "contributions": { + "heading": "Contribuições", + "column-one": "Último ano", + "column-two": "Total" + }, + "languages": { + "heading": "Distribuição de Linguagens em Repositórios Públicos", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Ano de Inscrição", + "featured": "Repositório em Primeiro Plano", + "mostStarred": "Repositório com mais estrelas", + "mostForked": "Repositório mais bifurcado", + "followers": "Seguidores", + "following": "A seguir", + "sponsors": "Patrocinado", + "sponsoring": "A patrocinar", + "public": "Repositórios Possuídos", + "starredBy": "Com Estrela De", + "forkedBy": "Bifurcado Por", + "watchedBy": "Visto Por", + "templates": "Modelos", + "archived": "Arquivados", + "commits": "Commits", + "issues": "Problemas", + "prs": "Pull Requests", + "reviews": "Avaliação de Pull Requests", + "contribTo": "Contribuiu Para", + "private": "Contribuições Privadas" + } +} \ No newline at end of file diff --git a/src/locales/ro.json b/src/locales/ro.json new file mode 100644 index 00000000..cdb31739 --- /dev/null +++ b/src/locales/ro.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "Activitatea GitHub a lui {0}", + "categoryLabels": { + "general": { + "heading": "Statistici generale și informații", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Depozitele", + "column-one": "Non-bifurcatii", + "column-two": "Toate" + }, + "contributions": { + "heading": "Contribuții", + "column-one": "Anul trecut", + "column-two": "Total" + }, + "languages": { + "heading": "Distribuția limbii în arhivele publice", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "An alăturat", + "featured": "Repo recomandate", + "mostStarred": "Cel mai marcat Repo", + "mostForked": "Repo cel mai bifurcat", + "followers": "Urmaritori", + "following": "Ca urmare a", + "sponsors": "Sponsori", + "sponsoring": "Sponsorizare", + "public": "Arhivele mele", + "starredBy": "Înscris de", + "forkedBy": "Bifurcat de", + "watchedBy": "Vizionat de", + "templates": "Șabloane", + "archived": "Arhivat", + "commits": "Commits", + "issues": "Probleme", + "prs": "Solicitări de tragere", + "reviews": "Recenzii Pull Request", + "contribTo": "Contribuit la", + "private": "Contribuții private" + } +} \ No newline at end of file diff --git a/src/locales/ru.json b/src/locales/ru.json new file mode 100644 index 00000000..73564bde --- /dev/null +++ b/src/locales/ru.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "Активность пользователя {0} на гитхабе", + "categoryLabels": { + "general": { + "heading": "Общая статистика и информация", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Статистика репозиториев", + "column-one": "Без форков", + "column-two": "Все" + }, + "contributions": { + "heading": "Работа в репозиториях", + "column-one": "За последний год", + "column-two": "За все время" + }, + "languages": { + "heading": "Использование языков в общедоступных репозиториях", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Год регистрации на гитхабе", + "featured": "Избранное репо", + "mostStarred": "Самое популярное репо", + "mostForked": "Самое клонированное репо", + "followers": "Подписчики", + "following": "Подписан", + "sponsors": "Спонсоры", + "sponsoring": "Спонсирует", + "public": "Собственные репозитории", + "starredBy": "Добавили в избранное", + "forkedBy": "Клонирован", + "watchedBy": "Наблюдатели", + "templates": "Шаблоны", + "archived": "Заархивировано", + "commits": "Коммиты", + "issues": "Проблемы", + "prs": "Пулл реквесты", + "reviews": "Ревью пулл реквестов", + "contribTo": "Участие в", + "private": "Приватные изменения" + } +} \ No newline at end of file diff --git a/src/locales/sat.json b/src/locales/sat.json new file mode 100644 index 00000000..455eb9cc --- /dev/null +++ b/src/locales/sat.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0}ᱟᱜ ᱜᱤᱴᱦᱚᱵᱽ ᱠᱟᱹᱢᱤᱦᱚᱨᱟᱠᱚ", + "categoryLabels": { + "general": { + "heading": "ᱥᱟᱫᱷᱟᱨᱚᱬ ᱵᱟᱛᱟᱣ ᱟᱨ ᱵᱤᱵᱨᱚᱬ", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "ᱜᱩᱫᱟᱢ", + "column-one": "ᱵᱤᱱ ᱯᱷᱚᱨᱠ ᱠᱚ", + "column-two": "ᱢᱩᱴ" + }, + "contributions": { + "heading": "ᱮᱱᱮᱢᱤᱭᱟᱹᱠᱚ", + "column-one": "ᱪᱟᱞᱟᱣᱮᱱ ᱥᱮᱨᱢᱟᱸ", + "column-two": "ᱢᱩᱴ" + }, + "languages": { + "heading": "ᱥᱟᱱᱟᱢ ᱜᱩᱫᱟᱢ ᱨᱮ ᱯᱟᱹᱨᱥᱤ ᱠᱚᱣᱟᱜ ᱯᱟᱥᱱᱟᱣ", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "ᱥᱮᱞᱮᱫ ᱥᱮᱨᱢᱟᱸ", + "featured": "ᱵᱤᱥᱮᱥ ᱜᱩᱫᱟᱢ", + "mostStarred": "ᱡᱷᱚᱛᱚ ᱠᱷᱚᱱ ᱰᱷᱮᱨ ᱪᱤᱱᱦᱟᱹ ᱦᱟᱜ ᱜᱩᱫᱟᱹᱢ", + "mostForked": "ᱡᱟᱹᱥᱛᱤ ᱱᱚᱠᱚᱞ ᱠᱟᱱ ᱜᱚᱫᱟᱢ", + "followers": "ᱯᱟᱧᱡᱟ ᱠᱩᱜ", + "following": "ᱯᱟᱧᱡᱟ ᱮᱫᱟᱢ", + "sponsors": "ᱨᱚᱠᱚᱢᱤᱭᱟᱹ", + "sponsoring": "ᱨᱚᱠᱚᱢᱚᱜ ᱠᱟᱱᱟ", + "public": "ᱤᱧᱟᱜ ᱜᱩᱫᱟᱢ ᱠᱚ", + "starredBy": "ᱪᱤᱱᱦᱟᱹᱤᱭᱟᱹ", + "forkedBy": "ᱱᱚᱠᱚᱞᱤᱭᱟᱹ", + "watchedBy": "ᱛᱤᱱᱹᱜ ᱠᱚ ᱧᱮᱞ ᱠᱟᱫᱟ", + "templates": "ᱪᱷᱟᱸᱪᱠᱚ", + "archived": "ᱜᱟᱵᱟᱱᱮᱱᱟ", + "commits": "ᱰᱟᱞᱟᱣᱠᱚ", + "issues": "ᱯᱚᱞᱚᱡᱽᱠᱚ", + "prs": "ᱚᱨ ᱱᱮᱦᱚᱨᱠᱚ", + "reviews": "ᱚᱨ ᱱᱮᱦᱚᱨ ᱧᱮᱞᱯᱚᱨᱚᱠᱷ ᱠᱚ", + "contribTo": "ᱮᱱᱮᱢ", + "private": "ᱱᱤᱡᱚᱨᱟᱜ ᱩᱠᱩ ᱮᱱᱮᱢᱠᱚ" + } +} \ No newline at end of file diff --git a/src/locales/sr.json b/src/locales/sr.json new file mode 100644 index 00000000..95f14bc4 --- /dev/null +++ b/src/locales/sr.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0} - Aktivnost na Githabu", + "categoryLabels": { + "general": { + "heading": "Opšta statistika i informacije", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repozitoriji", + "column-one": "Ne-forkovani", + "column-two": "Svi" + }, + "contributions": { + "heading": "Doprinosi", + "column-one": "Prošla godina", + "column-two": "Ukupno" + }, + "languages": { + "heading": "Zastupljenost jezika u javnim repozitorijima", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Godina pristupa", + "featured": "Izabrani repozitorij", + "mostStarred": "Najviše zvezdica na repou", + "mostForked": "Najviše forkovan repo", + "followers": "Pratilaca", + "following": "Prati", + "sponsors": "Sponzori", + "sponsoring": "Sponzoriše", + "public": "Lični repozitoriji", + "starredBy": "Dodeljenih zvezdica", + "forkedBy": "Broj forkovanja", + "watchedBy": "Pregledi", + "templates": "Šabloni", + "archived": "Arhive", + "commits": "Komiti", + "issues": "Problemi", + "prs": "Pul zahtevi", + "reviews": "Revizije pul zahteva", + "contribTo": "Doprinosi", + "private": "Privatni doprinosi" + } +} \ No newline at end of file diff --git a/src/locales/th.json b/src/locales/th.json new file mode 100644 index 00000000..91cf68e4 --- /dev/null +++ b/src/locales/th.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "กิจกรรมของ {0} บน GitHub", + "categoryLabels": { + "general": { + "heading": "สถิติและข้อมูลทั่วไป", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Repositories", + "column-one": "ที่ไม่ใช่ Fork", + "column-two": "รวมทั้งหมด" + }, + "contributions": { + "heading": "Contributions", + "column-one": "ปีที่แล้ว", + "column-two": "รวมทั้งหมด" + }, + "languages": { + "heading": "ภาษาที่ใช้ใน Repo สาธารณะ", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "ปีที่เข้าร่วม", + "featured": "Repo ที่โดดเด่น", + "mostStarred": "Repo ที่ติดดาวมากที่สุด", + "mostForked": "Repo ที่มีการ Fork มากที่สุด", + "followers": "ผู้ติดตาม", + "following": "กำลังติดตาม", + "sponsors": "ผู้สนับสนุน", + "sponsoring": "กำลังสนับสนุน", + "public": "Repo ทั้งหมดของฉัน", + "starredBy": "ติดดาวทั้งหมด", + "forkedBy": "มีการ Fork ทั้งหมด", + "watchedBy": "ผู้ติดตาม", + "templates": "เทมเพลตแม่แบบ", + "archived": "เก็บถาวร", + "commits": "คอมมิท", + "issues": "ปัญหา", + "prs": "Pull Requests", + "reviews": "รีวิว Pull Request", + "contribTo": "มีการช่วยไปแล้ว", + "private": "Contributions ส่วนตัว" + } +} \ No newline at end of file diff --git a/src/locales/tr.json b/src/locales/tr.json new file mode 100644 index 00000000..a5c523b3 --- /dev/null +++ b/src/locales/tr.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0}'in GitHub Etkinliği", + "categoryLabels": { + "general": { + "heading": "Genel İstatistikler ve Bilgiler", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Depolar", + "column-one": "Çatalsız", + "column-two": "Tüm" + }, + "contributions": { + "heading": "Katkılar", + "column-one": "Geçen sene", + "column-two": "Total" + }, + "languages": { + "heading": "Genel Depolarda Dil Dağılımı", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Katıldığı Yıl", + "featured": "Öne Çıkan Repo", + "mostStarred": "En Çok Yıldızlı Repo", + "mostForked": "En Çatallı Repo", + "followers": "Takipçiler", + "following": "Takip etmek", + "sponsors": "Sponsorlar", + "sponsoring": "Sponsorluk", + "public": "Sahip Olunan Depolar", + "starredBy": "Tarafından yıldız", + "forkedBy": "Tarafından çatallandı", + "watchedBy": "İzleyen", + "templates": "Sablonlar", + "archived": "Arşivlenmiş", + "commits": "Taahhütler", + "issues": "Sorunlar", + "prs": "Çekme İstekleri", + "reviews": "İstek İncelemelerini Çekin", + "contribTo": "Katkıda Bulunanlar", + "private": "Özel Katkılar" + } +} \ No newline at end of file diff --git a/src/locales/uk.json b/src/locales/uk.json new file mode 100644 index 00000000..801565af --- /dev/null +++ b/src/locales/uk.json @@ -0,0 +1,47 @@ +{ + "titleTemplate": "{0} активностей на GitHub", + "categoryLabels": { + "general": { + "heading": "Загальна статистика та інформація", + "column-one": null, + "column-two": null + }, + "repositories": { + "heading": "Репозиторіїв", + "column-one": "Без форків", + "column-two": "Всі" + }, + "contributions": { + "heading": "Внески", + "column-one": "За останній рік", + "column-two": "Всього" + }, + "languages": { + "heading": "Використання мов у загальнодоступних репозиторіях", + "column-one": null, + "column-two": null + } + }, + "statLabels": { + "joined": "Рік приєднання", + "featured": "Вибрані ререпозиторії", + "mostStarred": "Найпопулярніший репозиторій", + "mostForked": "Найбільш клонований репозиторій", + "followers": "Підписники", + "following": "Підписки", + "sponsors": "Спонсори", + "sponsoring": "Спонсорство", + "public": "Власні репозиторії", + "starredBy": "Відмітили", + "forkedBy": "Клонували", + "watchedBy": "Підписники", + "templates": "Шаблони", + "archived": "Заархівовано", + "commits": "Комміти", + "issues": "Проблеми", + "prs": "Пулл реквести", + "reviews": "Огляди пулл реквестів", + "contribTo": "Участь в", + "private": "Приватна участь" + } +} \ No newline at end of file diff --git a/tests/tests.py b/tests/tests.py index 78aa43e3..584aaf47 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,6 +1,6 @@ # user-statistician: Github action for generating a user stats card # -# Copyright (c) 2021-2022 Vincent A Cicirello +# Copyright (c) 2021-2023 Vincent A Cicirello # https://www.cicirello.org/ # # MIT License @@ -32,7 +32,8 @@ from StatsImageGenerator import StatsImageGenerator from UserStatistician import writeImageToFile from Colors import * -from StatConfig import * +import StatConfig +from StatConfig import loadLocale, supportedLocales, icons, categoryOrder, statsByCategory from ColorUtil import isValidColor, _namedColors, highContrastingColor, contrastRatio from TextLength import * import copy @@ -41,6 +42,12 @@ outputSampleSVG = False localeCode = "en" +# Adjust the location of the locales for running the tests +# (when the action is running, the locales directory is a +# root level directory in the Docker container, but within +# src folder during unit testing outside of the container). +StatConfig._locale_directory = "src" + StatConfig._locale_directory + executedQueryResultsOriginal = [ {'data': {'user': {'contributionsCollection': {'totalCommitContributions': 3602, 'totalIssueContributions': 79, 'totalPullRequestContributions': 289, 'totalPullRequestReviewContributions': 315, 'totalRepositoryContributions': 18, 'restrictedContributionsCount': 105, 'contributionYears': [2021, 2020, 2019, 2018, 2017, 2016, 2015, 2014, 2013, 2012, 2011]}, 'followers': {'totalCount': 9}, 'following': {'totalCount': 7}, 'issues': {'totalCount': 81}, 'login': 'someuser', 'name': 'Firstname M. Lastname', 'pullRequests': {'totalCount': 289}, 'repositoriesContributedTo': {'totalCount': 3}, 'sponsorshipsAsMaintainer': {'totalCount': 7}, 'sponsorshipsAsSponsor': {'totalCount': 5}}}}, @@ -190,11 +197,17 @@ def test_color_contrast_text_vs_bg(self) : self.assertTrue(crTitle >= 4.5, msg=theme+" "+str(crTitle)) def test_title_templates(self) : + # For the title template, {0} corresponds to repository + # owner's name. Some languages may not have the equivalent + # of English's apostrophe for possession. For those, use an + # approach like that used in locale sr. The test cases enforce + # having the user's name somewhere in the title template. unlikelyInTemplate = "qwertyuiop" try : for locale in supportedLocales : - title = titleTemplates[locale].format(unlikelyInTemplate) - self.assertTrue(titleTemplates[locale].find("{0}") < 0 or title.find(unlikelyInTemplate)>=0) + template = loadLocale(locale)["titleTemplate"] + title = template.format(unlikelyInTemplate) + self.assertTrue(template.find(unlikelyInTemplate) < 0 and title.find(unlikelyInTemplate)>=0) except IndexError : self.fail() @@ -220,12 +233,18 @@ def test_category_labels(self) : categories = categoryOrder types = {"heading", "column-one", "column-two"} for locale in supportedLocales : - self.assertTrue(locale in categoryLabels) - labelMap = categoryLabels[locale] + labelMap = loadLocale(locale)["categoryLabels"] for cat in categories : self.assertTrue(cat in labelMap) for t in types : self.assertTrue(t in labelMap[cat]) + self.assertTrue(isinstance(labelMap[cat]["heading"], str)) + if cat == "repositories" or cat == "contributions": + self.assertTrue(isinstance(labelMap[cat]["column-one"], str)) + self.assertTrue(isinstance(labelMap[cat]["column-two"], str)) + else: + self.assertEqual(None, labelMap[cat]["column-one"]) + self.assertEqual(None, labelMap[cat]["column-two"]) def test_stat_labels(self) : keys = { @@ -235,14 +254,23 @@ def test_stat_labels(self) : "forkedBy", "watchedBy", "templates", "archived", "commits", "issues", "prs", "reviews", "contribTo", "private" } - self.assertTrue(all(k in statLabels for k in keys)) + for locale in supportedLocales : + labelMap = loadLocale(locale)["statLabels"] + self.assertTrue(all(k in labelMap for k in keys)) + for k in keys : + self.assertTrue(isinstance(labelMap[k], str)) + + def test_icons(self): + keys = { + "joined", "featured", "mostStarred", "mostForked", + "followers", "following", "sponsors", "sponsoring", + "public", "starredBy", + "forkedBy", "watchedBy", "templates", "archived", "commits", + "issues", "prs", "reviews", "contribTo", "private" + } for k in keys : - self.assertTrue("icon" in statLabels[k]) - self.assertTrue(statLabels[k]["icon"].startswith("")) - labelsByLocale = statLabels[k]["label"] - for locale in supportedLocales : - self.assertTrue(locale in labelsByLocale) + self.assertTrue(icons[k].startswith("")) def test_isValidColor(self) : for colorName, colorHex in _namedColors.items() : diff --git a/util/refactor-locales-to-json.py b/util/refactor-locales-to-json.py new file mode 100644 index 00000000..e7698f9e --- /dev/null +++ b/util/refactor-locales-to-json.py @@ -0,0 +1,41 @@ +# +# user-statistician: Github action for generating a user stats card +# +# Copyright (c) 2021-2023 Vincent A Cicirello +# https://www.cicirello.org/ +# +# MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +from StatConfig import * +import json + +if __name__ == "__main__": + for locale in supportedLocales: + combined = { + "titleTemplate" : titleTemplates[locale], + "categoryLabels" : categoryLabels[locale], + "statLabels" : { key : value["label"][locale] for key, value in statLabels.items() } + } + print("Processing", locale) + with open("locales/" + locale + ".json", "w", encoding='utf8') as f : + json.dump(combined, f, ensure_ascii=False, indent=2) +