Skip to content

Conversation

tomasz1986
Copy link
Member

Currently, action and type are both displayed only in English. This commit makes it possible to translate both of them.

Signed-off-by: Tomasz Wilczyński twilczynski@naver.com

Screenshots

image

@tomasz1986 tomasz1986 changed the title gui: allow to translate action and type in Recent Changes modal gui: Allow to translate action and type in Recent Changes modal Sep 17, 2022
Currently, action and type are both displayed only in English. This
commit makes it possible to translate both of them.

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
@tomasz1986 tomasz1986 force-pushed the tomasz86/gui/translate-recent-changes branch from ac3fe55 to a56bd8c Compare September 17, 2022 17:48
@@ -17,8 +17,16 @@
<tr ng-repeat="changeEvent in globalChangeEvents">
<td ng-if="changeEvent.data.modifiedBy">{{friendlyNameFromShort(changeEvent.data.modifiedBy)}}</td>
<td ng-if="!changeEvent.data.modifiedBy"><span translate>Unknown</span></td>
<td>{{changeEvent.data.action}}</td>
<td>{{changeEvent.data.type}}</td>
<td ng-switch="changeEvent.data.action">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be easier to treat these as keys directly? Just like we do for the theme-name-* strings. Like this:

<td>{{("change-action-" + changeEvent.data.action) | translate}}</td>

That would allow some more context to distinguish from other uses of the single words.

Copy link
Member Author

@tomasz1986 tomasz1986 Sep 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to make this work, but for me, it seems that a clean and non-hard-coded solution without fiddling around with JS could be to allow syntax like the one described at https://toptal.com/angular-js/internationalize-your-angularjs-app, which in HTML is

<span translate="change-action-modified">modified</span>
<span translate="change-action-deleted">deleted</span>

and in JSON

"change-action-modified": "modified",
"change-action-deleted": "deleted",

and so on. The current translate.go script doesn't support it though. I've tried to incorporate the syntax into the script, but my Go skills and knowledge are extremely rudimentary, so I've only managed to go as far as save the value to a variable when it's non empty. Not really sure where exactly it should be passed then and added to json as name though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it's not impossible to add strings to the translation template without help from the script. Apparently it does catch the theme names, so maybe have a look at how that's done (I don't remember now).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The theme names are actually a combination of Go, JS, and the actual filenames, so it's quite different than this one. Here, I only want to let the script accept <div translate="key">value</div> syntax universally 😉.

I've now given this a try and modified the script code to use translate attribute value as key name when present. Please have a look and let me know if this is the right way to go.

Also, do you think that the two other strings, i.e. file and folder should also follow the same change-type-* pattern, or is it OK to use them as is?

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
Copy link
Member

@acolomb acolomb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, do you think that the two other strings, i.e. file and folder should also follow the same change-type-* pattern, or is it OK to use them as is?

IMHO they should be treated the same as change-action-*.

I'll let others chime in on the question whether some translation keys in lang-en.json that won't get regenerated by subsequent translation script runs are acceptable. If so, I'd err for a simpler solution.

@@ -102,12 +106,16 @@ func inTranslate(n *html.Node, filename string) {
}
}

func translation(v string) {
func translation(v string, k string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need a separate key here. The v argument holds the key, which also serves as the initial translation unless there is a better one.

Comment on lines 21 to 22
<span ng-switch-when="modified" translate="change-action-modified">modified</span>
<span ng-switch-when="deleted" translate="change-action-deleted">deleted</span>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little too much repetition for my taste. You need a separate branch for every possible value, plus an entry in the language file, and finally it's unclear why the string reads "modified" when the translation key is "change-action-modified". If we're going to use abstract key strings, the (English) translation should live in the JSON file just like for other languages.

My proposal avoids all that complexity, with the only downside that not necessarily all keys are gathered from the source. But what's already in lang-en.json won't get lost by running the translation script anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and finally it's unclear why the string reads "modified" when the translation key is "change-action-modified". If we're going to use abstract key strings, the (English) translation should live in the JSON file just like for other languages.

I'm not sure if I understand this one. How is

   "change-action-deleted": "deleted",
   "change-action-modified": "modified",

different than what's already used for themes?

   "theme-name-black": "Black",
   "theme-name-dark": "Dark",
   "theme-name-default": "Default",
   "theme-name-light": "Light",

Do you mean that the key names are too cryptic?

My proposal avoids all that complexity, with the only downside that not necessarily all keys are gathered from the source. But what's already in lang-en.json won't get lost by running the translation script anyway.

But the ability to regenerate from scratch fully automatically is the big advantage (and arguably the main point) of the script… I don't really think it's worth to break it to add a few hard-coded lines…

Copy link
Member

@acolomb acolomb Sep 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the theme names, the key is derived from the directory name, and the latter (capitalized) is used as initial translation. That happens to be the same as the English translation value for the key. But it's certainly possible we might want to set the English value to something else in the future. For example we've seen "master" being widely replaced by "main" for sociological reasons. Or a typo might be found in the English string. Both would be reasons to adjust lang-en.json, but keeping the key (and thus the source file) untouched in order to not cause retranslation in all languages. That's the reason (IIUC) why the translation(v string) function does not overwrite messages which already exist in the input (lang-en.json).

Your solution puts both the key and the English translation inside the source HTML file. While the purpose of the key is having a unique reference to which translation is needed. The English translation does not need to live there separately.

But the ability to regenerate from scratch fully automatically is the big advantage (and arguably the main point) of the script…

IMHO the main point is updating translations from the source. Since we do keep lang-en.json in version control, there's nothing wrong with making manual changes to it, despite having a helper script for automating the most common updates, which respects those manual adjustments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I don't really see anything wrong in keeping everything in the HTML… Isn't this how all existing translations (but themes) work in Syncthing currently? Meaning, all strings live in HTML and JS, and the JSON files can all be re-created 100% automatically.

The only reason for making the keys different from values in this particular case is to avoid re-using the single words in unrelated places, isn't it? I understand that ideally it would be better to separate (English) translations from the code, but currently both HTML and JS are English first, and translations come second… Isn't it a bit too late for changing that?

IMHO the main point is updating translations from the source. Since we do keep lang-en.json in version control, there's nothing wrong with making manual changes to it, despite having a helper script for automating the most common updates, which respects those manual adjustments.

Not really a fan of doing manual changes to the JSON files, to tell the truth… This would also make it impossible to easily distinguish between current and obsolete strings, wouldn't it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really a fan of doing manual changes to the JSON files, to tell the truth… This would also make it impossible to easily distinguish between current and obsolete strings, wouldn't it?

The HTML and JS files contain keys, not English translations. Although they do usually match, it's a semantic difference for the reasons given above. The reason why we should have different key and English translation in this case is because the key is actually a programmatic value coming from the backend, not meant as a GUI label, just like the theme directory names. It doesn't mean we need to separate key and (English) value for all other cases.

If the lang-en.json file could definitely always be completely regenerated, there would be little reason to keep it versioned. You don't need to do manual changes there, as the script will still pick up new stuff. If you want to to weed out obsolete strings, you still can in the same way by emptying it first. But before committing, you should review the changes to only remove strings that are definitely no longer in use.

Yes, there are manual steps involved, but only for rather uncommon tasks. By trying to make everything automatic, you lose the ability to manually override when needed (which we have now). And bending over backwards to adjust the automation for every special case that comes along is not worth the added complexity. Just utilize the flexibility we have.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, for me it still seems to be more of a debate between "flexible yet fragile" and "inflexible but automated". I personally always prefer automated solutions over something that can easily break due to user error (e.g. if someone removes the hard-coded strings, maybe considering them obsolete, or by mistake, or simply not knowing that the file can't be generated from scratch with the script). On top of that, I think the point about being unable to distinguish between used and unused strings is still relevant too.

That workflow is based on the assumption that regenerating is always completely possible from an empty state, which is not unconditionally true.

Well, it's not written anywhere, but that is what seems to have always been the case here since the very beginning. There's literally not even one line in the JSON file that can't be automatically generated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On top of that, I think the point about being unable to distinguish between used and unused strings is still relevant too.

In general I agree with you that automation is a good thing to avoid human errors. We can build more automation, trying to make that distinction without human consideration. But in this case, the involved complexity outweighs the benefit. It goes like this: You regenerate from scratch, see that some change-action-... lines were removed, grep for change-action and find the place where they are used and why they're not put back automatically. Then you discard that change before committing. Which happened roughly once in the history of Syncthing so far (#8288).

That workflow is based on the assumption that regenerating is always completely possible from an empty state, which is not unconditionally true.

Well, it's not written anywhere, but that is what seems to have always been the case here since the very beginning.

It was always possible to regenerate from the checked in lang-en.json file, exactly because existing strings were not discarded. I didn't make that decision, but it looks like intentional behavior in the code.

There's literally not even one line in the JSON file that can't be automatically generated.

That's only true since you argued to remove them in #8288, before it has not always been the case. And if we go the route of manual additions for this PR, it will not be again, which is also acceptable. Whoever, for whatever reason, tries to clear out that translation file (which is usually unneeded), will see that some lines don't come back and better be ready to invest some time figuring out why (actually removed or just not auto-extracted). I for one will watch out for commits that remove translation strings for no reason.

This PR is a Good Thing (thank you) and shouldn't be held up by fundamental discussions. Let's aim for less complexity unless there is a clear, large benefit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for the record, removing unused strings wasn't my idea. #8288 was only a follow-up to ec718e7 (which is actually linked to from one of the comments in #8288). The example to remove them had already been set before.

I understand that cluttering the HTML may not be the best method, but why not put everything in the translation script then? This will allow to keep the HTML clean while still not having to add any hard-coded lines to JSON.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is hard to find and not necessary. Again, there's nothing wrong with having manual additions in the JSON file. If it was a purely generated file, we wouldn't need to track it in Git.

The proper solution to this whole thing would be to extend the translations to cover the backend code as well. There are more examples IIRC of English phrases that might get passed to the world untranslated. Even taking aside the CLI strings and log messages. But that's definitely not in scope for this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, some research turns out that i18n for a golang app is rather complicated compared to our current system. Tested both the gotext and goi18n packages, but no quick and easy solution to just mark some strings that should be extracted for translation. These packages would allow adding proper translations to more parts of the backend code later. Some custom solution could be cooked up, but again it's not really worth the effort just to get these automatically put into the GUI strings JSON resource.

Another approach would be to use the translation as suggested above (without ng-switch). Then put some $translate.instant() calls into a JS function that never gets to run, just for the script to pick up expected values that should be translated. Could be hidden inside a function along these lines:

$scope.changeEventAction = function (changeEvent) {
    return $translate.instant('change-action-' + changeEvent.data.action);
    // Only for extracting expected translation keys:
    $translate.instant('change-action-modified');
    $translate.instant('change-action-deleted');
};

This would lead to the initial translation being identical to the key though, when regenerating from an empty lang-en.json file. Instead the fall-back value should be the action itself (the single English word from the API response). Which, again, is drop-dead easy when just adding it to the JSON file directly.

@imsodin
Copy link
Member

imsodin commented Sep 26, 2022

I stopped reading the lengthy exchange at some point. Is it correct to say a huge can of worms was opened over having a separate translation for "modified" anywhere versus "modified" as in the file change action? If that's the case, is it really worth opening that can over this distinction (as in I don't really see it's point, what is it?).

@acolomb
Copy link
Member

acolomb commented Sep 27, 2022

That's not really my pain point at least. I dislike the current state where the HTML line contains almost the same value three times (switch, translation key and another translation) where just passing the variable to the translate function once would be enough.

If we want to have keys differing from the initial English translation, I think it's still not worth the added complexity of adding different parsing rules. The goal of being able to completely automatically regenerate lang-en.json at every moment is not worth it IMHO. Rather just pipe the string (modified, deleted, file, directory) to translate, with or without prefix. Add the four entries to lang-en.json once and forget that file again for the foreseeable future. Everything else is already handled by existing automation.

@tomasz1986
Copy link
Member Author

tomasz1986 commented Sep 27, 2022

Well, to be specific, the original PR looked like this, without any translation keys. The span with ng-switch-default isn't really needed either, but I added it there just in case.

            <td ng-switch="changeEvent.data.action">
              <span ng-switch-when="modified" translate>modified</span>
              <span ng-switch-when="deleted" translate>deleted</span>
              <span ng-switch-default>{{changeEvent.data.action}}</span>
            </td>
            <td ng-switch="changeEvent.data.type">
              <span ng-switch-when="file" translate>file</span>
              <span ng-switch-when="folder" translate>folder</span>
              <span ng-switch-default>{{changeEvent.data.type}}</span>
            </td>

@imsodin
Copy link
Member

imsodin commented Sep 27, 2022

Ok, so options are a) using the event type and action directly as translation keys and b) handle actions/types explicitly which means having "deleted" et. al. twice on a line (above/first commit). Personally I prefer the simple, explicit and automated option with inline duplication over the concise and manual one.

@tomasz1986
Copy link
Member Author

tomasz1986 commented Sep 27, 2022

It's a little bit more complicated 😉. Three solutions have been proposed actually.

  1. The original PR.

HTML:

<td ng-switch="changeEvent.data.action">
	<span ng-switch-when="modified" translate>modified</span>
	<span ng-switch-when="deleted" translate>deleted</span>
	<span ng-switch-default>{{changeEvent.data.action}}</span>
</td>

and JSON generated automatically with go run build.go translate:

"deleted": "deleted",
"modified": "modified",

This is the simplest solution, but problems may arise when the same single words are re-used in other places in a different context.

  1. The current code.

HTML:

<td ng-switch="changeEvent.data.action">
	<span ng-switch-when="modified" translate="change-action-modified">modified</span>
	<span ng-switch-when="deleted" translate="change-action-deleted">deleted</span>
	<span ng-switch-default>{{changeEvent.data.action}}</span>
</td>

and JSON still generated automatically with go run build.go translate:

"change-action-modified": "deleted",
"change-action-deleted": "modified",

This solution is similar to the first one, albeit more verbose, but it does solve the problem of re-using the same single words in other places.

  1. @acolomb's solution.

HTML:

<td>{{("change-action-" + changeEvent.data.action) | translate}}</td>

and JSON modified manually.

"change-action-modified": "deleted",
"change-action-deleted": "modified",

This solution is very concise. However, it requires essentially hard-coding the strings in JSON instead of using the translate.go script to generate them.

The main point of the lengthy discussion was that I didn't want to add the hard-coded lines, so that we could still use translate.go to regenerate the JSON from scratch when needed, while @acolomb believed that there was no problem with adding the hard-coded lines to JSON manually.

@imsodin
Copy link
Member

imsodin commented Sep 27, 2022

Ah well I believed the key names mattered in transifex, but I see now they actually don't. So my original concern over "change-action-modified" being translated literally doesn't apply.

Anyway, still the main difference is more concise html vs being able to autogenerate translations, right (with a variation on key names)? And in the concise version a missing translation key would be displayed in the GUI as "change-action-missing" instead of just "missing", right?
My 2cents are that I'd prefer "generatability" with verbosity over concise and manual - and I don't really care either way.

@calmh
Copy link
Member

calmh commented Sep 27, 2022

The original sounds fine to me. We have a history of blowing away lang-en.json and trusting it to be regenerated, and even if we don't do that very often it seems like a property it might be worth spending at least minimal effort to retain.

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
@tomasz1986
Copy link
Member Author

I have now reverted everything to the original PR 😵.

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
@acolomb acolomb merged commit 458d6cf into syncthing:main Jan 5, 2023
@tomasz1986 tomasz1986 deleted the tomasz86/gui/translate-recent-changes branch January 5, 2023 14:43
calmh added a commit to calmh/syncthing that referenced this pull request Jan 23, 2023
* main: (69 commits)
  Handle relay connect timeout (fixes syncthing#8749) (syncthing#8755)
  gui, man, authors: Update docs, translations, and contributors
  build: Go 1.19.5
  gui, man, authors: Update docs, translations, and contributors
  script: Add weblatedl.go for downloading updated translations (syncthing#8723)
  gui: Allow to translate action and type in Recent Changes modal (syncthing#8548)
  gui, man, authors: Update docs, translations, and contributors
  gui: Fix undefined lastSeenDays error in disconnected-inactive status check (ref syncthing#8530) (syncthing#8730)
  gui, man, authors: Update docs, translations, and contributors
  gui, api: Indicate running under container (syncthing#8728)
  lib/fs: Use io/fs errors as recommended in std lib (syncthing#8726)
  build: Handle co-authors (ref syncthing#3744) (syncthing#8708)
  lib/fs: Watching is unsupported on android/amd64 (fixes syncthing#8709) (syncthing#8710)
  lib/model: Only log at info level if setting change time fails (syncthing#8725)
  lib/model: Don't lower rescan interval from default on auto accepted enc folder (fixes syncthing#8572) (syncthing#8573)
  gui, man, authors: Update docs, translations, and contributors
  gui: Remove unmaintained language variant nl-BE (syncthing#8722)
  gui, script: Fix indentation in lang-en.json to match others (syncthing#8721)
  docker: Ensure entrypoint is executable (syncthing#8719)
  Go 1.19.4
  ...
@st-review st-review added the frozen-due-to-age Issues closed and untouched for a long time, together with being locked for discussion label Jan 5, 2024
@syncthing syncthing locked and limited conversation to collaborators Jan 5, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
frozen-due-to-age Issues closed and untouched for a long time, together with being locked for discussion
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants