diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index f6720bf2f6d9d..a1b0f760c890d 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -178,6 +178,16 @@ jobs: GOOS: linux 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: if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed diff --git a/Makefile b/Makefile index 6a3fa60e495ce..df59ac98a8ff2 100644 --- a/Makefile +++ b/Makefile @@ -898,6 +898,16 @@ update-translations: mv ./translations/*.ini ./options/locale/ 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 generate-gitignore: ## update gitignore files $(GO) run build/generate-gitignores.go diff --git a/models/actions/runner.go b/models/actions/runner.go index 81d4249ae0b85..6e98f15f5d05a 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -114,6 +114,7 @@ func (r *ActionRunner) StatusName() string { } func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string { + // i18n-check: actions.runners.status.* return lang.TrString("actions.runners.status." + r.StatusName()) } diff --git a/models/actions/status.go b/models/actions/status.go index 2b1d70613c71b..895fd52fb36de 100644 --- a/models/actions/status.go +++ b/models/actions/status.go @@ -43,6 +43,7 @@ func (s Status) String() string { // LocaleString returns the locale string name of the Status func (s Status) LocaleString(lang translation.Locale) string { + // i18n-check: actions.status.* return lang.TrString("actions.status." + s.String()) } diff --git a/models/git/commit_status.go b/models/git/commit_status.go index f85e1b15e5600..7f2f7d4030991 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -206,6 +206,7 @@ func (status *CommitStatus) APIURL(ctx context.Context) string { // LocaleString returns the locale string name of the Status func (status *CommitStatus) LocaleString(lang translation.Locale) string { + // i18n-check: repo.commitstatus.* return lang.TrString("repo.commitstatus." + status.State.String()) } diff --git a/models/issues/comment.go b/models/issues/comment.go index 9bef96d0ddfcc..9d2cdb0bfe71e 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -225,11 +225,13 @@ const ( // LocaleString returns the locale string name of the role func (r RoleInRepo) LocaleString(lang translation.Locale) string { + // i18n-check: repo.issues.role.* return lang.TrString("repo.issues.role." + string(r)) } // LocaleHelper returns the locale tooltip of the role func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { + // i18n-check: repo.issues.role.*_helper return lang.TrString("repo.issues.role." + string(r) + "_helper") } diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index a1e101dd621cb..e0781cbf96f6e 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -128,6 +128,7 @@ func BuildComplexityError(locale translation.Locale) template.HTML { buffer.WriteString("
%[2]s
to %[4]s
%[6]s`
issues.force_push_compare = Compare
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_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_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_remove = "removed the due date %s %s"
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.issue_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.content.empty = You need to leave a comment indicating the requested change(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.remove_review_request = "removed review request for %s %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.stale = Updated since 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.content_history.deleted = deleted
issues.content_history.edited = edited
@@ -2151,7 +2062,6 @@ contributors.contribution_type.additions = Additions
contributors.contribution_type.deletions = Deletions
settings = Settings
-settings.desc = Settings is where you can manage the settings for the repository
settings.options = Repository
settings.public_access = Public Access
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.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.pulling_remote_title = Pulling from a remote repository
settings.mirror_settings.mirrored_repository = Mirrored repository
settings.mirror_settings.pushed_repository = Pushed repository
settings.mirror_settings.direction = Direction
@@ -2285,13 +2194,10 @@ settings.signing_settings = Signing Verification Settings
settings.trust_model = Signature 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.collaborator = Collaborator
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.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.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.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
@@ -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_content = Hook Content
settings.update_githook = Update Hook
-settings.add_webhook_desc = Gitea will send POST
requests with a specified content type to the target URL. Read more in the webhooks guide.
settings.payload_url = Target URL
settings.http_method = HTTP Method
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.delete_webhook = Remove Webhook
settings.recent_deliveries = Recent Deliveries
-settings.hook_type = Hook Type
-settings.slack_token = Token
-settings.slack_domain = Domain
settings.slack_channel = Channel
settings.add_web_hook_desc = Integrate %s into your repository.
settings.web_hook_name_gitea = Gitea
@@ -2469,12 +2371,7 @@ settings.branches = Branches
settings.protected_branch = Branch Protection
settings.protected_branch.save_rule = Save 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 '%s'
-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_desc = No pushing will be allowed to this branch.
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 %[2]s documentation for pattern syntax. Examples: .drone.yml
, /docs/**/*.txt
."
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 %[2]s documentation for pattern syntax. Examples: .drone.yml
, /docs/**/*.txt
."
-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.remove_protected_branch_success = Branch protection for rule "%s" has been removed.
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.merge_style_desc = Merge Styles
settings.default_merge_style_desc = Default Merge Style
-settings.choose_branch = Choose a branch…
settings.no_protected_branch = There are no protected branches.
settings.edit_protected_branch = Edit
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_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_from=old branch name
-settings.rename_branch_to=new branch name
settings.rename_branch=Rename branch
diff.browse_source = Browse Source
@@ -2676,7 +2568,6 @@ diff.protected = Protected
diff.image.side_by_side = Side by Side
diff.image.swipe = Swipe
diff.image.overlay = Overlay
-diff.has_escaped = This line has hidden Unicode characters
diff.show_file_tree = Show file tree
diff.hide_file_tree = Hide file tree
diff.submodule_added = Submodule %[1]s added at %[2]s
@@ -2696,7 +2587,6 @@ release.compare = Compare
release.edit = edit
release.ahead.commits = %d commits
release.ahead.target = to %s since this release
-tag.ahead.target = to %s since this tag
release.source_code = Source Code
release.new_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_invalid = The tag name is not valid.
release.tag_name_protected = The tag name is protected.
-release.tag_already_exist = This tag name already exists.
release.downloads = Downloads
release.download_count = Downloads: %s
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.already_exists = A branch named "%s" already exists.
-branch.delete_head = Delete
branch.delete = Delete Branch "%s"
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?
@@ -2762,7 +2650,6 @@ branch.create_new_branch = Create branch from branch:
branch.confirm_create_branch = Create branch
branch.warning_rename_default_branch = You are renaming the default branch.
branch.rename_branch_to = Rename "%s" to:
-branch.confirm_rename_branch = Rename branch
branch.create_branch_operation = Create branch
branch.new_branch = Create new branch
branch.new_branch_from = Create new branch from "%s"
@@ -2778,7 +2665,6 @@ tag.create_tag_from = Create new tag from "%s"
tag.create_success = Tag "%s" has been created.
topic.manage_topics = Manage Topics
-topic.done = Done
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.
@@ -2829,7 +2715,6 @@ form.create_org_not_allowed = You are not allowed to create an organization.
settings = Settings
settings.options = Organization
-settings.full_name = Full Name
settings.email = Contact Email
settings.website = Website
settings.location = Location
@@ -2867,11 +2752,8 @@ settings.confirm_delete_account = Confirm Deletion
settings.delete_failed = Delete Organization failed because of internal error
settings.delete_successful = Organization %s has been deleted successfully.
settings.hooks_desc = Add webhooks which will be triggered for all repositories under this organization.
-
settings.labels_desc = Add labels which can be used on issues for all repositories under this organization.
-members.membership_visibility = Membership Visibility:
-members.public = Visible
members.public_helper = make hidden
members.private = Hidden
members.private_helper = make visible
@@ -2882,8 +2764,6 @@ members.remove = Remove
members.remove.detail = Remove %[1]s from %[2]s?
members.leave = Leave
members.leave.detail = Leave %s?
-members.invite_desc = Add a new member to %s:
-members.invite_now = Invite Now
teams.join = Join
teams.leave = Leave
@@ -2903,7 +2783,6 @@ teams.admin_access_helper = Members can pull and push to team repositories and a
teams.no_desc = This team has no description
teams.settings = Settings
teams.owners_permission_desc = Owners have full access to all repositories and have administrator access to the organization.
-teams.members = Team Members
teams.update_settings = Update Settings
teams.delete_team = Delete Team
teams.add_team_member = Add Team Member
@@ -2912,14 +2791,9 @@ teams.invite_team_member.list = Pending Invitations
teams.delete_team_title = Delete Team
teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue?
teams.delete_team_success = The team has been deleted.
-teams.read_permission_desc = This team grants Read access: members can view and clone team repositories.
teams.write_permission_desc = This team grants Write access: members can read from and push to team repositories.
teams.admin_permission_desc = This team grants Admin access: members can read from, push to and add collaborators to team repositories.
-teams.create_repo_permission_desc = Additionally, this team grants Create repository 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.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_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.
@@ -2930,9 +2804,6 @@ teams.specific_repositories = Specific repositories
teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this will not automatically remove repositories already added with All repositories.
teams.all_repositories = All repositories
teams.all_repositories_helper = Team has access to all repositories. Selecting this will add all existing repositories to the team.
-teams.all_repositories_read_permission_desc = This team grants Read access to all repositories: members can view and clone repositories.
-teams.all_repositories_write_permission_desc = This team grants Write access to all repositories: members can read from and push to repositories.
-teams.all_repositories_admin_permission_desc = This team grants Admin access to all repositories: members can read from, push to and add collaborators to repositories.
teams.invite.title = You have been invited to join team %s in organization %s.
teams.invite.by = Invited by %s
teams.invite.description = Please click the button below to join the team.
@@ -3146,7 +3017,6 @@ repos.unadopted = Unadopted Repositories
repos.unadopted.no_more = No more unadopted repositories found
repos.owner = Owner
repos.name = Name
-repos.private = Private
repos.issues = Issues
repos.size = Size
repos.lfs_size = LFS Size
@@ -3185,7 +3055,6 @@ auths.updated = Updated
auths.auth_type = Authentication Type
auths.auth_name = Authentication Name
auths.security_protocol = Security Protocol
-auths.domain = Domain
auths.host = Host
auths.port = Port
auths.bind_dn = Bind DN
@@ -3214,7 +3083,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_removal = Remove users from synchronized teams if user does not belong to corresponding LDAP group
auths.enable_ldap_groups = Enable LDAP groups
-auths.ms_ad_sa = MS AD Search Attributes
auths.smtp_auth = SMTP Authentication Type
auths.smtphost = SMTP Host
auths.smtpport = SMTP Port
@@ -3251,7 +3119,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_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.enable_auto_register = Enable Auto Registration
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_activate_users = Automatically activate users
@@ -3308,7 +3175,6 @@ config.run_mode = Run Mode
config.git_version = Git Version
config.app_data_path = App Data Path
config.repo_root_path = Repository Root Path
-config.lfs_root_path = LFS Root Path
config.log_file_root_path = Log Path
config.script_type = Script Type
config.reverse_auth_user = Reverse Authentication User
@@ -3383,9 +3249,6 @@ config.send_test_mail_submit = Send
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.oauth_config = OAuth Configuration
-config.oauth_enabled = Enabled
-
config.cache_config = Cache Configuration
config.cache_adapter = Cache Adapter
config.cache_interval = Cache Interval
@@ -3403,10 +3266,8 @@ config.cookie_name = Cookie Name
config.gc_interval_time = GC Interval Time
config.session_life_time = Session Life Time
config.https_only = HTTPS Only
-config.cookie_life_time = Cookie Life Time
config.picture_config = Picture and Avatar Configuration
-config.picture_service = Picture Service
config.disable_gravatar = Disable Gravatar
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.
@@ -3426,7 +3287,6 @@ config.git_gc_timeout = GC Operation Timeout
config.log_config = Log Configuration
config.logger_name_fmt = Logger: %s
config.disabled_logger = Disabled
-config.access_log_mode = Access Log Mode
config.access_log_template = Access Log Template
config.xorm_log_sql = Log SQL
@@ -3446,13 +3306,9 @@ monitor.trace = Trace
monitor.performance_logs = Performance Logs
monitor.processes_count = %d Processes
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.process.cancel = Cancel process
monitor.process.cancel_desc = Cancelling a process may cause data loss
-monitor.process.children = Children
monitor.queues = Queues
monitor.queue = Queue: %s
@@ -3572,7 +3428,6 @@ watching = Watching
no_subscriptions = No subscriptions
[gpg]
-default_key=Signed with default key
error.extract_sign = Failed to extract signature
error.generate_hash = Failed to generate hash of commit
error.no_committer_account = No account linked to committer's email address
@@ -3585,7 +3440,6 @@ error.probable_bad_default_signature = "WARNING! Although the default key has th
[units]
unit = Unit
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]
title = Packages
@@ -3640,7 +3494,6 @@ composer.registry = Setup this registry in your ~/.composer/config.json.condarc
file:
@@ -3731,7 +3584,6 @@ owner.settings.cleanuprules.none = No cleanup rules available. Please consult th
owner.settings.cleanuprules.preview = Cleanup Rule Preview
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.enabled = Enabled
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.count = Keep the most recent
@@ -3868,7 +3720,6 @@ variables.none = There are no variables yet.
variables.deletion = Remove variable
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.id_not_exist = Variable with ID %d does not exist.
variables.edit = Edit Variable
variables.deletion.failed = Failed to remove variable.
variables.deletion.success = The variable has been removed.
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index 0cd13acf6012b..efa6c67d88d94 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -180,6 +180,7 @@ func DashboardPost(ctx *context.Context) {
task := cron.GetTask(form.Op)
if task != 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)))
} else {
ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op))
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 7e1b923fa4b05..f19cbf1b9cb87 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -847,6 +847,7 @@ func Run(ctx *context_module.Context) {
})
if err != nil {
if errLocale := util.ErrorAsLocale(err); errLocale != nil {
+ // i18n-check: ignore
ctx.Flash.Error(ctx.Tr(errLocale.TrKey, errLocale.TrArgs...))
ctx.Redirect(redirectURL)
} else {
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index 8232f0cc04bc3..0a883e774acbd 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -51,6 +51,7 @@ func Activity(ctx *context.Context) {
}
ctx.Data["DateFrom"] = timeFrom
ctx.Data["DateUntil"] = timeUntil
+ // i18n-check: repo.activity.period.*
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
canReadCode := ctx.Repo.CanRead(unit.TypeCode)
diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go
index 245226a039e4d..04904a178157a 100644
--- a/routers/web/repo/editor_error.go
+++ b/routers/web/repo/editor_error.go
@@ -39,6 +39,7 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message,
func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
if errAs := util.ErrorAsLocale(err); errAs != nil {
+ // i18n-check: ignore
ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
} else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
index ea15e90e5c297..9b3e52191aea4 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -314,10 +314,11 @@ func MigrateStatus(ctx *context.Context) {
var translatableMessage admin_model.TranslatableMessage
if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil {
translatableMessage = admin_model.TranslatableMessage{
- Format: "migrate.migrating_failed.error",
+ Format: "repo.migrate.migrating_failed.error",
Args: []any{task.Message},
}
}
+ // i18n-check: repo.migrate.migrating_failed.*
message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...)
}
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 98995cd69c435..622c897cc8b6c 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -71,7 +71,7 @@ func ProfilePost(ctx *context.Context) {
if form.Name != "" {
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")
return
}
@@ -107,7 +107,7 @@ func ProfilePost(ctx *context.Context) {
if form.FullName != "" {
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")
return
}
diff --git a/services/context/base.go b/services/context/base.go
index f3f92b7eeb906..a302658bc9828 100644
--- a/services/context/base.go
+++ b/services/context/base.go
@@ -167,6 +167,7 @@ func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
}
func (b *Base) Tr(msg string, args ...any) template.HTML {
+ // i18n-check: ignore
return b.Locale.Tr(msg, args...)
}
diff --git a/services/cron/setting.go b/services/cron/setting.go
index 6dad88830abb5..09fac9166293b 100644
--- a/services/cron/setting.go
+++ b/services/cron/setting.go
@@ -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.
func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string {
realArgs := make([]any, 0, len(args)+2)
+ // i18n-check: admin.dashboard.*
realArgs = append(realArgs, locale.TrString("admin.dashboard."+name))
if doer == "" {
realArgs = append(realArgs, "(Cron)")
@@ -80,7 +81,9 @@ func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer
realArgs = append(realArgs, args...)
}
if doer == "" {
+ // i18n-check: admin.dashboard.cron.*
return locale.TrString("admin.dashboard.cron."+status, realArgs...)
}
+ // i18n-check: admin.dashboard.task.*
return locale.TrString("admin.dashboard.task."+status, realArgs...)
}
diff --git a/services/cron/tasks.go b/services/cron/tasks.go
index f8a7444c49e5c..6e29b5fbb6b89 100644
--- a/services/cron/tasks.go
+++ b/services/cron/tasks.go
@@ -159,6 +159,7 @@ func RegisterTask(name string, config Config, fun func(context.Context, *user_mo
log.Debug("Registering task: %s", name)
i18nKey := "admin.dashboard." + name
+ // i18n-check: admin.dashboard.*
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)
}
diff --git a/build/backport-locales.go b/tools/i18n/backport.go
similarity index 100%
rename from build/backport-locales.go
rename to tools/i18n/backport.go
diff --git a/tools/i18n/check.go b/tools/i18n/check.go
new file mode 100644
index 0000000000000..33de999fb0ab5
--- /dev/null
+++ b/tools/i18n/check.go
@@ -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: