English base localization

Forgejo base localization is English. This means that all translations are derived from it.

Managing strings

English localization strings are stored in two locations:

  • options/locale_next/locale_en-US.json - preferred
  • options/locale/locale_en-US.ini - legacy, doesn’t support culture-specific plurals

Strings are translated on Weblate and string management is partially done by it.

Additions, deletions and minor edits

Addition of new strings, deletion of obsolete strings and minor changes should be be submitted in pull requests as is. Edits are considered minor unless they include changes to handling of placeholders (%s, %[n]s, %d, %[n]d).

+ "new-string": "Hello, world!",
//
- "obsolete-string": "This string is no longer used",
//
- "edited-string": "Remove this runner",
+ "edited-string": "Remove runner",

When a new string is added, it must be double-checked that its key is unique and is not present already.

"repo.pulls.poster_trust_deny": "Deny",
// <1000 other lines>
"repo.pulls.poster_trust_deny": "Deny access", // Oh, no!

Changes to non-base files (translations) are should only be done on Weblate and not via individual pull requests to

  • prevent merge conflicts with Weblate
  • not bypass of reviews on Weblate, where translation reviers know better than code reviewers
  • avoid wasting CI time and cluttering PR review queue

When a UI change renders some string unused, it must be deleted.

Unused strings should only be deleted in base language. They will disappear from Weblate automatically after the PR is merged. Removal of orphaned translations from the files is done by Weblate for JSON and manually by the Localization team admins for INI components.

Incompatible edits

String placeholders cannot be changed as is.

// This change to base locale
- "initial-key": "Remove this runner?",
+ "initial-key": "Remove runner %s?",
// Breaks string rendering in all other locales
"initial-key": "Изтриване на изпълнител?",

If there’s an intention to preserve existing translations, a migration can be prepared in coordination with the Localization team admins:

  • preform the change in all locales - likely to conflict with Weblate, making it a rather complicated way

  • prepare replacement that can be executed in Weblate UI if the change is possible to automate by text replacement

    For example, this change can be performed for all strings in Weblate UI avoiding conflicts with parameters:

    • filter: initial-key
    • search for: %s
    • replacement string: %[1]s
    - "initial-key": "Remove %s?",
    + "initial-key": "Remove %[1]s?",

If it is deemed fine to replace the string, the key needs to be changed:

- "initial-key": "Remove this runner?",
+ "updated-key": "Remove %[1]s?",

This is the easiest option for those working on the UI changes, but the translators will have to re-do their work.

Refactoring keys

Sometime string keys are named poorly and need to be changed. In such cases they need to be mass-changed for all languages into which the strings have already been translated, so that the existing translations aren’t lost. This includes merging the Weblate PR first and then re-applying the rename to avoid race conditions with changes from Weblate. It is a complicated process, so renaming keys is generally not recommended unless there’s a good reason.

Localization style

Capitalization

All strings should have regular capitalization. Headers, labels, buttons and such should start with a capital letter. Only names, product names and such should be capitalized after that. Git/Forgejo specific measurement units should not be capitalized.

Follow these examples for string capitalization:

Context❌ Bad✅ Good
ButtoneditEdit
HeaderManage OrganizationsManage organizations
OptionUse Custom AvatarUse custom avatar
ButtonAdd Cleanup RuleAdd cleanup rule
LabelIntegrate matrix into your repository.Integrate Matrix into your repository.
Label%s Commits%s commits

Other stylistic choices

Form labels should not end with any punctuation marks.

Follow these examples:

Context❌ Bad✅ Good
Form labelUsername.Username
Form labelUsername:Username

Ensuring good translatability

Strings expose the ability to edit the entire parts of text shown in the UI.

Using placeholders

When possible, strings should include placeholders for everything displayed within the given context. When there’s more than one placeholder, it’s best to use numbered placeholders.

Good example:

"event.user_pushed": "%[1]s pushed %[2]s on %[3]s"
{{ctx.Locale.Tr "event.user_pushed" .User .Commit .Commit.Time}}

Bad example:

In this example, the text is constructed in the template rather than with placeholders. This greatly reduces flexibility of translations and is confusing to translate.

"event.push.pushed": "pushed",
"event.push.on": "on"
{{.User}} {{ctx.Locale.Tr "event.push.pushed"}} {{.Commit}} {{ctx.Locale.Tr "event.push.on"}} {{.Commit.Time}}

Using complex plurals

For strings that include a number and an associated noun, a specific plural format must be used so the words in the translated text can be adapted to the appropriate plural forms, which vary in different languages.

Good example:

"n_commits_found": {
	"one": "%d commit was found.",
	"other": "%d commits were found."
}
{{ctx.Locale.TrPluralString .NumCommits "n_commits_found" .NumCommits}}
Note

There’s no dedicated zero case where placeholders would be unused like so:

"n_commits_found": {
	//...
	"zero": "No commits were found.",
}

Instead, use Go template logic to handle such cases if needed:

"no_commits_found": "No commits were found.",
{{if ne .NumCommits 0}}
	{{ctx.Locale.TrPluralString .NumCommits "n_commits_found" .NumCommits}}
{{else}}
	<!-- This could also be displayed differently. E.g. more like a placeholder -->
	{{ctx.Locale.Tr "no_commits_found"}}
{{end}}

Bad example:

In this example, no plural router is used. Such text can only be correctly displayed with all different numbers only in few languages.

"commits_found": "commits were found"
{{.NumCommits}} {{ctx.Locale.Tr "commits_found"}}.

Bad example:

In this example, simple plural router is used, but the strings include the numbers. This way, translated variants of the text can be displayed correctly in more languages than in previous example, but still in far from all languages.

"n_commits_found.one": "%d commit was found.",
"n_commits_found.few": "%d commits were found."
{{ctx.Locale.TrN .NumCommits "commits_found.one" "commits_found.few" .NumCommits}}

Using simple plurals

When strings should be written out differently depending on the number of objects they reference, but do not include the number, different variants should not be stored in the dict format as they would not benefit translatability in other languages.

Good example:

"config.parse_errors.one": "There was an error parsing the config:",
"config.parse_errors.few": "There were errors parsing the config:"
{{ctx.Locale.TrN (len .Errors) "config.parse_errors.one" "config.parse_errors.few"}}
<ul>
	{{range .Errors}}
		<li>{{.}}</li>
	{{end}}
</ul>

Bad example:

In this example, usage of dict format and TrPluralString provides no benefit to languages that use more than two plural variants.

"config.parse_errors": {
	"one": "There was error parsing the config:",
	"other": "There were errors parsing the config:"
}
{{ctx.Locale.TrPluralString (len .Errors) "config.parse_errors"}}

Formatting numbers

Large numbers can be formatted as pretty strings like so:

"n_commits_found": {
	"one": "%s commit was found.",
	"other": "%s commits were found."
}
{{ctx.Locale.TrPluralString .NumCommits "n_commits_found" (CountFmt .NumCommits)}}

Result: 1,000,000 commits were found.

Styling of placeholders

It is best to avoid wrapping placeholders in HTML tags. This makes translating unnecessarily harder.

Good example:

"n_commits_found": {
	"one": "%s commit was found.",
	"other": "%s commits were found."
}

For trusted content such as numbers, printf and TrustHTML can be used:

{{ctx.Locale.TrPluralString .CommitsCount "n_commits_found" (printf "<b>%d</b>" .CommitsCount | TrustHTML)}}

Good example:

"user_invited": "You're invited to team %s."

For untrusted content such as user provided strings, HTMLFormat must be used:

{{ctx.Locale.Tr "user_invited" (HTMLFormat "<b>%s</b>" .Team.Name)}}

Bad example:

In this example, HTML tags are polluting the translation file.

"n_commits_found": {
	"one": "<b>%s</b> commit was found.",
	"other": "<b>%s</b> commits were found."
}

Styling of text

When the intention is to highlight a part the string, or to make a link, tags are welcome in the strings. For links, storing the actual URLs in the strings must be avoided.

Good example:

"warning": "This change is <strong>irreversible</strong>. Proceed?",
"with_link": "See <a href=\"%s\">the documentation</a> for pattern syntax."
{{ctx.Locale.Tr "warning"}}
{{ctx.Locale.Tr "with_link" "https://forgejo.org/docs/latest/"}}

Bad example:

In this example, the word irreversible is moved out of the full string to avoid HTML tags in translations. However, this creates unnecessary complexity in form of multiple strings.

Additionally, the link is stored in the translations, making it more difficult to update.

"warning": "This change is %s. Proceed?",
"irreversible": "irreversible",
"with_link": "See <a href=\"https://forgejo.org/docs/latest/\">the documentation</a> for pattern syntax."
{{ctx.Locale.Tr "warning" (HTMLFormat "<strong>%s</strong>" ctx.Locale.Tr "irreversible")}}
{{ctx.Locale.Tr "with_link"}}
Note

The link example doesn’t apply to links not included in <a> tags. For example, it is good to leave example addresses for translators. example.com has localized variants.

"email.placeholder": "Email (e.g. test@example.com)",

Parts that don’t need to be translated

If something doesn’t need to be translated, it is best to include it via placeholder.

Good example:

"unsupported_field.permissions": "The %s field is not supported."
{{ctx.Locale.Tr "unsupported_field.permissions" ("<code>permissions</code>" | TrustHTML)}}

Bad example:

In this example, the part that doesn’t need to be translated is stored in the string along with tags. Translators will have to copy-paste it. Some will try to translate it.

"unsupported_field.permissions": "The <code>permissions</code> field is not supported."
{{ctx.Locale.Tr "unsupported_field.permissions"}}

Contributing

This section is to help you contribute to the English localization of Forgejo.

Suggesting improvements

You send bugs, suggestions and feedback via Forgejo issue tracker or Forgejo Localization Matrix chatroom. Comments on Weblate also might work but are not recommended because they’re checked rarely and might not be noticed.

Proposing changes

Similarly to other projects you can propose changes via a pull request. Read the informative sections for string managing and style guidelines above. You can perform small changes via Forgejo web UI. Otherwise you can make a fork or use AGit workflow, clone, checkout a new branch, make your changes, commit, push and open a pull request. Put some useful explanation of the change in the PR description. Screenshots are also highly welcome.

If it’s a small change to the string, you don’t need to test it locally. However, if you want to make a larger text refactoring, consider verifying that everything looks good together in live by building and running Forgejo locally. See For contributors for more information how to do this.

If your refactor affects key names, make sure to perform a mass-rename of the affected keys in all code, templates and translations. Sometimes keys are generated in code with conditions. Please check for that as well. With such a refactoring you might be asked to resolve merge conflicts before your PR can be merged. If you have low availability, it is best to avoid renaming translation keys.