Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Plural and singular translation without complex implementation #15323

Closed
XTorLukas opened this issue May 7, 2024 · 26 comments
Closed

Comments

@XTorLukas
Copy link
Contributor

XTorLukas commented May 7, 2024

What feature or improvement do you think would benefit Files?

I've got an idea to enhance the efficiency of implementing translation for both plural and singular words in Files.

  • Personally, I've developed and utilize my own straightforward extension adhering to the i18n standard, compatible with crowdin. This eliminates the need for duplicate keys, streamlining the writing process.
  • I personally employ RESX with ResourceManager, and adapting its usage here would entail minor modifications.
  • Additionally, all spell checks operate through a RegEx pattern, fine-tuned to disregard most whitespace.
    There's also the option to incorporate prefixes and postfixes.
  • Nevertheless, experimentation should provide a good understanding of its functionality.

Sample of examples

Snímek obrazovky 2024-05-07 134849

Snímek obrazovky 2024-05-07 135052

Examples of display and use in crowdin

Snímek obrazovky 2024-05-07 135407

Snímek obrazovky 2024-05-07 135454

Test RegExr online work

Crowdin ICU message implementation (doc)

@XTorLukas
Copy link
Contributor Author

XTorLukas commented May 7, 2024

Sample of personal use

StringExtensions.cs

// Copyright (c) 2024 Laštůvka Lukáš
// Licensed under the Apache-2.0 license. See the LICENSE.


using System.Text.RegularExpressions;

namespace ProjectName.Extensions;

public static partial class StringExtensions
{
    // Default resource manager
    private static readonly ResourceManager _resourceManager = new("ProjectName.Strings.Resources", typeof(Program).Assembly);

    // Default pattern for plural strings
    private const string _regexPattern = @"\{\s*(?'index'\d+)\s*,\s*plural\s*,\s*=\s*(?'num'\d+)\s*\{\s*(?'std'[^{]+)\s*\}\s*(?:(one|few|many|other)\s*\{\s*([^{}]+)\s*\})?\s*(?:(one|few|many|other)\s*\{\s*([^{}]+)\s*\})?\s*(?:(one|few|many|other)\s*\{\s*([^{}]+)\s*\})?\s*(?:(one|few|many|other)\s*\{\s*([^{}]+)\s*\})?\s*\}";

    // Default string to replace with amount
    private const string _replaceString = "#";

    // Default regex object for pattern
    [GeneratedRegex(_regexPattern, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace)]
    private static partial Regex ResourceRegex();
    private static readonly Regex _regex = ResourceRegex();

    /// <summary>
    /// Tests if a resource with the specified key exists in the resource manager with the specified culture.
    /// </summary>
    /// <param name="resKey">The key of the resource to look for.</param>
    /// <param name="culture">The culture to use when looking for the resource.</param>
    /// <param name="value">The value of the resource, if it exists.</param>
    /// <returns>True if the resource exists, false otherwise.</returns>
    private static bool ExistsLocalizedResource(this string resKey, CultureInfo culture, out string value)
    {
        try
        {
            var keyValue = _resourceManager.GetResourceSet(culture, true, true)?.GetString(resKey);
            if (keyValue != null)
            {
                value = keyValue;
                return true;
            }
        }
        catch { }
        value = string.Empty;
        return false;
    }

    /// <summary>
    /// Gets the plural category for the specified number.
    /// </summary>
    /// <param name="amount">The number for which to get the plural category.</param>
    /// <returns>The plural category for the specified number.</returns>
    private static string GetPluralCategory(int amount)
    {
        if (amount == 1)
        {
            return "one";
        }
        else if (amount % 10 >= 2 && amount % 10 <= 4 && (amount % 100 < 10 || amount % 100 >= 20))
        {
            return "few";
        }
        else if ((amount % 10 == 0 || (amount % 10 >= 5 && amount % 10 <= 9) || (amount % 100 >= 11 && amount % 100 <= 19)))
        {
            return "many";
        }
        else
        {
            return "other";
        }
    }

    /// <summary>
    /// Tests if a resource with the specified key exists in the resource manager with the current UI culture.
    /// </summary>
    /// <param name="resKey">The key of the resource to look for.</param>
    /// <returns>True if the resource exists, false otherwise.</returns>
    public static bool ExistsLocalizedResource(this string resKey) => ExistsLocalizedResource(resKey, CultureInfo.CurrentUICulture, out var _);

    /// <summary>
    /// Gets the localized string for the specified resource key from the resource manager with the specified culture.
    /// </summary>
    /// <param name="resKey">The key of the resource to look for.</param>
    /// <param name="culture">The culture to use when looking for the resource.</param>
    /// <returns>The localized string for the resource, or the resource key if the resource could not be found.</returns>
    public static string GetLocalizedResource(this string resKey, CultureInfo culture) => ExistsLocalizedResource(resKey, culture, out string value) ? value : resKey;

    /// <summary>
    /// Gets the localized string for the specified resource key from the resource manager with the current UI culture.
    /// </summary>
    /// <param name="resKey">The key of the resource to look for.</param>
    /// <returns>The localized string for the resource, or the resource key if the resource could not be found.</returns>
    public static string GetLocalizedResource(this string resKey) => GetLocalizedResource(resKey, CultureInfo.CurrentUICulture);

    /// <summary>
    /// Gets the plural localized resource for the specified resource key and culture, using the specified amounts.
    /// </summary>
    /// <param name="resKey">The key of the resource to look for.</param>
    /// <param name="culture">The culture to use when looking for the resource.</param>
    /// <param name="amounts">The amounts to use for pluralization.</param>
    /// <returns>The pluralized localized resource for the specified key and culture.</returns>
    public static string GetPluralLocalizedResource(this string resKey, CultureInfo culture, params int[] amounts)
    {
        var res = GetLocalizedResource(resKey, culture);
        var matches = _regex.Matches(res);

        if (matches.Count == 0)
        {
            return res;
        }

        foreach (var match in matches.Cast<Match>())
        {
            var index = int.Parse(match.Groups["index"].Value);
            var num = int.Parse(match.Groups["num"].Value);
            

            if (amounts[index] == num)
            {
                var std = match.Groups["std"].Value;
                res = res.ReplaceFirst(match.Groups[0].Value, std.ReplaceFirst(_replaceString, amounts[index].ToString()));
                continue;
            }

            for (int i = 1; i < match.Groups.Count; i += 2)
            {
                var groupName = match.Groups[i].Value;
                var groupText = match.Groups[i + 1].Value;

                if (groupName == GetPluralCategory(amounts[index]) || groupName == "other")
                {
                    res = res.ReplaceFirst(match.Groups[0].Value, groupText.ReplaceFirst(_replaceString, amounts[index].ToString()).TrimEnd());
                    break;
                }
            }
        }

        return res;
    }

    /// <summary>
    /// Gets the plural localized resource for the specified resource key and culture, using the specified amounts with the current UI culture.
    /// </summary>
    /// <param name="resKey">The key of the resource to look for.</param>
    /// <param name="amounts">The amounts to use for pluralization.</param>
    /// <returns>The pluralized localized resource for the specified key and culture.</returns>
    public static string GetPluralLocalizedResource(this string resKey, params int[] amounts) => GetPluralLocalizedResource(resKey, CultureInfo.CurrentUICulture, amounts);

    /// <summary>
    /// Replaces the first occurrence of a specified string with another specified string in a given string.
    /// </summary>
    /// <param name="str">The given string.</param>
    /// <param name="oldValue">The string to be replaced.</param>
    /// <param name="newValue">The new string to replace the old string.</param>
    /// <returns>A string that is identical to the given string except that the first occurrence of oldValue is replaced with newValue.</returns>
    public static string ReplaceFirst(this string str, string oldValue, string newValue)
    {
        int position = str.IndexOf(oldValue);
        if (position < 0)
        {
            return str;
        }
        str = string.Concat(str.AsSpan(0, position), newValue, str.AsSpan(position + oldValue.Length));
        return str;
    }
}

Example using in code

// Copyright (c) 2024 Laštůvka Lukáš
// Licensed under the Apache-2.0 license. See the LICENSE.

// Test New Version

CultureInfo.CurrentUICulture = new("en-US");

string[] testStrings = ["CreateShortcutDescription", "UnpinFolderFromSidebarDescription"];

foreach (string testString in testStrings)
{
    Console.WriteLine($"KEY: {testString}");
    Console.WriteLine($"Source: \"{testString.GetLocalizedResource()}\"");
    Console.WriteLine($"\nResult:\n");

    for (int i = 1; i <= 10; i++)
    {
        Console.WriteLine($"NUM:{i}\t->\t{testString.GetPluralLocalizedResource(i)}");
    }
    Console.WriteLine();
}

string[] testStrings2 = ["ConflictingItemsDialogSubtitleMultipleConflictsMultipleNonConflicts", "CopyItemsDialogSubtitleMultiple"];

foreach (string testString in testStrings2)
{
    Console.WriteLine($"KEY: {testString}");
    Console.WriteLine($"Source: \"{testString.GetLocalizedResource()}\"");
    Console.WriteLine($"\nResult:\n");

    for (int i = 0; i <= 10; i++)
    {
        Console.WriteLine($"NUM:{i}\t->\t{testString.GetPluralLocalizedResource(i,0)}");
    }
    Console.WriteLine();
}

Console.ReadKey();

@XTorLukas
Copy link
Contributor Author

XTorLukas commented May 7, 2024

Testing on real data

  • For compatibility reasons, I created a new version that handles multivalued text better.

  • I have made a demonstration of the possibilities and implementation

  • Test New Version RegExr online work

  • Test New Version regex101 online work

Samples

Sample A

KEY: CreateShortcutDescription
Source: "Create new {0, plural, =1 {shortcut} other {shortcuts}} to selected {0, plural, =1 {item} other {items}}"

Result:

NUM:1   ->      Create new shortcut to selected item
NUM:2   ->      Create new shortcuts to selected items
NUM:3   ->      Create new shortcuts to selected items
NUM:4   ->      Create new shortcuts to selected items
NUM:5   ->      Create new shortcuts to selected items
NUM:6   ->      Create new shortcuts to selected items
NUM:7   ->      Create new shortcuts to selected items
NUM:8   ->      Create new shortcuts to selected items
NUM:9   ->      Create new shortcuts to selected items
NUM:10  ->      Create new shortcuts to selected items

Sample B

KEY: UnpinFolderFromSidebarDescription
Source: "Unpin {0, plural, =1 {folder} other {folders}} from Sidebar"

Result:

NUM:1   ->      Unpin folder from Sidebar
NUM:2   ->      Unpin folders from Sidebar
NUM:3   ->      Unpin folders from Sidebar
NUM:4   ->      Unpin folders from Sidebar
NUM:5   ->      Unpin folders from Sidebar
NUM:6   ->      Unpin folders from Sidebar
NUM:7   ->      Unpin folders from Sidebar
NUM:8   ->      Unpin folders from Sidebar
NUM:9   ->      Unpin folders from Sidebar
NUM:10  ->      Unpin folders from Sidebar

Sample C

KEY: ConflictingItemsDialogSubtitleMultipleConflictsMultipleNonConflicts
Source: "There are {0, plural, =0 {no conflicting file names} one {# conflicting file name} other {# conflicting file names}}, and {1, plural, =0 {zero outgoing items} one {# outgoing item} other {# outgoing items}}."

Result:

NUM:0   ->      There are no conflicting file names, and zero outgoing items.
NUM:1   ->      There are 1 conflicting file name, and zero outgoing items.
NUM:2   ->      There are 2 conflicting file names, and zero outgoing items.
NUM:3   ->      There are 3 conflicting file names, and zero outgoing items.
NUM:4   ->      There are 4 conflicting file names, and zero outgoing items.
NUM:5   ->      There are 5 conflicting file names, and zero outgoing items.
NUM:6   ->      There are 6 conflicting file names, and zero outgoing items.
NUM:7   ->      There are 7 conflicting file names, and zero outgoing items.
NUM:8   ->      There are 8 conflicting file names, and zero outgoing items.
NUM:9   ->      There are 9 conflicting file names, and zero outgoing items.
NUM:10  ->      There are 10 conflicting file names, and zero outgoing items.

Sample D

KEY: CopyItemsDialogSubtitleMultiple
Source: "{0, plural, =1 {# item} other {# items}} will be copied"

Result:

NUM:0   ->      0 items will be copied
NUM:1   ->      1 item will be copied
NUM:2   ->      2 items will be copied
NUM:3   ->      3 items will be copied
NUM:4   ->      4 items will be copied
NUM:5   ->      5 items will be copied
NUM:6   ->      6 items will be copied
NUM:7   ->      7 items will be copied
NUM:8   ->      8 items will be copied
NUM:9   ->      9 items will be copied
NUM:10  ->      10 items will be copied

@yaira2
Copy link
Member

yaira2 commented May 9, 2024

@Jay-o-Way do you have any input?

@Jay-o-Way
Copy link
Contributor

Jay-o-Way commented May 9, 2024

Hm, looks sweet! But not really my expertise... Would be nice as a nuget package? 😉
One detail: I see it doesn't work for the "is/are" words.

There are 1 conflicting file name

@XTorLukas
Copy link
Contributor Author

XTorLukas commented May 9, 2024

is/are can also:

image

KEY: FilesReady
Source: "{0, plural, =1 {# file} other {# files}} {0, plural, =1 {is} other {are}} ready"

Result:

NUM:0   ->      0 files are ready
NUM:1   ->      1 file is ready
NUM:2   ->      2 files are ready
NUM:3   ->      3 files are ready
NUM:4   ->      4 files are ready
NUM:5   ->      5 files are ready
NUM:6   ->      6 files are ready
NUM:7   ->      7 files are ready
NUM:8   ->      8 files are ready
NUM:9   ->      9 files are ready
NUM:10  ->      10 files are ready

Test regex101 online work

@XTorLukas
Copy link
Contributor Author

XTorLukas commented May 10, 2024

MessageFormat

Install NuGet

Install-Package MessageFormat

Demo code

// Copyright (c) 2024 Laštůvka Lukáš
// Licensed under the Apache-2.0 license. See the LICENSE.

// Test With MessageFormat

using Jeffijoe.MessageFormat;

CultureInfo.CurrentUICulture = new("en-US");
var mf = new MessageFormatter(useCache: true, locale: CultureInfo.CurrentUICulture.Name);

string[] testStrings = ["CreateShortcutDescription", "UnpinFolderFromSidebarDescription"];

foreach (string testString in testStrings)
{
    Console.WriteLine($"KEY: {testString}");
    Console.WriteLine($"Source: \"{testString.GetLocalizedResource()}\"");
    Console.WriteLine($"\nResult:\n");

    for (int i = 1; i <= 10; i++)
    {
        Console.WriteLine($"NUM:{i}\t->\t{mf.FormatMessage(testString.GetLocalizedResource(), new Dictionary<string, object?> { { "0", i } })}");
    }
    Console.WriteLine();
}

string[] testStrings2 = ["ConflictingItemsDialogSubtitleMultipleConflictsMultipleNonConflicts", "CopyItemsDialogSubtitleMultiple", "FilesReady"];

foreach (string testString in testStrings2)
{
    Console.WriteLine($"KEY: {testString}");
    Console.WriteLine($"Source: \"{testString.GetLocalizedResource()}\"");
    Console.WriteLine($"\nResult:\n");

    for (int i = 0; i <= 10; i++)
    {
        Console.WriteLine($"NUM:{i}\t->\t{mf.FormatMessage(testString.GetLocalizedResource(), new Dictionary<string, object?> { { "0", i },{ "1", 0} })}");
    }
    Console.WriteLine();
}

Console.ReadKey();

Results

KEY: CreateShortcutDescription
Source: "Create new {0, plural, =1 {shortcut} other {shortcuts}} to selected {0, plural, =1 {item} other {items}}"

Result:

NUM:1   ->      Create new shortcut to selected item
NUM:2   ->      Create new shortcuts to selected items
NUM:3   ->      Create new shortcuts to selected items
NUM:4   ->      Create new shortcuts to selected items
NUM:5   ->      Create new shortcuts to selected items
NUM:6   ->      Create new shortcuts to selected items
NUM:7   ->      Create new shortcuts to selected items
NUM:8   ->      Create new shortcuts to selected items
NUM:9   ->      Create new shortcuts to selected items
NUM:10  ->      Create new shortcuts to selected items

KEY: UnpinFolderFromSidebarDescription
Source: "Unpin {0, plural, =1 {folder} other {folders}} from Sidebar"

Result:

NUM:1   ->      Unpin folder from Sidebar
NUM:2   ->      Unpin folders from Sidebar
NUM:3   ->      Unpin folders from Sidebar
NUM:4   ->      Unpin folders from Sidebar
NUM:5   ->      Unpin folders from Sidebar
NUM:6   ->      Unpin folders from Sidebar
NUM:7   ->      Unpin folders from Sidebar
NUM:8   ->      Unpin folders from Sidebar
NUM:9   ->      Unpin folders from Sidebar
NUM:10  ->      Unpin folders from Sidebar

KEY: ConflictingItemsDialogSubtitleMultipleConflictsMultipleNonConflicts
Source: "There are {0, plural, =0 {no conflicting file names} one {# conflicting file name} other {# conflicting file names}}, and {1, plural, =0 {zero outgoing items} one {# outgoing item} other {# outgoing items}}."

Result:

NUM:0   ->      There are no conflicting file names, and zero outgoing items.
NUM:1   ->      There are 1 conflicting file name, and zero outgoing items.
NUM:2   ->      There are 2 conflicting file names, and zero outgoing items.
NUM:3   ->      There are 3 conflicting file names, and zero outgoing items.
NUM:4   ->      There are 4 conflicting file names, and zero outgoing items.
NUM:5   ->      There are 5 conflicting file names, and zero outgoing items.
NUM:6   ->      There are 6 conflicting file names, and zero outgoing items.
NUM:7   ->      There are 7 conflicting file names, and zero outgoing items.
NUM:8   ->      There are 8 conflicting file names, and zero outgoing items.
NUM:9   ->      There are 9 conflicting file names, and zero outgoing items.
NUM:10  ->      There are 10 conflicting file names, and zero outgoing items.

KEY: CopyItemsDialogSubtitleMultiple
Source: "{0, plural, =1 {# item} other {# items}} will be copied"

Result:

NUM:0   ->      0 items will be copied
NUM:1   ->      1 item will be copied
NUM:2   ->      2 items will be copied
NUM:3   ->      3 items will be copied
NUM:4   ->      4 items will be copied
NUM:5   ->      5 items will be copied
NUM:6   ->      6 items will be copied
NUM:7   ->      7 items will be copied
NUM:8   ->      8 items will be copied
NUM:9   ->      9 items will be copied
NUM:10  ->      10 items will be copied

KEY: FilesReady
Source: "{0, plural, one {# file} other {# files}} {0, plural, =1 {is} other {are}} ready"

Result:

NUM:0   ->      0 files are ready
NUM:1   ->      1 file is ready
NUM:2   ->      2 files are ready
NUM:3   ->      3 files are ready
NUM:4   ->      4 files are ready
NUM:5   ->      5 files are ready
NUM:6   ->      6 files are ready
NUM:7   ->      7 files are ready
NUM:8   ->      8 files are ready
NUM:9   ->      9 files are ready
NUM:10  ->      10 files are ready

@0x5bfa
Copy link
Member

0x5bfa commented May 10, 2024

This has low priority comparing to other urgent issues but definitely worth trying and if someone gonna make a PR for this we are happy to help.

@XTorLukas
Copy link
Contributor Author

XTorLukas commented May 10, 2024

This has low priority comparing to other urgent issues but definitely worth trying and if someone gonna make a PR for this we are happy to help.

Personally, I try to find a compromise between speed, compatibility and simplicity. Personally, I am creating my own new project and trying to implement this feature.
In addition, I try to contribute to extensions in community projects.
Thus, in my personal development, I only share the main points of progress so that a working prototype already exists when It start development.

@yaira2
Copy link
Member

yaira2 commented May 10, 2024

The main question about this implementation is if it will cause confusion. This is something we need translators to provide input on as they are the ones putting the time and effort into localizing Files.

@yaira2 yaira2 moved this from 🆕 New to 📋 Planning stage in Files task board May 10, 2024
@yaira2
Copy link
Member

yaira2 commented May 10, 2024

We can also start with a couple strings as a test and see how it goes.

@yaira2
Copy link
Member

yaira2 commented May 15, 2024

@XTorLukas do you want to open a PR using this method for a couple strings so we can get a feel for it?

@XTorLukas
Copy link
Contributor Author

@yaira2 why not, let's see if the translators know their stuff

@0x5bfa
Copy link
Member

0x5bfa commented May 17, 2024

I don't think we need string extension for this as Crowdin seems to support ICU message syntax.

The issue that should be fixed here is to have this syntax in en-US resources.
I'll open an issue for this and list up all strings that have to be inflected.

Btw, it looks like XCode has a brilliant feature for this type of thing called Grammatical Agreement: you can put ^[item](inflect: true) for singular and plural, ^[them].(agreeWithConcept: true) to match with other pronounce in other sentences and (agreeWithArgument) to match with other words in the same sentence, for dependency agreement. Looks nice but Crowdin will do instead.

@0x5bfa 0x5bfa closed this as not planned Won't fix, can't repro, duplicate, stale May 17, 2024
@github-project-automation github-project-automation bot moved this from 📋 Planning stage to ✅ Done in Files task board May 17, 2024
@yaira2 yaira2 reopened this May 17, 2024
@github-project-automation github-project-automation bot moved this from ✅ Done to 🆕 New in Files task board May 17, 2024
@yaira2
Copy link
Member

yaira2 commented May 17, 2024

@0x5bfa we already agreed to try this approach but if you have a simpler solution, please share the details here so we can track in one place.

@Josh65-2201 Josh65-2201 moved this from 🆕 New to 📋 Planning stage in Files task board May 17, 2024
@0x5bfa
Copy link
Member

0x5bfa commented May 17, 2024

@XTorLukas already posted it as a first solution to this, as a second solution to this they suggested to have an extension class. This issue is too crowded to list up.

@0x5bfa
Copy link
Member

0x5bfa commented May 17, 2024

Picked some up.

<data name="DaysAgo" xml:space="preserve">
<value>{0} days ago</value>
</data>
<data name="DayAgo" xml:space="preserve">
<value>{0} day ago</value>
</data>
<data name="HoursAgo" xml:space="preserve">
<value>{0} hours ago</value>
</data>
<data name="HourAgo" xml:space="preserve">
<value>{0} hour ago</value>
</data>
<data name="MinutesAgo" xml:space="preserve">
<value>{0} minutes ago</value>
</data>
<data name="MinuteAgo" xml:space="preserve">
<value>{0} minute ago</value>
</data>
<data name="SecondsAgo" xml:space="preserve">
<value>{0} seconds ago</value>
</data>
<data name="OneSecondAgo" xml:space="preserve">
<value>1 second ago</value>
</data>

<data name="ItemSelected.Text" xml:space="preserve">
<value>item selected</value>
</data>
<data name="ItemsSelected.Text" xml:space="preserve">
<value>items selected</value>
</data>
<data name="ItemCount.Text" xml:space="preserve">
<value>item</value>
</data>
<data name="ItemsCount.Text" xml:space="preserve">
<value>items</value>
</data>

<data name="ItemSizeBytes" xml:space="preserve">
<value>bytes</value>
</data>

<data name="PropertiesFilesAndFoldersCountString" xml:space="preserve">
<value>{0:#,##0} files, {1:#,##0} folders</value>
</data>
<data name="PropertiesFilesFoldersAndLocationsCountString" xml:space="preserve">
<value>{0:#,##0} files, {1:#,##0} folders from {2:#,##0} locations</value>
</data>

<data name="CopyItemsDialogSubtitleMultiple" xml:space="preserve">
<value>{0} items will be copied</value>
</data>
<data name="CopyItemsDialogTitle" xml:space="preserve">
<value>Copy item(s)</value>
</data>
<data name="DeleteItemsDialogTitle" xml:space="preserve">
<value>Delete item(s)</value>
</data>

<data name="MoveItemsDialogSubtitleMultiple" xml:space="preserve">
<value>{0} items will be moved</value>
</data>
<data name="MoveItemsDialogTitle" xml:space="preserve">
<value>Move item(s)</value>
</data>
<data name="DeleteItemsDialogSubtitleMultiple" xml:space="preserve">
<value>{0} items will be deleted</value>
</data>

<data name="ConflictingItemsDialogSubtitleMultipleConflictsMultipleNonConflicts" xml:space="preserve">
<value>There are {0} conflicting file names, and {1} outgoing item(s).</value>
</data>
<data name="ConflictingItemsDialogTitle" xml:space="preserve">
<value>Conflicting file name(s)</value>
</data>
<data name="ConflictingItemsDialogSubtitleSingleConflictMultipleNonConflicts" xml:space="preserve">
<value>There is one conflicting file name, and {0} outgoing item(s).</value>
</data>
<data name="CopyItemsDialogSubtitleSingle" xml:space="preserve">
<value>One item will be copied</value>
</data>
<data name="DeleteItemsDialogSubtitleSingle" xml:space="preserve">
<value>One item will be deleted</value>
</data>
<data name="MoveItemsDialogSubtitleSingle" xml:space="preserve">
<value>One item will be moved</value>
</data>

<data name="ConflictingItemsDialogSubtitleMultipleConflictsNoNonConflicts" xml:space="preserve">
<value>There are {0} conflicting file names.</value>
</data>
<data name="ConflictingItemsDialogSubtitleSingleConflictNoNonConflicts" xml:space="preserve">
<value>There is one conflicting file name.</value>
</data>

<data name="GroupItemsCount_Singular" xml:space="preserve">
<value>{0} item</value>
</data>
<data name="GroupItemsCount_Plural" xml:space="preserve">
<value>{0} items</value>
</data>

<data name="DetailsArchiveItemCount" xml:space="preserve">
<value>{0} items ({1} files, {2} folders)</value>
</data>

<data name="CopyItemDescription" xml:space="preserve">
<value>Copy item(s) to clipboard</value>
</data>
<data name="CopyPathDescription" xml:space="preserve">
<value>Copy path of selected items to the clipboard</value>
</data>
<data name="CopyPathWithQuotesDescription" xml:space="preserve">
<value>Copy path of selected items with quotes to the clipboard</value>
</data>
<data name="CutItemDescription" xml:space="preserve">
<value>Cut item(s) to clipboard</value>
</data>
<data name="PasteItemDescription" xml:space="preserve">
<value>Paste item(s) from clipboard to current folder</value>
</data>
<data name="PasteItemToSelectionDescription" xml:space="preserve">
<value>Paste item(s) from clipboard to selected folder</value>
</data>
<data name="DeleteItemDescription" xml:space="preserve">
<value>Delete item(s)</value>
</data>
<data name="CreateFolderDescription" xml:space="preserve">
<value>Create new folder</value>
</data>
<data name="CreateShortcutDescription" xml:space="preserve">
<value>Create new shortcut(s) to selected item(s)</value>
</data>
<data name="CreateShortcutFromDialogDescription" xml:space="preserve">
<value>Create new shortcut to any item</value>
</data>

<data name="RestoreRecycleBinDescription" xml:space="preserve">
<value>Restore selected item(s) from recycle bin</value>
</data>
<data name="RestoreAllRecycleBinDescription" xml:space="preserve">
<value>Restore all items from recycle bin</value>
</data>
<data name="OpenItemDescription" xml:space="preserve">
<value>Open item(s)</value>
</data>
<data name="OpenItemWithApplicationPickerDescription" xml:space="preserve">
<value>Open item(s) with selected application</value>
</data>

<data name="ShareItemDescription" xml:space="preserve">
<value>Share selected file(s) with others</value>
</data>
<data name="PinToStartDescription" xml:space="preserve">
<value>Pin item(s) to the Start Menu</value>
</data>
<data name="UnpinFromStartDescription" xml:space="preserve">
<value>Unpin item(s) from the Start Menu</value>
</data>
<data name="PinFolderToSidebarDescription" xml:space="preserve">
<value>Pin folder(s) to Sidebar</value>
</data>
<data name="UnpinFolderFromSidebarDescription" xml:space="preserve">
<value>Unpin folder(s) from Sidebar</value>
</data>

<data name="InstallFontDescription" xml:space="preserve">
<value>Install selected font(s)</value>
</data>
<data name="InstallInfDriverDescription" xml:space="preserve">
<value>Install driver(s) using selected inf file(s)</value>
</data>
<data name="InstallCertificateDescription" xml:space="preserve">
<value>Install selected certificate(s)</value>
</data>

<data name="CompressIntoArchiveDescription" xml:space="preserve">
<value>Create archive with selected item(s)</value>
</data>
<data name="CompressIntoSevenZipDescription" xml:space="preserve">
<value>Create 7z archive instantly with selected item(s)</value>
</data>
<data name="CompressIntoZipDescription" xml:space="preserve">
<value>Create zip archive instantly with selected item(s)</value>
</data>
<data name="DecompressArchiveDescription" xml:space="preserve">
<value>Extract items from selected archive(s) to any folder</value>
</data>
<data name="DecompressArchiveHereDescription" xml:space="preserve">
<value>Extract items from selected archive(s) to current folder</value>
</data>
<data name="DecompressArchiveToChildFolderDescription" xml:space="preserve">
<value>Extract items from selected archive(s) to new folder</value>
</data>
<data name="RotateLeftDescription" xml:space="preserve">
<value>Rotate selected image(s) to the left</value>
</data>
<data name="RotateRightDescription" xml:space="preserve">
<value>Rotate selected image(s) to the right</value>
</data>

@yaira2
Copy link
Member

yaira2 commented May 19, 2024

@0x5bfa can you open a PR for one of them and we can see how it goes (the Status Bar is a good one to start with)?

I'd like to keep this issue open as a backup plan.

@XTorLukas
Copy link
Contributor Author

MessageFormat #15323 (comment)

I recommend using the MessageFormat solution, I'm trying it out, and it's versatile and can handle text formats like '{0}' or '{num}'
Alternatively, I could help with testing and deployment

@yaira2
Copy link
Member

yaira2 commented May 19, 2024

@0x5bfa is already busy with other tasks so if you can help that would be great!

@XTorLukas
Copy link
Contributor Author

@yaira2 But I don't know the syntax of your code yet, so if a foundation is established, I could start helping.

@yaira2
Copy link
Member

yaira2 commented May 19, 2024

Each area is different but I think we can start with the Status Bar.

@XTorLukas
Copy link
Contributor Author

I wrote a prototype of a possible implementation and tested this part for my region where more than two possible values are used:

 <data name="ItemSelected.Text" xml:space="preserve">
   <value>položka vybrána</value>
 </data>
 <data name="ItemsSelected.Text" xml:space="preserve">
   <value>položky vybrány</value>
 </data>

Change to only one pluralKey with prefix p

 <data name="pItemsSelected.Text" xml:space="preserve">
   <value>{0, plural, one {# položka vybrána} few {# položky vybrány} other {# položek vybráno}}</value>
 </data>

Results:
image
image
image

Maybe I'll create a PR for my solution

@yaira2
Copy link
Member

yaira2 commented May 20, 2024

Looks good to me

@0x5bfa
Copy link
Member

0x5bfa commented May 20, 2024

Looks absolutely great. We can reduce a number of string resources.

@XTorLukas
Copy link
Contributor Author

XTorLukas commented May 22, 2024

I'll start working on finding all the text strings that will need to be reworked. I won't be opening any more PRs, just in the private branch for now, or if necessary I'll create a PR as a draft

I'll start with these: #15323 (comment)

Picked some up.

<data name="DaysAgo" xml:space="preserve">
<value>{0} days ago</value>
</data>
<data name="DayAgo" xml:space="preserve">
<value>{0} day ago</value>
</data>
<data name="HoursAgo" xml:space="preserve">
<value>{0} hours ago</value>
</data>
<data name="HourAgo" xml:space="preserve">
<value>{0} hour ago</value>
</data>
<data name="MinutesAgo" xml:space="preserve">
<value>{0} minutes ago</value>
</data>
<data name="MinuteAgo" xml:space="preserve">
<value>{0} minute ago</value>
</data>
<data name="SecondsAgo" xml:space="preserve">
<value>{0} seconds ago</value>
</data>
<data name="OneSecondAgo" xml:space="preserve">
<value>1 second ago</value>
</data>

<data name="ItemSelected.Text" xml:space="preserve">
<value>item selected</value>
</data>
<data name="ItemsSelected.Text" xml:space="preserve">
<value>items selected</value>
</data>
<data name="ItemCount.Text" xml:space="preserve">
<value>item</value>
</data>
<data name="ItemsCount.Text" xml:space="preserve">
<value>items</value>
</data>

<data name="ItemSizeBytes" xml:space="preserve">
<value>bytes</value>
</data>

<data name="PropertiesFilesAndFoldersCountString" xml:space="preserve">
<value>{0:#,##0} files, {1:#,##0} folders</value>
</data>
<data name="PropertiesFilesFoldersAndLocationsCountString" xml:space="preserve">
<value>{0:#,##0} files, {1:#,##0} folders from {2:#,##0} locations</value>
</data>

<data name="CopyItemsDialogSubtitleMultiple" xml:space="preserve">
<value>{0} items will be copied</value>
</data>
<data name="CopyItemsDialogTitle" xml:space="preserve">
<value>Copy item(s)</value>
</data>
<data name="DeleteItemsDialogTitle" xml:space="preserve">
<value>Delete item(s)</value>
</data>

<data name="MoveItemsDialogSubtitleMultiple" xml:space="preserve">
<value>{0} items will be moved</value>
</data>
<data name="MoveItemsDialogTitle" xml:space="preserve">
<value>Move item(s)</value>
</data>
<data name="DeleteItemsDialogSubtitleMultiple" xml:space="preserve">
<value>{0} items will be deleted</value>
</data>

<data name="ConflictingItemsDialogSubtitleMultipleConflictsMultipleNonConflicts" xml:space="preserve">
<value>There are {0} conflicting file names, and {1} outgoing item(s).</value>
</data>
<data name="ConflictingItemsDialogTitle" xml:space="preserve">
<value>Conflicting file name(s)</value>
</data>
<data name="ConflictingItemsDialogSubtitleSingleConflictMultipleNonConflicts" xml:space="preserve">
<value>There is one conflicting file name, and {0} outgoing item(s).</value>
</data>
<data name="CopyItemsDialogSubtitleSingle" xml:space="preserve">
<value>One item will be copied</value>
</data>
<data name="DeleteItemsDialogSubtitleSingle" xml:space="preserve">
<value>One item will be deleted</value>
</data>
<data name="MoveItemsDialogSubtitleSingle" xml:space="preserve">
<value>One item will be moved</value>
</data>

<data name="ConflictingItemsDialogSubtitleMultipleConflictsNoNonConflicts" xml:space="preserve">
<value>There are {0} conflicting file names.</value>
</data>
<data name="ConflictingItemsDialogSubtitleSingleConflictNoNonConflicts" xml:space="preserve">
<value>There is one conflicting file name.</value>
</data>

<data name="GroupItemsCount_Singular" xml:space="preserve">
<value>{0} item</value>
</data>
<data name="GroupItemsCount_Plural" xml:space="preserve">
<value>{0} items</value>
</data>

<data name="DetailsArchiveItemCount" xml:space="preserve">
<value>{0} items ({1} files, {2} folders)</value>
</data>

<data name="CopyItemDescription" xml:space="preserve">
<value>Copy item(s) to clipboard</value>
</data>
<data name="CopyPathDescription" xml:space="preserve">
<value>Copy path of selected items to the clipboard</value>
</data>
<data name="CopyPathWithQuotesDescription" xml:space="preserve">
<value>Copy path of selected items with quotes to the clipboard</value>
</data>
<data name="CutItemDescription" xml:space="preserve">
<value>Cut item(s) to clipboard</value>
</data>
<data name="PasteItemDescription" xml:space="preserve">
<value>Paste item(s) from clipboard to current folder</value>
</data>
<data name="PasteItemToSelectionDescription" xml:space="preserve">
<value>Paste item(s) from clipboard to selected folder</value>
</data>
<data name="DeleteItemDescription" xml:space="preserve">
<value>Delete item(s)</value>
</data>
<data name="CreateFolderDescription" xml:space="preserve">
<value>Create new folder</value>
</data>
<data name="CreateShortcutDescription" xml:space="preserve">
<value>Create new shortcut(s) to selected item(s)</value>
</data>
<data name="CreateShortcutFromDialogDescription" xml:space="preserve">
<value>Create new shortcut to any item</value>
</data>

<data name="RestoreRecycleBinDescription" xml:space="preserve">
<value>Restore selected item(s) from recycle bin</value>
</data>
<data name="RestoreAllRecycleBinDescription" xml:space="preserve">
<value>Restore all items from recycle bin</value>
</data>
<data name="OpenItemDescription" xml:space="preserve">
<value>Open item(s)</value>
</data>
<data name="OpenItemWithApplicationPickerDescription" xml:space="preserve">
<value>Open item(s) with selected application</value>
</data>

<data name="ShareItemDescription" xml:space="preserve">
<value>Share selected file(s) with others</value>
</data>
<data name="PinToStartDescription" xml:space="preserve">
<value>Pin item(s) to the Start Menu</value>
</data>
<data name="UnpinFromStartDescription" xml:space="preserve">
<value>Unpin item(s) from the Start Menu</value>
</data>
<data name="PinFolderToSidebarDescription" xml:space="preserve">
<value>Pin folder(s) to Sidebar</value>
</data>
<data name="UnpinFolderFromSidebarDescription" xml:space="preserve">
<value>Unpin folder(s) from Sidebar</value>
</data>

<data name="InstallFontDescription" xml:space="preserve">
<value>Install selected font(s)</value>
</data>
<data name="InstallInfDriverDescription" xml:space="preserve">
<value>Install driver(s) using selected inf file(s)</value>
</data>
<data name="InstallCertificateDescription" xml:space="preserve">
<value>Install selected certificate(s)</value>
</data>

<data name="CompressIntoArchiveDescription" xml:space="preserve">
<value>Create archive with selected item(s)</value>
</data>
<data name="CompressIntoSevenZipDescription" xml:space="preserve">
<value>Create 7z archive instantly with selected item(s)</value>
</data>
<data name="CompressIntoZipDescription" xml:space="preserve">
<value>Create zip archive instantly with selected item(s)</value>
</data>
<data name="DecompressArchiveDescription" xml:space="preserve">
<value>Extract items from selected archive(s) to any folder</value>
</data>
<data name="DecompressArchiveHereDescription" xml:space="preserve">
<value>Extract items from selected archive(s) to current folder</value>
</data>
<data name="DecompressArchiveToChildFolderDescription" xml:space="preserve">
<value>Extract items from selected archive(s) to new folder</value>
</data>
<data name="RotateLeftDescription" xml:space="preserve">
<value>Rotate selected image(s) to the left</value>
</data>
<data name="RotateRightDescription" xml:space="preserve">
<value>Rotate selected image(s) to the right</value>
</data>

@yaira2
Copy link
Member

yaira2 commented May 29, 2024

Merging with #15503

@yaira2 yaira2 closed this as completed May 29, 2024
@github-project-automation github-project-automation bot moved this from 📋 Planning stage to ✅ Done in Files task board May 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

No branches or pull requests

4 participants