- How Logen fits in your toolchain
- Public repository checklist
- End-to-end flows (manual vs remote)
- Integration guide
- GitHub Actions from zero
- Troubleshooting
- Google Cloud and Sheets access
- CLI reference
- How to set up (Python and Xcode)
- How to use (spreadsheet rules)
- Code reference
Logen is a Python script (logen2.py) that you add to your app project yourself—for example by cloning or downloading this repository into a folder inside your app repo, or by adding it as a git submodule. It is not integrated via Swift Package Manager (SPM). The script reads your localization matrix (from Google Sheets in CI, or from a CSV export on a developer machine) and writes .strings and .stringsdict files into your app’s existing {Project}.lproj folder structure.
Design goals:
- No per-developer Google OAuth — nobody stores OAuth client JSON or refresh tokens on laptops for Logen.
- CI-friendly secrets — production reads use a service account JSON in the environment as
LOGEN_SA_JSON(paste the full key JSON; GitHub Actions supports multiline secrets). - Offline / ad-hoc local runs — use
--csvafter downloading the sheet as CSV from the browser.
Complete this before changing the Logen repo (or any clone that ever held keys) to public:
- Rotate and revoke any Google OAuth client secrets or service account keys that ever appeared in a commit—even if later deleted from the tip of a branch. Public repos expose full git history.
- Remove committed key files from history if required by your security policy (e.g.
git filter-repo); rotating keys in GCP is mandatory regardless. - Confirm
credentials.jsonand similar are not in the default branch and are listed in.gitignore. - After the repo is public, app workflows can
actions/checkoutLogen without a PAT; private Logen still needs atoken:on that checkout step.
Use this when you want to iterate quickly without wiring CI, or when you cannot use the service account locally. You must not have LOGEN_SA_JSON set while using --csv, or Logen will exit with an error.
sequenceDiagram
autonumber
participant Editor as SpreadsheetEditor
participant Sheets as GoogleSheets
participant Dev as Developer
participant Logen as LogenScript
participant Tree as ProjectWorkspace
Editor->>Sheets: Update copy and keys in the shared sheet
Dev->>Sheets: File then Download then CommaSeparatedValues
Sheets-->>Dev: UTF8 CSV file for the active tab
Dev->>Dev: Unset LOGEN_SA_JSON in shell
Dev->>Tree: Save CSV path e.g. next to the repo or in tmp
Dev->>Logen: python3 logen2.py --csv path --project MyApp
Logen->>Tree: Write strings under MyApp/Resources/Localization
Logen-->>Dev: Print success and language list
Dev->>Tree: git diff, commit, or discard changes
Important: Run logen2.py with the current working directory set to the parent of your Xcode project folder (the folder whose name matches --project). The script builds paths as /{project}/{localization_path}/{lang}.lproj/... from cwd.
Use this as the team default: one secret, reproducible runs, and developers pull updated strings. The diagram assumes workflow_dispatch (or a similar guarded trigger); adjust names to your repo.
sequenceDiagram
autonumber
participant Human as MaintainerOrBot
participant GHA as GitHubActions
participant Secrets as GitHubSecrets
participant API as GoogleSheetsAPI
participant Logen as LogenScript
participant Repo as GitRepository
Human->>GHA: Run workflow e.g. workflow_dispatch
GHA->>Secrets: Load LOGEN_SA_JSON into job env
GHA->>Repo: actions/checkout with fetch depth for push if needed
GHA->>GHA: setup-python and pip install Google client libs
GHA->>Logen: Run from parent of Xcode project folder
Logen->>API: spreadsheets.values.get with service account JWT
API-->>Logen: Rows for configured range
Logen->>Repo: Overwrite matching strings files on disk
Logen-->>GHA: Exit zero on success
GHA->>Repo: Optional git commit push or open PR
Repo-->>Human: Updated branch or PR
Note over Human: Other developers git pull or merge PR
Logen is not distributed as an SPM dependency. Pick how the app repo (and CI) get logen2.py.
Option C — CI-only dual checkout (recommended)
Keep no Logen files in the app source tree. In GitHub Actions, actions/checkout the app repo, then actions/checkout this public Logen repo into _logen/, run python3 _logen/Logen/logen2.py, and commit only localization outputs.
- Copy the ready-made workflow from
examples/ci-dual-checkout/into your app repo (see that folder’sREADME.mdandregenerate-localizations.yml). - Add
_logen/to your app’s.gitignoreusing the snippet inexamples/ci-dual-checkout/APP_REPO_DOTGITIGNOREso the second checkout is never committed. - See Public repository checklist before publishing Logen.
Option A — Vendored folder
- Clone or download this repository.
- Copy at least
logen2.pyinto your app repo (e.g.Scripts/logen2.py). - Commit; refresh the file when Logen updates.
Option B — Git submodule
git submodule add https://github.com/YourOrg/Logen.git Vendor/Logen
git submodule update --init --recursiveThe script path is typically Vendor/Logen/Logen/logen2.py. In CI, use actions/checkout with submodules: true.
Use one stable path in any Run Script phases and in python3 … commands.
Follow How to use → Spreadsheet setup: include a column whose header contains iOS, and use two-letter language codes for locale columns. Keep one tab per run (or run Logen multiple times with different --sheet values if you split locales across tabs).
For each language you generate, the script expects an existing path:
<ParentOfXcodeProject>/<ProjectName>/<LocalizationPath>/<xx>.lproj/
Default LocalizationPath is Resources/Localization. Example:
MyAppRepo/
MyApp/ ← --project MyApp
Resources/
Localization/
en.lproj/
Localizable.strings
sk.lproj/
Localizable.strings
Create missing .lproj folders in Xcode (add empty Localizable.strings if needed) before the first run so prepare_path can resolve targets.
| Mode | When to use | Data source | Env vars |
|---|---|---|---|
| CSV | Local only, no SA on machine | --csv /path |
LOGEN_SA_JSON must be unset |
| Sheets API | CI or trusted runner | --id, --sheet, --last_column |
LOGEN_SA_JSON (full service account JSON string) |
You cannot combine --csv with LOGEN_SA_JSON in the same invocation.
Complete Google Cloud and Sheets access first, then follow GitHub Actions from zero below. The canonical workflow file for the recommended dual checkout setup lives in examples/ci-dual-checkout/regenerate-localizations.yml—copy it into your app repo and edit repository:, ref:, and Logen CLI arguments.
What this is: A workflow is a YAML file in your repo that tells GitHub’s servers to run commands when something happens (here: when you click Run workflow). You do not install anything on your laptop for the job itself—GitHub runs it in a temporary virtual machine.
What you need: A GitHub repo with your app source code, permission to change Settings, and permission to push to the branch the workflow will update (often main).
“How does GitHub Actions get logen2.py?”
The workflow only sees what actions/checkout brings into the job workspace.
| Approach | Best for | Idea |
|---|---|---|
| C. Dual checkout (recommended) | No Logen files in the app repo; public Logen | Second actions/checkout of this repo into _logen/, then python3 _logen/Logen/logen2.py. Add _logen/ to the app .gitignore. |
| A. Vendored script | Simplest disk layout | Scripts/logen2.py (or similar) committed in the app repo. |
| B. Git submodule | Logen pinned as a submodule | Submodule + submodules: true on the app checkout. |
The Step 4 template below follows Option C. For A or B, change the python3 path (and remove the second checkout when using A only).
- On GitHub, open your app repository (not necessarily the Logen package repo).
- Click Settings (top bar of the repo).
- In the left sidebar, open Secrets and variables → Actions.
- Click New repository secret.
- Name: type exactly:
LOGEN_SA_JSON(all caps, underscores as shown). - Secret: open the service account
.jsonfile from Google in a text editor, Select All, Copy, Paste into the secret field. It can be multiple lines—that is OK. - Click Add secret.
You will not see the secret again after saving (GitHub only shows “Updated”). To change it, delete the secret and create a new one, or use Update.
A workflow file must live under .github/workflows/ and end in .yml or .yaml.
- On your computer, open your app repo clone in an editor (or use GitHub’s Add file → Create new file in the browser).
- Create folders
.githubthenworkflowsif they do not exist. - Create a new file, for example:
.github/workflows/regenerate-localizations.yml - Copy from
examples/ci-dual-checkout/regenerate-localizations.ymlor paste the Step 4 template below. ReplaceOWNER/Logen,ref:, spreadsheet args,CoffeeShop, andgit addpaths. - Commit to
main(or your default branch) and push.
Until this file exists on the default branch, the workflow may not appear under the Actions tab for workflow_dispatch.
Uses a second checkout of this (Logen) repo into _logen/ so the app repo does not need to contain logen2.py. Add _logen/ to your app .gitignore (see examples/ci-dual-checkout/APP_REPO_DOTGITIGNORE). Replace OWNER/Logen, ref:, --project, sheet args, and git add paths.
# .github/workflows/regenerate-localizations.yml (in your APP repo)
name: Regenerate localizations
on:
workflow_dispatch:
permissions:
contents: write
jobs:
logen:
runs-on: ubuntu-latest
steps:
- name: Check out app repository
uses: actions/checkout@v4
with:
persist-credentials: true
- name: Check out public Logen
uses: actions/checkout@v4
with:
repository: OWNER/Logen
path: _logen
ref: main
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Google API libraries
run: pip install google-api-python-client google-auth google-auth-httplib2
- name: Run Logen
env:
LOGEN_SA_JSON: ${{ secrets.LOGEN_SA_JSON }}
working-directory: ${{ github.workspace }}
run: |
python3 _logen/Logen/logen2.py \
--id "${{ vars.LOGEN_SHEET_ID }}" \
--sheet "Strings" \
--first_row 2 \
--last_column H \
--project "CoffeeShop"
- name: Commit and push if files changed
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -- "CoffeeShop/Resources/Localization/"
if git diff --staged --quiet; then
echo "No localization changes to commit."
else
git commit -m "chore: regenerate localizations"
git push
fiWhy working-directory: ${{ github.workspace }}: Must be the parent of your Xcode project folder so paths like ./CoffeeShop/Resources/Localization/ resolve.
Why _logen/Logen/logen2.py: Matches this repository’s layout (Logen/logen2.py at clone root _logen). If Logen is private, add token: ${{ secrets.LOGEN_READ_PAT }} (or similar) to the Check out public Logen step. For vendored (A) or submodule (B) setups, change the python3 path and drop the second checkout when appropriate.
- On GitHub, open the repo → tab Actions.
- In the left sidebar, click the workflow name Regenerate localizations (the
name:from YAML). - On the right, click Run workflow → leave branch as
main(or your default) → Run workflow. - A row appears in the list; click it, then click the
logenjob to see logs line by line.
If something fails, open the red ❌ step and read the last lines—they usually contain Python’s error message.
Permission denied/403: The defaultGITHUB_TOKENmay not be allowed to push tomain. Fix: Settings → Actions → General → Workflow permissions → enable Read and write permissions, and allow GitHub Actions to create and approve pull requests if your org policy requires it—or use a Personal Access Token stored as another secret and configureactions/checkoutwithtoken: ${{ secrets.YOUR_PAT }}.- Branch protection blocking direct pushes: remove the Commit and push step for now and only inspect the generated files from the workflow artifact, or switch to a workflow that opens a pull request instead of pushing to
main(search GitHub forcreate-pull-requestaction examples).
If you do not want the spreadsheet ID in the committed file:
- Settings → Secrets and variables → Actions → tab Variables → New repository variable.
- Name e.g.
LOGEN_SHEET_ID, value = the ID from the sheet URL. - In YAML, replace the
--idvalue with"${{ vars.LOGEN_SHEET_ID }}"(notevars, notsecrets). Example variable value:1Bx8fJsqTziA7kqBLNzboFsOgDefGhIjKlMnOpQr0123(still use your real id).
| Term | Meaning |
|---|---|
| Workflow | The whole YAML file under .github/workflows/. |
| Job | A named block like logen:; runs on one machine. |
| Step | One item under steps:; often run: shell commands or uses: a premade action. |
workflow_dispatch |
Manual trigger only (no automatic runs until you click Run). |
secrets.LOGEN_SA_JSON |
Injects the secret you created in Settings as an environment variable for that step. |
If you want a one-click regenerate without secrets in Xcode, add an Aggregate target whose Run Script only invokes --csv pointing at a file path (ignored by git or refreshed by the developer). Do not embed LOGEN_SA_JSON in the project file.
- Service account email has Viewer (or higher) access on the spreadsheet.
-
--sheetmatches the exact tab name in Google Sheets. -
--last_columncovers all locale columns you need. -
--projectmatches the Xcode project / folder name under the parent directory used ascwd. -
LOGEN_SA_JSONparses as JSON withtypeservice_account. - CSV export uses the same tab you would pass as
--sheetin API mode.
| Symptom | Likely cause | What to do |
|---|---|---|
use either --csv or LOGEN_SA_JSON, not both |
Shell still exports LOGEN_SA_JSON locally |
unset LOGEN_SA_JSON (or a clean subshell) before --csv. |
choose one data source |
LOGEN_SA_JSON not set in Sheets mode |
In the workflow step, set env: LOGEN_SA_JSON: ${{ secrets.LOGEN_SA_JSON }}. |
NO DATA FOUND IN RANGE / empty API result |
Wrong --sheet, --last_column, or --first_row |
Open the sheet and confirm tab name and last column letter; widen --last_column. |
CSV file not found |
Wrong path relative to where you run Python | Pass an absolute path or cd to the directory containing the CSV first. |
Localization file not found warnings |
Missing .lproj directory on disk |
Create xx.lproj folders under Resources/Localization (or your --localization_path) in Xcode. |
403 or permission errors from Google API |
Sheet not shared with the service account | Share the spreadsheet with the SA email from the JSON client_email. |
LOGEN_SA_JSON is not valid JSON |
Truncated or bad paste in the secret | Re-paste the full JSON from the .json key file in GitHub → Settings → Secrets. |
- In Google Cloud Console, select or create a project.
- Enable Google Sheets API for that project (APIs & Services → Library).
- IAM & Admin → Service Accounts → Create service account (dedicated account for Logen is recommended).
- Keys → Add key → JSON; download the key once. Do not commit this file.
- Share each Google Sheet with the service account’s email (
something@...iam.gserviceaccount.com) as Viewer (read-only matches Logen’s scope). - Add the key to GitHub: create a secret
LOGEN_SA_JSONand paste the full JSON from the downloaded key file. Never commit the key file to git.
Rotate keys by creating a new key, updating the secret, deleting the old key in GCP, and re-running the workflow.
First of all you need to install python 3.9 on your machine:
Recommended way of installing python on macOS is via Homebrew Package Manager. You can grab Homebrew from the official website:
https://brew.sh
Use Homebrew to download the latest Python 3 version using the following command:
brew install python
Check if Python is available and installed correctly. You should see a path to the Python binary in your system.
which python3
Check if you have pip python package manager installed. The path should lead to the same directory as the command above.
which pip3
Add Logen without Swift Package Manager:
- If you use CI-only dual checkout, you do not need
logen2.pyon disk for Xcode—strings arrive via git pull after the workflow runs. For local runs (optional), follow integration guide §1 so the script exists under your app project (e.g.${PROJECT_DIR}/Scripts/logen2.py) or via a submodule path. - Optional: create an Aggregate target with a Run Script phase that invokes
python3with--csvand paths relative to${PROJECT_DIR}, or run the same command from Terminal.
Xcode does not need a Logen entry in Package.swift; the script is plain Python and is not linked into your app binary.
Logen supports exactly one data source per run:
-
CI / API (Google Sheets) — set
LOGEN_SA_JSONto the full service account JSON string (paste as plain text). The service account must have access to the spreadsheet (share the sheet with its client email). Then pass--id,--sheet,--last_column, and--projectas below. Do not setLOGEN_SA_JSONwhen using local CSV mode. -
Local (no Google credentials) — in Google Sheets use File → Download → Comma-separated values (.csv) for the correct tab, then run with
--csv /path/to/export.csv. IfLOGEN_SA_JSONis set in your shell, unset it for this run (the script errors if CSV andLOGEN_SA_JSONare combined).
Recommended team flow: regenerate strings in GitHub Actions (or another CI) with LOGEN_SA_JSON as a secret, commit or open a PR, and developers pull updated localizations. Avoid putting the service account secret into Xcode Run Script phases on every machine.
Optional: you can still add an Aggregate target with a Run Script phase that only runs --csv … against a path inside the repo (e.g. a checked-in or ignored export), if you want a one-click local regenerate without API keys.
| Parameters | Required | Description |
|---|---|---|
| --project | yes | Project name. Use "${PROJECT_NAME}" when invoking from Xcode. |
| --csv | no* | Path to a UTF-8 CSV export of the sheet. *Required for local mode (mutually exclusive with LOGEN_SA_JSON). |
| --id | no* | Spreadsheet ID from the sheet URL (e.g. https://docs.google.com/spreadsheets/d/ASDFGHJKL/edit). *Required with Sheets API when --csv is not used. |
| --sheet | no* | Tab name (bottom of the sheet). *Required with Sheets API when --csv is not used. |
| --last_column | no* | Last column letter with data (e.g. G). *Required with Sheets API when --csv is not used. |
| --first_row | no | First data row (default 1). |
| --localization_path | no | Path inside the project directory for output (no leading/trailing slash). Default: Resources/Localization. |
| --filename | no | Base name for .strings / .stringsdict files. Default: Localizable. |
These are illustrative — replace the spreadsheet --id with the real value from your sheet URL (same string as in the URL path). The app folder CoffeeShop must match a real directory under the directory where you run the command.
Browser URL (illustration):
https://docs.google.com/spreadsheets/d/1Bx8fJsqTziA7kqBLNzboFsOgDefGhIjKlMnOpQr0123/edit#gid=0
└────────────────────────── spreadsheetId ──────────────────────────┘
Sheets API run in GitHub Actions (inside the workflow run: step; dual checkout; LOGEN_SA_JSON from Secrets):
run: |
python3 _logen/Logen/logen2.py \
--id "${{ vars.LOGEN_SHEET_ID }}" \
--sheet "Strings" \
--first_row 2 \
--last_column H \
--project "CoffeeShop"Use a literal --id "..." instead of vars if you prefer. Same fragment appears in Step 4 above.
Sheets API run locally (vendored script at Scripts/logen2.py):
cd /path/to/parent-of-xcode-project # e.g. repo root that contains CoffeeShop/
python3 Scripts/logen2.py \
--id "1Bx8fJsqTziA7kqBLNzboFsOgDefGhIjKlMnOpQr0123" \
--sheet "Strings" \
--first_row 2 \
--last_column H \
--project "CoffeeShop"--id— the segment after/d/in the URL (no slashes; Google’s ids are often 44 characters, as in the example below).--sheet— the tab name at the bottom of the spreadsheet (must match exactly, including spaces and case).--first_row— here2means “data starts on row 2” (row 1 is headers / description rows).--last_column—Hmeans the range ends at column H (soA…His read for each row).--project— Xcode project folder name undercwd(hereCoffeeShop), so outputs go underCoffeeShop/Resources/Localization/.
Local CSV run (no Google env vars set):
cd /path/to/parent-of-xcode-project
python3 Scripts/logen2.py \
--csv "./exports/CoffeeShop-Strings-tab.csv" \
--project "CoffeeShop"Use a CSV exported from the same tab you would target with --sheet in API mode.
There isn't much to set up in the spreadsheets. They should follow this general formatting:
| Description | 1234 iOS ASDF | Android | SK | EN |
|---|---|---|---|---|
| Any | Title of this column | This should | Preklad 1 | Translation 1 |
| description | should contain | burn in hell | Preklad 2 | Translation 2 |
| you | the word "iOS" | Preklad 3 | Translation 3 | |
| want | somewhere | Preklad 4 | ... |
It's important to note that the language column should be exactly 2 letters long and reflect the same language code that's used in the project.
To inform Logen that this localization should be treated as pluralized you will need to include (pluralized) tag somewhere in localization id. Example: ios.pluralized_string(pluralized)
Localizing strings that contain plurals | Apple Developer Documentation
| Location | Identifier iOS | CS |
|---|---|---|
| test.pluralized.normalString | %s string | |
| test.pluralized.pluralString(pluralized) | %1$d den dní dnů | |
| test.pluralized.multiplePluralString(pluralized) | %1$@ GB, %2$@. zóna, zbývá %3$d den dní dnů | |
| test.pluralized.multipleMorePluralString(pluralized) | %1$d den dní dnů, %2$@. zóna, zbývá %3$d den dní dnů | |
| test.pluralized.multiplePluralTagOnly(pluralized) | %@ GB, %@. zóna, zbývá %d den dní dnů | |
| test.pluralized.multiplePluralStringReverseOrder(pluralized) | %1$@ GB, %2$@. zóna, zbývá %3$d den dní dnů |
You can also use Logen to generate different files that you want to be localized. For example InfoPlist.stings, in this case all you need to do is to write Property List Key in Identifier iOS column and mark it with name of the file → in this case (InfoPlist)
| Location | Identifier iOS | CS |
|---|---|---|
| NSFaceIDUsageDescription(InfoPlist) | NSFaceIDUsageDescription CS | |
| CFBundleDisplayName(InfoPlist) | CFBundleDisplayName CS |
The source of truth is logen2.py. This section summarizes behavior; open the file for exact logic.
LOGEN_SA_JSON(environment): full service account JSON as a string. Used with--id,--sheet,--last_columnto fetch the range via the Sheets API. Google client libraries are imported only on this path (import_google_sheets_api,fetch_spreadsheet_from_sheets).--csv: loads a UTF-8 CSV (with BOM tolerated) into the samelist[list[str]]shape as the API; no Google packages required (load_spreadsheet_from_csv).
LOCALIZATION_PATH, SPREADSHEET_ID, RANGE, and PROJECT_NAME are set from CLI args where applicable.
Represents a translation with a type and its corresponding value.
class Translation:
def __init__(self, type, translation):
self.type = type
self.translation = translation
def output(self):
return """<key>{}</key>
<string>{}</string>""".format(self.type, self.translation)Represents a variable with a key and a list of translations.
class Variable:
def __init__(self, key, translations):
self.key = key
self.translations = translations
def string_of_translations(self, translation):
return translation.output()
def output(self):
list_of_transalations_strings = map(self.string_of_translations, list(self.translations))
joined_strings = "\n".join(list_of_transalations_strings)
return """<key>{}</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
{}
</dict>""".format(self.key, joined_strings)Represents a localized string with a key, a base string, and a list of variables.
class LocalizedString:
def __init__(self, key, string, variables):
self.key = key
self.string = string
self.variables = variables
def string_of_variables(self, variables):
return variables.output()
def output(self):
list_of_transalations_strings = map(self.string_of_variables, list(self.variables))
joined_strings = "\n".join(list_of_transalations_strings)
value = """<key>{}</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>{}</string>
{}
</dict>"""
return value.format(self.key, self.string, joined_strings)CLI and I/O helpers include parse_arguments, validate_and_resolve_mode, load_spreadsheet, import_google_sheets_api, pip_install_google_apis, fetch_spreadsheet_from_sheets, and load_spreadsheet_from_csv. See logen2.py.
This function extracts language headers from the spreadsheet.
- Parameters:
spreadsheet(list of lists): The data retrieved from the spreadsheet.
- Returns:
- List of language headers.
def get_languages(spreadsheet):
headers = spreadsheet[0]
languages = []
for header in headers:
if len(header) == 2:
languages.append(header)
return languagesGenerates localized strings for the specified language based on the spreadsheet data.
- Parameters:
spreadsheet(list of lists): The data retrieved from the spreadsheet.language(str): The language for which to generate localized strings.
- Returns:
- Tuple containing a list of simple localized strings and a list of pluralized localized strings.
def generate_strings(spreadsheet, language):
ios_column_name = "iOS"
ios_column_first_match = [header for header in spreadsheet[0] if ios_column_name in header][0]
ios_column_index = spreadsheet[0].index(ios_column_first_match)
language_column_index = spreadsheet[0].index(language)
list_of_strings = []
pluralized_rows = []
for row in spreadsheet:
try:
if not row[ios_column_index]:
continue
else:
key = row[ios_column_index]
if "(pluralized)" in key:
pluralized_rows.append(row)
translation = fix_special_substrings(row[language_column_index])
localized_entry = as_localized_entry(key, translation)
list_of_strings.append(localized_entry)
except IndexError:
continue
pluralized_strings = create_pluralized_file(pluralized_rows, language_column_index, ios_column_index)
return list_of_strings, pluralized_stringsGenerates localized strings for the specified language based on the spreadsheet data.
- Parameters:
spreadsheet(list of lists): The data retrieved from the spreadsheet.language(str): The language for which to generate localized strings.
- Returns:
- Dictionary containing a file_name as key and a list of simple localized strings as values
def generate_special_strings(spreadsheet, language):
ios_column_name = "iOS"
ios_column_first_match = [header for header in spreadsheet[0] if ios_column_name in header][0]
ios_column_index = spreadsheet[0].index(ios_column_first_match)
language_column_index = spreadsheet[0].index(language)
dict_of_special_strings = dict()
for row in spreadsheet:
try:
if not row[ios_column_index]:
continue
else:
key = row[ios_column_index]
if "(" in key and "(pluralized)" not in key:
file_name = key[key.find("(") + 1:key.find(")")]
translation = fix_special_substrings(row[language_column_index])
localized_entry = as_special_localized_entry(key, file_name, translation)
if file_name in dict_of_special_strings:
dict_of_special_strings.get(file_name).append(localized_entry)
else:
dict_of_special_strings.update({file_name : [localized_entry]})
except IndexError:
continue
return dict_of_special_stringsCreates localized strings for pluralized entries based on the provided rows.
- Parameters:
rows(list of lists): Rows containing pluralized entries.language_index(int): Index of the language column.ios_column_index(int): Index of the iOS column.
- Returns:
- List of
LocalizedStringobjects representing pluralized entries.
- List of
def create_pluralized_file(rows, language_index, ios_column_index):
localized_strings = []
key = ""
for row in rows:
localized_string = ""
variables = []
key = row[ios_column_index]
for index, variable in enumerate(separate_strings(row[language_index])):
if "%" in variable:
localized_string = localized_string + "%#@variable{}@".format(index)
variables.append(Variable("variable{}".format(index), extract_pluralized_translations(variable)))
else:
localized_string = localized_string + variable
localized_strings.append(LocalizedString(key.replace("(pluralized)", ""), localized_string, variables))
return localized_stringsSplits a string into segments based on specified patterns.
- Parameters:
input_string(str): The input string to be split.
- Returns:
- List of string segments.
def separate_strings(input_string):
# Define a regular expression pattern to capture substrings around '%[0-9]+\$@'
pattern = r'([^%]*)(%[0-9]*\$*[@dDuUxXoOfeEgGcCsSpaAF][^%]*)'
# Use re.split to split the input string around the specified pattern
result = re.split(pattern, input_string)
# Remove empty strings from the result
result = [segment for segment in result if segment]
return resultExtracts translations from a pluralized string.
- Parameters:
input_string(str): Pluralized input string.
- Returns:
- List of
Translationobjects representing pluralized translations.
- List of
def extract_pluralized_translations(input_string):
translations = []
# Continue loop as long as there are pairs of tags
while "<" in input_string and ">" in input_string:
# Find the opening tag
start_index = input_string.find("<")
# Find the closing tag
end_index = input_string.find(">")
if start_index != -1 and end_index != -1:
# Extract the tag content
tag = input_string[start_index + 1:end_index]
# Check if the tag is a valid pluralization tag
if tag in ["one", "two", "few", "many", "other", "zero"]:
# Find the opening tag of the translation content
translation_start = input_string.find("<{}>".format(tag))
# Find the closing tag of the translation content
translation_end = input_string.find("</{}>".format(tag))
if translation_start != -1 and translation_end != -1:
# Extract the translation content
translation = input_string[:start_index] + input_string[translation_start + len(tag) + 2:translation_end]
# Append the translation to the list
translations.append(Translation(tag, translation))
# Modify input_string to remove the current translation tag pair
input_string = input_string[:start_index] + input_string[translation_end + len(tag) + 3:]
if not translations:
# Default to 'other' if no specific pluralization tag is found
translations.append(Translation("other", input_string))
return translationsHandles special substrings and escapes in a string.
- Parameters:
string(str): The input string to be processed.
- Returns:
- Processed string.
def fix_special_substrings(string):
s_substring = "%s"
s_substring_replacement = "%@"
quote_substring = '"'
quote_substring_replacement = '\\"'
if re.search(quote_substring, string):
string = string.replace(
quote_substring,
quote_substring_replacement,
string.count(quote_substring)
)
if re.search(s_substring, string):
string = string.replace(
s_substring,
s_substring_replacement,
string.count(s_substring)
)
string = html.unescape(string)
return stringFormats a localized entry for output.
- Parameters:
key(str): The key for the localized entry.translation(str): The translation for the localized entry.
- Returns:
- Formatted string representing the localized entry.
def as_localized_entry(key, translation):
return "\"{}\" = \"{}\";".format(key.replace("(pluralized)", ""), translation)Formats a localized entry for output.
- Parameters:
key(str): The key for the localized entry.file_name(str): The name for the localized file.translation(str): The translation for the localized entry.
- Returns:
- Formatted string representing the localized entry.
def as_special_localized_entry(key, file_name, translation):
return "{} = \"{}\";".format(key.replace("({})".format(file_name), ""), translation)Saves generated strings to a file.
- Parameters:
strings(list of str): List of simple localized strings.language(str): The language for which to save the strings.
def save_strings(strings, language):
file_path = prepare_path(language, False)
if file_path:
with open(file_path, "w") as output_file:
output_file.write("/*\n * Generated by Logen v2\n")
output_file.write(" */\n\n")
for line in strings:
output_file.write(line)
output_file.write("\n")
else:
print("⚠️ Localization file not found - {} ⚠️".format(language))Saves generated pluralized strings to a file.
- Parameters:
strings(list ofLocalizedString): List of pluralized localized strings.language(str): The language for which to save the pluralized strings.
def save_pluralized_strings(strings, language):
file_path = prepare_path(language, True)
mapped_values = map(lambda value: value.output(), list(strings))
output = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
{}
</dict>
</plist>""".format("\n".join(mapped_values))
if file_path:
with open(file_path, "w+") as output_file:
output_file.write(output)
else:
print("⚠️ Output language file not found - {} ⚠️".format(language))Saves generated strings to a file.
- Parameters:
strings(list of str): Dictionary of files and localized strings.language(str): The language for which to save the strings.
def save_special_strings(strings, language):
for file_name in strings:
file_path = prepare_path(language, False, file_name)
if file_path:
with open(file_path, "w") as output_file:
output_file.write("/*\n * Generated by Logen v2\n")
output_file.write(" */\n\n")
for line in strings.get(file_name):
output_file.write(line)
output_file.write("\n")
else:
print("⚠️ Localization file not found - {} ⚠️".format(language))Prepares the path for saving localization files (uses global FILENAME / per-file name for special strings).
- Parameters:
language(str): The language for which to prepare the path.isPluralizedFile(bool): Whether the file is.stringsdict.fileName(str): Base name (Localizable,InfoPlist, etc.).
- Returns:
- Absolute path if the
.lprojdirectory exists, elseNone.
- Absolute path if the
See logen2.py for the implementation.
Prints a success message with the generated languages.
- Parameters:
languages(list of str): List of languages for which strings were generated.
def print_success_message(languages):
print("✅ Generated strings for {} language(s):".format(len(languages)))
for language in languages:
print(" - {}".format(language))Parses CLI args; --id / --sheet / --last_column are required only when not using --csv. See logen2.py for the full implementation (including --filename and validate_and_resolve_mode after parse).



