Compare commits

...

11 Commits

22 changed files with 344 additions and 152 deletions

View File

@ -178,6 +178,16 @@ jobs:
GOOS: linux GOOS: linux
GOARCH: 386 GOARCH: 386
i18n-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
check-latest: true
- run: make i18n-check
docs: docs:
if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true' if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed

View File

@ -898,6 +898,16 @@ update-translations:
mv ./translations/*.ini ./options/locale/ mv ./translations/*.ini ./options/locale/
rmdir ./translations rmdir ./translations
.PHONY: i18n-backport
i18n-backport:
@echo "Backport translations ..."
$(GO) run tools/i18n/backport.go
.PHONY: i18n-check
i18n-check:
@echo "Checking unused translations..."
$(GO) run tools/i18n/check.go
.PHONY: generate-gitignore .PHONY: generate-gitignore
generate-gitignore: ## update gitignore files generate-gitignore: ## update gitignore files
$(GO) run build/generate-gitignores.go $(GO) run build/generate-gitignores.go

View File

@ -114,6 +114,7 @@ func (r *ActionRunner) StatusName() string {
} }
func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string { func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string {
// i18n-check: actions.runners.status.*
return lang.TrString("actions.runners.status." + r.StatusName()) return lang.TrString("actions.runners.status." + r.StatusName())
} }

View File

@ -43,6 +43,7 @@ func (s Status) String() string {
// LocaleString returns the locale string name of the Status // LocaleString returns the locale string name of the Status
func (s Status) LocaleString(lang translation.Locale) string { func (s Status) LocaleString(lang translation.Locale) string {
// i18n-check: actions.status.*
return lang.TrString("actions.status." + s.String()) return lang.TrString("actions.status." + s.String())
} }

View File

@ -206,6 +206,7 @@ func (status *CommitStatus) APIURL(ctx context.Context) string {
// LocaleString returns the locale string name of the Status // LocaleString returns the locale string name of the Status
func (status *CommitStatus) LocaleString(lang translation.Locale) string { func (status *CommitStatus) LocaleString(lang translation.Locale) string {
// i18n-check: repo.commitstatus.*
return lang.TrString("repo.commitstatus." + status.State.String()) return lang.TrString("repo.commitstatus." + status.State.String())
} }

View File

@ -225,11 +225,13 @@ const (
// LocaleString returns the locale string name of the role // LocaleString returns the locale string name of the role
func (r RoleInRepo) LocaleString(lang translation.Locale) string { func (r RoleInRepo) LocaleString(lang translation.Locale) string {
// i18n-check: repo.issues.role.*
return lang.TrString("repo.issues.role." + string(r)) return lang.TrString("repo.issues.role." + string(r))
} }
// LocaleHelper returns the locale tooltip of the role // LocaleHelper returns the locale tooltip of the role
func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
// i18n-check: repo.issues.role.*_helper
return lang.TrString("repo.issues.role." + string(r) + "_helper") return lang.TrString("repo.issues.role." + string(r) + "_helper")
} }

View File

@ -128,6 +128,7 @@ func BuildComplexityError(locale translation.Locale) template.HTML {
buffer.WriteString("<ul>") buffer.WriteString("<ul>")
for _, c := range requiredList { for _, c := range requiredList {
buffer.WriteString("<li>") buffer.WriteString("<li>")
// i18n-check: form.password_*
buffer.WriteString(locale.TrString(c.TrNameOne)) buffer.WriteString(locale.TrString(c.TrNameOne))
buffer.WriteString("</li>") buffer.WriteString("</li>")
} }

View File

@ -144,6 +144,7 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
args[i] = template.HTMLEscapeString(fmt.Sprint(v)) args[i] = template.HTMLEscapeString(fmt.Sprint(v))
} }
} }
// i18n-check: ignore
return template.HTML(l.TrString(trKey, args...)) return template.HTML(l.TrString(trKey, args...))
} }

View File

@ -238,6 +238,7 @@ func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
} else if t, ok := cnt.(int64); ok { } else if t, ok := cnt.(int64); ok {
c = t c = t
} else { } else {
// i18n-check: ignore
return l.Tr(keyN, args...) return l.Tr(keyN, args...)
} }

View File

@ -104,8 +104,10 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
trName := field.Tag.Get("locale") trName := field.Tag.Get("locale")
if len(trName) == 0 { if len(trName) == 0 {
// i18n-check: form.*
trName = l.TrString("form." + field.Name) trName = l.TrString("form." + field.Name)
} else { } else {
// i18n-check: ignore
trName = l.TrString(trName) trName = l.TrString(trName)
} }

View File

@ -49,7 +49,6 @@ webauthn_error_unable_to_process = The server could not process your request.
webauthn_error_duplicated = The security key is not permitted for this request. Please make sure that the key is not already registered. webauthn_error_duplicated = The security key is not permitted for this request. Please make sure that the key is not already registered.
webauthn_error_empty = You must set a name for this key. webauthn_error_empty = You must set a name for this key.
webauthn_error_timeout = Timeout reached before your key could be read. Please reload this page and retry. webauthn_error_timeout = Timeout reached before your key could be read. Please reload this page and retry.
webauthn_reload = Reload
repository = Repository repository = Repository
organization = Organization organization = Organization
@ -57,14 +56,10 @@ mirror = Mirror
issue_milestone = Milestone issue_milestone = Milestone
new_repo = New Repository new_repo = New Repository
new_migrate = New Migration new_migrate = New Migration
new_mirror = New Mirror
new_fork = New Repository Fork new_fork = New Repository Fork
new_org = New Organization new_org = New Organization
new_project = New Project
new_project_column = New Column new_project_column = New Column
manage_org = Manage Organizations
admin_panel = Site Administration admin_panel = Site Administration
account_settings = Account Settings
settings = Settings settings = Settings
your_profile = Profile your_profile = Profile
your_starred = Starred your_starred = Starred
@ -368,13 +363,9 @@ config_write_file_prompt = These configuration options will be written into: %s
[home] [home]
nav_menu = Navigation Menu nav_menu = Navigation Menu
uname_holder = Username or Email Address uname_holder = Username or Email Address
password_holder = Password
switch_dashboard_context = Switch Dashboard Context switch_dashboard_context = Switch Dashboard Context
my_repos = Repositories my_repos = Repositories
show_more_repos = Show more repositories…
collaborative_repos = Collaborative Repositories
my_orgs = My Organizations my_orgs = My Organizations
my_mirrors = My Mirrors
view_home = View %s view_home = View %s
filter = Other Filters filter = Other Filters
filter_by_team_repositories = Filter by team repositories filter_by_team_repositories = Filter by team repositories
@ -436,7 +427,6 @@ resent_limit_prompt = You have already requested an activation email recently. P
has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below. has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below.
change_unconfirmed_mail_address = If your registration email address is incorrect, you can change it here and resend a new confirmation email. change_unconfirmed_mail_address = If your registration email address is incorrect, you can change it here and resend a new confirmation email.
resend_mail = Click here to resend your activation email resend_mail = Click here to resend your activation email
email_not_associate = The email address is not associated with any account.
send_reset_mail = Send Account Recovery Email send_reset_mail = Send Account Recovery Email
reset_password = Account Recovery reset_password = Account Recovery
invalid_code = Your confirmation code is invalid or has expired. invalid_code = Your confirmation code is invalid or has expired.
@ -454,7 +444,6 @@ twofa_passcode_incorrect = Your passcode is incorrect. If you misplaced your dev
twofa_scratch_token_incorrect = Your scratch code is incorrect. twofa_scratch_token_incorrect = Your scratch code is incorrect.
twofa_required = You must setup Two-Factor Authentication to get access to repositories, or try to login again. twofa_required = You must setup Two-Factor Authentication to get access to repositories, or try to login again.
login_userpass = Sign In login_userpass = Sign In
login_openid = OpenID
oauth_signup_tab = Register New Account oauth_signup_tab = Register New Account
oauth_signup_title = Complete New Account oauth_signup_title = Complete New Account
oauth_signup_submit = Complete Account oauth_signup_submit = Complete Account
@ -482,7 +471,6 @@ authorize_application_with_scopes = With scopes: %s
authorize_title = Authorize "%s" to access your account? authorize_title = Authorize "%s" to access your account?
authorization_failed = Authorization failed authorization_failed = Authorization failed
authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you have tried to authorize. authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you have tried to authorize.
sspi_auth_failed = SSPI authentication failed
password_pwned = The password you chose is on a <a target="_blank" rel="noopener noreferrer" href="%s">list of stolen passwords</a> previously exposed in public data breaches. Please try again with a different password and consider changing this password elsewhere too. password_pwned = The password you chose is on a <a target="_blank" rel="noopener noreferrer" href="%s">list of stolen passwords</a> previously exposed in public data breaches. Please try again with a different password and consider changing this password elsewhere too.
password_pwned_err = Could not complete request to HaveIBeenPwned password_pwned_err = Could not complete request to HaveIBeenPwned
last_admin = You cannot remove the last admin. There must be at least one admin. last_admin = You cannot remove the last admin. There must be at least one admin.
@ -514,8 +502,6 @@ reset_password = Recover your account
reset_password.title = %s, you have requested to recover your account reset_password.title = %s, you have requested to recover your account
reset_password.text = Please click the following link to recover your account within <b>%s</b>: reset_password.text = Please click the following link to recover your account within <b>%s</b>:
register_success = Registration successful
issue_assigned.pull = @%[1]s assigned you to pull request %[2]s in repository %[3]s. issue_assigned.pull = @%[1]s assigned you to pull request %[2]s in repository %[3]s.
issue_assigned.issue = @%[1]s assigned you to issue %[2]s in repository %[3]s. issue_assigned.issue = @%[1]s assigned you to issue %[2]s in repository %[3]s.
@ -560,30 +546,12 @@ yes = Yes
no = No no = No
confirm = Confirm confirm = Confirm
cancel = Cancel cancel = Cancel
modify = Update
[form] [form]
UserName = Username
RepoName = Repository name
Email = Email address
Password = Password
Retype = Confirm Password
SSHTitle = SSH key name
HttpsUrl = HTTPS URL
PayloadUrl = Payload URL
TeamName = Team name
AuthName = Authorization name
AdminEmail = Admin email
NewBranchName = New branch name NewBranchName = New branch name
CommitSummary = Commit summary
CommitMessage = Commit message
CommitChoice = Commit choice
TreeName = File path
Content = Content
SSPISeparatorReplacement = Separator SSPISeparatorReplacement = Separator
SSPIDefaultLanguage = Default Language
require_error = ` cannot be empty.` require_error = ` cannot be empty.`
alpha_dash_error = ` should contain only alphanumeric, dash ('-') and underscore ('_') characters.` alpha_dash_error = ` should contain only alphanumeric, dash ('-') and underscore ('_') characters.`
@ -608,7 +576,6 @@ username_been_taken = The username is already taken.
username_change_not_local_user = Non-local users are not allowed to change their username. username_change_not_local_user = Non-local users are not allowed to change their username.
change_username_disabled = Changing username is disabled. change_username_disabled = Changing username is disabled.
change_full_name_disabled = Changing full name is disabled. change_full_name_disabled = Changing full name is disabled.
username_has_not_been_changed = Username has not been changed
repo_name_been_taken = The repository name is already used. repo_name_been_taken = The repository name is already used.
repository_force_private = Force Private is enabled: private repositories cannot be made public. repository_force_private = Force Private is enabled: private repositories cannot be made public.
repository_files_already_exist = Files already exist for this repository. Contact the system administrator. repository_files_already_exist = Files already exist for this repository. Contact the system administrator.
@ -719,15 +686,11 @@ password = Password
security = Security security = Security
avatar = Avatar avatar = Avatar
ssh_gpg_keys = SSH / GPG Keys ssh_gpg_keys = SSH / GPG Keys
social = Social Accounts
applications = Applications applications = Applications
orgs = Manage Organizations orgs = Manage Organizations
repos = Repositories repos = Repositories
delete = Delete Account
twofa = Two-Factor Authentication (TOTP) twofa = Two-Factor Authentication (TOTP)
account_link = Linked Accounts
organization = Organizations organization = Organizations
uid = UID
webauthn = Two-Factor Authentication (Security Keys) webauthn = Two-Factor Authentication (Security Keys)
public_profile = Public Profile public_profile = Public Profile
@ -745,10 +708,8 @@ update_language = Update Language
update_language_not_found = Language "%s" is not available. update_language_not_found = Language "%s" is not available.
update_language_success = Language has been updated. update_language_success = Language has been updated.
update_profile_success = Your profile has been updated. update_profile_success = Your profile has been updated.
change_username = Your username has been changed.
change_username_prompt = Note: Changing your username also changes your account URL. change_username_prompt = Note: Changing your username also changes your account URL.
change_username_redirect_prompt = The old username will redirect until someone claims it. change_username_redirect_prompt = The old username will redirect until someone claims it.
continue = Continue
cancel = Cancel cancel = Cancel
language = Language language = Language
ui = Theme ui = Theme
@ -776,7 +737,6 @@ keep_activity_private = Hide Activity from profile page
keep_activity_private_popup = Makes the activity visible only for you and the admins keep_activity_private_popup = Makes the activity visible only for you and the admins
lookup_avatar_by_mail = Look Up Avatar by Email Address lookup_avatar_by_mail = Look Up Avatar by Email Address
federated_avatar_lookup = Federated Avatar Lookup
enable_custom_avatar = Use Custom Avatar enable_custom_avatar = Use Custom Avatar
choose_new_avatar = Choose new avatar choose_new_avatar = Choose new avatar
update_avatar = Update Avatar update_avatar = Update Avatar
@ -795,7 +755,6 @@ password_incorrect = The current password is incorrect.
change_password_success = Your password has been updated. Sign in using your new password from now on. change_password_success = Your password has been updated. Sign in using your new password from now on.
password_change_disabled = Non-local users cannot update their password through the Gitea web interface. password_change_disabled = Non-local users cannot update their password through the Gitea web interface.
emails = Email Addresses
manage_emails = Manage Email Addresses manage_emails = Manage Email Addresses
manage_themes = Select default theme manage_themes = Select default theme
manage_openid = Manage OpenID Addresses manage_openid = Manage OpenID Addresses
@ -840,8 +799,6 @@ principal_desc = These SSH certificate principals are associated with your accou
gpg_desc = These public GPG keys are associated with your account. Keep your private keys safe as they allow commits to be verified. gpg_desc = These public GPG keys are associated with your account. Keep your private keys safe as they allow commits to be verified.
ssh_helper = <strong>Need help?</strong> Have a look at GitHub's guide to <a href="%s">create your own SSH keys</a> or solve <a href="%s">common problems</a> you may encounter using SSH. ssh_helper = <strong>Need help?</strong> Have a look at GitHub's guide to <a href="%s">create your own SSH keys</a> or solve <a href="%s">common problems</a> you may encounter using SSH.
gpg_helper = <strong>Need help?</strong> Have a look at GitHub's guide <a href="%s">about GPG</a>. gpg_helper = <strong>Need help?</strong> Have a look at GitHub's guide <a href="%s">about GPG</a>.
add_new_key = Add SSH Key
add_new_gpg_key = Add GPG Key
key_content_ssh_placeholder = Begins with 'ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com' key_content_ssh_placeholder = Begins with 'ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'
key_content_gpg_placeholder = Begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----' key_content_gpg_placeholder = Begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'
add_new_principal = Add Principal add_new_principal = Add Principal
@ -905,10 +862,6 @@ hide_openid = Hide from profile
ssh_disabled = SSH Disabled ssh_disabled = SSH Disabled
ssh_signonly = SSH is currently disabled so these keys are only used for commit signature verification. ssh_signonly = SSH is currently disabled so these keys are only used for commit signature verification.
ssh_externally_managed = This SSH key is externally managed for this user ssh_externally_managed = This SSH key is externally managed for this user
manage_social = Manage Associated Social Accounts
social_desc = These social accounts can be used to sign in to your account. Make sure you recognize all of them.
unbind = Unlink
unbind_success = The social account has been removed successfully.
manage_access_token = Manage Access Tokens manage_access_token = Manage Access Tokens
generate_new_token = Generate New Token generate_new_token = Generate New Token
@ -919,8 +872,6 @@ generate_token_success = Your new token has been generated. Copy it now as it wi
generate_token_name_duplicate = <strong>%s</strong> has been used as an application name already. Please use a new one. generate_token_name_duplicate = <strong>%s</strong> has been used as an application name already. Please use a new one.
delete_token = Delete delete_token = Delete
access_token_deletion = Delete Access Token access_token_deletion = Delete Access Token
access_token_deletion_cancel_action = Cancel
access_token_deletion_confirm_action = Delete
access_token_deletion_desc = Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue? access_token_deletion_desc = Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue?
delete_token_success = The token has been deleted. Applications using it no longer have access to your account. delete_token_success = The token has been deleted. Applications using it no longer have access to your account.
repo_and_org_access = Repository and Organization Access repo_and_org_access = Repository and Organization Access
@ -939,9 +890,7 @@ permissions_list = Permissions:
manage_oauth2_applications = Manage OAuth2 Applications manage_oauth2_applications = Manage OAuth2 Applications
edit_oauth2_application = Edit OAuth2 Application edit_oauth2_application = Edit OAuth2 Application
oauth2_applications_desc = OAuth2 applications enables your third-party application to securely authenticate users at this Gitea instance.
remove_oauth2_application = Remove OAuth2 Application remove_oauth2_application = Remove OAuth2 Application
remove_oauth2_application_desc = Removing an OAuth2 application will revoke access to all signed access tokens. Continue?
remove_oauth2_application_success = The application has been deleted. remove_oauth2_application_success = The application has been deleted.
create_oauth2_application = Create a new OAuth2 Application create_oauth2_application = Create a new OAuth2 Application
create_oauth2_application_button = Create Application create_oauth2_application_button = Create Application
@ -956,7 +905,6 @@ oauth2_client_id = Client ID
oauth2_client_secret = Client Secret oauth2_client_secret = Client Secret
oauth2_regenerate_secret = Regenerate Secret oauth2_regenerate_secret = Regenerate Secret
oauth2_regenerate_secret_hint = Lost your secret? oauth2_regenerate_secret_hint = Lost your secret?
oauth2_client_secret_hint = The secret will not be shown again after you leave or refresh this page. Please ensure that you have saved it.
oauth2_application_edit = Edit oauth2_application_edit = Edit
oauth2_application_create_description = OAuth2 applications gives your third-party application access to user accounts on this instance. oauth2_application_create_description = OAuth2 applications gives your third-party application access to user accounts on this instance.
oauth2_application_remove_description = Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue? oauth2_application_remove_description = Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue?
@ -998,7 +946,6 @@ webauthn_alternative_tip = You may want to configure an additional authenticatio
manage_account_links = Manage Linked Accounts manage_account_links = Manage Linked Accounts
manage_account_links_desc = These external accounts are linked to your Gitea account. manage_account_links_desc = These external accounts are linked to your Gitea account.
account_links_not_available = There are currently no external accounts linked to your Gitea account.
link_account = Link Account link_account = Link Account
remove_account_link = Remove Linked Account remove_account_link = Remove Linked Account
remove_account_link_desc = Removing a linked account will revoke its access to your Gitea account. Continue? remove_account_link_desc = Removing a linked account will revoke its access to your Gitea account. Continue?
@ -1042,7 +989,6 @@ repo_size = Repository Size
template = Template template = Template
template_select = Select a template. template_select = Select a template.
template_helper = Make repository a template template_helper = Make repository a template
template_description = Template repositories let users generate new repositories with the same directory structure, files, and optional settings.
visibility = Visibility visibility = Visibility
visibility_description = Only the owner or the organization members if they have rights, will be able to see it. visibility_description = Only the owner or the organization members if they have rights, will be able to see it.
visibility_helper = Make repository private visibility_helper = Make repository private
@ -1065,8 +1011,6 @@ open_with_editor = Open with %s
download_zip = Download ZIP download_zip = Download ZIP
download_tar = Download TAR.GZ download_tar = Download TAR.GZ
download_bundle = Download BUNDLE download_bundle = Download BUNDLE
generate_repo = Generate Repository
generate_from = Generate From
repo_desc = Description repo_desc = Description
repo_desc_helper = Enter short description (optional) repo_desc_helper = Enter short description (optional)
repo_no_desc = No description provided repo_no_desc = No description provided
@ -1085,11 +1029,6 @@ readme = README
readme_helper = Select a README file template. readme_helper = Select a README file template.
readme_helper_desc = This is the place where you can write a complete description for your project. readme_helper_desc = This is the place where you can write a complete description for your project.
auto_init = Initialize Repository (Adds .gitignore, License and README) auto_init = Initialize Repository (Adds .gitignore, License and README)
trust_model_helper = Select trust model for signature verification. Possible options are:
trust_model_helper_collaborator = Collaborator: Trust signatures by collaborators
trust_model_helper_committer = Committer: Trust signatures that match committers
trust_model_helper_collaborator_committer = Collaborator+Committer: Trust signatures by collaborators which match the committer
trust_model_helper_default = Default: Use the default trust model for this installation
create_repo = Create Repository create_repo = Create Repository
default_branch = Default Branch default_branch = Default Branch
default_branch_label = default default_branch_label = default
@ -1108,13 +1047,11 @@ mirror_lfs = Large File Storage (LFS)
mirror_lfs_desc = Activate mirroring of LFS data. mirror_lfs_desc = Activate mirroring of LFS data.
mirror_lfs_endpoint = LFS Endpoint mirror_lfs_endpoint = LFS Endpoint
mirror_lfs_endpoint_desc = Sync will attempt to use the clone url to <a target="_blank" rel="noopener noreferrer" href="%s">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else. mirror_lfs_endpoint_desc = Sync will attempt to use the clone url to <a target="_blank" rel="noopener noreferrer" href="%s">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else.
mirror_last_synced = Last Synchronized
mirror_password_placeholder = (Unchanged) mirror_password_placeholder = (Unchanged)
mirror_password_blank_placeholder = (Unset) mirror_password_blank_placeholder = (Unset)
mirror_password_help = Change the username to erase a stored password. mirror_password_help = Change the username to erase a stored password.
watchers = Watchers watchers = Watchers
stargazers = Stargazers stargazers = Stargazers
stars_remove_warning = This will remove all stars from this repository.
forks = Forks forks = Forks
stars = Stars stars = Stars
reactions_more = and %d more reactions_more = and %d more
@ -1144,7 +1081,6 @@ transfer.no_permission_to_accept = You do not have permission to accept this tra
transfer.no_permission_to_reject = You do not have permission to reject this transfer. transfer.no_permission_to_reject = You do not have permission to reject this transfer.
desc.private = Private desc.private = Private
desc.public = Public
desc.public_access = Public Access desc.public_access = Public Access
desc.template = Template desc.template = Template
desc.internal = Internal desc.internal = Internal
@ -1174,7 +1110,6 @@ form.name_pattern_not_allowed = The pattern "%s" is not allowed in a repository
need_auth = Authorization need_auth = Authorization
migrate_options = Migration Options migrate_options = Migration Options
migrate_service = Migration Service
migrate_options_mirror_helper = This repository will be a mirror migrate_options_mirror_helper = This repository will be a mirror
migrate_options_lfs = Migrate LFS files migrate_options_lfs = Migrate LFS files
migrate_options_lfs_endpoint.label = LFS Endpoint migrate_options_lfs_endpoint.label = LFS Endpoint
@ -1244,8 +1179,6 @@ unstar = Unstar
star = Star star = Star
fork = Fork fork = Fork
action.blocked_user = Cannot perform action because you are blocked by the repository owner. action.blocked_user = Cannot perform action because you are blocked by the repository owner.
download_archive = Download Repository
more_operations = More Operations
quick_guide = Quick Guide quick_guide = Quick Guide
clone_this_repo = Clone this repository clone_this_repo = Clone this repository
@ -1259,7 +1192,6 @@ no_branch = This repository doesnt have any branches.
code = Code code = Code
code.desc = Access source code, files, commits and branches. code.desc = Access source code, files, commits and branches.
branch = Branch branch = Branch
tree = Tree
clear_ref = `Clear current reference` clear_ref = `Clear current reference`
filter_branch_and_tag = Filter branch or tag filter_branch_and_tag = Filter branch or tag
find_tag = Find tag find_tag = Find tag
@ -1282,7 +1214,6 @@ release = Release
releases = Releases releases = Releases
tag = Tag tag = Tag
released_this = released this released_this = released this
tagged_this = tagged this
file.title = %s at %s file.title = %s at %s
file_raw = Raw file_raw = Raw
file_history = History file_history = History
@ -1366,7 +1297,6 @@ editor.propose_file_change = Propose file change
editor.new_branch_name = Name the new branch for this commit editor.new_branch_name = Name the new branch for this commit
editor.new_branch_name_desc = New branch name… editor.new_branch_name_desc = New branch name…
editor.cancel = Cancel editor.cancel = Cancel
editor.filename_cannot_be_empty = The filename cannot be empty.
editor.filename_is_invalid = The filename is invalid: "%s". editor.filename_is_invalid = The filename is invalid: "%s".
editor.commit_email = Commit email editor.commit_email = Commit email
editor.invalid_commit_email = The email for the commit is invalid. editor.invalid_commit_email = The email for the commit is invalid.
@ -1406,7 +1336,6 @@ editor.fork_not_editable = You have forked this repository but your fork is not
editor.fork_failed_to_push_branch = Failed to push branch %s to your repository. editor.fork_failed_to_push_branch = Failed to push branch %s to your repository.
editor.fork_branch_exists = Branch "%s" already exists in your fork, please choose a new branch name. editor.fork_branch_exists = Branch "%s" already exists in your fork, please choose a new branch name.
commits.desc = Browse source code change history.
commits.commits = Commits commits.commits = Commits
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories. commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
commits.nothing_to_compare = These branches are equal. commits.nothing_to_compare = These branches are equal.
@ -1416,9 +1345,6 @@ commits.search_all = All Branches
commits.author = Author commits.author = Author
commits.message = Message commits.message = Message
commits.date = Date commits.date = Date
commits.older = Older
commits.newer = Newer
commits.signed_by = Signed by
commits.signed_by_untrusted_user = Signed by untrusted user commits.signed_by_untrusted_user = Signed by untrusted user
commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does not match committer commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does not match committer
commits.gpg_key_id = GPG Key ID commits.gpg_key_id = GPG Key ID
@ -1471,7 +1397,6 @@ projects.column.set_default = "Set Default"
projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls" projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls"
projects.column.delete = "Delete Column" projects.column.delete = "Delete Column"
projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?" projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?"
projects.column.color = "Color"
projects.open = Open projects.open = Open
projects.close = Close projects.close = Close
projects.column.assigned_to = Assigned to projects.column.assigned_to = Assigned to
@ -1520,8 +1445,6 @@ issues.create = Create Issue
issues.new_label = New Label issues.new_label = New Label
issues.new_label_placeholder = Label name issues.new_label_placeholder = Label name
issues.new_label_desc_placeholder = Description issues.new_label_desc_placeholder = Description
issues.create_label = Create Label
issues.label_templates.title = Load a predefined set of labels
issues.label_templates.info = No labels exist yet. Create a label with 'New Label' or use a predefined label set: issues.label_templates.info = No labels exist yet. Create a label with 'New Label' or use a predefined label set:
issues.label_templates.helper = Select a label set issues.label_templates.helper = Select a label set
issues.label_templates.use = Use Label Set issues.label_templates.use = Use Label Set
@ -1633,8 +1556,6 @@ issues.ref_issue_from = `<a href="%[3]s">referenced this issue %[4]s</a> <a id="
issues.ref_pull_from = `<a href="%[3]s">referenced this pull request %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_pull_from = `<a href="%[3]s">referenced this pull request %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_closing_from = `<a href="%[3]s">referenced a pull request %[4]s that will close this issue</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_closing_from = `<a href="%[3]s">referenced a pull request %[4]s that will close this issue</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopening_from = `<a href="%[3]s">referenced a pull request %[4]s that will reopen this issue</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_reopening_from = `<a href="%[3]s">referenced a pull request %[4]s that will reopen this issue</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_closed_from = `<a href="%[3]s">closed this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopened_from = `<a href="%[3]s">reopened this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_from = `from %[1]s` issues.ref_from = `from %[1]s`
issues.author = Author issues.author = Author
issues.author_helper = This user is the author. issues.author_helper = This user is the author.
@ -1684,7 +1605,6 @@ issues.label.filter_sort.by_size = Smallest size
issues.label.filter_sort.reverse_by_size = Largest size issues.label.filter_sort.reverse_by_size = Largest size
issues.num_participants = %d Participants issues.num_participants = %d Participants
issues.attachment.open_tab = `Click to see "%s" in a new tab` issues.attachment.open_tab = `Click to see "%s" in a new tab`
issues.attachment.download = `Click to download "%s"`
issues.subscribe = Subscribe issues.subscribe = Subscribe
issues.unsubscribe = Unsubscribe issues.unsubscribe = Unsubscribe
issues.unpin = Unpin issues.unpin = Unpin
@ -1740,28 +1660,21 @@ issues.add_time_manually = Manually Add Time
issues.add_time_hours = Hours issues.add_time_hours = Hours
issues.add_time_minutes = Minutes issues.add_time_minutes = Minutes
issues.add_time_sum_to_small = No time was entered. issues.add_time_sum_to_small = No time was entered.
issues.time_spent_total = Total Time Spent
issues.time_spent_from_all_authors = `Total Time Spent: %s` issues.time_spent_from_all_authors = `Total Time Spent: %s`
issues.due_date = Due Date issues.due_date = Due Date
issues.invalid_due_date_format = "Due date format must be 'yyyy-mm-dd'."
issues.error_modifying_due_date = "Failed to modify the due date."
issues.error_removing_due_date = "Failed to remove the due date."
issues.push_commit_1 = "added %d commit %s" issues.push_commit_1 = "added %d commit %s"
issues.push_commits_n = "added %d commits %s" issues.push_commits_n = "added %d commits %s"
issues.force_push_codes = `force-pushed %[1]s from <a class="ui sha" href="%[3]s"><code>%[2]s</code></a> to <a class="ui sha" href="%[5]s"><code>%[4]s</code></a> %[6]s` issues.force_push_codes = `force-pushed %[1]s from <a class="ui sha" href="%[3]s"><code>%[2]s</code></a> to <a class="ui sha" href="%[5]s"><code>%[4]s</code></a> %[6]s`
issues.force_push_compare = Compare issues.force_push_compare = Compare
issues.due_date_form = "yyyy-mm-dd" issues.due_date_form = "yyyy-mm-dd"
issues.due_date_form_add = "Add due date"
issues.due_date_form_edit = "Edit" issues.due_date_form_edit = "Edit"
issues.due_date_form_remove = "Remove" issues.due_date_form_remove = "Remove"
issues.due_date_not_writer = "You need write access to this repository in order to update the due date of an issue."
issues.due_date_not_set = "No due date set." issues.due_date_not_set = "No due date set."
issues.due_date_added = "added the due date %s %s" issues.due_date_added = "added the due date %s %s"
issues.due_date_modified = "modified the due date from %[2]s to %[1]s %[3]s" issues.due_date_modified = "modified the due date from %[2]s to %[1]s %[3]s"
issues.due_date_remove = "removed the due date %s %s" issues.due_date_remove = "removed the due date %s %s"
issues.due_date_overdue = "Overdue" issues.due_date_overdue = "Overdue"
issues.due_date_invalid = "The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'."
issues.dependency.title = Dependencies issues.dependency.title = Dependencies
issues.dependency.issue_no_dependencies = No dependencies set. issues.dependency.issue_no_dependencies = No dependencies set.
issues.dependency.pr_no_dependencies = No dependencies set. issues.dependency.pr_no_dependencies = No dependencies set.
@ -1802,7 +1715,6 @@ issues.review.dismissed_label = Dismissed
issues.review.left_comment = left a comment issues.review.left_comment = left a comment
issues.review.content.empty = You need to leave a comment indicating the requested change(s). issues.review.content.empty = You need to leave a comment indicating the requested change(s).
issues.review.reject = "requested changes %s" issues.review.reject = "requested changes %s"
issues.review.wait = "was requested for review %s"
issues.review.add_review_request = "requested review from %s %s" issues.review.add_review_request = "requested review from %s %s"
issues.review.remove_review_request = "removed review request for %s %s" issues.review.remove_review_request = "removed review request for %s %s"
issues.review.remove_review_request_self = "refused to review %s" issues.review.remove_review_request_self = "refused to review %s"
@ -1827,7 +1739,6 @@ issues.review.requested = Review pending
issues.review.rejected = Changes requested issues.review.rejected = Changes requested
issues.review.stale = Updated since approval issues.review.stale = Updated since approval
issues.review.unofficial = Uncounted approval issues.review.unofficial = Uncounted approval
issues.assignee.error = Not all assignees was added due to an unexpected error.
issues.reference_issue.body = Body issues.reference_issue.body = Body
issues.content_history.deleted = deleted issues.content_history.deleted = deleted
issues.content_history.edited = edited issues.content_history.edited = edited
@ -2151,7 +2062,6 @@ contributors.contribution_type.additions = Additions
contributors.contribution_type.deletions = Deletions contributors.contribution_type.deletions = Deletions
settings = Settings settings = Settings
settings.desc = Settings is where you can manage the settings for the repository
settings.options = Repository settings.options = Repository
settings.public_access = Public Access settings.public_access = Public Access
settings.public_access_desc = Configure public visitor's access permissions to override the defaults of this repository. settings.public_access_desc = Configure public visitor's access permissions to override the defaults of this repository.
@ -2181,7 +2091,6 @@ settings.mirror_settings.docs.pull_mirror_instructions = To set up a pull mirror
settings.mirror_settings.docs.more_information_if_disabled = You can find out more about push and pull mirrors here: settings.mirror_settings.docs.more_information_if_disabled = You can find out more about push and pull mirrors here:
settings.mirror_settings.docs.doc_link_title = How do I mirror repositories? settings.mirror_settings.docs.doc_link_title = How do I mirror repositories?
settings.mirror_settings.docs.doc_link_pull_section = the "Pulling from a remote repository" section of the documentation. settings.mirror_settings.docs.doc_link_pull_section = the "Pulling from a remote repository" section of the documentation.
settings.mirror_settings.docs.pulling_remote_title = Pulling from a remote repository
settings.mirror_settings.mirrored_repository = Mirrored repository settings.mirror_settings.mirrored_repository = Mirrored repository
settings.mirror_settings.pushed_repository = Pushed repository settings.mirror_settings.pushed_repository = Pushed repository
settings.mirror_settings.direction = Direction settings.mirror_settings.direction = Direction
@ -2285,13 +2194,10 @@ settings.signing_settings = Signing Verification Settings
settings.trust_model = Signature Trust Model settings.trust_model = Signature Trust Model
settings.trust_model.default = Default Trust Model settings.trust_model.default = Default Trust Model
settings.trust_model.default.desc= Use the default repository trust model for this installation. settings.trust_model.default.desc= Use the default repository trust model for this installation.
settings.trust_model.collaborator = Collaborator
settings.trust_model.collaborator.long = Collaborator: Trust signatures by collaborators settings.trust_model.collaborator.long = Collaborator: Trust signatures by collaborators
settings.trust_model.collaborator.desc = Valid signatures by collaborators of this repository will be marked "trusted" - (whether they match the committer or not). Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" if not. settings.trust_model.collaborator.desc = Valid signatures by collaborators of this repository will be marked "trusted" - (whether they match the committer or not). Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" if not.
settings.trust_model.committer = Committer
settings.trust_model.committer.long = Committer: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the committer) settings.trust_model.committer.long = Committer: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the committer)
settings.trust_model.committer.desc = Valid signatures will only be marked "trusted" if they match the committer, otherwise they will be marked "unmatched". This forces Gitea to be the committer on signed commits with the actual committer marked as Co-authored-by: and Co-committed-by: trailer in the commit. The default Gitea key must match a User in the database. settings.trust_model.committer.desc = Valid signatures will only be marked "trusted" if they match the committer, otherwise they will be marked "unmatched". This forces Gitea to be the committer on signed commits with the actual committer marked as Co-authored-by: and Co-committed-by: trailer in the commit. The default Gitea key must match a User in the database.
settings.trust_model.collaboratorcommitter = Collaborator+Committer
settings.trust_model.collaboratorcommitter.long = Collaborator+Committer: Trust signatures by collaborators which match the committer settings.trust_model.collaboratorcommitter.long = Collaborator+Committer: Trust signatures by collaborators which match the committer
settings.trust_model.collaboratorcommitter.desc = Valid signatures by collaborators of this repository will be marked "trusted" if they match the committer. Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" otherwise. This will force Gitea to be marked as the committer on signed commits with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a User in the database. settings.trust_model.collaboratorcommitter.desc = Valid signatures by collaborators of this repository will be marked "trusted" if they match the committer. Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" otherwise. This will force Gitea to be marked as the committer on signed commits with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a User in the database.
settings.wiki_delete = Delete Wiki Data settings.wiki_delete = Delete Wiki Data
@ -2350,7 +2256,6 @@ settings.githook_edit_desc = If the hook is inactive, sample content will be pre
settings.githook_name = Hook Name settings.githook_name = Hook Name
settings.githook_content = Hook Content settings.githook_content = Hook Content
settings.update_githook = Update Hook settings.update_githook = Update Hook
settings.add_webhook_desc = Gitea will send <code>POST</code> requests with a specified content type to the target URL. Read more in the <a target="_blank" rel="noopener noreferrer" href="%s">webhooks guide</a>.
settings.payload_url = Target URL settings.payload_url = Target URL
settings.http_method = HTTP Method settings.http_method = HTTP Method
settings.content_type = POST Content Type settings.content_type = POST Content Type
@ -2430,9 +2335,6 @@ settings.update_webhook = Update Webhook
settings.update_hook_success = The webhook has been updated. settings.update_hook_success = The webhook has been updated.
settings.delete_webhook = Remove Webhook settings.delete_webhook = Remove Webhook
settings.recent_deliveries = Recent Deliveries settings.recent_deliveries = Recent Deliveries
settings.hook_type = Hook Type
settings.slack_token = Token
settings.slack_domain = Domain
settings.slack_channel = Channel settings.slack_channel = Channel
settings.add_web_hook_desc = Integrate <a target="_blank" rel="noreferrer" href="%s">%s</a> into your repository. settings.add_web_hook_desc = Integrate <a target="_blank" rel="noreferrer" href="%s">%s</a> into your repository.
settings.web_hook_name_gitea = Gitea settings.web_hook_name_gitea = Gitea
@ -2469,12 +2371,7 @@ settings.branches = Branches
settings.protected_branch = Branch Protection settings.protected_branch = Branch Protection
settings.protected_branch.save_rule = Save Rule settings.protected_branch.save_rule = Save Rule
settings.protected_branch.delete_rule = Delete Rule settings.protected_branch.delete_rule = Delete Rule
settings.protected_branch_can_push = Allow push?
settings.protected_branch_can_push_yes = You can push
settings.protected_branch_can_push_no = You cannot push
settings.branch_protection = Branch Protection Rules for Branch '<b>%s</b>' settings.branch_protection = Branch Protection Rules for Branch '<b>%s</b>'
settings.protect_this_branch = Enable Branch Protection
settings.protect_this_branch_desc = Prevents deletion and restricts Git pushing and merging to the branch.
settings.protect_disable_push = Disable Push settings.protect_disable_push = Disable Push
settings.protect_disable_push_desc = No pushing will be allowed to this branch. settings.protect_disable_push_desc = No pushing will be allowed to this branch.
settings.protect_disable_force_push = Disable Force Push settings.protect_disable_force_push = Disable Force Push
@ -2526,8 +2423,6 @@ settings.protect_protected_file_patterns = "Protected file patterns (separated u
settings.protect_protected_file_patterns_desc = "Protected files are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon (';'). See <a href='%[1]s'>%[2]s</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>." settings.protect_protected_file_patterns_desc = "Protected files are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon (';'). See <a href='%[1]s'>%[2]s</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>."
settings.protect_unprotected_file_patterns = "Unprotected file patterns (separated using semicolon ';'):" settings.protect_unprotected_file_patterns = "Unprotected file patterns (separated using semicolon ';'):"
settings.protect_unprotected_file_patterns_desc = "Unprotected files that are allowed to be changed directly if user has write access, bypassing push restriction. Multiple patterns can be separated using semicolon (';'). See <a href='%[1]s'>%[2]s</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>." settings.protect_unprotected_file_patterns_desc = "Unprotected files that are allowed to be changed directly if user has write access, bypassing push restriction. Multiple patterns can be separated using semicolon (';'). See <a href='%[1]s'>%[2]s</a> documentation for pattern syntax. Examples: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>."
settings.add_protected_branch = Enable protection
settings.delete_protected_branch = Disable protection
settings.update_protect_branch_success = Branch protection for rule "%s" has been updated. settings.update_protect_branch_success = Branch protection for rule "%s" has been updated.
settings.remove_protected_branch_success = Branch protection for rule "%s" has been removed. settings.remove_protected_branch_success = Branch protection for rule "%s" has been removed.
settings.remove_protected_branch_failed = Removing branch protection rule "%s" failed. settings.remove_protected_branch_failed = Removing branch protection rule "%s" failed.
@ -2544,7 +2439,6 @@ settings.block_admin_merge_override_desc = Administrators must follow branch pro
settings.default_branch_desc = Select a default repository branch for pull requests and code commits: settings.default_branch_desc = Select a default repository branch for pull requests and code commits:
settings.merge_style_desc = Merge Styles settings.merge_style_desc = Merge Styles
settings.default_merge_style_desc = Default Merge Style settings.default_merge_style_desc = Default Merge Style
settings.choose_branch = Choose a branch…
settings.no_protected_branch = There are no protected branches. settings.no_protected_branch = There are no protected branches.
settings.edit_protected_branch = Edit settings.edit_protected_branch = Edit
settings.protected_branch_required_rule_name = Required rule name settings.protected_branch_required_rule_name = Required rule name
@ -2621,8 +2515,6 @@ settings.lfs_pointers.associateAccessible=Associate accessible %d OIDs
settings.rename_branch_failed_exist=Cannot rename branch because target branch %s exists. settings.rename_branch_failed_exist=Cannot rename branch because target branch %s exists.
settings.rename_branch_failed_not_exist=Cannot rename branch %s because it does not exist. settings.rename_branch_failed_not_exist=Cannot rename branch %s because it does not exist.
settings.rename_branch_success =Branch %s was successfully renamed to %s. settings.rename_branch_success =Branch %s was successfully renamed to %s.
settings.rename_branch_from=old branch name
settings.rename_branch_to=new branch name
settings.rename_branch=Rename branch settings.rename_branch=Rename branch
diff.browse_source = Browse Source diff.browse_source = Browse Source
@ -2676,7 +2568,6 @@ diff.protected = Protected
diff.image.side_by_side = Side by Side diff.image.side_by_side = Side by Side
diff.image.swipe = Swipe diff.image.swipe = Swipe
diff.image.overlay = Overlay diff.image.overlay = Overlay
diff.has_escaped = This line has hidden Unicode characters
diff.show_file_tree = Show file tree diff.show_file_tree = Show file tree
diff.hide_file_tree = Hide file tree diff.hide_file_tree = Hide file tree
diff.submodule_added = Submodule %[1]s added at %[2]s diff.submodule_added = Submodule %[1]s added at %[2]s
@ -2696,7 +2587,6 @@ release.compare = Compare
release.edit = edit release.edit = edit
release.ahead.commits = <strong>%d</strong> commits release.ahead.commits = <strong>%d</strong> commits
release.ahead.target = to %s since this release release.ahead.target = to %s since this release
tag.ahead.target = to %s since this tag
release.source_code = Source Code release.source_code = Source Code
release.new_subheader = Releases organize project versions. release.new_subheader = Releases organize project versions.
release.edit_subheader = Releases organize project versions. release.edit_subheader = Releases organize project versions.
@ -2724,7 +2614,6 @@ release.deletion_tag_success = The tag has been deleted.
release.tag_name_already_exist = A release with this tag name already exists. release.tag_name_already_exist = A release with this tag name already exists.
release.tag_name_invalid = The tag name is not valid. release.tag_name_invalid = The tag name is not valid.
release.tag_name_protected = The tag name is protected. release.tag_name_protected = The tag name is protected.
release.tag_already_exist = This tag name already exists.
release.downloads = Downloads release.downloads = Downloads
release.download_count = Downloads: %s release.download_count = Downloads: %s
release.add_tag_msg = Use the title and content of release as tag message. release.add_tag_msg = Use the title and content of release as tag message.
@ -2734,7 +2623,6 @@ release.tags_for = Tags for %s
branch.name = Branch Name branch.name = Branch Name
branch.already_exists = A branch named "%s" already exists. branch.already_exists = A branch named "%s" already exists.
branch.delete_head = Delete
branch.delete = Delete Branch "%s" branch.delete = Delete Branch "%s"
branch.delete_html = Delete Branch branch.delete_html = Delete Branch
branch.delete_desc = Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue? branch.delete_desc = Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
@ -2762,7 +2650,6 @@ branch.create_new_branch = Create branch from branch:
branch.confirm_create_branch = Create branch branch.confirm_create_branch = Create branch
branch.warning_rename_default_branch = You are renaming the default branch. branch.warning_rename_default_branch = You are renaming the default branch.
branch.rename_branch_to = Rename "%s" to: branch.rename_branch_to = Rename "%s" to:
branch.confirm_rename_branch = Rename branch
branch.create_branch_operation = Create branch branch.create_branch_operation = Create branch
branch.new_branch = Create new branch branch.new_branch = Create new branch
branch.new_branch_from = Create new branch from "%s" branch.new_branch_from = Create new branch from "%s"
@ -2780,7 +2667,6 @@ tag.create_tag_from = Create new tag from "%s"
tag.create_success = Tag "%s" has been created. tag.create_success = Tag "%s" has been created.
topic.manage_topics = Manage Topics topic.manage_topics = Manage Topics
topic.done = Done
topic.count_prompt = You cannot select more than 25 topics topic.count_prompt = You cannot select more than 25 topics
topic.format_prompt = Topics must start with a letter or number, can include dashes ('-') and dots ('.'), can be up to 35 characters long. Letters must be lowercase. topic.format_prompt = Topics must start with a letter or number, can include dashes ('-') and dots ('.'), can be up to 35 characters long. Letters must be lowercase.
@ -2832,7 +2718,6 @@ form.create_org_not_allowed = You are not allowed to create an organization.
settings = Settings settings = Settings
settings.options = Organization settings.options = Organization
settings.full_name = Full Name
settings.email = Contact Email settings.email = Contact Email
settings.website = Website settings.website = Website
settings.location = Location settings.location = Location
@ -2870,11 +2755,8 @@ settings.confirm_delete_account = Confirm Deletion
settings.delete_failed = Delete Organization failed because of internal error settings.delete_failed = Delete Organization failed because of internal error
settings.delete_successful = Organization <b>%s</b> has been deleted successfully. settings.delete_successful = Organization <b>%s</b> has been deleted successfully.
settings.hooks_desc = Add webhooks which will be triggered for <strong>all repositories</strong> under this organization. settings.hooks_desc = Add webhooks which will be triggered for <strong>all repositories</strong> under this organization.
settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization. settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization.
members.membership_visibility = Membership Visibility:
members.public = Visible
members.public_helper = make hidden members.public_helper = make hidden
members.private = Hidden members.private = Hidden
members.private_helper = make visible members.private_helper = make visible
@ -2885,8 +2767,6 @@ members.remove = Remove
members.remove.detail = Remove %[1]s from %[2]s? members.remove.detail = Remove %[1]s from %[2]s?
members.leave = Leave members.leave = Leave
members.leave.detail = Leave %s? members.leave.detail = Leave %s?
members.invite_desc = Add a new member to %s:
members.invite_now = Invite Now
teams.join = Join teams.join = Join
teams.leave = Leave teams.leave = Leave
@ -2906,7 +2786,6 @@ teams.admin_access_helper = Members can pull and push to team repositories and a
teams.no_desc = This team has no description teams.no_desc = This team has no description
teams.settings = Settings teams.settings = Settings
teams.owners_permission_desc = Owners have full access to <strong>all repositories</strong> and have <strong>administrator access</strong> to the organization. teams.owners_permission_desc = Owners have full access to <strong>all repositories</strong> and have <strong>administrator access</strong> to the organization.
teams.members = Team Members
teams.update_settings = Update Settings teams.update_settings = Update Settings
teams.delete_team = Delete Team teams.delete_team = Delete Team
teams.add_team_member = Add Team Member teams.add_team_member = Add Team Member
@ -2915,14 +2794,9 @@ teams.invite_team_member.list = Pending Invitations
teams.delete_team_title = Delete Team teams.delete_team_title = Delete Team
teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue? teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue?
teams.delete_team_success = The team has been deleted. teams.delete_team_success = The team has been deleted.
teams.read_permission_desc = This team grants <strong>Read</strong> access: members can view and clone team repositories.
teams.write_permission_desc = This team grants <strong>Write</strong> access: members can read from and push to team repositories. teams.write_permission_desc = This team grants <strong>Write</strong> access: members can read from and push to team repositories.
teams.admin_permission_desc = This team grants <strong>Admin</strong> access: members can read from, push to and add collaborators to team repositories. teams.admin_permission_desc = This team grants <strong>Admin</strong> access: members can read from, push to and add collaborators to team repositories.
teams.create_repo_permission_desc = Additionally, this team grants <strong>Create repository</strong> permission: members can create new repositories in organization.
teams.repositories = Team Repositories
teams.remove_all_repos_title = Remove all team repositories
teams.remove_all_repos_desc = This will remove all repositories from the team. teams.remove_all_repos_desc = This will remove all repositories from the team.
teams.add_all_repos_title = Add all repositories
teams.add_all_repos_desc = This will add all the organization's repositories to the team. teams.add_all_repos_desc = This will add all the organization's repositories to the team.
teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist, please create it first." teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist, please create it first."
teams.add_duplicate_users = User is already a team member. teams.add_duplicate_users = User is already a team member.
@ -2933,9 +2807,6 @@ teams.specific_repositories = Specific repositories
teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>. teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.
teams.all_repositories = All repositories teams.all_repositories = All repositories
teams.all_repositories_helper = Team has access to all repositories. Selecting this will <strong>add all existing</strong> repositories to the team. teams.all_repositories_helper = Team has access to all repositories. Selecting this will <strong>add all existing</strong> repositories to the team.
teams.all_repositories_read_permission_desc = This team grants <strong>Read</strong> access to <strong>all repositories</strong>: members can view and clone repositories.
teams.all_repositories_write_permission_desc = This team grants <strong>Write</strong> access to <strong>all repositories</strong>: members can read from and push to repositories.
teams.all_repositories_admin_permission_desc = This team grants <strong>Admin</strong> access to <strong>all repositories</strong>: members can read from, push to and add collaborators to repositories.
teams.invite.title = You have been invited to join team <strong>%s</strong> in organization <strong>%s</strong>. teams.invite.title = You have been invited to join team <strong>%s</strong> in organization <strong>%s</strong>.
teams.invite.by = Invited by %s teams.invite.by = Invited by %s
teams.invite.description = Please click the button below to join the team. teams.invite.description = Please click the button below to join the team.
@ -3149,7 +3020,6 @@ repos.unadopted = Unadopted Repositories
repos.unadopted.no_more = No more unadopted repositories found repos.unadopted.no_more = No more unadopted repositories found
repos.owner = Owner repos.owner = Owner
repos.name = Name repos.name = Name
repos.private = Private
repos.issues = Issues repos.issues = Issues
repos.size = Size repos.size = Size
repos.lfs_size = LFS Size repos.lfs_size = LFS Size
@ -3188,7 +3058,6 @@ auths.updated = Updated
auths.auth_type = Authentication Type auths.auth_type = Authentication Type
auths.auth_name = Authentication Name auths.auth_name = Authentication Name
auths.security_protocol = Security Protocol auths.security_protocol = Security Protocol
auths.domain = Domain
auths.host = Host auths.host = Host
auths.port = Port auths.port = Port
auths.bind_dn = Bind DN auths.bind_dn = Bind DN
@ -3217,7 +3086,6 @@ auths.user_attribute_in_group = User Attribute Listed In Group
auths.map_group_to_team = Map LDAP groups to Organization teams (leave the field empty to skip) auths.map_group_to_team = Map LDAP groups to Organization teams (leave the field empty to skip)
auths.map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding LDAP group auths.map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding LDAP group
auths.enable_ldap_groups = Enable LDAP groups auths.enable_ldap_groups = Enable LDAP groups
auths.ms_ad_sa = MS AD Search Attributes
auths.smtp_auth = SMTP Authentication Type auths.smtp_auth = SMTP Authentication Type
auths.smtphost = SMTP Host auths.smtphost = SMTP Host
auths.smtpport = SMTP Port auths.smtpport = SMTP Port
@ -3254,7 +3122,6 @@ auths.oauth2_admin_group = Group Claim value for administrator users. (Optional
auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above) auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above)
auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above) auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above)
auths.oauth2_map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding group. auths.oauth2_map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding group.
auths.enable_auto_register = Enable Auto Registration
auths.sspi_auto_create_users = Automatically create users auths.sspi_auto_create_users = Automatically create users
auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time
auths.sspi_auto_activate_users = Automatically activate users auths.sspi_auto_activate_users = Automatically activate users
@ -3311,7 +3178,6 @@ config.run_mode = Run Mode
config.git_version = Git Version config.git_version = Git Version
config.app_data_path = App Data Path config.app_data_path = App Data Path
config.repo_root_path = Repository Root Path config.repo_root_path = Repository Root Path
config.lfs_root_path = LFS Root Path
config.log_file_root_path = Log Path config.log_file_root_path = Log Path
config.script_type = Script Type config.script_type = Script Type
config.reverse_auth_user = Reverse Authentication User config.reverse_auth_user = Reverse Authentication User
@ -3386,9 +3252,6 @@ config.send_test_mail_submit = Send
config.test_mail_failed = Failed to send a testing email to "%s": %v config.test_mail_failed = Failed to send a testing email to "%s": %v
config.test_mail_sent = A testing email has been sent to "%s". config.test_mail_sent = A testing email has been sent to "%s".
config.oauth_config = OAuth Configuration
config.oauth_enabled = Enabled
config.cache_config = Cache Configuration config.cache_config = Cache Configuration
config.cache_adapter = Cache Adapter config.cache_adapter = Cache Adapter
config.cache_interval = Cache Interval config.cache_interval = Cache Interval
@ -3406,10 +3269,8 @@ config.cookie_name = Cookie Name
config.gc_interval_time = GC Interval Time config.gc_interval_time = GC Interval Time
config.session_life_time = Session Life Time config.session_life_time = Session Life Time
config.https_only = HTTPS Only config.https_only = HTTPS Only
config.cookie_life_time = Cookie Life Time
config.picture_config = Picture and Avatar Configuration config.picture_config = Picture and Avatar Configuration
config.picture_service = Picture Service
config.disable_gravatar = Disable Gravatar config.disable_gravatar = Disable Gravatar
config.enable_federated_avatar = Enable Federated Avatars config.enable_federated_avatar = Enable Federated Avatars
config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default. config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default.
@ -3429,7 +3290,6 @@ config.git_gc_timeout = GC Operation Timeout
config.log_config = Log Configuration config.log_config = Log Configuration
config.logger_name_fmt = Logger: %s config.logger_name_fmt = Logger: %s
config.disabled_logger = Disabled config.disabled_logger = Disabled
config.access_log_mode = Access Log Mode
config.access_log_template = Access Log Template config.access_log_template = Access Log Template
config.xorm_log_sql = Log SQL config.xorm_log_sql = Log SQL
@ -3449,13 +3309,9 @@ monitor.trace = Trace
monitor.performance_logs = Performance Logs monitor.performance_logs = Performance Logs
monitor.processes_count = %d Processes monitor.processes_count = %d Processes
monitor.download_diagnosis_report = Download diagnosis report monitor.download_diagnosis_report = Download diagnosis report
monitor.desc = Description
monitor.start = Start Time
monitor.execute_time = Execution Time
monitor.last_execution_result = Result monitor.last_execution_result = Result
monitor.process.cancel = Cancel process monitor.process.cancel = Cancel process
monitor.process.cancel_desc = Cancelling a process may cause data loss monitor.process.cancel_desc = Cancelling a process may cause data loss
monitor.process.children = Children
monitor.queues = Queues monitor.queues = Queues
monitor.queue = Queue: %s monitor.queue = Queue: %s
@ -3575,7 +3431,6 @@ watching = Watching
no_subscriptions = No subscriptions no_subscriptions = No subscriptions
[gpg] [gpg]
default_key=Signed with default key
error.extract_sign = Failed to extract signature error.extract_sign = Failed to extract signature
error.generate_hash = Failed to generate hash of commit error.generate_hash = Failed to generate hash of commit
error.no_committer_account = No account linked to committer's email address error.no_committer_account = No account linked to committer's email address
@ -3588,7 +3443,6 @@ error.probable_bad_default_signature = "WARNING! Although the default key has th
[units] [units]
unit = Unit unit = Unit
error.no_unit_allowed_repo = You are not allowed to access any section of this repository. error.no_unit_allowed_repo = You are not allowed to access any section of this repository.
error.unit_not_allowed = You are not allowed to access this repository section.
[packages] [packages]
title = Packages title = Packages
@ -3643,7 +3497,6 @@ composer.registry = Setup this registry in your <code>~/.composer/config.json</c
composer.install = To install the package using Composer, run the following command: composer.install = To install the package using Composer, run the following command:
composer.dependencies = Dependencies composer.dependencies = Dependencies
composer.dependencies.development = Development Dependencies composer.dependencies.development = Development Dependencies
conan.details.repository = Repository
conan.registry = Setup this registry from the command line: conan.registry = Setup this registry from the command line:
conan.install = To install the package using Conan, run the following command: conan.install = To install the package using Conan, run the following command:
conda.registry = Setup this registry as a Conda repository in your <code>.condarc</code> file: conda.registry = Setup this registry as a Conda repository in your <code>.condarc</code> file:
@ -3734,7 +3587,6 @@ owner.settings.cleanuprules.none = No cleanup rules available. Please consult th
owner.settings.cleanuprules.preview = Cleanup Rule Preview owner.settings.cleanuprules.preview = Cleanup Rule Preview
owner.settings.cleanuprules.preview.overview = %d packages are scheduled to be removed. owner.settings.cleanuprules.preview.overview = %d packages are scheduled to be removed.
owner.settings.cleanuprules.preview.none = Cleanup rule does not match any packages. owner.settings.cleanuprules.preview.none = Cleanup rule does not match any packages.
owner.settings.cleanuprules.enabled = Enabled
owner.settings.cleanuprules.pattern_full_match = Apply pattern to full package name owner.settings.cleanuprules.pattern_full_match = Apply pattern to full package name
owner.settings.cleanuprules.keep.title = Versions that match these rules are kept, even if they match a removal rule below. owner.settings.cleanuprules.keep.title = Versions that match these rules are kept, even if they match a removal rule below.
owner.settings.cleanuprules.keep.count = Keep the most recent owner.settings.cleanuprules.keep.count = Keep the most recent
@ -3871,7 +3723,6 @@ variables.none = There are no variables yet.
variables.deletion = Remove variable variables.deletion = Remove variable
variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue? variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue?
variables.description = Variables will be passed to certain actions and cannot be read otherwise. variables.description = Variables will be passed to certain actions and cannot be read otherwise.
variables.id_not_exist = Variable with ID %d does not exist.
variables.edit = Edit Variable variables.edit = Edit Variable
variables.deletion.failed = Failed to remove variable. variables.deletion.failed = Failed to remove variable.
variables.deletion.success = The variable has been removed. variables.deletion.success = The variable has been removed.

View File

@ -180,6 +180,7 @@ func DashboardPost(ctx *context.Context) {
task := cron.GetTask(form.Op) task := cron.GetTask(form.Op)
if task != nil { if task != nil {
go task.RunWithUser(ctx.Doer, nil) go task.RunWithUser(ctx.Doer, nil)
// i18n-check: admin.dashboard.*
ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op))) ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op)))
} else { } else {
ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op)) ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op))

View File

@ -847,6 +847,7 @@ func Run(ctx *context_module.Context) {
}) })
if err != nil { if err != nil {
if errLocale := util.ErrorAsLocale(err); errLocale != nil { if errLocale := util.ErrorAsLocale(err); errLocale != nil {
// i18n-check: ignore
ctx.Flash.Error(ctx.Tr(errLocale.TrKey, errLocale.TrArgs...)) ctx.Flash.Error(ctx.Tr(errLocale.TrKey, errLocale.TrArgs...))
ctx.Redirect(redirectURL) ctx.Redirect(redirectURL)
} else { } else {

View File

@ -51,6 +51,7 @@ func Activity(ctx *context.Context) {
} }
ctx.Data["DateFrom"] = timeFrom ctx.Data["DateFrom"] = timeFrom
ctx.Data["DateUntil"] = timeUntil ctx.Data["DateUntil"] = timeUntil
// i18n-check: repo.activity.period.*
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string)) ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
canReadCode := ctx.Repo.CanRead(unit.TypeCode) canReadCode := ctx.Repo.CanRead(unit.TypeCode)

View File

@ -39,6 +39,7 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message,
func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) { func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
if errAs := util.ErrorAsLocale(err); errAs != nil { if errAs := util.ErrorAsLocale(err); errAs != nil {
// i18n-check: ignore
ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...)) ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
} else if errAs, ok := errorAs[git.ErrNotExist](err); ok { } else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath)) ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))

View File

@ -314,10 +314,11 @@ func MigrateStatus(ctx *context.Context) {
var translatableMessage admin_model.TranslatableMessage var translatableMessage admin_model.TranslatableMessage
if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil { if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil {
translatableMessage = admin_model.TranslatableMessage{ translatableMessage = admin_model.TranslatableMessage{
Format: "migrate.migrating_failed.error", Format: "repo.migrate.migrating_failed.error",
Args: []any{task.Message}, Args: []any{task.Message},
} }
} }
// i18n-check: repo.migrate.migrating_failed.*
message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...) message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...)
} }

View File

@ -71,7 +71,7 @@ func ProfilePost(ctx *context.Context) {
if form.Name != "" { if form.Name != "" {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeUsername) { if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeUsername) {
ctx.Flash.Error(ctx.Tr("user.form.change_username_disabled")) ctx.Flash.Error(ctx.Tr("form.change_username_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings") ctx.Redirect(setting.AppSubURL + "/user/settings")
return return
} }
@ -107,7 +107,7 @@ func ProfilePost(ctx *context.Context) {
if form.FullName != "" { if form.FullName != "" {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeFullName) { if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeFullName) {
ctx.Flash.Error(ctx.Tr("user.form.change_full_name_disabled")) ctx.Flash.Error(ctx.Tr("form.change_full_name_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings") ctx.Redirect(setting.AppSubURL + "/user/settings")
return return
} }

View File

@ -167,6 +167,7 @@ func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
} }
func (b *Base) Tr(msg string, args ...any) template.HTML { func (b *Base) Tr(msg string, args ...any) template.HTML {
// i18n-check: ignore
return b.Locale.Tr(msg, args...) return b.Locale.Tr(msg, args...)
} }

View File

@ -70,6 +70,7 @@ func (b *BaseConfig) DoNoticeOnSuccess() bool {
// Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task. // Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task.
func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string { func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string {
realArgs := make([]any, 0, len(args)+2) realArgs := make([]any, 0, len(args)+2)
// i18n-check: admin.dashboard.*
realArgs = append(realArgs, locale.TrString("admin.dashboard."+name)) realArgs = append(realArgs, locale.TrString("admin.dashboard."+name))
if doer == "" { if doer == "" {
realArgs = append(realArgs, "(Cron)") realArgs = append(realArgs, "(Cron)")
@ -80,7 +81,9 @@ func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer
realArgs = append(realArgs, args...) realArgs = append(realArgs, args...)
} }
if doer == "" { if doer == "" {
// i18n-check: admin.dashboard.cron.*
return locale.TrString("admin.dashboard.cron."+status, realArgs...) return locale.TrString("admin.dashboard.cron."+status, realArgs...)
} }
// i18n-check: admin.dashboard.task.*
return locale.TrString("admin.dashboard.task."+status, realArgs...) return locale.TrString("admin.dashboard.task."+status, realArgs...)
} }

View File

@ -159,6 +159,7 @@ func RegisterTask(name string, config Config, fun func(context.Context, *user_mo
log.Debug("Registering task: %s", name) log.Debug("Registering task: %s", name)
i18nKey := "admin.dashboard." + name i18nKey := "admin.dashboard." + name
// i18n-check: admin.dashboard.*
if value := translation.NewLocale("en-US").TrString(i18nKey); value == i18nKey { if value := translation.NewLocale("en-US").TrString(i18nKey); value == i18nKey {
return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey) return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey)
} }

301
tools/i18n/check.go Normal file
View File

@ -0,0 +1,301 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build ignore
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"slices"
"strings"
"text/template"
"text/template/parse"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"github.com/gobwas/glob"
)
func searchTranslationKeyInDirs(keys []string) ([]bool, []string, error) {
res := make([]bool, len(keys))
untranslatedKeysSum := make([]string, 0, 20)
for _, dir := range []string{
"cmd",
"models",
"modules",
"routers",
"services",
"templates",
} {
untranslatedKeys, err := checkTranslationKeysInDir(dir, keys, &res)
if err != nil {
return nil, nil, err
}
untranslatedKeysSum = append(untranslatedKeysSum, untranslatedKeys...)
}
return res, untranslatedKeysSum, nil
}
func checkTranslationKeysInDir(dir string, keys []string, res *[]bool) ([]string, error) {
var untranslatedSum []string
if err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() ||
(!strings.HasSuffix(d.Name(), ".go") && !strings.HasSuffix(d.Name(), ".tmpl")) ||
strings.HasSuffix(d.Name(), "_test.go") { // don't search in test files
return nil
}
// search unused keys in the file
if err := searchUnusedKeyInFile(dir, path, keys, res); err != nil {
return err
}
// search untranslated keys in the file
untranslated, err := searchUnTranslatedKeyInFile(dir, path, keys)
if err != nil {
return err
}
untranslatedSum = append(untranslatedSum, untranslated...)
return nil
}); err != nil {
return nil, err
}
return untranslatedSum, nil
}
func searchUnusedKeyInFile(dir, path string, keys []string, res *[]bool) error {
bs, err := os.ReadFile(path)
if err != nil {
return err
}
for i, key := range keys {
if !(*res)[i] && strings.Contains(string(bs), `"`+key+`"`) {
(*res)[i] = true
}
}
return nil
}
func searchUntranslatedKeyInCall(path string, fset *token.FileSet, astf *ast.File, call *ast.CallExpr, arg ast.Expr, keys []string) string {
if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING {
key := strings.Trim(lit.Value, `"`)
if !slices.Contains(keys, key) {
return key
}
return ""
}
var lastCg *ast.CommentGroup
for _, cg := range astf.Comments {
if cg.End() < call.Pos() {
if lastCg == nil || cg.End() > lastCg.End() {
lastCg = cg
}
}
}
if lastCg == nil {
fmt.Printf("no comment found for a dynamic translation key: %s:%d\n", path, fset.Position(call.Pos()).Line)
os.Exit(1)
return ""
}
transKeyMatch, ok := strings.CutPrefix(lastCg.Text(), "i18n-check:")
if !ok {
fmt.Printf("no comment found for a dynamic translation key: %s:%d\n", path, fset.Position(call.Pos()).Line)
os.Exit(1)
return ""
}
transKeyMatch = strings.TrimSpace(transKeyMatch)
switch transKeyMatch {
case "ignore": // i18n-check: ignore
return ""
default: // i18n-check: <transKeyMatch>
g := glob.MustCompile(transKeyMatch)
found := false
for _, key := range keys {
if g.Match(key) {
found = true
break
}
}
if !found {
return transKeyMatch
}
}
return ""
}
func searchUntranslatedKeyInGoFile(dir, path string, keys []string) ([]string, error) {
if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
return nil, nil
}
var untranslated []string
fs := token.NewFileSet()
node, err := parser.ParseFile(fs, path, nil, parser.ParseComments)
if err != nil {
return nil, err
}
ast.Inspect(node, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
if funIdent, ok := call.Fun.(*ast.SelectorExpr); ok {
switch funIdent.Sel.Name {
case "Tr", "TrString":
if len(call.Args) >= 1 {
if key := searchUntranslatedKeyInCall(path, fs, node, call, call.Args[0], keys); key != "" {
untranslated = append(untranslated, key)
}
}
case "TrN":
if len(call.Args) >= 3 {
if key := searchUntranslatedKeyInCall(path, fs, node, call, call.Args[1], keys); key != "" {
untranslated = append(untranslated, key)
}
if key := searchUntranslatedKeyInCall(path, fs, node, call, call.Args[2], keys); key != "" {
untranslated = append(untranslated, key)
}
}
}
}
return true
})
return untranslated, err
}
func extractI18nKeys(node parse.Node) []string {
switch n := node.(type) {
case *parse.ListNode:
var keys []string
for _, sub := range n.Nodes {
keys = append(keys, extractI18nKeys(sub)...)
}
return keys
case *parse.ActionNode:
return extractI18nKeys(n.Pipe)
case *parse.PipeNode:
var keys []string
for _, cmd := range n.Cmds {
keys = append(keys, extractI18nKeys(cmd)...)
}
return keys
case *parse.CommandNode:
if len(n.Args) >= 2 {
if ident, ok := n.Args[0].(*parse.IdentifierNode); ok && ident.Ident == "ctx.locale.Tr" {
if str, ok := n.Args[1].(*parse.StringNode); ok {
return []string{str.Text}
}
}
}
}
return nil
}
func searchUntranslatedKeyInTemplateFile(dir, path string, keys []string) ([]string, error) {
if filepath.Ext(path) != ".tmpl" {
return nil, nil
}
bs, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// The template parser requires the function map otherwise it will return failure
t, err := template.New("test").Funcs(templates.NewFuncMap()).Parse(string(bs))
if err != nil {
return nil, err
}
untranslatedKeys := []string{}
keysFoundInTempl := extractI18nKeys(t.Root)
for _, key := range keysFoundInTempl {
if !slices.Contains(keys, key) {
untranslatedKeys = append(untranslatedKeys, key)
}
}
return untranslatedKeys, nil
}
func searchUnTranslatedKeyInFile(dir, path string, keys []string) ([]string, error) {
untranslatedKeys, err := searchUntranslatedKeyInGoFile(dir, path, keys)
if err != nil {
return nil, err
}
untranslatedKeysInTmpl, err := searchUntranslatedKeyInTemplateFile(dir, path, keys)
if err != nil {
return nil, err
}
return append(untranslatedKeys, untranslatedKeysInTmpl...), nil
}
func main() {
if len(os.Args) != 1 {
println("usage: clean-locales")
os.Exit(1)
}
iniFile, err := setting.NewConfigProviderForLocale("options/locale/locale_en-US.ini")
if err != nil {
panic(err)
}
keys := []string{}
for _, section := range iniFile.Sections() {
for _, key := range section.Keys() {
var trKey string
if section.Name() == "" || section.Name() == "DEFAULT" {
trKey = key.Name()
} else {
trKey = section.Name() + "." + key.Name()
}
keys = append(keys, trKey)
}
}
results, untranslatedKeys, err := searchTranslationKeyInDirs(keys)
if err != nil {
panic(err)
}
var found bool
for i, result := range results {
if !result {
if !found {
println("unused locale keys found\n---")
found = true
}
println(keys[i])
}
}
if len(untranslatedKeys) > 0 {
found = true
println("\nuntranslated locale keys found\n---")
}
for _, key := range untranslatedKeys {
println(key)
}
println()
if found {
os.Exit(1) // exit with error if any unused locale key is found
}
}