Add automatic software attribute translation system.

- Updated SoftwareAttributesService to include language parameter in DistinctKeys and DistinctValues API calls.
- Enhanced SoftwareService to pass language parameter when fetching software specifications and attributes for specific software and releases.
This commit is contained in:
2026-05-14 20:41:53 +01:00
parent 30147bf7a9
commit 990a5c465d
39 changed files with 12939 additions and 4045 deletions

View File

@@ -14,6 +14,22 @@ namespace Marechai.ApiClient.Models
{
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
public IDictionary<string, object> AdditionalData { get; set; }
/// <summary>The displayKey property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? DisplayKey { get; set; }
#nullable restore
#else
public string DisplayKey { get; set; }
#endif
/// <summary>The displayValues property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public List<string>? DisplayValues { get; set; }
#nullable restore
#else
public List<string> DisplayValues { get; set; }
#endif
/// <summary>The key property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
@@ -55,6 +71,8 @@ namespace Marechai.ApiClient.Models
{
return new Dictionary<string, Action<IParseNode>>
{
{ "displayKey", n => { DisplayKey = n.GetStringValue(); } },
{ "displayValues", n => { DisplayValues = n.GetCollectionOfPrimitiveValues<string>()?.AsList(); } },
{ "key", n => { Key = n.GetStringValue(); } },
{ "values", n => { Values = n.GetCollectionOfPrimitiveValues<string>()?.AsList(); } },
};
@@ -66,6 +84,8 @@ namespace Marechai.ApiClient.Models
public virtual void Serialize(ISerializationWriter writer)
{
if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
writer.WriteStringValue("displayKey", DisplayKey);
writer.WriteCollectionOfPrimitiveValues<string>("displayValues", DisplayValues);
writer.WriteStringValue("key", Key);
writer.WriteCollectionOfPrimitiveValues<string>("values", Values);
writer.WriteAdditionalData(AdditionalData);

View File

@@ -21,7 +21,7 @@ namespace Marechai.ApiClient.Software.Attributes.DistinctKeys
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public DistinctKeysRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/attributes/distinct-keys{?category*}", pathParameters)
public DistinctKeysRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/attributes/distinct-keys{?category*,lang*}", pathParameters)
{
}
/// <summary>
@@ -29,7 +29,7 @@ namespace Marechai.ApiClient.Software.Attributes.DistinctKeys
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public DistinctKeysRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/attributes/distinct-keys{?category*}", rawUrl)
public DistinctKeysRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/attributes/distinct-keys{?category*,lang*}", rawUrl)
{
}
/// <returns>A List&lt;string&gt;</returns>
@@ -86,6 +86,15 @@ namespace Marechai.ApiClient.Software.Attributes.DistinctKeys
#else
[QueryParameter("category")]
public string Category { get; set; }
#endif
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
[QueryParameter("lang")]
public string? Lang { get; set; }
#nullable restore
#else
[QueryParameter("lang")]
public string Lang { get; set; }
#endif
}
/// <summary>

View File

@@ -21,7 +21,7 @@ namespace Marechai.ApiClient.Software.Attributes.DistinctValues
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public DistinctValuesRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/attributes/distinct-values{?category*,key*}", pathParameters)
public DistinctValuesRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/attributes/distinct-values{?category*,key*,lang*}", pathParameters)
{
}
/// <summary>
@@ -29,7 +29,7 @@ namespace Marechai.ApiClient.Software.Attributes.DistinctValues
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public DistinctValuesRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/attributes/distinct-values{?category*,key*}", rawUrl)
public DistinctValuesRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/attributes/distinct-values{?category*,key*,lang*}", rawUrl)
{
}
/// <returns>A List&lt;string&gt;</returns>
@@ -95,6 +95,15 @@ namespace Marechai.ApiClient.Software.Attributes.DistinctValues
#else
[QueryParameter("key")]
public string Key { get; set; }
#endif
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
[QueryParameter("lang")]
public string? Lang { get; set; }
#nullable restore
#else
[QueryParameter("lang")]
public string Lang { get; set; }
#endif
}
/// <summary>

View File

@@ -22,7 +22,7 @@ namespace Marechai.ApiClient.Software.Item.Attributes
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public AttributesRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/{%2Did}/attributes", pathParameters)
public AttributesRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/{%2Did}/attributes{?lang*}", pathParameters)
{
}
/// <summary>
@@ -30,7 +30,7 @@ namespace Marechai.ApiClient.Software.Item.Attributes
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public AttributesRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/{%2Did}/attributes", rawUrl)
public AttributesRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/{%2Did}/attributes{?lang*}", rawUrl)
{
}
/// <returns>A List&lt;global::Marechai.ApiClient.Models.SoftwareAttributeDto&gt;</returns>
@@ -39,11 +39,11 @@ namespace Marechai.ApiClient.Software.Item.Attributes
/// <exception cref="global::Marechai.ApiClient.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<List<global::Marechai.ApiClient.Models.SoftwareAttributeDto>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
public async Task<List<global::Marechai.ApiClient.Models.SoftwareAttributeDto>?> GetAsync(Action<RequestConfiguration<global::Marechai.ApiClient.Software.Item.Attributes.AttributesRequestBuilder.AttributesRequestBuilderGetQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::Marechai.ApiClient.Models.SoftwareAttributeDto>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
public async Task<List<global::Marechai.ApiClient.Models.SoftwareAttributeDto>> GetAsync(Action<RequestConfiguration<global::Marechai.ApiClient.Software.Item.Attributes.AttributesRequestBuilder.AttributesRequestBuilderGetQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
@@ -58,11 +58,11 @@ namespace Marechai.ApiClient.Software.Item.Attributes
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<global::Marechai.ApiClient.Software.Item.Attributes.AttributesRequestBuilder.AttributesRequestBuilderGetQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<global::Marechai.ApiClient.Software.Item.Attributes.AttributesRequestBuilder.AttributesRequestBuilderGetQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
@@ -79,12 +79,27 @@ namespace Marechai.ApiClient.Software.Item.Attributes
{
return new global::Marechai.ApiClient.Software.Item.Attributes.AttributesRequestBuilder(rawUrl, RequestAdapter);
}
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class AttributesRequestBuilderGetQueryParameters
#pragma warning restore CS1591
{
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
[QueryParameter("lang")]
public string? Lang { get; set; }
#nullable restore
#else
[QueryParameter("lang")]
public string Lang { get; set; }
#endif
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class AttributesRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
public partial class AttributesRequestBuilderGetRequestConfiguration : RequestConfiguration<global::Marechai.ApiClient.Software.Item.Attributes.AttributesRequestBuilder.AttributesRequestBuilderGetQueryParameters>
{
}
}

View File

@@ -22,7 +22,7 @@ namespace Marechai.ApiClient.Software.Specifications
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public SpecificationsRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/specifications", pathParameters)
public SpecificationsRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/specifications{?lang*}", pathParameters)
{
}
/// <summary>
@@ -30,7 +30,7 @@ namespace Marechai.ApiClient.Software.Specifications
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public SpecificationsRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/specifications", rawUrl)
public SpecificationsRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/specifications{?lang*}", rawUrl)
{
}
/// <returns>A List&lt;global::Marechai.ApiClient.Models.SoftwareSpecKeyDto&gt;</returns>
@@ -39,11 +39,11 @@ namespace Marechai.ApiClient.Software.Specifications
/// <exception cref="global::Marechai.ApiClient.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<List<global::Marechai.ApiClient.Models.SoftwareSpecKeyDto>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
public async Task<List<global::Marechai.ApiClient.Models.SoftwareSpecKeyDto>?> GetAsync(Action<RequestConfiguration<global::Marechai.ApiClient.Software.Specifications.SpecificationsRequestBuilder.SpecificationsRequestBuilderGetQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::Marechai.ApiClient.Models.SoftwareSpecKeyDto>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
public async Task<List<global::Marechai.ApiClient.Models.SoftwareSpecKeyDto>> GetAsync(Action<RequestConfiguration<global::Marechai.ApiClient.Software.Specifications.SpecificationsRequestBuilder.SpecificationsRequestBuilderGetQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
@@ -58,11 +58,11 @@ namespace Marechai.ApiClient.Software.Specifications
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<global::Marechai.ApiClient.Software.Specifications.SpecificationsRequestBuilder.SpecificationsRequestBuilderGetQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<global::Marechai.ApiClient.Software.Specifications.SpecificationsRequestBuilder.SpecificationsRequestBuilderGetQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
@@ -79,12 +79,27 @@ namespace Marechai.ApiClient.Software.Specifications
{
return new global::Marechai.ApiClient.Software.Specifications.SpecificationsRequestBuilder(rawUrl, RequestAdapter);
}
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class SpecificationsRequestBuilderGetQueryParameters
#pragma warning restore CS1591
{
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
[QueryParameter("lang")]
public string? Lang { get; set; }
#nullable restore
#else
[QueryParameter("lang")]
public string Lang { get; set; }
#endif
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class SpecificationsRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
public partial class SpecificationsRequestBuilderGetRequestConfiguration : RequestConfiguration<global::Marechai.ApiClient.Software.Specifications.SpecificationsRequestBuilder.SpecificationsRequestBuilderGetQueryParameters>
{
}
}

View File

@@ -1,5 +1,5 @@
{
"descriptionHash": "9D10F6B3031E838BDA40E3A24927698766CF8F5961B6BB47E07AEF53DF5FE36A16A2598AF019F8BF63E79121D439889FBD7653CC3C3413DFF68B492D8A594DE5",
"descriptionHash": "A4FAF9EBADCC040C39060A8463042B256D2F0406F26D4E269124F5B17C054B44D7782B05B6E0A42248B6F696C59836454DDAA4683B816EBC967A8874B6294030",
"descriptionLocation": "../../../../../tmp/openapi.json",
"lockFileVersion": "1.0.0",
"kiotaVersion": "1.31.1",

View File

@@ -157,18 +157,21 @@ public partial class SoftwareViewModel : ObservableObject
foreach(string val in spec.Values ?? [])
{
// Server pre-translates non-Rating spec keys/values via the
// SoftwareAttributeTranslationCache (see /software/specifications?lang=...).
// Use the value verbatim — DO NOT re-localize.
values.Add(new SoftwareSpecValueItem
{
Key = spec.Key ?? string.Empty,
Value = val,
DisplayValue = _localizer[val]
DisplayValue = val
});
}
SpecificationGroups.Add(new SoftwareSpecGroup
{
Key = spec.Key ?? string.Empty,
DisplayKey = _localizer[spec.Key ?? string.Empty],
DisplayKey = spec.Key ?? string.Empty,
Values = values
});
}

View File

@@ -375,10 +375,13 @@ public partial class SoftwareViewViewModel : ObservableObject, IRegionAware
foreach(SoftwareAttributeDto spec in platformGroup)
{
// Server already pre-translates non-Rating attribute keys/values via the
// SoftwareAttributeTranslationCache (see /software/{id}/attributes?lang=...).
// Use the values verbatim — DO NOT re-localize.
specGroup.Specs.Add(new SpecItem
{
Key = _localizer[spec.Key ?? string.Empty],
Value = _localizer[spec.Value ?? string.Empty]
Key = spec.Key ?? string.Empty,
Value = spec.Value ?? string.Empty
});
}

View File

@@ -185,7 +185,12 @@ public class SoftwareBrowsingService
{
_logger.LogInformation("Fetching software specifications from API");
List<SoftwareSpecKeyDto> specs = await _apiClient.Software.Specifications.GetAsync();
string lang = GetIso639CodeFromCulture();
List<SoftwareSpecKeyDto> specs = await _apiClient.Software.Specifications.GetAsync(config =>
{
config.QueryParameters.Lang = lang;
});
if(specs == null) return [];
@@ -654,7 +659,12 @@ public class SoftwareBrowsingService
{
_logger.LogInformation("Fetching attributes for software {SoftwareId} from API", softwareId);
List<SoftwareAttributeDto> attributes = await _apiClient.Software[softwareId].Attributes.GetAsync();
string lang = GetIso639CodeFromCulture();
List<SoftwareAttributeDto> attributes = await _apiClient.Software[softwareId].Attributes.GetAsync(config =>
{
config.QueryParameters.Lang = lang;
});
if(attributes == null) return [];
@@ -671,4 +681,22 @@ public class SoftwareBrowsingService
return [];
}
}
private static string GetIso639CodeFromCulture()
{
string twoLetter = System.Globalization.CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
return twoLetter switch
{
"en" => "eng",
"es" => "spa",
"de" => "deu",
"fr" => "fra",
"it" => "ita",
"nl" => "nld",
"la" => "lat",
"pt" => "por",
_ => "eng"
};
}
}

View File

@@ -6,11 +6,37 @@ namespace Marechai.Data.Dtos;
public class SoftwareSpecKeyDto
{
/// <summary>
/// Canonical (English, NBSP-normalised) attribute key. ALWAYS the same regardless of the
/// <c>?lang=</c> query parameter — used as a filter argument by <c>/software/by-spec</c>.
/// </summary>
[JsonPropertyName("key")]
[Required]
public required string Key { get; set; }
/// <summary>
/// Canonical (English) attribute values for this key. Pair with <see cref="Key" /> for any
/// filter / lookup that needs to round-trip back to the DB.
/// </summary>
[JsonPropertyName("values")]
[Required]
public required List<string> Values { get; set; }
/// <summary>
/// Translated display label for <see cref="Key" /> (or the canonical English when no
/// translation exists or the request resolved to <c>"eng"</c>). Use this for UI rendering
/// and the canonical <see cref="Key" /> for any URL parameter / DB filter.
/// </summary>
[JsonPropertyName("displayKey")]
[Required]
public required string DisplayKey { get; set; }
/// <summary>
/// Translated display values aligned positionally with <see cref="Values" />.
/// <c>DisplayValues[i]</c> is the translation of <c>Values[i]</c>.
/// </summary>
[JsonPropertyName("displayValues")]
[Required]
public required List<string> DisplayValues { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marechai.Database.Migrations
{
/// <inheritdoc />
public partial class AddSoftwareAttributeStringTranslations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SoftwareAttributeStrings",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Text = table.Column<string>(type: "varchar(512)", maxLength: 512, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedOn = table.Column<DateTime>(type: "datetime(6)", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UpdatedOn = table.Column<DateTime>(type: "datetime(6)", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.ComputedColumn)
},
constraints: table =>
{
table.PrimaryKey("PK_SoftwareAttributeStrings", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "SoftwareAttributeStringTranslations",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
StringId = table.Column<int>(type: "int", nullable: false),
LanguageCode = table.Column<string>(type: "char(3)", maxLength: 3, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Translation = table.Column<string>(type: "varchar(512)", maxLength: 512, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedOn = table.Column<DateTime>(type: "datetime(6)", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UpdatedOn = table.Column<DateTime>(type: "datetime(6)", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.ComputedColumn)
},
constraints: table =>
{
table.PrimaryKey("PK_SoftwareAttributeStringTranslations", x => x.Id);
table.ForeignKey(
name: "fk_software_attribute_string_translations_language",
column: x => x.LanguageCode,
principalTable: "ISO_639-3",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_software_attribute_string_translations_string",
column: x => x.StringId,
principalTable: "SoftwareAttributeStrings",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "idx_software_attribute_strings_text",
table: "SoftwareAttributeStrings",
column: "Text",
unique: true);
migrationBuilder.CreateIndex(
name: "idx_software_attribute_string_translations_string_language",
table: "SoftwareAttributeStringTranslations",
columns: new[] { "StringId", "LanguageCode" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_SoftwareAttributeStringTranslations_LanguageCode",
table: "SoftwareAttributeStringTranslations",
column: "LanguageCode");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SoftwareAttributeStringTranslations");
migrationBuilder.DropTable(
name: "SoftwareAttributeStrings");
}
}
}

View File

@@ -6689,6 +6689,84 @@ namespace Marechai.Database.Migrations
b.ToTable("SoftwareAttributes");
});
modelBuilder.Entity("Marechai.Database.Models.SoftwareAttributeString", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedOn")
.ValueGeneratedOnAdd()
.HasColumnType("datetime(6)");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<DateTime>("CreatedOn"));
b.Property<string>("Text")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("varchar(512)");
b.Property<DateTime>("UpdatedOn")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("datetime(6)");
MySqlPropertyBuilderExtensions.UseMySqlComputedColumn(b.Property<DateTime>("UpdatedOn"));
b.HasKey("Id");
b.HasIndex("Text")
.IsUnique()
.HasDatabaseName("idx_software_attribute_strings_text");
b.ToTable("SoftwareAttributeStrings");
});
modelBuilder.Entity("Marechai.Database.Models.SoftwareAttributeStringTranslation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedOn")
.ValueGeneratedOnAdd()
.HasColumnType("datetime(6)");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<DateTime>("CreatedOn"));
b.Property<string>("LanguageCode")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("char(3)");
b.Property<int>("StringId")
.HasColumnType("int");
b.Property<string>("Translation")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("varchar(512)");
b.Property<DateTime>("UpdatedOn")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("datetime(6)");
MySqlPropertyBuilderExtensions.UseMySqlComputedColumn(b.Property<DateTime>("UpdatedOn"));
b.HasKey("Id");
b.HasIndex("LanguageCode");
b.HasIndex("StringId", "LanguageCode")
.IsUnique()
.HasDatabaseName("idx_software_attribute_string_translations_string_language");
b.ToTable("SoftwareAttributeStringTranslations");
});
modelBuilder.Entity("Marechai.Database.Models.SoftwareBarcode", b =>
{
b.Property<ulong>("Id")
@@ -10062,6 +10140,27 @@ namespace Marechai.Database.Migrations
b.Navigation("SoftwareRelease");
});
modelBuilder.Entity("Marechai.Database.Models.SoftwareAttributeStringTranslation", b =>
{
b.HasOne("Marechai.Database.Models.Iso639", "Language")
.WithMany()
.HasForeignKey("LanguageCode")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_software_attribute_string_translations_language");
b.HasOne("Marechai.Database.Models.SoftwareAttributeString", "String")
.WithMany()
.HasForeignKey("StringId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_software_attribute_string_translations_string");
b.Navigation("Language");
b.Navigation("String");
});
modelBuilder.Entity("Marechai.Database.Models.SoftwareBarcode", b =>
{
b.HasOne("Marechai.Database.Models.SoftwareRelease", "Release")

View File

@@ -174,6 +174,8 @@ public class MarechaiContext : IdentityDbContext<ApplicationUser, ApplicationRol
public virtual DbSet<GenreBySoftware> GenresBySoftware { get; set; }
public virtual DbSet<PeopleBySoftware> PeopleBySoftware { get; set; }
public virtual DbSet<SoftwareAttribute> SoftwareAttributes { get; set; }
public virtual DbSet<SoftwareAttributeString> SoftwareAttributeStrings { get; set; }
public virtual DbSet<SoftwareAttributeStringTranslation> SoftwareAttributeStringTranslations { get; set; }
public virtual DbSet<MobyGamesImportState> MobyGamesImportStates { get; set; }
public virtual DbSet<MobyGamesRejection> MobyGamesRejections { get; set; }
public virtual DbSet<MobyGamesCoverDownloadState> MobyGamesCoverDownloadStates { get; set; }
@@ -595,6 +597,31 @@ public class MarechaiContext : IdentityDbContext<ApplicationUser, ApplicationRol
.HasConstraintName("fk_software_genre_translations_language");
});
modelBuilder.Entity<SoftwareAttributeString>(entity =>
{
entity.HasIndex(e => e.Text)
.IsUnique()
.HasDatabaseName("idx_software_attribute_strings_text");
});
modelBuilder.Entity<SoftwareAttributeStringTranslation>(entity =>
{
entity.HasIndex(e => new { e.StringId, e.LanguageCode })
.IsUnique()
.HasDatabaseName("idx_software_attribute_string_translations_string_language");
entity.HasOne(e => e.String)
.WithMany()
.HasForeignKey(e => e.StringId)
.HasConstraintName("fk_software_attribute_string_translations_string")
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Language)
.WithMany()
.HasForeignKey(e => e.LanguageCode)
.HasConstraintName("fk_software_attribute_string_translations_language");
});
modelBuilder.Entity<CompanyLogo>(entity =>
{
entity.HasKey(e => new

View File

@@ -0,0 +1,44 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
using System.ComponentModel.DataAnnotations;
namespace Marechai.Database.Models;
/// <summary>
/// Pool of unique strings that appear as either <see cref="SoftwareAttribute.Key" /> or
/// <see cref="SoftwareAttribute.Value" /> across non-Rating attribute rows. Keys and values share
/// the same pool — the same text is stored once even if it appeared as both. The
/// <c>SoftwareAttributeTranslationCache</c> normalizes incoming text (NBSP → space, trim) before
/// pool lookup so MobyGames-imported rows merge with hand-curated rows. Default MariaDB
/// <c>utf8mb4_general_ci</c> collation is intentional — case variants like <c>"Color"</c> and
/// <c>"color"</c> merge into one pool entry.
/// </summary>
public class SoftwareAttributeString : BaseModel<int>
{
[StringLength(512)]
[Required]
public string Text { get; set; }
}

View File

@@ -0,0 +1,53 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
using System.ComponentModel.DataAnnotations;
namespace Marechai.Database.Models;
/// <summary>
/// One translated <see cref="SoftwareAttributeString.Text" /> per (<see cref="StringId" />,
/// <see cref="LanguageCode" />) pair. English (<c>eng</c>) is treated as the identity copy of
/// <see cref="SoftwareAttributeString.Text" /> and is NEVER stored here — the in-memory
/// <c>SoftwareAttributeTranslationCache</c> + the controllers fall back to the canonical pool
/// <c>Text</c> when no translation row exists for the requested language. The background
/// <c>TranslationWorker</c> populates this table by calling OpenAI / NLLB; rows are append-only.
/// Rating-category attributes are NEVER translated and bypass the pool entirely.
/// </summary>
public class SoftwareAttributeStringTranslation : BaseModel<int>
{
public int StringId { get; set; }
[StringLength(3)]
[Required]
public string LanguageCode { get; set; }
[StringLength(512)]
[Required]
public string Translation { get; set; }
public virtual SoftwareAttributeString String { get; set; }
public virtual Iso639 Language { get; set; }
}

View File

@@ -29,6 +29,9 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Marechai.Data.Dtos;
using Marechai.Database.Models;
using Marechai.Server.Helpers;
using Marechai.Server.Services;
using Marechai.Server.Suggestions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -39,7 +42,9 @@ namespace Marechai.Server.Controllers;
[Route("/software/attributes")]
[ApiController]
public class SoftwareAttributesController(MarechaiContext context, IMemoryCache cache) : ControllerBase
public class SoftwareAttributesController(MarechaiContext context,
IMemoryCache cache,
SoftwareAttributeTranslationCache attrCache) : ControllerBase
{
const string DEFAULT_SEPARATOR = ", ";
@@ -134,13 +139,27 @@ public class SoftwareAttributesController(MarechaiContext context, IMemoryCache
[HttpGet("distinct-keys")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<List<string>> GetDistinctKeysAsync([FromQuery] string category = null)
public async Task<List<string>> GetDistinctKeysAsync([FromQuery] string category = null,
[FromQuery] string lang = null)
{
IQueryable<SoftwareAttribute> query = context.SoftwareAttributes;
if(!string.IsNullOrWhiteSpace(category)) query = query.Where(a => a.Category == category);
return query.Select(a => a.Key).Distinct().OrderBy(k => k).ToListAsync();
List<string> raw = await query.Select(a => a.Key).Distinct().ToListAsync();
// Rating-category keys are language-independent rating system codes — return verbatim
// (only NBSP-normalised). Every other category routes through the translation cache.
bool isRating = string.Equals(category, SoftwareReleaseSuggestionApplier.AttributeCategoryRating,
System.StringComparison.Ordinal);
string resolvedLang = LanguageResolver.Resolve(HttpContext, lang);
var translated = isRating
? raw.Select(SoftwareAttributeTranslationCache.NormalizeText)
: raw.Select(k => attrCache.GetTranslated(k, resolvedLang));
return translated.Distinct().OrderBy(k => k).ToList();
}
/// <summary>
@@ -155,8 +174,9 @@ public class SoftwareAttributesController(MarechaiContext context, IMemoryCache
[HttpGet("distinct-values")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<List<string>> GetDistinctValuesAsync([FromQuery] string category = null,
[FromQuery] string key = null)
public async Task<List<string>> GetDistinctValuesAsync([FromQuery] string category = null,
[FromQuery] string key = null,
[FromQuery] string lang = null)
{
IQueryable<SoftwareAttribute> query = context.SoftwareAttributes
.Where(a => !string.IsNullOrEmpty(a.Value));
@@ -164,7 +184,18 @@ public class SoftwareAttributesController(MarechaiContext context, IMemoryCache
if(!string.IsNullOrWhiteSpace(category)) query = query.Where(a => a.Category == category);
if(!string.IsNullOrWhiteSpace(key)) query = query.Where(a => a.Key == key);
return query.Select(a => a.Value).Distinct().OrderBy(v => v).ToListAsync();
List<string> raw = await query.Select(a => a.Value).Distinct().ToListAsync();
bool isRating = string.Equals(category, SoftwareReleaseSuggestionApplier.AttributeCategoryRating,
System.StringComparison.Ordinal);
string resolvedLang = LanguageResolver.Resolve(HttpContext, lang);
var translated = isRating
? raw.Select(SoftwareAttributeTranslationCache.NormalizeText)
: raw.Select(v => attrCache.GetTranslated(v, resolvedLang));
return translated.Distinct().OrderBy(v => v).ToList();
}
[HttpGet("lookup-releases")]

View File

@@ -50,7 +50,8 @@ namespace Marechai.Server.Controllers;
[Route("/software")]
[ApiController]
public class SoftwareController(MarechaiContext context, IMemoryCache cache, UserManager<ApplicationUser> userManager,
SoftwareGenreTranslationCache genreCache) : ControllerBase
SoftwareGenreTranslationCache genreCache,
SoftwareAttributeTranslationCache attrCache) : ControllerBase
{
// Cache key + duration for the global Marechai score ranking.
// The full catalog ranking changes only when reviews/ratings are
@@ -1446,42 +1447,45 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<List<SoftwareSpecKeyDto>> GetSpecificationsAsync()
public async Task<List<SoftwareSpecKeyDto>> GetSpecificationsAsync([FromQuery] string lang = null)
{
if(cache.TryGetValue(SOFTWARE_SPECS_CACHE_KEY, out List<SoftwareSpecKeyDto> cached) && cached is not null)
return cached;
var raw = await context.SoftwareAttributes
.Where(a => a.Category == "Spec" && a.Key != "Notes")
.Select(a => new { a.Key, a.Value })
.Distinct()
.ToListAsync();
List<SoftwareSpecKeyDto> result = raw.GroupBy(a => a.Key)
.OrderBy(g => g.Key)
.Select(g => new SoftwareSpecKeyDto
{
Key = g.Key,
Values = g.Select(a => a.Value).OrderBy(v => v).ToList()
})
.ToList();
// Importer stores spec keys/values with U+00A0 (non-breaking space);
// the resx localization keys use a regular ASCII space, so normalize
// here before caching/returning so `L[key]` / `L[value]` resolve.
foreach(SoftwareSpecKeyDto s in result)
// The DB-derived list (keys + grouped values) is cached normalised in English under a
// single key. Translation runs per-request against the in-memory cache, which is cheap
// (dict lookup per string × ~hundreds of values). Keying the IMemoryCache entry on lang
// would multiply the cached payload by N supported languages for no real win.
if(!cache.TryGetValue(SOFTWARE_SPECS_CACHE_KEY,
out List<(string Key, List<string> Values)> cached) || cached is null)
{
s.Key = s.Key?.Replace('\u00A0', ' ');
var raw = await context.SoftwareAttributes
.Where(a => a.Category == "Spec" && a.Key != "Notes")
.Select(a => new { a.Key, a.Value })
.Distinct()
.ToListAsync();
if(s.Values is null) continue;
cached = raw.GroupBy(a => SoftwareAttributeTranslationCache.NormalizeText(a.Key))
.OrderBy(g => g.Key)
.Select(g => (Key: g.Key,
Values: g.Select(a => SoftwareAttributeTranslationCache.NormalizeText(a.Value))
.Distinct()
.OrderBy(v => v)
.ToList()))
.ToList();
for(int i = 0; i < s.Values.Count; i++)
s.Values[i] = s.Values[i]?.Replace('\u00A0', ' ');
cache.Set(SOFTWARE_SPECS_CACHE_KEY, cached, _catalogCacheTtl);
}
cache.Set(SOFTWARE_SPECS_CACHE_KEY, result, _catalogCacheTtl);
string resolvedLang = ResolveGenreLanguage(lang);
return result;
// Always populate Display* — when resolvedLang == "eng" the cache returns the canonical
// text verbatim, so DisplayKey == Key (free, no allocation in the cache path).
return cached.Select(s => new SoftwareSpecKeyDto
{
Key = s.Key,
Values = s.Values,
DisplayKey = attrCache.GetTranslated(s.Key, resolvedLang),
DisplayValues = s.Values.Select(v => attrCache.GetTranslated(v, resolvedLang)).ToList()
})
.ToList();
}
[HttpGet("by-spec")]
@@ -1613,7 +1617,8 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<List<SoftwareAttributeDto>> GetAttributesAsync(ulong softwareId)
public async Task<List<SoftwareAttributeDto>> GetAttributesAsync(ulong softwareId,
[FromQuery] string lang = null)
{
List<SoftwareAttributeDto> attributes = await context.SoftwareAttributes
.Where(a => a.SoftwareRelease.SoftwareId == softwareId)
@@ -1632,13 +1637,27 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
.ThenBy(a => a.Key)
.ToListAsync();
// Importer stores attribute keys/values with U+00A0 (non-breaking
// space); the resx localization keys use a regular ASCII space, so
// normalize here before returning so `L[key]` / `L[value]` resolve.
// Importer stores attribute keys/values with U+00A0 (non-breaking space) — normalise here
// before lookup. Rating-category attributes are returned verbatim (only normalised); every
// other category is translated via the SoftwareAttributeTranslationCache. The cache falls
// back to the (normalised) original text when no translation row exists for the requested
// language, so callers always receive a populated string.
string resolvedLang = ResolveGenreLanguage(lang);
const string ratingCategory = Suggestions.SoftwareReleaseSuggestionApplier.AttributeCategoryRating;
foreach(SoftwareAttributeDto a in attributes)
{
a.Key = a.Key?.Replace('\u00A0', ' ');
a.Value = a.Value?.Replace('\u00A0', ' ');
if(string.Equals(a.Category, ratingCategory, StringComparison.Ordinal))
{
a.Key = SoftwareAttributeTranslationCache.NormalizeText(a.Key);
a.Value = SoftwareAttributeTranslationCache.NormalizeText(a.Value);
continue;
}
a.Key = attrCache.GetTranslated(a.Key, resolvedLang);
a.Value = attrCache.GetTranslated(a.Value, resolvedLang);
}
return attributes;
@@ -2460,61 +2479,10 @@ public class SoftwareController(MarechaiContext context, IMemoryCache cache, Use
}
/// <summary>
/// Resolves the requested ISO 639-3 language code for the genre endpoints. Honours an explicit
/// <c>?lang=</c> query parameter first (validated against the supported set), then falls back
/// to the request's <c>Accept-Language</c> header (mapped two-letter → three-letter), then
/// defaults to <c>"eng"</c>. Returns <c>"eng"</c> for any unknown / unsupported value so the
/// cache fallback path always succeeds.
/// Resolves the requested ISO 639-3 language code for the genre endpoints. Delegates to
/// <see cref="Marechai.Server.Helpers.LanguageResolver.Resolve" /> — kept as a thin wrapper
/// so the existing call sites in this controller need no edits.
/// </summary>
string ResolveGenreLanguage(string explicitLang)
{
if(!string.IsNullOrWhiteSpace(explicitLang))
{
string normalized = explicitLang.Trim().ToLowerInvariant();
if(Marechai.Translation.TranslationService.IsLanguageSupported(normalized))
return normalized;
}
string header = HttpContext?.Request?.Headers.AcceptLanguage.ToString();
if(string.IsNullOrWhiteSpace(header)) return "eng";
IList<StringWithQualityHeaderValue> parsed;
try
{
parsed = StringWithQualityHeaderValue.ParseList(new[] { header });
}
catch
{
return "eng";
}
foreach(StringWithQualityHeaderValue entry in parsed.OrderByDescending(e => e.Quality ?? 1.0))
{
string tag = entry.Value.Value;
if(string.IsNullOrWhiteSpace(tag) || tag == "*") continue;
// Use only the two-letter language part; ignore region (e.g. fr-CA → fr).
int dash = tag.IndexOf('-');
string twoLetter = (dash > 0 ? tag[..dash] : tag).ToLowerInvariant();
string mapped = twoLetter switch
{
"en" => "eng",
"es" => "spa",
"de" => "deu",
"fr" => "fra",
"it" => "ita",
"nl" => "nld",
"la" => "lat",
"pt" => "por",
_ => null
};
if(mapped is not null && Marechai.Translation.TranslationService.IsLanguageSupported(mapped))
return mapped;
}
return "eng";
}
string ResolveGenreLanguage(string explicitLang) =>
Marechai.Server.Helpers.LanguageResolver.Resolve(HttpContext, explicitLang);
}

View File

@@ -31,6 +31,9 @@ using System.Threading.Tasks;
using Marechai.Data;
using Marechai.Data.Dtos;
using Marechai.Database.Models;
using Marechai.Server.Helpers;
using Marechai.Server.Services;
using Marechai.Server.Suggestions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -40,7 +43,8 @@ namespace Marechai.Server.Controllers;
[Route("/software/releases")]
[ApiController]
public class SoftwareReleasesController(MarechaiContext context) : ControllerBase
public class SoftwareReleasesController(MarechaiContext context,
SoftwareAttributeTranslationCache attrCache) : ControllerBase
{
[HttpGet("count")]
[AllowAnonymous]
@@ -297,7 +301,8 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<List<SoftwareAttributeDto>> GetAttributesAsync(ulong releaseId)
public async Task<List<SoftwareAttributeDto>> GetAttributesAsync(ulong releaseId,
[FromQuery] string lang = null)
{
List<SoftwareAttributeDto> attributes = await context.SoftwareAttributes
.Where(a => a.SoftwareReleaseId == releaseId)
@@ -315,13 +320,26 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
.ThenBy(a => a.Key)
.ToListAsync();
// Importer stores attribute keys/values with U+00A0 (non-breaking
// space); the resx localization keys use a regular ASCII space, so
// normalize here before returning so `L[key]` / `L[value]` resolve.
// Importer stores attribute keys/values with U+00A0 (non-breaking space) — normalise here
// before lookup. Rating-category attributes are returned verbatim (only normalised); every
// other category routes through the SoftwareAttributeTranslationCache which falls back to
// the original normalised text when no translation row exists for the requested language.
string resolvedLang = LanguageResolver.Resolve(HttpContext, lang);
const string ratingCategory = SoftwareReleaseSuggestionApplier.AttributeCategoryRating;
foreach(SoftwareAttributeDto a in attributes)
{
a.Key = a.Key?.Replace('\u00A0', ' ');
a.Value = a.Value?.Replace('\u00A0', ' ');
if(string.Equals(a.Category, ratingCategory, StringComparison.Ordinal))
{
a.Key = SoftwareAttributeTranslationCache.NormalizeText(a.Key);
a.Value = SoftwareAttributeTranslationCache.NormalizeText(a.Value);
continue;
}
a.Key = attrCache.GetTranslated(a.Key, resolvedLang);
a.Value = attrCache.GetTranslated(a.Value, resolvedLang);
}
return attributes;

View File

@@ -0,0 +1,96 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
using System.Collections.Generic;
using System.Linq;
using Marechai.Translation;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Marechai.Server.Helpers;
/// <summary>
/// Shared helper used by every translated endpoint to pick a 3-letter ISO 639-3 language code.
/// Resolution order: explicit <c>?lang=</c> query parameter → <c>Accept-Language</c> header
/// (BCP-47 → 3-letter mapping) → <c>"eng"</c>. Validates against
/// <see cref="TranslationService.IsLanguageSupported" /> so callers always receive a code the
/// translation cache can serve.
/// </summary>
public static class LanguageResolver
{
/// <summary>
/// Resolves the request's preferred translation language. Safe to call with a null
/// <paramref name="httpContext" /> (returns <c>"eng"</c> when no header is available).
/// </summary>
public static string Resolve(HttpContext httpContext, string explicitLang)
{
if(!string.IsNullOrWhiteSpace(explicitLang))
{
string normalized = explicitLang.Trim().ToLowerInvariant();
if(TranslationService.IsLanguageSupported(normalized)) return normalized;
}
string header = httpContext?.Request?.Headers.AcceptLanguage.ToString();
if(string.IsNullOrWhiteSpace(header)) return "eng";
IList<StringWithQualityHeaderValue> parsed;
try
{
parsed = StringWithQualityHeaderValue.ParseList(new[] { header });
}
catch
{
return "eng";
}
foreach(StringWithQualityHeaderValue entry in parsed.OrderByDescending(e => e.Quality ?? 1.0))
{
string tag = entry.Value.Value;
if(string.IsNullOrWhiteSpace(tag) || tag == "*") continue;
// Use only the two-letter language part; ignore region (e.g. fr-CA → fr).
int dash = tag.IndexOf('-');
string twoLetter = (dash > 0 ? tag[..dash] : tag).ToLowerInvariant();
string mapped = twoLetter switch
{
"en" => "eng",
"es" => "spa",
"de" => "deu",
"fr" => "fra",
"it" => "ita",
"nl" => "nld",
"la" => "lat",
"pt" => "por",
_ => null
};
if(mapped is not null && TranslationService.IsLanguageSupported(mapped)) return mapped;
}
return "eng";
}
}

View File

@@ -362,6 +362,22 @@ file class Program
// TranslationWorker. Controllers READ-only via SoftwareGenreTranslationCache.GetName(...).
builder.Services.AddSingleton<SoftwareGenreTranslationCache>();
// Process-lifetime cache holding the SoftwareAttributeStrings pool + every
// SoftwareAttributeStringTranslation. Same lifecycle as the genre cache. Read-only from
// controllers via SoftwareAttributeTranslationCache.GetTranslated(text, lang).
builder.Services.AddSingleton<SoftwareAttributeTranslationCache>();
// Genre translation provider (one of N — future entities register their own provider).
// The TranslationWorker iterates ITranslationProvider implementations in registration
// order; keep registrations grouped by entity for readability.
builder.Services.AddSingleton<Marechai.Translation.ITranslationProvider,
SoftwareGenreTranslationProvider>();
// Software attribute translation provider — pools every non-Rating Key/Value into
// SoftwareAttributeStrings and translates each text once per language.
builder.Services.AddSingleton<Marechai.Translation.ITranslationProvider,
SoftwareAttributeTranslationProvider>();
// Background worker that fills SoftwareGenreTranslations using OpenAI (preferred) /
// NLLB (fallback). Exits permanently if neither provider is configured. MUST be
// registered after AddMarechaiTranslation + AddSingleton<SoftwareGenreTranslationCache>.
@@ -464,6 +480,23 @@ file class Program
end = DateTime.Now;
Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds);
start = DateTime.Now;
Console.WriteLine("\e[31;1mWarming software attribute translation cache...\e[0m");
try
{
SoftwareAttributeTranslationCache attrCache =
app.Services.GetRequiredService<SoftwareAttributeTranslationCache>();
attrCache.EnsureLoadedAsync().GetAwaiter().GetResult();
}
catch(Exception ex)
{
Console.WriteLine("\e[31;1mAttribute cache warm-up failed: {0}\e[0m", ex.Message);
}
end = DateTime.Now;
Console.WriteLine("\e[31;1mTook \e[32;1m{0} seconds\e[31;1m...\e[0m", (end - start).TotalSeconds);
start = DateTime.Now;
Console.WriteLine("\e[31;1mImporting company logos...\e[0m");
SvgRender.ImportCompanyLogos(assetRootPath, context);

View File

@@ -0,0 +1,237 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Marechai.Database.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Marechai.Server.Services;
/// <summary>
/// Process-lifetime singleton holding every <see cref="SoftwareAttributeString" /> (the pool of
/// unique non-Rating attribute keys/values) plus every <see cref="SoftwareAttributeStringTranslation" />.
/// Loaded ONCE on first access (the server eager-warms it on startup), then kept current entirely
/// in memory: the <see cref="SoftwareAttributeTranslationProvider" /> mutates it via
/// <see cref="RegisterString" /> / <see cref="Upsert" />, and the controllers READ it for every
/// request — eliminating the per-RTT DB cost on the attribute endpoints.
/// </summary>
/// <remarks>
/// <para>English (<c>"eng"</c>) is treated as the identity copy of the canonical pool
/// <see cref="SoftwareAttributeString.Text" /> and is NEVER stored in the translations dictionary;
/// <see cref="GetTranslated" /> falls back to the original normalised text when no translation
/// exists for the requested language. Looking up an unknown text returns the input verbatim
/// (i.e. text not yet discovered by the worker — typical for fresh DB rows between worker ticks).</para>
/// <para>All inputs are NBSP-normalised via <see cref="NormalizeText" /> (U+00A0 → space, trim)
/// so MobyGames-imported rows merge with hand-curated rows in the same pool entry.</para>
/// </remarks>
public sealed class SoftwareAttributeTranslationCache(IServiceScopeFactory scopeFactory,
ILogger<SoftwareAttributeTranslationCache> logger)
{
/// <summary>normalised Text → StringId. Case-sensitive; MariaDB default collation handles case folding at the unique index.</summary>
readonly ConcurrentDictionary<string, int> _textToId = new(StringComparer.Ordinal);
/// <summary>StringId → canonical (normalised) pool Text.</summary>
readonly ConcurrentDictionary<int, string> _idToText = new();
/// <summary>StringId → (langCode → translated text). langCode is ISO-639-3 (e.g. "spa", "deu"); "eng" is NOT stored.</summary>
readonly ConcurrentDictionary<int, ConcurrentDictionary<string, string>> _translations = new();
readonly SemaphoreSlim _loadGate = new(1, 1);
bool _loaded;
/// <summary>
/// Highest <see cref="SoftwareAttributeString.Id" /> seen at the last load. The provider
/// queries only rows with <c>Id &gt; LastSeenStringId</c> on each tick to discover rows
/// inserted by the discovery pass without re-streaming the whole table.
/// </summary>
public int LastSeenStringId { get; private set; }
/// <summary>
/// Replaces non-breaking spaces (U+00A0) with regular spaces and trims surrounding whitespace.
/// Used at every cache boundary (insert / lookup / endpoint read) so MobyGames-imported strings
/// merge with hand-curated ones. Returns <see cref="string.Empty" /> for null input.
/// </summary>
public static string NormalizeText(string text)
{
if(string.IsNullOrEmpty(text)) return string.Empty;
return text.Replace('\u00A0', ' ').Trim();
}
/// <summary>
/// Loads the cache from the DB if not yet loaded. Idempotent &amp; thread-safe; the second
/// concurrent caller waits on the semaphore and then returns immediately. Safe to call from
/// both the eager-warm code path in <c>Program.cs</c> and from any reader.
/// </summary>
public async Task EnsureLoadedAsync(CancellationToken ct = default)
{
if(_loaded) return;
await _loadGate.WaitAsync(ct);
try
{
if(_loaded) return;
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
MarechaiContext ctx = scope.ServiceProvider.GetRequiredService<MarechaiContext>();
var allStrings = await ctx.SoftwareAttributeStrings
.AsNoTracking()
.OrderBy(s => s.Id)
.Select(s => new { s.Id, s.Text })
.ToListAsync(ct);
foreach(var s in allStrings)
{
string normalised = NormalizeText(s.Text);
if(string.IsNullOrEmpty(normalised)) continue;
_idToText[s.Id] = normalised;
_textToId[normalised] = s.Id;
if(s.Id > LastSeenStringId) LastSeenStringId = s.Id;
}
List<SoftwareAttributeStringTranslation> allTranslations =
await ctx.SoftwareAttributeStringTranslations
.AsNoTracking()
.ToListAsync(ct);
foreach(SoftwareAttributeStringTranslation t in allTranslations)
{
if(string.IsNullOrEmpty(t.LanguageCode) || t.Translation is null) continue;
ConcurrentDictionary<string, string> langs =
_translations.GetOrAdd(t.StringId, _ => new ConcurrentDictionary<string, string>());
langs[t.LanguageCode] = t.Translation;
}
logger.LogInformation(
"SoftwareAttributeTranslationCache loaded {StringCount} pool strings and {TranslationCount} translations.",
allStrings.Count, allTranslations.Count);
_loaded = true;
}
finally
{
_loadGate.Release();
}
}
/// <summary>
/// Returns the translated text for <paramref name="text" /> in
/// <paramref name="languageCode" />, falling back to the (NBSP-normalised) original text if no
/// translation exists. NEVER throws — unknown strings return the normalised input verbatim
/// (typical between worker ticks for newly inserted attributes).
/// </summary>
public string GetTranslated(string text, string languageCode)
{
string normalised = NormalizeText(text);
if(string.IsNullOrEmpty(normalised)) return normalised;
if(string.IsNullOrEmpty(languageCode) || string.Equals(languageCode, "eng", StringComparison.Ordinal))
return normalised;
if(!_textToId.TryGetValue(normalised, out int id)) return normalised;
if(_translations.TryGetValue(id, out ConcurrentDictionary<string, string> langs) &&
langs.TryGetValue(languageCode, out string translated) &&
!string.IsNullOrWhiteSpace(translated))
return translated;
return normalised;
}
/// <summary>
/// Adds or updates a single translation in-memory. Used by the provider after each successful
/// DB insert so subsequent requests see the new translation immediately.
/// </summary>
public void Upsert(int stringId, string languageCode, string translation)
{
if(string.IsNullOrEmpty(languageCode) || string.Equals(languageCode, "eng", StringComparison.Ordinal) ||
translation is null)
return;
ConcurrentDictionary<string, string> langs =
_translations.GetOrAdd(stringId, _ => new ConcurrentDictionary<string, string>());
langs[languageCode] = translation;
}
/// <summary>
/// Registers a brand-new pool string discovered by the provider. Updates
/// <see cref="LastSeenStringId" /> if the new id is higher.
/// </summary>
public void RegisterString(int id, string text)
{
string normalised = NormalizeText(text);
if(string.IsNullOrEmpty(normalised)) return;
_idToText[id] = normalised;
_textToId[normalised] = id;
if(id > LastSeenStringId) LastSeenStringId = id;
}
/// <summary>
/// Returns true and sets <paramref name="id" /> if the (normalised) text is already in the
/// pool. Used by the discovery pass to skip strings that already exist.
/// </summary>
public bool TryGetIdByText(string normalisedText, out int id) => _textToId.TryGetValue(normalisedText, out id);
/// <summary>
/// Snapshot of every pool string that lacks a translation for <paramref name="languageCode" />.
/// Returned as a fresh list so subsequent cache mutation does not affect the snapshot. Returns
/// an empty list for <c>"eng"</c>.
/// </summary>
public IReadOnlyList<(int Id, string Text)> SnapshotMissingByLanguage(string languageCode)
{
if(string.IsNullOrEmpty(languageCode) || string.Equals(languageCode, "eng", StringComparison.Ordinal))
return [];
var missing = new List<(int Id, string Text)>();
foreach(KeyValuePair<int, string> kv in _idToText)
{
if(string.IsNullOrWhiteSpace(kv.Value)) continue;
bool hasLang = _translations.TryGetValue(kv.Key, out ConcurrentDictionary<string, string> langs) &&
langs.ContainsKey(languageCode);
if(!hasLang) missing.Add((kv.Key, kv.Value));
}
return missing;
}
}

View File

@@ -0,0 +1,211 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Marechai.Database.Models;
using Marechai.Server.Suggestions;
using Marechai.Translation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Marechai.Server.Services;
/// <summary>
/// <see cref="ITranslationProvider" /> for the <see cref="SoftwareAttributeString" /> pool. Both
/// the <c>Key</c> and <c>Value</c> columns of every non-Rating <see cref="SoftwareAttribute" /> are
/// pooled into this single table, and translated once per language.
/// </summary>
/// <remarks>
/// The Rating category is NEVER pooled — rating system codes (ESRB "T", PEGI "16+", etc.) are
/// language-independent and the controllers return them verbatim.
/// </remarks>
public sealed class SoftwareAttributeTranslationProvider(
IServiceScopeFactory scopeFactory,
TranslationService translationService,
SoftwareAttributeTranslationCache cache,
ILogger<SoftwareAttributeTranslationProvider> logger) : ITranslationProvider
{
public string Name => "SoftwareAttribute";
public Task EnsureCacheLoadedAsync(CancellationToken ct) => cache.EnsureLoadedAsync(ct);
/// <summary>
/// Discovers new pool strings: scans every distinct (non-Rating) <c>Key</c> and <c>Value</c>
/// across the <c>SoftwareAttributes</c> table, normalises each, and inserts any text not yet
/// in the pool. Newly inserted rows are registered in the cache so the per-language sweep
/// picks them up immediately. Returns the count of inserted rows.
/// </summary>
public async Task<int> DiscoverNewItemsAsync(CancellationToken ct)
{
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
MarechaiContext ctx = scope.ServiceProvider.GetRequiredService<MarechaiContext>();
// Two cheap distinct projections + in-memory union. Rating attributes are excluded — they're
// language-independent rating codes.
const string ratingCategory = SoftwareReleaseSuggestionApplier.AttributeCategoryRating;
List<string> distinctKeys = await ctx.SoftwareAttributes
.AsNoTracking()
.Where(a => a.Category != ratingCategory)
.Select(a => a.Key)
.Distinct()
.ToListAsync(ct);
List<string> distinctValues = await ctx.SoftwareAttributes
.AsNoTracking()
.Where(a => a.Category != ratingCategory)
.Select(a => a.Value)
.Distinct()
.ToListAsync(ct);
// Normalise + de-duplicate against the cache. Use a HashSet to coalesce keys/values that
// collide after normalisation (NBSP variants, casing collapse via DB collation).
var unseen = new HashSet<string>(StringComparer.Ordinal);
foreach(string raw in distinctKeys.Concat(distinctValues))
{
string n = SoftwareAttributeTranslationCache.NormalizeText(raw);
if(string.IsNullOrEmpty(n)) continue;
if(cache.TryGetIdByText(n, out _)) continue;
unseen.Add(n);
}
if(unseen.Count == 0) return 0;
// Re-check against the DB in case a previous tick inserted these strings before the cache
// was warmed (defensive — should never trip in steady state). Also handles MariaDB's
// case-insensitive collation: an in-memory ordinal HashSet may surface "color" + "Color"
// as two unseen entries, but the DB unique index treats them as one — let the DB tell us.
List<string> existingInDb = await ctx.SoftwareAttributeStrings
.AsNoTracking()
.Where(s => unseen.Contains(s.Text))
.Select(s => s.Text)
.ToListAsync(ct);
foreach(string already in existingInDb) unseen.Remove(already);
if(unseen.Count == 0) return 0;
var batch = unseen
.Select(t => new SoftwareAttributeString { Text = t.Length > 512 ? t[..512] : t })
.ToList();
ctx.SoftwareAttributeStrings.AddRange(batch);
try
{
await ctx.SaveChangesAsync(ct);
}
catch(DbUpdateException ex)
{
// Race against a concurrent tick or a manual insert — log and skip; next tick will
// discover whatever survived the unique-index collision.
logger.LogWarning(ex,
"SoftwareAttribute provider: bulk insert of {Count} pool strings hit a unique-index collision; will retry next tick.",
batch.Count);
return 0;
}
foreach(SoftwareAttributeString s in batch) cache.RegisterString(s.Id, s.Text);
return batch.Count;
}
/// <summary>
/// Snapshot pool strings missing translation for <paramref name="languageCode" />, translate
/// each via OpenAI/NLLB serially, then bulk-insert the results in a single
/// <c>SaveChangesAsync</c>. Successful rows are pushed to the cache.
/// </summary>
public async Task<int> TranslateMissingAsync(string languageCode, CancellationToken ct)
{
IReadOnlyList<(int Id, string Text)> missing = cache.SnapshotMissingByLanguage(languageCode);
if(missing.Count == 0) return 0;
logger.LogInformation("SoftwareAttribute provider: {Count} pool strings missing translation into {Lang}.",
missing.Count, languageCode);
// Domain hint for OpenAI — the pool mixes spec keys ("Minimum RAM Required",
// "Video Resolutions Supported") AND spec values ("1 MB", "Mouse, Keyboard",
// "Windows 95"). Without this context, short labels translate in the wrong register
// (e.g. "Mouse" → the animal in some languages instead of the input device).
const string domainContext =
"The text is a video-game / software technical specification — either the NAME of a "
+ "specification (e.g. \"Minimum RAM Required\", \"Operating System\") or its VALUE "
+ "(e.g. \"1 MB\", \"Mouse, Keyboard\", \"Windows 95\"). Use computer / video-game "
+ "terminology, NOT general-purpose translations. Keep brand names, version numbers, "
+ "units (MB, GB, MHz) and product titles unchanged.";
var batch = new List<SoftwareAttributeStringTranslation>(missing.Count);
foreach((int id, string text) in missing)
{
ct.ThrowIfCancellationRequested();
if(string.IsNullOrWhiteSpace(text)) continue;
(string translated, string error) = await translationService.TranslateAsync(
text, languageCode, progress: null, plainText: true);
if(string.IsNullOrWhiteSpace(translated))
{
logger.LogWarning(
"SoftwareAttribute provider: failed to translate pool string {Id} (\"{Text}\") into {Lang}: {Error}",
id, text, languageCode, error ?? "no result");
continue;
}
// Defensive: trim to the column max length (varchar(512)).
if(translated.Length > 512) translated = translated[..512];
batch.Add(new SoftwareAttributeStringTranslation
{
StringId = id,
LanguageCode = languageCode,
Translation = translated
});
}
if(batch.Count == 0) return 0;
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
MarechaiContext ctx = scope.ServiceProvider.GetRequiredService<MarechaiContext>();
ctx.SoftwareAttributeStringTranslations.AddRange(batch);
await ctx.SaveChangesAsync(ct);
foreach(SoftwareAttributeStringTranslation row in batch)
cache.Upsert(row.StringId, row.LanguageCode, row.Translation);
return batch.Count;
}
}

View File

@@ -0,0 +1,144 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Marechai.Database.Models;
using Marechai.Translation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Marechai.Server.Services;
/// <summary>
/// <see cref="ITranslationProvider" /> for <see cref="SoftwareGenre" />. Wraps the existing
/// <see cref="SoftwareGenreTranslationCache" /> and writes to <see cref="SoftwareGenreTranslation" />.
/// Logic is identical to the pre-refactor <c>TranslationWorker</c> body — extracted verbatim.
/// </summary>
public sealed class SoftwareGenreTranslationProvider(IServiceScopeFactory scopeFactory,
TranslationService translationService,
SoftwareGenreTranslationCache cache,
ILogger<SoftwareGenreTranslationProvider> logger)
: ITranslationProvider
{
public string Name => "SoftwareGenre";
public Task EnsureCacheLoadedAsync(CancellationToken ct) => cache.EnsureLoadedAsync(ct);
/// <summary>
/// Delta query for genres added to the DB since the cache was last refreshed. Newly seen
/// genres are registered in the cache so the per-language snapshot pass picks them up. Updates
/// <see cref="SoftwareGenreTranslationCache.LastSeenGenreId" />.
/// </summary>
public async Task<int> DiscoverNewItemsAsync(CancellationToken ct)
{
int lastSeen = cache.LastSeenGenreId;
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
MarechaiContext ctx = scope.ServiceProvider.GetRequiredService<MarechaiContext>();
List<(int Id, string Name)> added = (await ctx.SoftwareGenres
.AsNoTracking()
.Where(g => g.Id > lastSeen)
.OrderBy(g => g.Id)
.Select(g => new { g.Id, g.Name })
.ToListAsync(ct))
.Select(x => (x.Id, x.Name))
.ToList();
if(added.Count == 0) return 0;
foreach((int id, string name) in added) cache.RegisterGenre(id, name);
return added.Count;
}
/// <summary>
/// Snapshot missing genres from the cache (no DB query), translate them one by one
/// (OpenAI/NLLB serialised), then bulk-insert the resulting rows in a single
/// <c>SaveChangesAsync</c>. Successful rows are also pushed to the cache so subsequent
/// requests see them immediately.
/// </summary>
public async Task<int> TranslateMissingAsync(string languageCode, CancellationToken ct)
{
IReadOnlyList<(int Id, string EnglishName)> missing = cache.SnapshotGenresMissingLanguage(languageCode);
if(missing.Count == 0) return 0;
logger.LogInformation("SoftwareGenre provider: {Count} genres missing translation into {Lang}.",
missing.Count, languageCode);
// Domain hint passed to OpenAI so short labels translate in the right register
// (e.g. "Action" → action genre, not the noun "act"; "Adventure" → genre, not noun).
const string domainContext =
"The text is the name of a video-game / software genre or sub-genre (entertainment classification).";
var batch = new List<SoftwareGenreTranslation>(missing.Count);
foreach((int id, string englishName) in missing)
{
ct.ThrowIfCancellationRequested();
if(string.IsNullOrWhiteSpace(englishName)) continue;
(string translated, string error) = await translationService.TranslateAsync(
englishName, languageCode, progress: null, plainText: true, domainContext: domainContext);
if(string.IsNullOrWhiteSpace(translated))
{
logger.LogWarning(
"SoftwareGenre provider: failed to translate genre {GenreId} (\"{English}\") into {Lang}: {Error}",
id, englishName, languageCode, error ?? "no result");
continue;
}
// Defensive: trim to the column max length (varchar(128)).
if(translated.Length > 128) translated = translated[..128];
batch.Add(new SoftwareGenreTranslation
{
GenreId = id,
LanguageCode = languageCode,
Name = translated
});
}
if(batch.Count == 0) return 0;
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
MarechaiContext ctx = scope.ServiceProvider.GetRequiredService<MarechaiContext>();
ctx.SoftwareGenreTranslations.AddRange(batch);
await ctx.SaveChangesAsync(ct);
foreach(SoftwareGenreTranslation row in batch) cache.Upsert(row.GenreId, row.LanguageCode, row.Name);
return batch.Count;
}
}

View File

@@ -25,40 +25,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Marechai.Database.Models;
using Marechai.Translation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Marechai.Server.Services;
/// <summary>
/// Hosted service that fills missing rows in <see cref="SoftwareGenreTranslation" /> by calling
/// <see cref="TranslationService" /> (OpenAI preferred, NLLB fallback). Designed to grow per-entity
/// branches in the future (e.g. SoftwareAttributes) without renaming — see the architecture note
/// in the session plan.
/// Hosted service that drives every registered <see cref="ITranslationProvider" /> on a periodic
/// tick to fill missing translation rows by calling <see cref="TranslationService" /> (OpenAI
/// preferred, NLLB fallback). Per-entity logic lives in the providers; the worker is purely a
/// scheduler.
/// </summary>
/// <remarks>
/// <para>Lifecycle:</para>
/// <list type="number">
/// <item>On start, exits permanently if <see cref="TranslationService.IsAvailable" /> is false.</item>
/// <item>Loop: refresh the in-memory cache for newly-added genres (one delta query
/// <c>WHERE Id &gt; LastSeenGenreId</c>); for each non-<c>eng</c> language, snapshot the missing
/// genres, translate them, and bulk-insert the rows in a single DB roundtrip; sleep 1h.</item>
/// <item>Loop: for each registered provider, (a) refresh the in-memory cache for newly added
/// items via <see cref="ITranslationProvider.DiscoverNewItemsAsync" />, (b) for each
/// non-<c>eng</c> language call <see cref="ITranslationProvider.TranslateMissingAsync" />.
/// Sleep 1 hour between full sweeps.</item>
/// </list>
/// <para>The DB is touched once per language per tick (one read implicitly via the in-memory snapshot
/// + one bulk insert). The OpenAI/NLLB calls are serialised inside the worker so multiple entity
/// types added later cannot collide on the rate limit.</para>
/// <para>Providers are iterated SERIALLY and per-language batches are run SERIALLY inside each
/// provider — keeps OpenAI rate-limit handling trivial. Do NOT introduce parallelism here.</para>
/// </remarks>
public sealed class TranslationWorker(IServiceScopeFactory scopeFactory,
TranslationService translationService,
SoftwareGenreTranslationCache genreCache,
ILogger<TranslationWorker> logger) : BackgroundService
public sealed class TranslationWorker(TranslationService translationService,
IEnumerable<ITranslationProvider> providers,
ILogger<TranslationWorker> logger) : BackgroundService
{
static readonly TimeSpan _interval = TimeSpan.FromHours(1);
@@ -72,136 +67,75 @@ public sealed class TranslationWorker(IServiceScopeFactory scopeF
return;
}
// Make sure the cache is populated before the first sweep — even if the eager warm-up in
// Program.cs failed, this still gives the worker a consistent view of all genres.
try
{
await genreCache.EnsureLoadedAsync(stoppingToken);
}
catch(OperationCanceledException) when(stoppingToken.IsCancellationRequested)
// Materialise once — DI returns a fresh enumerator each time IEnumerable<T> is iterated.
var providerList = new List<ITranslationProvider>(providers);
if(providerList.Count == 0)
{
logger.LogInformation("No ITranslationProvider implementations are registered; TranslationWorker exiting.");
return;
}
catch(Exception ex)
// Make sure every provider's cache is populated before the first sweep — even if the
// eager warm-up in Program.cs failed, this still gives the worker a consistent view.
foreach(ITranslationProvider provider in providerList)
{
logger.LogError(ex, "TranslationWorker initial cache load failed; will retry next tick.");
try
{
await provider.EnsureCacheLoadedAsync(stoppingToken);
}
catch(OperationCanceledException) when(stoppingToken.IsCancellationRequested)
{
return;
}
catch(Exception ex)
{
logger.LogError(ex,
"TranslationWorker initial cache load failed for provider {Provider}; will retry next tick.",
provider.Name);
}
}
while(!stoppingToken.IsCancellationRequested)
{
try
foreach(ITranslationProvider provider in providerList)
{
await DiscoverNewGenresAsync(stoppingToken);
await TranslateMissingAsync(stoppingToken);
}
catch(OperationCanceledException) when(stoppingToken.IsCancellationRequested)
{
break;
}
catch(Exception ex)
{
logger.LogError(ex, "TranslationWorker tick failed; will retry next interval.");
try
{
int discovered = await provider.DiscoverNewItemsAsync(stoppingToken);
if(discovered > 0)
logger.LogInformation("TranslationWorker: provider {Provider} discovered {Count} new items.",
provider.Name, discovered);
foreach(string lang in TranslationService.SupportedLanguageCodes)
{
if(string.Equals(lang, "eng", StringComparison.Ordinal)) continue;
stoppingToken.ThrowIfCancellationRequested();
int inserted = await provider.TranslateMissingAsync(lang, stoppingToken);
if(inserted > 0)
logger.LogInformation(
"TranslationWorker: provider {Provider} inserted {Count} translations into {Lang}.",
provider.Name, inserted, lang);
}
}
catch(OperationCanceledException) when(stoppingToken.IsCancellationRequested)
{
return;
}
catch(Exception ex)
{
logger.LogError(ex, "TranslationWorker tick failed for provider {Provider}; continuing.",
provider.Name);
}
}
try { await Task.Delay(_interval, stoppingToken); }
catch(OperationCanceledException) { break; }
}
}
/// <summary>
/// One delta query against <c>SoftwareGenres</c> for rows added since the cache was last
/// refreshed. Newly seen genres are registered in the cache so the per-language snapshot
/// pass picks them up. Updates <see cref="SoftwareGenreTranslationCache.LastSeenGenreId" />.
/// </summary>
async Task DiscoverNewGenresAsync(CancellationToken ct)
{
int lastSeen = genreCache.LastSeenGenreId;
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
MarechaiContext ctx = scope.ServiceProvider.GetRequiredService<MarechaiContext>();
List<(int Id, string Name)> added = await ctx.SoftwareGenres
.AsNoTracking()
.Where(g => g.Id > lastSeen)
.OrderBy(g => g.Id)
.Select(g => new { g.Id, g.Name })
.ToListAsync(ct)
.ContinueWith(t => t.Result.Select(x => (x.Id, x.Name)).ToList(),
ct);
if(added.Count == 0) return;
foreach((int id, string name) in added)
genreCache.RegisterGenre(id, name);
logger.LogInformation("TranslationWorker discovered {Count} new SoftwareGenre rows.", added.Count);
}
/// <summary>
/// For each non-<c>eng</c> language: snapshot missing genres from the cache (no DB query),
/// translate them one by one (OpenAI/NLLB serialised), then bulk-insert the resulting rows
/// in a single <see cref="DbContext.SaveChangesAsync(CancellationToken)" /> call. Successful
/// rows are also pushed to the cache so subsequent requests see them immediately.
/// </summary>
async Task TranslateMissingAsync(CancellationToken ct)
{
foreach(string lang in TranslationService.SupportedLanguageCodes)
{
if(string.Equals(lang, "eng", StringComparison.Ordinal)) continue;
ct.ThrowIfCancellationRequested();
IReadOnlyList<(int Id, string EnglishName)> missing = genreCache.SnapshotGenresMissingLanguage(lang);
if(missing.Count == 0) continue;
logger.LogInformation("TranslationWorker: {Count} genres missing translation into {Lang}.",
missing.Count, lang);
var batch = new List<SoftwareGenreTranslation>(missing.Count);
foreach((int id, string englishName) in missing)
{
ct.ThrowIfCancellationRequested();
if(string.IsNullOrWhiteSpace(englishName)) continue;
(string translated, string error) = await translationService.TranslateAsync(
englishName, lang, progress: null, plainText: true);
if(string.IsNullOrWhiteSpace(translated))
{
logger.LogWarning(
"TranslationWorker: failed to translate genre {GenreId} (\"{English}\") into {Lang}: {Error}",
id, englishName, lang, error ?? "no result");
continue;
}
// Defensive: trim to the column max length (varchar(128)).
if(translated.Length > 128) translated = translated[..128];
batch.Add(new SoftwareGenreTranslation
{
GenreId = id,
LanguageCode = lang,
Name = translated
});
}
if(batch.Count == 0) continue;
await using AsyncServiceScope scope = scopeFactory.CreateAsyncScope();
MarechaiContext ctx = scope.ServiceProvider.GetRequiredService<MarechaiContext>();
ctx.SoftwareGenreTranslations.AddRange(batch);
await ctx.SaveChangesAsync(ct);
foreach(SoftwareGenreTranslation row in batch)
genreCache.Upsert(row.GenreId, row.LanguageCode, row.Name);
logger.LogInformation("TranslationWorker: inserted {Count} translations into {Lang}.",
batch.Count, lang);
}
}
}

View File

@@ -0,0 +1,72 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
using System.Threading;
using System.Threading.Tasks;
namespace Marechai.Translation;
/// <summary>
/// Plug-in contract used by the background <c>TranslationWorker</c> to discover and translate
/// entities for which a DB-backed translation table exists. One implementation per entity type
/// (e.g. <c>SoftwareGenreTranslationProvider</c>, <c>SoftwareAttributeTranslationProvider</c>).
/// The worker iterates registered providers SERIALLY on each tick to keep OpenAI rate-limit
/// handling trivial — providers must NOT spawn parallel translation calls themselves.
/// </summary>
/// <remarks>
/// Lifecycle on each tick:
/// <list type="number">
/// <item><see cref="EnsureCacheLoadedAsync" /> — first tick only; subsequent ticks are no-ops once loaded.</item>
/// <item><see cref="DiscoverNewItemsAsync" /> — delta query for entities added since last tick.</item>
/// <item><see cref="TranslateMissingAsync" /> — once per non-<c>eng</c> language, in order.</item>
/// </list>
/// </remarks>
public interface ITranslationProvider
{
/// <summary>Short diagnostic name (e.g. <c>"SoftwareGenre"</c>) used in worker log messages.</summary>
string Name { get; }
/// <summary>
/// Make sure the in-memory cache backing this provider has been loaded from the DB. Idempotent
/// and thread-safe; the second concurrent caller waits and returns immediately.
/// </summary>
Task EnsureCacheLoadedAsync(CancellationToken ct);
/// <summary>
/// Discovers entities added to the DB since the last tick (typically via a delta query
/// <c>WHERE Id &gt; LastSeenId</c>) and registers them in the cache so the next
/// <see cref="TranslateMissingAsync" /> sweep picks them up. Returns the number of newly
/// discovered items so the worker can log a summary.
/// </summary>
Task<int> DiscoverNewItemsAsync(CancellationToken ct);
/// <summary>
/// For the given non-<c>eng</c> language, snapshot the items that lack a translation, call the
/// <c>TranslationService</c> serially for each, then bulk-insert the resulting rows in a single
/// <c>SaveChangesAsync</c> call. Returns the number of rows inserted (may be 0 if every item
/// is already translated or every translation call failed).
/// </summary>
Task<int> TranslateMissingAsync(string languageCode, CancellationToken ct);
}

View File

@@ -74,7 +74,7 @@ public class TranslationService(IHttpClientFactory httpClientFactory, IConfigura
public async Task<(string translatedText, string error)> TranslateAsync(string text,
string targetLanguageIso639_3, IProgress<(int current, int total)> progress = null,
bool plainText = false)
bool plainText = false, string domainContext = null)
{
if(!IsAvailable)
return (null, "Translation server is not configured.");
@@ -82,11 +82,19 @@ public class TranslationService(IHttpClientFactory httpClientFactory, IConfigura
if(!_languages.TryGetValue(targetLanguageIso639_3, out LanguageInfo target))
return (null, $"Language '{targetLanguageIso639_3}' is not supported for translation.");
// Normalise non-breaking space (U+00A0) to regular ASCII space BEFORE sending to either
// backend. MobyGames-imported attribute strings carry NBSPs that confuse OpenAI on short
// labels (e.g. "Minimum\u00A0RAM\u00A0Required" gets back-translated literally with the
// NBSPs preserved, defeating cache lookups downstream). Other Unicode line/paragraph
// separators flow through unchanged — only NBSP is normalised here.
if(!string.IsNullOrEmpty(text) && text.IndexOf('\u00A0') >= 0)
text = text.Replace('\u00A0', ' ');
// OpenAI takes precedence; on any failure we fall through to NLLB if configured.
if(IsOpenAIConfigured)
{
(string openAiText, string openAiError) =
await TranslateViaOpenAIAsync(text, target.EnglishName, progress, plainText);
await TranslateViaOpenAIAsync(text, target.EnglishName, progress, plainText, domainContext);
if(openAiText is not null)
return (openAiText, null);
@@ -107,7 +115,7 @@ public class TranslationService(IHttpClientFactory httpClientFactory, IConfigura
#region OpenAI backend
async Task<(string translated, string error)> TranslateViaOpenAIAsync(string text, string targetEnglishName,
IProgress<(int current, int total)> progress, bool plainText)
IProgress<(int current, int total)> progress, bool plainText, string domainContext = null)
{
progress?.Report((0, 1));
@@ -129,6 +137,14 @@ public class TranslationService(IHttpClientFactory httpClientFactory, IConfigura
+ "code blocks, tables, emphasis). Output ONLY the translated markdown — no preamble, no "
+ "explanation, no surrounding code fence.";
// Optional caller-supplied domain hint (e.g. "The text is a software genre name." or
// "The text is a video-game technical specification key or value (computer/console
// hardware terminology)."). Steers the model away from over-generic translations on
// short labels — without this, e.g. "Mouse" gets translated as the animal in some
// languages instead of the input device.
if(!string.IsNullOrWhiteSpace(domainContext))
systemPrompt += " Context: " + domainContext.Trim();
// Build the body as a Dictionary so optional fields (model, max_tokens, response_format)
// can be omitted entirely when not needed, which matches what local OpenAI-compatible
// servers expect.

View File

@@ -148,11 +148,24 @@
</MudStack>
@foreach(var specKey in _specsByKey)
{
<MudText Typo="Typo.subtitle2" Class="mt-2 mb-1"><strong>@L[specKey.Key]</strong></MudText>
<MudText Typo="Typo.subtitle2" Class="mt-2 mb-1"><strong>@specKey.DisplayKey</strong></MudText>
<div class="d-flex flex-wrap gap-1">
@foreach(string specValue in specKey.Values.OrderBy(v => (string)L[v], NaturalUnitComparer.Instance))
@{
// Pair canonical Values[i] (used in the URL filter) with DisplayValues[i] (the
// translated label) and sort by display so the UI order matches the user's locale.
var paired = specKey.Values
.Select((v, i) => new
{
Raw = v,
Display = i < specKey.DisplayValues.Count
? specKey.DisplayValues[i]
: v
})
.OrderBy(p => p.Display, NaturalUnitComparer.Instance);
}
@foreach(var pair in paired)
{
<MudChip T="string" Href="@($"/software/spec?key={Uri.EscapeDataString(specKey.Key)}&value={Uri.EscapeDataString(specValue)}")">@L[specValue]</MudChip>
<MudChip T="string" Href="@($"/software/spec?key={Uri.EscapeDataString(specKey.Key)}&value={Uri.EscapeDataString(pair.Raw)}")">@pair.Display</MudChip>
}
</div>
}

View File

@@ -485,8 +485,8 @@
@foreach(SoftwareAttributeDto spec in _releaseSpecs)
{
<tr>
<td style="width: 33%; text-align: right;"><strong>@L[spec.Key]</strong></td>
<td>@L[spec.Value]</td>
<td style="width: 33%; text-align: right;"><strong>@spec.Key</strong></td>
<td>@spec.Value</td>
</tr>
}
</tbody>
@@ -523,7 +523,7 @@
<div class="d-flex flex-wrap gap-2">
@foreach(SoftwareAttributeDto rating in _releaseRatings)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Warning">@L[rating.Key]: @L[rating.Value]</MudChip>
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Warning">@rating.Key: @rating.Value</MudChip>
}
</div>
}

View File

@@ -68,7 +68,10 @@
}
else if(!string.IsNullOrEmpty(SpecKey) && !string.IsNullOrEmpty(SpecValue))
{
<MudText Typo="Typo.h6" Style="color: rgba(255,255,255,0.85);">@string.Format(L["Software with {0}: {1}"], L[SpecKey], L[SpecValue])</MudText>
@* Server canonical (English) values for filter round-trip; UI heading echoes them
verbatim. A per-string translation endpoint could replace this if a localized
heading is desired (low-impact today: heading is small, page body is fully localized). *@
<MudText Typo="Typo.h6" Style="color: rgba(255,255,255,0.85);">@string.Format(L["Software with {0}: {1}"], SpecKey, SpecValue)</MudText>
}
@if(_kind.HasValue)
{

View File

@@ -530,8 +530,8 @@ else
@foreach(SoftwareAttributeDto spec in platformGroup.Value)
{
<tr>
<td style="width: 33%; text-align: right;"><strong>@L[spec.Key]</strong></td>
<td>@L[spec.Value]</td>
<td style="width: 33%; text-align: right;"><strong>@spec.Key</strong></td>
<td>@spec.Value</td>
</tr>
}
</tbody>
@@ -550,7 +550,7 @@ else
<div class="d-flex flex-wrap gap-2 mb-2">
@foreach(SoftwareAttributeDto rating in platformGroup.Value)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Warning">@L[rating.Key]: @L[rating.Value]</MudChip>
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Warning">@rating.Key: @rating.Value</MudChip>
}
</div>
}

View File

@@ -647,13 +647,7 @@
<data name="Ratings" xml:space="preserve">
<value>Bewertungen</value>
</data>
<data name="Business Model" xml:space="preserve">
<value>Geschäftsmodell</value>
</data>
<data name="Media Type" xml:space="preserve">
<value>Medientyp</value>
</data>
<data name="Video Modes" xml:space="preserve">
<data name="Video Modes" xml:space="preserve">
<value>Videomodi</value>
</data>
<data name="Sound" xml:space="preserve">
@@ -662,188 +656,43 @@
<data name="Players" xml:space="preserve">
<value>Spieler</value>
</data>
<data name="Notes" xml:space="preserve">
<value>Anmerkungen</value>
</data>
<data name="Number of Players" xml:space="preserve">
<data name="Number of Players" xml:space="preserve">
<value>Spieleranzahl</value>
</data>
<data name="Controller Support" xml:space="preserve">
<value>Controller-Unterstützung</value>
</data>
<data name="Input Devices Supported" xml:space="preserve">
<value>Unterstützte Eingabegeräte</value>
</data>
<data name="Minimum OS Class Required" xml:space="preserve">
<value>Mindestens erforderliche Betriebssystemversion</value>
</data>
<data name="Number of Offline Players" xml:space="preserve">
<value>Anzahl der Offline-Spieler</value>
</data>
<data name="Screen Orientation" xml:space="preserve">
<value>Bildschirmausrichtung</value>
</data>
<data name="Miscellaneous Attributes" xml:space="preserve">
<value>Sonstige Attribute</value>
</data>
<data name="Apple Rating" xml:space="preserve">
<data name="Apple Rating" xml:space="preserve">
<value>Apple-Bewertung</value>
</data>
<data name="Color Depth" xml:space="preserve">
<value>Farbtiefe</value>
</data>
<data name="ESRB Rating" xml:space="preserve">
<data name="ESRB Rating" xml:space="preserve">
<value>ESRB-Bewertung</value>
</data>
<data name="Minimum CPU Class Required" xml:space="preserve">
<value>Mindestens erforderliche CPU</value>
</data>
<data name="Minimum RAM Required" xml:space="preserve">
<value>Mindestens erforderlicher RAM</value>
</data>
<data name="Minimum Video Memory Required" xml:space="preserve">
<value>Mindestens erforderlicher Videospeicher</value>
</data>
<data name="Multiplayer Game Modes" xml:space="preserve">
<value>Mehrspieler-Spielmodi</value>
</data>
<data name="Multiplayer Options" xml:space="preserve">
<value>Mehrspieler-Optionen</value>
</data>
<data name="Number of Online Players" xml:space="preserve">
<value>Anzahl der Online-Spieler</value>
</data>
<data name="Number of Players Supported" xml:space="preserve">
<value>Anzahl unterstützter Spieler</value>
</data>
<data name="PEGI Rating" xml:space="preserve">
<data name="PEGI Rating" xml:space="preserve">
<value>PEGI-Bewertung</value>
</data>
<data name="USK Rating" xml:space="preserve">
<value>USK-Bewertung</value>
</data>
<data name="Video Modes Supported" xml:space="preserve">
<value>Unterstützte Videomodi</value>
</data>
<data name="Xbox/Windows Live" xml:space="preserve">
<value>Xbox/Windows Live</value>
</data>
<data name="1 Player" xml:space="preserve">
<value>1 Spieler</value>
</data>
<data name="1-2 Players" xml:space="preserve">
<value>1-2 Spieler</value>
</data>
<data name="1-4 Players" xml:space="preserve">
<value>1-4 Spieler</value>
</data>
<data name="16 Players" xml:space="preserve">
<value>16 Spieler</value>
</data>
<data name="256 MB" xml:space="preserve">
<value>256 MB</value>
</data>
<data name="3.5&quot; Floppy Disk" xml:space="preserve">
<value>3,5"-Diskette</value>
</data>
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<value>4 Bit (16 Graustufen), 8 Bit (256 Farben)</value>
</data>
<data name="64 MB" xml:space="preserve">
<value>64 MB</value>
</data>
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<value>Erfolge, Inhaltsdownload, Matchmaking, Online-Mehrspieler, Bestenliste, Sprachchat</value>
</data>
<data name="Amiga 500/2000" xml:space="preserve">
<value>Amiga 500/2000</value>
</data>
<data name="Android 2.1 (Eclair)" xml:space="preserve">
<value>Android 2.1 (Eclair)</value>
</data>
<data name="Android 4.0.3" xml:space="preserve">
<value>Android 4.0.3</value>
</data>
<data name="Blu-Ray Disc" xml:space="preserve">
<value>Blu-Ray-Disc</value>
</data>
<data name="CD-ROM" xml:space="preserve">
<value>CD-ROM</value>
</data>
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<value>Kooperativ, Jeder gegen jeden / Eins gegen eins (VS), Team</value>
</data>
<data name="Commercial" xml:space="preserve">
<value>Kommerziell</value>
</data>
<data name="Download" xml:space="preserve">
<value>Download</value>
</data>
<data name="Freeware / Free-to-play / Public Domain" xml:space="preserve">
<value>Freeware / Free-to-play / Gemeinfrei</value>
</data>
<data name="Game Center" xml:space="preserve">
<value>Game Center</value>
</data>
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<value>Game Center, In-App-Kauf</value>
</data>
<data name="Google Play game services, In-app/game/skill Purchase" xml:space="preserve">
<value>Google Play game services, In-App-Kauf</value>
</data>
<data name="Hot Seat" xml:space="preserve">
<value>Hot Seat</value>
</data>
<data name="Intel Pentium III" xml:space="preserve">
<value>Intel Pentium III</value>
</data>
<data name="Internet" xml:space="preserve">
<value>Internet</value>
</data>
<data name="iOS 7.0" xml:space="preserve">
<value>iOS 7.0</value>
</data>
<data name="iPhone OS 3.1.2" xml:space="preserve">
<value>iPhone OS 3.1.2</value>
</data>
<data name="iPhone OS 3.2" xml:space="preserve">
<value>iPhone OS 3.2</value>
</data>
<data name="Joystick (Digital)" xml:space="preserve">
<value>Joystick (digital)</value>
</data>
<data name="Landscape" xml:space="preserve">
<value>Querformat</value>
</data>
<data name="Mac OS X 10.6 (Snow Leopard)" xml:space="preserve">
<value>Mac OS X 10.6 (Snow Leopard)</value>
</data>
<data name="Mouse" xml:space="preserve">
<value>Maus</value>
</data>
<data name="OCS/ECS" xml:space="preserve">
<value>OCS/ECS</value>
</data>
<data name="Palm OS 3.5" xml:space="preserve">
<value>Palm OS 3.5</value>
</data>
<data name="Portrait" xml:space="preserve">
<value>Hochformat</value>
</data>
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<value>Separate Schwarz-Weiß- und Farbversionen.</value>
</data>
<data name="Shareware" xml:space="preserve">
<value>Shareware</value>
</data>
<data name="Touch Screen" xml:space="preserve">
<value>Touchscreen</value>
</data>
<data name="Windows XP" xml:space="preserve">
<value>Windows XP</value>
</data>
<data name="Search software by genre" xml:space="preserve">
<data name="Search software by genre" xml:space="preserve">
<value>Software nach Genre suchen</value>
<comment>Search software by genre</comment>
</data>
@@ -1030,460 +879,10 @@
<data name="Search software by specifications" xml:space="preserve">
<value>Software nach Spezifikationen suchen</value>
</data>
<data name="Additional Hardware Required" xml:space="preserve">
<value>Zusätzliche Hardware erforderlich</value>
</data>
<data name="Additional Hardware Supported" xml:space="preserve">
<value>Unterstützte zusätzliche Hardware</value>
</data>
<data name="Controller Types Supported" xml:space="preserve">
<value>Unterstützte Controller-Typen</value>
</data>
<data name="Drivers/APIs Supported" xml:space="preserve">
<value>Unterstützte Treiber/APIs</value>
</data>
<data name="Gamepads Supported" xml:space="preserve">
<value>Unterstützte Gamepads</value>
</data>
<data name="Input Device Features Supported" xml:space="preserve">
<value>Unterstützte Eingabegerätefunktionen</value>
</data>
<data name="Input Devices Required" xml:space="preserve">
<value>Erforderliche Eingabegeräte</value>
</data>
<data name="Light Guns / Attachments Supported" xml:space="preserve">
<value>Unterstützte Lightguns / Aufsätze</value>
</data>
<data name="Minimum CD-ROM Drive Speed Required" xml:space="preserve">
<value>Mindestgeschwindigkeit des CD-ROM-Laufwerks</value>
</data>
<data name="Minimum DirectX Version Required" xml:space="preserve">
<value>Mindestversion von DirectX erforderlich</value>
</data>
<data name="Minimum DVD-ROM Drive Speed Required" xml:space="preserve">
<value>Mindestgeschwindigkeit des DVD-ROM-Laufwerks</value>
</data>
<data name="Minimum Supported System" xml:space="preserve">
<value>Mindestens unterstütztes System</value>
</data>
<data name="Mouses Supported" xml:space="preserve">
<value>Unterstützte Mäuse</value>
</data>
<data name="Number of Local Players via System Link / LAN Cable" xml:space="preserve">
<value>Anzahl lokaler Spieler über System Link / LAN-Kabel</value>
</data>
<data name="Number of Local Players via Wi-Fi" xml:space="preserve">
<value>Anzahl lokaler Spieler über WLAN</value>
</data>
<data name="Number of Offline Players via Handheld Mode" xml:space="preserve">
<value>Anzahl Offline-Spieler im Handheld-Modus</value>
</data>
<data name="Number of Offline Players via Multitap" xml:space="preserve">
<value>Anzahl Offline-Spieler über Multitap</value>
</data>
<data name="Number of Offline Players via Tabletop Mode" xml:space="preserve">
<value>Anzahl Offline-Spieler im Tisch-Modus</value>
</data>
<data name="Number of Offline Players via TV Mode" xml:space="preserve">
<value>Anzahl Offline-Spieler im TV-Modus</value>
</data>
<data name="Save Game Methods" xml:space="preserve">
<value>Speichermethoden</value>
</data>
<data name="Sound Capabilities" xml:space="preserve">
<value>Soundfähigkeiten</value>
</data>
<data name="Sound Devices Supported" xml:space="preserve">
<value>Unterstützte Audiogeräte</value>
</data>
<data name="Supported Languages and Operating Systems" xml:space="preserve">
<value>Unterstützte Sprachen und Betriebssysteme</value>
</data>
<data name="Supported Systems/Models" xml:space="preserve">
<value>Unterstützte Systeme/Modelle</value>
</data>
<data name="Technology" xml:space="preserve">
<value>Technologie</value>
</data>
<data name="Text Modes Supported" xml:space="preserve">
<value>Unterstützte Textmodi</value>
</data>
<data name="Video Resolutions Supported" xml:space="preserve">
<value>Unterstützte Videoauflösungen</value>
</data>
<data name="VR Helmets/Headsets Supported" xml:space="preserve">
<value>Unterstützte VR-Headsets/-Helme</value>
</data>
<data name="VR Play Area" xml:space="preserve">
<value>VR-Spielbereich</value>
</data>
<data name="3D Accelerator" xml:space="preserve">
<value>3D-Beschleuniger</value>
</data>
<data name="1-8 Players" xml:space="preserve">
<value>1-8 Spieler</value>
</data>
<data name="1-bit (monochrome)" xml:space="preserve">
<value>1 Bit (monochrom)</value>
</data>
<data name="16-bit (High Color)" xml:space="preserve">
<value>16 Bit (High Color)</value>
</data>
<data name="1 button" xml:space="preserve">
<value>1 Taste</value>
</data>
<data name="2-12 Players" xml:space="preserve">
<value>2-12 Spieler</value>
</data>
<data name="2-4 Players" xml:space="preserve">
<value>2-4 Spieler</value>
</data>
<data name="2-8 Players" xml:space="preserve">
<value>2-8 Spieler</value>
</data>
<data name="2-bit (4 colors)" xml:space="preserve">
<value>2 Bit (4 Farben)</value>
</data>
<data name="256 Colors Required" xml:space="preserve">
<value>256 Farben erforderlich</value>
</data>
<data name="2 Players" xml:space="preserve">
<value>2 Spieler</value>
</data>
<data name="5.25&quot; Floppy Disk" xml:space="preserve">
<value>5,25-Zoll-Diskette</value>
</data>
<data name="4 Players" xml:space="preserve">
<value>4 Spieler</value>
</data>
<data name="4-bit (16 greyscales)" xml:space="preserve">
<value>4 Bit (16 Graustufen)</value>
</data>
<data name="4-bit (16 colors)" xml:space="preserve">
<value>4 Bit (16 Farben)</value>
</data>
<data name="32 Players" xml:space="preserve">
<value>32 Spieler</value>
</data>
<data name="3&quot; Floppy Disk" xml:space="preserve">
<value>3-Zoll-Diskette</value>
</data>
<data name="6 Players" xml:space="preserve">
<value>6 Spieler</value>
</data>
<data name="8+ Players" xml:space="preserve">
<value>8+ Spieler</value>
</data>
<data name="8-bit (256 colors)" xml:space="preserve">
<value>8 Bit (256 Farben)</value>
</data>
<data name="Achievements" xml:space="preserve">
<value>Erfolge</value>
</data>
<data name="Add-on Content (DLC)" xml:space="preserve">
<value>Zusatzinhalte (DLC)</value>
</data>
<data name="Ad Hoc" xml:space="preserve">
<value>Ad-hoc</value>
</data>
<data name="Analog Controller" xml:space="preserve">
<value>Analog-Controller</value>
</data>
<data name="Avatars" xml:space="preserve">
<value>Avatare</value>
</data>
<data name="BASIC Cartridge" xml:space="preserve">
<value>BASIC-Modul</value>
</data>
<data name="Built in Rumble Feature" xml:space="preserve">
<value>Integrierte Vibrationsfunktion</value>
</data>
<data name="Cartridge" xml:space="preserve">
<value>Modul</value>
</data>
<data name="Cassette Tape" xml:space="preserve">
<value>Kassette</value>
</data>
<data name="Comlynx Cable" xml:space="preserve">
<value>Comlynx-Kabel</value>
</data>
<data name="Color Monitor Support" xml:space="preserve">
<value>Farbmonitor-Unterstützung</value>
</data>
<data name="Color Monitor Required" xml:space="preserve">
<value>Farbmonitor erforderlich</value>
</data>
<data name="Color" xml:space="preserve">
<value>Farbe</value>
</data>
<data name="Co-Op" xml:space="preserve">
<value>Koop</value>
</data>
<data name="Cloud" xml:space="preserve">
<value>Cloud</value>
</data>
<data name="CGA Composite (16 colors)" xml:space="preserve">
<value>CGA Composite (16 Farben)</value>
</data>
<data name="Content Download" xml:space="preserve">
<value>Inhalts-Download</value>
</data>
<data name="Cross-Buy" xml:space="preserve">
<value>Cross-Buy</value>
</data>
<data name="Cursor Joystick" xml:space="preserve">
<value>Cursor-Joystick</value>
</data>
<data name="Custom Soundtracks" xml:space="preserve">
<value>Eigene Soundtracks</value>
</data>
<data name="Definable Keys" xml:space="preserve">
<value>Frei belegbare Tasten</value>
</data>
<data name="Floppy Disk" xml:space="preserve">
<value>Diskette</value>
</data>
<data name="External Storage" xml:space="preserve">
<value>Externer Speicher</value>
</data>
<data name="Extended Basic Cartridge" xml:space="preserve">
<value>Extended-Basic-Modul</value>
</data>
<data name="Ethernet Compatible" xml:space="preserve">
<value>Ethernet-kompatibel</value>
</data>
<data name="Ethernet Broadband Required" xml:space="preserve">
<value>Ethernet-Breitband erforderlich</value>
</data>
<data name="Double-sided Disk" xml:space="preserve">
<value>Doppelseitige Diskette</value>
</data>
<data name="DOS 4.0 to 6.22" xml:space="preserve">
<value>DOS 4.0 bis 6.22</value>
</data>
<data name="DLC available" xml:space="preserve">
<value>DLC verfügbar</value>
</data>
<data name="Free-for-all / One-on-one (VS)" xml:space="preserve">
<value>Jeder gegen jeden / Eins gegen Eins (VS)</value>
</data>
<data name="Friends" xml:space="preserve">
<value>Freunde</value>
</data>
<data name="Full screen" xml:space="preserve">
<value>Vollbild</value>
</data>
<data name="GameCube Compatible, Multi-Link Cable" xml:space="preserve">
<value>GameCube-kompatibel, Multi-Link-Kabel</value>
</data>
<data name="Gamepad" xml:space="preserve">
<value>Gamepad</value>
</data>
<data name="Game Boy Advance Compatible" xml:space="preserve">
<value>Game Boy Advance-kompatibel</value>
</data>
<data name="Game Card" xml:space="preserve">
<value>Spielkarte</value>
</data>
<data name="Game Sharing" xml:space="preserve">
<value>Spielfreigabe</value>
</data>
<data name="Handheld mode compatible" xml:space="preserve">
<value>Mit Handheld-Modus kompatibel</value>
</data>
<data name="Hard Drive Installable" xml:space="preserve">
<value>Auf Festplatte installierbar</value>
</data>
<data name="Headset Compatible" xml:space="preserve">
<value>Headset-kompatibel</value>
</data>
<data name="In-app/game/skill Purchase" xml:space="preserve">
<value>In-App-/Spiel-/Skill-Kauf</value>
</data>
<data name="Keyboard" xml:space="preserve">
<value>Tastatur</value>
</data>
<data name="Kempston Mouse" xml:space="preserve">
<value>Kempston-Maus</value>
</data>
<data name="Kempston Joystick" xml:space="preserve">
<value>Kempston-Joystick</value>
</data>
<data name="Joystick (Analog)" xml:space="preserve">
<value>Joystick (analog)</value>
</data>
<data name="Other Input Devices" xml:space="preserve">
<value>Andere Eingabegeräte</value>
</data>
<data name="Leaderboards" xml:space="preserve">
<value>Bestenlisten</value>
</data>
<data name="Light Gun / Attachment" xml:space="preserve">
<value>Lightgun / Aufsatz</value>
</data>
<data name="Link Cable" xml:space="preserve">
<value>Link-Kabel</value>
</data>
<data name="Lobbies/Matchmaking" xml:space="preserve">
<value>Lobbys/Matchmaking</value>
</data>
<data name="Magnetic Card" xml:space="preserve">
<value>Magnetkarte</value>
</data>
<data name="Matchmaking" xml:space="preserve">
<value>Matchmaking</value>
</data>
<data name="MCGA 4-color" xml:space="preserve">
<value>MCGA 4 Farben</value>
</data>
<data name="Mega Cartridge" xml:space="preserve">
<value>Mega-Modul</value>
</data>
<data name="Messaging/Friend invite in game" xml:space="preserve">
<value>Nachrichten/Freundeseinladung im Spiel</value>
</data>
<data name="Mii Support" xml:space="preserve">
<value>Mii-Unterstützung</value>
</data>
<data name="Modem" xml:space="preserve">
<value>Modem</value>
</data>
<data name="Monochrome Monitor Support" xml:space="preserve">
<value>Monochrom-Monitor-Unterstützung</value>
</data>
<data name="Motion sensor" xml:space="preserve">
<value>Bewegungssensor</value>
</data>
<data name="Progressive Scan (480p)" xml:space="preserve">
<value>Progressive Scan (480p)</value>
</data>
<data name="Pressure/Touch Sensitive" xml:space="preserve">
<value>Druck-/berührungsempfindlich</value>
</data>
<data name="PlayStation Network compatible" xml:space="preserve">
<value>PlayStation Network-kompatibel</value>
</data>
<data name="Pedals" xml:space="preserve">
<value>Pedale</value>
</data>
<data name="PC Speaker (Tweaked)" xml:space="preserve">
<value>PC Speaker (optimiert)</value>
</data>
<data name="Password" xml:space="preserve">
<data name="Password" xml:space="preserve">
<value>Passwort</value>
</data>
<data name="Online Multiplayer" xml:space="preserve">
<value>Online-Mehrspieler</value>
</data>
<data name="Null-modem cable" xml:space="preserve">
<value>Nullmodem-Kabel</value>
</data>
<data name="Network Adaptor" xml:space="preserve">
<value>Netzwerkadapter</value>
</data>
<data name="Multiple players required" xml:space="preserve">
<value>Mehrere Spieler erforderlich</value>
</data>
<data name="Multiplayer" xml:space="preserve">
<value>Mehrspieler</value>
</data>
<data name="Multi-player" xml:space="preserve">
<value>Mehrspieler</value>
</data>
<data name="Multi-Link Cable" xml:space="preserve">
<value>Multi-Link-Kabel</value>
</data>
<data name="Racing/Steering Wheel" xml:space="preserve">
<value>Renn-/Lenkrad</value>
</data>
<data name="Raster" xml:space="preserve">
<value>Raster</value>
</data>
<data name="Remote Play" xml:space="preserve">
<value>Remote Play</value>
</data>
<data name="Room-Scale" xml:space="preserve">
<value>Raumskala</value>
</data>
<data name="Same/Split-Screen" xml:space="preserve">
<value>Gleicher Bildschirm/Splitscreen</value>
</data>
<data name="Save Data Cloud" xml:space="preserve">
<value>Speicherdaten-Cloud</value>
</data>
<data name="Scoreboard/Leaderboard" xml:space="preserve">
<value>Punktetafel/Bestenliste</value>
</data>
<data name="Sinclair Joystick" xml:space="preserve">
<value>Sinclair-Joystick</value>
</data>
<data name="Single-player" xml:space="preserve">
<value>Einzelspieler</value>
</data>
<data name="Single-sided Disk" xml:space="preserve">
<value>Einseitige Diskette</value>
</data>
<data name="SNES Mouse" xml:space="preserve">
<value>SNES-Maus</value>
</data>
<data name="Standing" xml:space="preserve">
<value>Stehend</value>
</data>
<data name="Steam account required" xml:space="preserve">
<value>Steam-Konto erforderlich</value>
</data>
<data name="Tabletop mode compatible" xml:space="preserve">
<value>Mit Tisch-Modus kompatibel</value>
</data>
<data name="Team" xml:space="preserve">
<value>Team</value>
</data>
<data name="Teletype/Teleprinter" xml:space="preserve">
<value>Fernschreiber</value>
</data>
<data name="Tracked motion controllers" xml:space="preserve">
<value>Verfolgte Bewegungscontroller</value>
</data>
<data name="Trading cards" xml:space="preserve">
<value>Sammelkarten</value>
</data>
<data name="Trophies" xml:space="preserve">
<value>Trophäen</value>
</data>
<data name="TV mode compatible" xml:space="preserve">
<value>Mit TV-Modus kompatibel</value>
</data>
<data name="Valve Anti-Cheat System (VAC)" xml:space="preserve">
<value>Valve Anti-Cheat-System (VAC)</value>
</data>
<data name="VGA (Tweaked)" xml:space="preserve">
<value>VGA (optimiert)</value>
</data>
<data name="Vibration" xml:space="preserve">
<value>Vibration</value>
</data>
<data name="XNA Indie Games" xml:space="preserve">
<value>XNA-Indie-Spiele</value>
</data>
<data name="Xbox 360 Compatible" xml:space="preserve">
<value>Xbox 360-kompatibel</value>
</data>
<data name="Window" xml:space="preserve">
<value>Fenster</value>
</data>
<data name="Widescreen (16 x 9)" xml:space="preserve">
<value>Widescreen (16 x 9)</value>
</data>
<data name="Wi-Fi Connection" xml:space="preserve">
<value>WLAN-Verbindung</value>
</data>
<data name="VR Helmet/Headset" xml:space="preserve">
<value>VR-Headset/-Helm</value>
</data>
<data name="Voice Chat" xml:space="preserve">
<value>Sprachchat</value>
</data>
<data name="Regions" xml:space="preserve">
<data name="Regions" xml:space="preserve">
<value>Regionen</value>
</data>
<data name="Suggest description" xml:space="preserve">

View File

@@ -392,13 +392,7 @@
<data name="Ratings" xml:space="preserve">
<value>Ratings</value>
</data>
<data name="Business Model" xml:space="preserve">
<value>Business Model</value>
</data>
<data name="Media Type" xml:space="preserve">
<value>Media Type</value>
</data>
<data name="Video Modes" xml:space="preserve">
<data name="Video Modes" xml:space="preserve">
<value>Video Modes</value>
</data>
<data name="Sound" xml:space="preserve">
@@ -407,188 +401,43 @@
<data name="Players" xml:space="preserve">
<value>Players</value>
</data>
<data name="Notes" xml:space="preserve">
<value>Notes</value>
</data>
<data name="Number of Players" xml:space="preserve">
<data name="Number of Players" xml:space="preserve">
<value>Number of Players</value>
</data>
<data name="Controller Support" xml:space="preserve">
<value>Controller Support</value>
</data>
<data name="Input Devices Supported" xml:space="preserve">
<value>Input Devices Supported</value>
</data>
<data name="Minimum OS Class Required" xml:space="preserve">
<value>Minimum OS Class Required</value>
</data>
<data name="Number of Offline Players" xml:space="preserve">
<value>Number of Offline Players</value>
</data>
<data name="Screen Orientation" xml:space="preserve">
<value>Screen Orientation</value>
</data>
<data name="Miscellaneous Attributes" xml:space="preserve">
<value>Miscellaneous Attributes</value>
</data>
<data name="Apple Rating" xml:space="preserve">
<data name="Apple Rating" xml:space="preserve">
<value>Apple Rating</value>
</data>
<data name="Color Depth" xml:space="preserve">
<value>Color Depth</value>
</data>
<data name="ESRB Rating" xml:space="preserve">
<data name="ESRB Rating" xml:space="preserve">
<value>ESRB Rating</value>
</data>
<data name="Minimum CPU Class Required" xml:space="preserve">
<value>Minimum CPU Class Required</value>
</data>
<data name="Minimum RAM Required" xml:space="preserve">
<value>Minimum RAM Required</value>
</data>
<data name="Minimum Video Memory Required" xml:space="preserve">
<value>Minimum Video Memory Required</value>
</data>
<data name="Multiplayer Game Modes" xml:space="preserve">
<value>Multiplayer Game Modes</value>
</data>
<data name="Multiplayer Options" xml:space="preserve">
<value>Multiplayer Options</value>
</data>
<data name="Number of Online Players" xml:space="preserve">
<value>Number of Online Players</value>
</data>
<data name="Number of Players Supported" xml:space="preserve">
<value>Number of Players Supported</value>
</data>
<data name="PEGI Rating" xml:space="preserve">
<data name="PEGI Rating" xml:space="preserve">
<value>PEGI Rating</value>
</data>
<data name="USK Rating" xml:space="preserve">
<value>USK Rating</value>
</data>
<data name="Video Modes Supported" xml:space="preserve">
<value>Video Modes Supported</value>
</data>
<data name="Xbox/Windows Live" xml:space="preserve">
<value>Xbox/Windows Live</value>
</data>
<data name="1 Player" xml:space="preserve">
<value>1 Player</value>
</data>
<data name="1-2 Players" xml:space="preserve">
<value>1-2 Players</value>
</data>
<data name="1-4 Players" xml:space="preserve">
<value>1-4 Players</value>
</data>
<data name="16 Players" xml:space="preserve">
<value>16 Players</value>
</data>
<data name="256 MB" xml:space="preserve">
<value>256 MB</value>
</data>
<data name="3.5&quot; Floppy Disk" xml:space="preserve">
<value>3.5" Floppy Disk</value>
</data>
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<value>4-bit (16 greyscales), 8-bit (256 colors)</value>
</data>
<data name="64 MB" xml:space="preserve">
<value>64 MB</value>
</data>
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<value>Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat</value>
</data>
<data name="Amiga 500/2000" xml:space="preserve">
<value>Amiga 500/2000</value>
</data>
<data name="Android 2.1 (Eclair)" xml:space="preserve">
<value>Android 2.1 (Eclair)</value>
</data>
<data name="Android 4.0.3" xml:space="preserve">
<value>Android 4.0.3</value>
</data>
<data name="Blu-Ray Disc" xml:space="preserve">
<value>Blu-Ray Disc</value>
</data>
<data name="CD-ROM" xml:space="preserve">
<value>CD-ROM</value>
</data>
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<value>Co-Op, Free-for-all / One-on-one (VS), Team</value>
</data>
<data name="Commercial" xml:space="preserve">
<value>Commercial</value>
</data>
<data name="Download" xml:space="preserve">
<value>Download</value>
</data>
<data name="Freeware / Free-to-play / Public Domain" xml:space="preserve">
<value>Freeware / Free-to-play / Public Domain</value>
</data>
<data name="Game Center" xml:space="preserve">
<value>Game Center</value>
</data>
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<value>Game Center, In-app/game/skill Purchase</value>
</data>
<data name="Google Play game services, In-app/game/skill Purchase" xml:space="preserve">
<value>Google Play game services, In-app/game/skill Purchase</value>
</data>
<data name="Hot Seat" xml:space="preserve">
<value>Hot Seat</value>
</data>
<data name="Intel Pentium III" xml:space="preserve">
<value>Intel Pentium III</value>
</data>
<data name="Internet" xml:space="preserve">
<value>Internet</value>
</data>
<data name="iOS 7.0" xml:space="preserve">
<value>iOS 7.0</value>
</data>
<data name="iPhone OS 3.1.2" xml:space="preserve">
<value>iPhone OS 3.1.2</value>
</data>
<data name="iPhone OS 3.2" xml:space="preserve">
<value>iPhone OS 3.2</value>
</data>
<data name="Joystick (Digital)" xml:space="preserve">
<value>Joystick (Digital)</value>
</data>
<data name="Landscape" xml:space="preserve">
<value>Landscape</value>
</data>
<data name="Mac OS X 10.6 (Snow Leopard)" xml:space="preserve">
<value>Mac OS X 10.6 (Snow Leopard)</value>
</data>
<data name="Mouse" xml:space="preserve">
<value>Mouse</value>
</data>
<data name="OCS/ECS" xml:space="preserve">
<value>OCS/ECS</value>
</data>
<data name="Palm OS 3.5" xml:space="preserve">
<value>Palm OS 3.5</value>
</data>
<data name="Portrait" xml:space="preserve">
<value>Portrait</value>
</data>
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<value>Separate black &amp; white and colour versions.</value>
</data>
<data name="Shareware" xml:space="preserve">
<value>Shareware</value>
</data>
<data name="Touch Screen" xml:space="preserve">
<value>Touch Screen</value>
</data>
<data name="Windows XP" xml:space="preserve">
<value>Windows XP</value>
</data>
<data name="Other" xml:space="preserve">
<data name="Other" xml:space="preserve">
<value>Other</value>
</data>
<data name="Item" xml:space="preserve">
@@ -841,460 +690,10 @@
<data name="Search software by specifications" xml:space="preserve">
<value>Search software by specifications</value>
</data>
<data name="Additional Hardware Required" xml:space="preserve">
<value>Additional Hardware Required</value>
</data>
<data name="Additional Hardware Supported" xml:space="preserve">
<value>Additional Hardware Supported</value>
</data>
<data name="Controller Types Supported" xml:space="preserve">
<value>Controller Types Supported</value>
</data>
<data name="Drivers/APIs Supported" xml:space="preserve">
<value>Drivers/APIs Supported</value>
</data>
<data name="Gamepads Supported" xml:space="preserve">
<value>Gamepads Supported</value>
</data>
<data name="Input Device Features Supported" xml:space="preserve">
<value>Input Device Features Supported</value>
</data>
<data name="Input Devices Required" xml:space="preserve">
<value>Input Devices Required</value>
</data>
<data name="Light Guns / Attachments Supported" xml:space="preserve">
<value>Light Guns / Attachments Supported</value>
</data>
<data name="Minimum CD-ROM Drive Speed Required" xml:space="preserve">
<value>Minimum CD-ROM Drive Speed Required</value>
</data>
<data name="Minimum DirectX Version Required" xml:space="preserve">
<value>Minimum DirectX Version Required</value>
</data>
<data name="Minimum DVD-ROM Drive Speed Required" xml:space="preserve">
<value>Minimum DVD-ROM Drive Speed Required</value>
</data>
<data name="Minimum Supported System" xml:space="preserve">
<value>Minimum Supported System</value>
</data>
<data name="Mouses Supported" xml:space="preserve">
<value>Mice Supported</value>
</data>
<data name="Number of Local Players via System Link / LAN Cable" xml:space="preserve">
<value>Number of Local Players via System Link / LAN Cable</value>
</data>
<data name="Number of Local Players via Wi-Fi" xml:space="preserve">
<value>Number of Local Players via Wi-Fi</value>
</data>
<data name="Number of Offline Players via Handheld Mode" xml:space="preserve">
<value>Number of Offline Players via Handheld Mode</value>
</data>
<data name="Number of Offline Players via Multitap" xml:space="preserve">
<value>Number of Offline Players via Multitap</value>
</data>
<data name="Number of Offline Players via Tabletop Mode" xml:space="preserve">
<value>Number of Offline Players via Tabletop Mode</value>
</data>
<data name="Number of Offline Players via TV Mode" xml:space="preserve">
<value>Number of Offline Players via TV Mode</value>
</data>
<data name="Save Game Methods" xml:space="preserve">
<value>Save Game Methods</value>
</data>
<data name="Sound Capabilities" xml:space="preserve">
<value>Sound Capabilities</value>
</data>
<data name="Sound Devices Supported" xml:space="preserve">
<value>Sound Devices Supported</value>
</data>
<data name="Supported Languages and Operating Systems" xml:space="preserve">
<value>Supported Languages and Operating Systems</value>
</data>
<data name="Supported Systems/Models" xml:space="preserve">
<value>Supported Systems/Models</value>
</data>
<data name="Technology" xml:space="preserve">
<value>Technology</value>
</data>
<data name="Text Modes Supported" xml:space="preserve">
<value>Text Modes Supported</value>
</data>
<data name="Video Resolutions Supported" xml:space="preserve">
<value>Video Resolutions Supported</value>
</data>
<data name="VR Helmets/Headsets Supported" xml:space="preserve">
<value>VR Helmets/Headsets Supported</value>
</data>
<data name="VR Play Area" xml:space="preserve">
<value>VR Play Area</value>
</data>
<data name="3D Accelerator" xml:space="preserve">
<value>3D Accelerator</value>
</data>
<data name="1-8 Players" xml:space="preserve">
<value>1-8 Players</value>
</data>
<data name="1-bit (monochrome)" xml:space="preserve">
<value>1-bit (monochrome)</value>
</data>
<data name="16-bit (High Color)" xml:space="preserve">
<value>16-bit (High Color)</value>
</data>
<data name="1 button" xml:space="preserve">
<value>1 button</value>
</data>
<data name="2-12 Players" xml:space="preserve">
<value>2-12 Players</value>
</data>
<data name="2-4 Players" xml:space="preserve">
<value>2-4 Players</value>
</data>
<data name="2-8 Players" xml:space="preserve">
<value>2-8 Players</value>
</data>
<data name="2-bit (4 colors)" xml:space="preserve">
<value>2-bit (4 colors)</value>
</data>
<data name="256 Colors Required" xml:space="preserve">
<value>256 Colors Required</value>
</data>
<data name="2 Players" xml:space="preserve">
<value>2 Players</value>
</data>
<data name="5.25&quot; Floppy Disk" xml:space="preserve">
<value>5.25" Floppy Disk</value>
</data>
<data name="4 Players" xml:space="preserve">
<value>4 Players</value>
</data>
<data name="4-bit (16 greyscales)" xml:space="preserve">
<value>4-bit (16 greyscales)</value>
</data>
<data name="4-bit (16 colors)" xml:space="preserve">
<value>4-bit (16 colors)</value>
</data>
<data name="32 Players" xml:space="preserve">
<value>32 Players</value>
</data>
<data name="3&quot; Floppy Disk" xml:space="preserve">
<value>3" Floppy Disk</value>
</data>
<data name="6 Players" xml:space="preserve">
<value>6 Players</value>
</data>
<data name="8+ Players" xml:space="preserve">
<value>8+ Players</value>
</data>
<data name="8-bit (256 colors)" xml:space="preserve">
<value>8-bit (256 colors)</value>
</data>
<data name="Achievements" xml:space="preserve">
<value>Achievements</value>
</data>
<data name="Add-on Content (DLC)" xml:space="preserve">
<value>Add-on Content (DLC)</value>
</data>
<data name="Ad Hoc" xml:space="preserve">
<value>Ad Hoc</value>
</data>
<data name="Analog Controller" xml:space="preserve">
<value>Analog Controller</value>
</data>
<data name="Avatars" xml:space="preserve">
<value>Avatars</value>
</data>
<data name="BASIC Cartridge" xml:space="preserve">
<value>BASIC Cartridge</value>
</data>
<data name="Built in Rumble Feature" xml:space="preserve">
<value>Built in Rumble Feature</value>
</data>
<data name="Cartridge" xml:space="preserve">
<value>Cartridge</value>
</data>
<data name="Cassette Tape" xml:space="preserve">
<value>Cassette Tape</value>
</data>
<data name="Comlynx Cable" xml:space="preserve">
<value>Comlynx Cable</value>
</data>
<data name="Color Monitor Support" xml:space="preserve">
<value>Color Monitor Support</value>
</data>
<data name="Color Monitor Required" xml:space="preserve">
<value>Color Monitor Required</value>
</data>
<data name="Color" xml:space="preserve">
<value>Color</value>
</data>
<data name="Co-Op" xml:space="preserve">
<value>Co-Op</value>
</data>
<data name="Cloud" xml:space="preserve">
<value>Cloud</value>
</data>
<data name="CGA Composite (16 colors)" xml:space="preserve">
<value>CGA Composite (16 colors)</value>
</data>
<data name="Content Download" xml:space="preserve">
<value>Content Download</value>
</data>
<data name="Cross-Buy" xml:space="preserve">
<value>Cross-Buy</value>
</data>
<data name="Cursor Joystick" xml:space="preserve">
<value>Cursor Joystick</value>
</data>
<data name="Custom Soundtracks" xml:space="preserve">
<value>Custom Soundtracks</value>
</data>
<data name="Definable Keys" xml:space="preserve">
<value>Definable Keys</value>
</data>
<data name="Floppy Disk" xml:space="preserve">
<value>Floppy Disk</value>
</data>
<data name="External Storage" xml:space="preserve">
<value>External Storage</value>
</data>
<data name="Extended Basic Cartridge" xml:space="preserve">
<value>Extended Basic Cartridge</value>
</data>
<data name="Ethernet Compatible" xml:space="preserve">
<value>Ethernet Compatible</value>
</data>
<data name="Ethernet Broadband Required" xml:space="preserve">
<value>Ethernet Broadband Required</value>
</data>
<data name="Double-sided Disk" xml:space="preserve">
<value>Double-sided Disk</value>
</data>
<data name="DOS 4.0 to 6.22" xml:space="preserve">
<value>DOS 4.0 to 6.22</value>
</data>
<data name="DLC available" xml:space="preserve">
<value>DLC available</value>
</data>
<data name="Free-for-all / One-on-one (VS)" xml:space="preserve">
<value>Free-for-all / One-on-one (VS)</value>
</data>
<data name="Friends" xml:space="preserve">
<value>Friends</value>
</data>
<data name="Full screen" xml:space="preserve">
<value>Full screen</value>
</data>
<data name="GameCube Compatible, Multi-Link Cable" xml:space="preserve">
<value>GameCube Compatible, Multi-Link Cable</value>
</data>
<data name="Gamepad" xml:space="preserve">
<value>Gamepad</value>
</data>
<data name="Game Boy Advance Compatible" xml:space="preserve">
<value>Game Boy Advance Compatible</value>
</data>
<data name="Game Card" xml:space="preserve">
<value>Game Card</value>
</data>
<data name="Game Sharing" xml:space="preserve">
<value>Game Sharing</value>
</data>
<data name="Handheld mode compatible" xml:space="preserve">
<value>Handheld mode compatible</value>
</data>
<data name="Hard Drive Installable" xml:space="preserve">
<value>Hard Drive Installable</value>
</data>
<data name="Headset Compatible" xml:space="preserve">
<value>Headset Compatible</value>
</data>
<data name="In-app/game/skill Purchase" xml:space="preserve">
<value>In-app/game/skill Purchase</value>
</data>
<data name="Keyboard" xml:space="preserve">
<value>Keyboard</value>
</data>
<data name="Kempston Mouse" xml:space="preserve">
<value>Kempston Mouse</value>
</data>
<data name="Kempston Joystick" xml:space="preserve">
<value>Kempston Joystick</value>
</data>
<data name="Joystick (Analog)" xml:space="preserve">
<value>Joystick (Analog)</value>
</data>
<data name="Other Input Devices" xml:space="preserve">
<value>Other Input Devices</value>
</data>
<data name="Leaderboards" xml:space="preserve">
<value>Leaderboards</value>
</data>
<data name="Light Gun / Attachment" xml:space="preserve">
<value>Light Gun / Attachment</value>
</data>
<data name="Link Cable" xml:space="preserve">
<value>Link Cable</value>
</data>
<data name="Lobbies/Matchmaking" xml:space="preserve">
<value>Lobbies/Matchmaking</value>
</data>
<data name="Magnetic Card" xml:space="preserve">
<value>Magnetic Card</value>
</data>
<data name="Matchmaking" xml:space="preserve">
<value>Matchmaking</value>
</data>
<data name="MCGA 4-color" xml:space="preserve">
<value>MCGA 4-color</value>
</data>
<data name="Mega Cartridge" xml:space="preserve">
<value>Mega Cartridge</value>
</data>
<data name="Messaging/Friend invite in game" xml:space="preserve">
<value>Messaging/Friend invite in game</value>
</data>
<data name="Mii Support" xml:space="preserve">
<value>Mii Support</value>
</data>
<data name="Modem" xml:space="preserve">
<value>Modem</value>
</data>
<data name="Monochrome Monitor Support" xml:space="preserve">
<value>Monochrome Monitor Support</value>
</data>
<data name="Motion sensor" xml:space="preserve">
<value>Motion sensor</value>
</data>
<data name="Progressive Scan (480p)" xml:space="preserve">
<value>Progressive Scan (480p)</value>
</data>
<data name="Pressure/Touch Sensitive" xml:space="preserve">
<value>Pressure/Touch Sensitive</value>
</data>
<data name="PlayStation Network compatible" xml:space="preserve">
<value>PlayStation Network compatible</value>
</data>
<data name="Pedals" xml:space="preserve">
<value>Pedals</value>
</data>
<data name="PC Speaker (Tweaked)" xml:space="preserve">
<value>PC Speaker (Tweaked)</value>
</data>
<data name="Password" xml:space="preserve">
<data name="Password" xml:space="preserve">
<value>Password</value>
</data>
<data name="Online Multiplayer" xml:space="preserve">
<value>Online Multiplayer</value>
</data>
<data name="Null-modem cable" xml:space="preserve">
<value>Null-modem cable</value>
</data>
<data name="Network Adaptor" xml:space="preserve">
<value>Network Adaptor</value>
</data>
<data name="Multiple players required" xml:space="preserve">
<value>Multiple players required</value>
</data>
<data name="Multiplayer" xml:space="preserve">
<value>Multiplayer</value>
</data>
<data name="Multi-player" xml:space="preserve">
<value>Multi-player</value>
</data>
<data name="Multi-Link Cable" xml:space="preserve">
<value>Multi-Link Cable</value>
</data>
<data name="Racing/Steering Wheel" xml:space="preserve">
<value>Racing/Steering Wheel</value>
</data>
<data name="Raster" xml:space="preserve">
<value>Raster</value>
</data>
<data name="Remote Play" xml:space="preserve">
<value>Remote Play</value>
</data>
<data name="Room-Scale" xml:space="preserve">
<value>Room-Scale</value>
</data>
<data name="Same/Split-Screen" xml:space="preserve">
<value>Same/Split-Screen</value>
</data>
<data name="Save Data Cloud" xml:space="preserve">
<value>Save Data Cloud</value>
</data>
<data name="Scoreboard/Leaderboard" xml:space="preserve">
<value>Scoreboard/Leaderboard</value>
</data>
<data name="Sinclair Joystick" xml:space="preserve">
<value>Sinclair Joystick</value>
</data>
<data name="Single-player" xml:space="preserve">
<value>Single-player</value>
</data>
<data name="Single-sided Disk" xml:space="preserve">
<value>Single-sided Disk</value>
</data>
<data name="SNES Mouse" xml:space="preserve">
<value>SNES Mouse</value>
</data>
<data name="Standing" xml:space="preserve">
<value>Standing</value>
</data>
<data name="Steam account required" xml:space="preserve">
<value>Steam account required</value>
</data>
<data name="Tabletop mode compatible" xml:space="preserve">
<value>Tabletop mode compatible</value>
</data>
<data name="Team" xml:space="preserve">
<value>Team</value>
</data>
<data name="Teletype/Teleprinter" xml:space="preserve">
<value>Teletype/Teleprinter</value>
</data>
<data name="Tracked motion controllers" xml:space="preserve">
<value>Tracked motion controllers</value>
</data>
<data name="Trading cards" xml:space="preserve">
<value>Trading cards</value>
</data>
<data name="Trophies" xml:space="preserve">
<value>Trophies</value>
</data>
<data name="TV mode compatible" xml:space="preserve">
<value>TV mode compatible</value>
</data>
<data name="Valve Anti-Cheat System (VAC)" xml:space="preserve">
<value>Valve Anti-Cheat System (VAC)</value>
</data>
<data name="VGA (Tweaked)" xml:space="preserve">
<value>VGA (Tweaked)</value>
</data>
<data name="Vibration" xml:space="preserve">
<value>Vibration</value>
</data>
<data name="XNA Indie Games" xml:space="preserve">
<value>XNA Indie Games</value>
</data>
<data name="Xbox 360 Compatible" xml:space="preserve">
<value>Xbox 360 Compatible</value>
</data>
<data name="Window" xml:space="preserve">
<value>Window</value>
</data>
<data name="Widescreen (16 x 9)" xml:space="preserve">
<value>Widescreen (16 x 9)</value>
</data>
<data name="Wi-Fi Connection" xml:space="preserve">
<value>Wi-Fi Connection</value>
</data>
<data name="VR Helmet/Headset" xml:space="preserve">
<value>VR Helmet/Headset</value>
</data>
<data name="Voice Chat" xml:space="preserve">
<value>Voice Chat</value>
</data>
<data name="Regions" xml:space="preserve">
<data name="Regions" xml:space="preserve">
<value>Regions</value>
</data>
<data name="Suggest description" xml:space="preserve">

View File

@@ -592,13 +592,7 @@
<data name="Ratings" xml:space="preserve">
<value>Clasificaciones</value>
</data>
<data name="Business Model" xml:space="preserve">
<value>Modelo de negocio</value>
</data>
<data name="Media Type" xml:space="preserve">
<value>Tipo de soporte</value>
</data>
<data name="Video Modes" xml:space="preserve">
<data name="Video Modes" xml:space="preserve">
<value>Modos de vídeo</value>
</data>
<data name="Sound" xml:space="preserve">
@@ -607,188 +601,43 @@
<data name="Players" xml:space="preserve">
<value>Jugadores</value>
</data>
<data name="Notes" xml:space="preserve">
<value>Notas</value>
</data>
<data name="Number of Players" xml:space="preserve">
<data name="Number of Players" xml:space="preserve">
<value>Número de jugadores</value>
</data>
<data name="Controller Support" xml:space="preserve">
<value>Soporte de mandos</value>
</data>
<data name="Input Devices Supported" xml:space="preserve">
<value>Dispositivos de entrada soportados</value>
</data>
<data name="Minimum OS Class Required" xml:space="preserve">
<value>Versión mínima de SO requerida</value>
</data>
<data name="Number of Offline Players" xml:space="preserve">
<value>Número de jugadores sin conexión</value>
</data>
<data name="Screen Orientation" xml:space="preserve">
<value>Orientación de pantalla</value>
</data>
<data name="Miscellaneous Attributes" xml:space="preserve">
<value>Atributos varios</value>
</data>
<data name="Apple Rating" xml:space="preserve">
<data name="Apple Rating" xml:space="preserve">
<value>Clasificación de Apple</value>
</data>
<data name="Color Depth" xml:space="preserve">
<value>Profundidad de color</value>
</data>
<data name="ESRB Rating" xml:space="preserve">
<data name="ESRB Rating" xml:space="preserve">
<value>Clasificación ESRB</value>
</data>
<data name="Minimum CPU Class Required" xml:space="preserve">
<value>CPU mínima requerida</value>
</data>
<data name="Minimum RAM Required" xml:space="preserve">
<value>RAM mínima requerida</value>
</data>
<data name="Minimum Video Memory Required" xml:space="preserve">
<value>Memoria de vídeo mínima requerida</value>
</data>
<data name="Multiplayer Game Modes" xml:space="preserve">
<value>Modos de juego multijugador</value>
</data>
<data name="Multiplayer Options" xml:space="preserve">
<value>Opciones multijugador</value>
</data>
<data name="Number of Online Players" xml:space="preserve">
<value>Número de jugadores en línea</value>
</data>
<data name="Number of Players Supported" xml:space="preserve">
<value>Número de jugadores soportados</value>
</data>
<data name="PEGI Rating" xml:space="preserve">
<data name="PEGI Rating" xml:space="preserve">
<value>Clasificación PEGI</value>
</data>
<data name="USK Rating" xml:space="preserve">
<value>Clasificación USK</value>
</data>
<data name="Video Modes Supported" xml:space="preserve">
<value>Modos de vídeo soportados</value>
</data>
<data name="Xbox/Windows Live" xml:space="preserve">
<value>Xbox/Windows Live</value>
</data>
<data name="1 Player" xml:space="preserve">
<value>1 jugador</value>
</data>
<data name="1-2 Players" xml:space="preserve">
<value>1-2 jugadores</value>
</data>
<data name="1-4 Players" xml:space="preserve">
<value>1-4 jugadores</value>
</data>
<data name="16 Players" xml:space="preserve">
<value>16 jugadores</value>
</data>
<data name="256 MB" xml:space="preserve">
<value>256 MB</value>
</data>
<data name="3.5&quot; Floppy Disk" xml:space="preserve">
<value>Disquete de 3,5"</value>
</data>
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<value>4 bits (16 escalas de gris), 8 bits (256 colores)</value>
</data>
<data name="64 MB" xml:space="preserve">
<value>64 MB</value>
</data>
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<value>Logros, Descarga de contenido, Emparejamiento, Multijugador en línea, Tabla de clasificación, Chat de voz</value>
</data>
<data name="Amiga 500/2000" xml:space="preserve">
<value>Amiga 500/2000</value>
</data>
<data name="Android 2.1 (Eclair)" xml:space="preserve">
<value>Android 2.1 (Eclair)</value>
</data>
<data name="Android 4.0.3" xml:space="preserve">
<value>Android 4.0.3</value>
</data>
<data name="Blu-Ray Disc" xml:space="preserve">
<value>Disco Blu-Ray</value>
</data>
<data name="CD-ROM" xml:space="preserve">
<value>CD-ROM</value>
</data>
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<value>Cooperativo, Todos contra todos / Uno contra uno (VS), Equipo</value>
</data>
<data name="Commercial" xml:space="preserve">
<value>Comercial</value>
</data>
<data name="Download" xml:space="preserve">
<value>Descarga</value>
</data>
<data name="Freeware / Free-to-play / Public Domain" xml:space="preserve">
<value>Gratuito / Free-to-play / Dominio público</value>
</data>
<data name="Game Center" xml:space="preserve">
<value>Game Center</value>
</data>
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<value>Game Center, Compra dentro de la aplicación</value>
</data>
<data name="Google Play game services, In-app/game/skill Purchase" xml:space="preserve">
<value>Google Play game services, Compra dentro de la aplicación</value>
</data>
<data name="Hot Seat" xml:space="preserve">
<value>Hot Seat</value>
</data>
<data name="Intel Pentium III" xml:space="preserve">
<value>Intel Pentium III</value>
</data>
<data name="Internet" xml:space="preserve">
<value>Internet</value>
</data>
<data name="iOS 7.0" xml:space="preserve">
<value>iOS 7.0</value>
</data>
<data name="iPhone OS 3.1.2" xml:space="preserve">
<value>iPhone OS 3.1.2</value>
</data>
<data name="iPhone OS 3.2" xml:space="preserve">
<value>iPhone OS 3.2</value>
</data>
<data name="Joystick (Digital)" xml:space="preserve">
<value>Joystick (digital)</value>
</data>
<data name="Landscape" xml:space="preserve">
<value>Horizontal</value>
</data>
<data name="Mac OS X 10.6 (Snow Leopard)" xml:space="preserve">
<value>Mac OS X 10.6 (Snow Leopard)</value>
</data>
<data name="Mouse" xml:space="preserve">
<value>Ratón</value>
</data>
<data name="OCS/ECS" xml:space="preserve">
<value>OCS/ECS</value>
</data>
<data name="Palm OS 3.5" xml:space="preserve">
<value>Palm OS 3.5</value>
</data>
<data name="Portrait" xml:space="preserve">
<value>Vertical</value>
</data>
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<value>Versiones separadas en blanco y negro y en color.</value>
</data>
<data name="Shareware" xml:space="preserve">
<value>Shareware</value>
</data>
<data name="Touch Screen" xml:space="preserve">
<value>Pantalla táctil</value>
</data>
<data name="Windows XP" xml:space="preserve">
<value>Windows XP</value>
</data>
<data name="Search software by genre" xml:space="preserve">
<data name="Search software by genre" xml:space="preserve">
<value>Buscar software por género</value>
<comment>Search software by genre</comment>
</data>
@@ -972,460 +821,10 @@
<data name="Search software by specifications" xml:space="preserve">
<value>Buscar software por especificaciones</value>
</data>
<data name="Additional Hardware Required" xml:space="preserve">
<value>Hardware adicional requerido</value>
</data>
<data name="Additional Hardware Supported" xml:space="preserve">
<value>Hardware adicional compatible</value>
</data>
<data name="Controller Types Supported" xml:space="preserve">
<value>Tipos de mando compatibles</value>
</data>
<data name="Drivers/APIs Supported" xml:space="preserve">
<value>Controladores/API compatibles</value>
</data>
<data name="Gamepads Supported" xml:space="preserve">
<value>Gamepads compatibles</value>
</data>
<data name="Input Device Features Supported" xml:space="preserve">
<value>Funciones de dispositivos de entrada compatibles</value>
</data>
<data name="Input Devices Required" xml:space="preserve">
<value>Dispositivos de entrada requeridos</value>
</data>
<data name="Light Guns / Attachments Supported" xml:space="preserve">
<value>Pistolas ópticas / accesorios compatibles</value>
</data>
<data name="Minimum CD-ROM Drive Speed Required" xml:space="preserve">
<value>Velocidad mínima de unidad CD-ROM requerida</value>
</data>
<data name="Minimum DirectX Version Required" xml:space="preserve">
<value>Versión mínima de DirectX requerida</value>
</data>
<data name="Minimum DVD-ROM Drive Speed Required" xml:space="preserve">
<value>Velocidad mínima de unidad DVD-ROM requerida</value>
</data>
<data name="Minimum Supported System" xml:space="preserve">
<value>Sistema mínimo compatible</value>
</data>
<data name="Mouses Supported" xml:space="preserve">
<value>Ratones compatibles</value>
</data>
<data name="Number of Local Players via System Link / LAN Cable" xml:space="preserve">
<value>Número de jugadores locales mediante System Link / cable LAN</value>
</data>
<data name="Number of Local Players via Wi-Fi" xml:space="preserve">
<value>Número de jugadores locales mediante Wi-Fi</value>
</data>
<data name="Number of Offline Players via Handheld Mode" xml:space="preserve">
<value>Número de jugadores sin conexión en modo portátil</value>
</data>
<data name="Number of Offline Players via Multitap" xml:space="preserve">
<value>Número de jugadores sin conexión mediante Multitap</value>
</data>
<data name="Number of Offline Players via Tabletop Mode" xml:space="preserve">
<value>Número de jugadores sin conexión en modo sobremesa</value>
</data>
<data name="Number of Offline Players via TV Mode" xml:space="preserve">
<value>Número de jugadores sin conexión en modo TV</value>
</data>
<data name="Save Game Methods" xml:space="preserve">
<value>Métodos de guardado de partida</value>
</data>
<data name="Sound Capabilities" xml:space="preserve">
<value>Capacidades de sonido</value>
</data>
<data name="Sound Devices Supported" xml:space="preserve">
<value>Dispositivos de sonido compatibles</value>
</data>
<data name="Supported Languages and Operating Systems" xml:space="preserve">
<value>Idiomas y sistemas operativos compatibles</value>
</data>
<data name="Supported Systems/Models" xml:space="preserve">
<value>Sistemas/modelos compatibles</value>
</data>
<data name="Technology" xml:space="preserve">
<value>Tecnología</value>
</data>
<data name="Text Modes Supported" xml:space="preserve">
<value>Modos de texto compatibles</value>
</data>
<data name="Video Resolutions Supported" xml:space="preserve">
<value>Resoluciones de vídeo compatibles</value>
</data>
<data name="VR Helmets/Headsets Supported" xml:space="preserve">
<value>Cascos/visores de RV compatibles</value>
</data>
<data name="VR Play Area" xml:space="preserve">
<value>Área de juego de RV</value>
</data>
<data name="3D Accelerator" xml:space="preserve">
<value>Acelerador 3D</value>
</data>
<data name="1-8 Players" xml:space="preserve">
<value>1-8 jugadores</value>
</data>
<data name="1-bit (monochrome)" xml:space="preserve">
<value>1 bit (monocromo)</value>
</data>
<data name="16-bit (High Color)" xml:space="preserve">
<value>16 bits (High Color)</value>
</data>
<data name="1 button" xml:space="preserve">
<value>1 botón</value>
</data>
<data name="2-12 Players" xml:space="preserve">
<value>2-12 jugadores</value>
</data>
<data name="2-4 Players" xml:space="preserve">
<value>2-4 jugadores</value>
</data>
<data name="2-8 Players" xml:space="preserve">
<value>2-8 jugadores</value>
</data>
<data name="2-bit (4 colors)" xml:space="preserve">
<value>2 bits (4 colores)</value>
</data>
<data name="256 Colors Required" xml:space="preserve">
<value>Requiere 256 colores</value>
</data>
<data name="2 Players" xml:space="preserve">
<value>2 jugadores</value>
</data>
<data name="5.25&quot; Floppy Disk" xml:space="preserve">
<value>Disquete de 5,25"</value>
</data>
<data name="4 Players" xml:space="preserve">
<value>4 jugadores</value>
</data>
<data name="4-bit (16 greyscales)" xml:space="preserve">
<value>4 bits (16 grises)</value>
</data>
<data name="4-bit (16 colors)" xml:space="preserve">
<value>4 bits (16 colores)</value>
</data>
<data name="32 Players" xml:space="preserve">
<value>32 jugadores</value>
</data>
<data name="3&quot; Floppy Disk" xml:space="preserve">
<value>Disquete de 3"</value>
</data>
<data name="6 Players" xml:space="preserve">
<value>6 jugadores</value>
</data>
<data name="8+ Players" xml:space="preserve">
<value>8+ jugadores</value>
</data>
<data name="8-bit (256 colors)" xml:space="preserve">
<value>8 bits (256 colores)</value>
</data>
<data name="Achievements" xml:space="preserve">
<value>Logros</value>
</data>
<data name="Add-on Content (DLC)" xml:space="preserve">
<value>Contenido adicional (DLC)</value>
</data>
<data name="Ad Hoc" xml:space="preserve">
<value>Ad hoc</value>
</data>
<data name="Analog Controller" xml:space="preserve">
<value>Mando analógico</value>
</data>
<data name="Avatars" xml:space="preserve">
<value>Avatares</value>
</data>
<data name="BASIC Cartridge" xml:space="preserve">
<value>Cartucho BASIC</value>
</data>
<data name="Built in Rumble Feature" xml:space="preserve">
<value>Función de vibración integrada</value>
</data>
<data name="Cartridge" xml:space="preserve">
<value>Cartucho</value>
</data>
<data name="Cassette Tape" xml:space="preserve">
<value>Cinta de casete</value>
</data>
<data name="Comlynx Cable" xml:space="preserve">
<value>Cable Comlynx</value>
</data>
<data name="Color Monitor Support" xml:space="preserve">
<value>Compatibilidad con monitor en color</value>
</data>
<data name="Color Monitor Required" xml:space="preserve">
<value>Requiere monitor en color</value>
</data>
<data name="Color" xml:space="preserve">
<value>Color</value>
</data>
<data name="Co-Op" xml:space="preserve">
<value>Cooperativo</value>
</data>
<data name="Cloud" xml:space="preserve">
<value>Nube</value>
</data>
<data name="CGA Composite (16 colors)" xml:space="preserve">
<value>CGA compuesto (16 colores)</value>
</data>
<data name="Content Download" xml:space="preserve">
<value>Descarga de contenido</value>
</data>
<data name="Cross-Buy" xml:space="preserve">
<value>Cross-Buy</value>
</data>
<data name="Cursor Joystick" xml:space="preserve">
<value>Joystick Cursor</value>
</data>
<data name="Custom Soundtracks" xml:space="preserve">
<value>Bandas sonoras personalizadas</value>
</data>
<data name="Definable Keys" xml:space="preserve">
<value>Teclas configurables</value>
</data>
<data name="Floppy Disk" xml:space="preserve">
<value>Disquete</value>
</data>
<data name="External Storage" xml:space="preserve">
<value>Almacenamiento externo</value>
</data>
<data name="Extended Basic Cartridge" xml:space="preserve">
<value>Cartucho Extended Basic</value>
</data>
<data name="Ethernet Compatible" xml:space="preserve">
<value>Compatible con Ethernet</value>
</data>
<data name="Ethernet Broadband Required" xml:space="preserve">
<value>Requiere banda ancha Ethernet</value>
</data>
<data name="Double-sided Disk" xml:space="preserve">
<value>Disco de doble cara</value>
</data>
<data name="DOS 4.0 to 6.22" xml:space="preserve">
<value>DOS 4.0 a 6.22</value>
</data>
<data name="DLC available" xml:space="preserve">
<value>DLC disponible</value>
</data>
<data name="Free-for-all / One-on-one (VS)" xml:space="preserve">
<value>Todos contra todos / uno contra uno (VS)</value>
</data>
<data name="Friends" xml:space="preserve">
<value>Amigos</value>
</data>
<data name="Full screen" xml:space="preserve">
<value>Pantalla completa</value>
</data>
<data name="GameCube Compatible, Multi-Link Cable" xml:space="preserve">
<value>Compatible con GameCube, cable Multi-Link</value>
</data>
<data name="Gamepad" xml:space="preserve">
<value>Gamepad</value>
</data>
<data name="Game Boy Advance Compatible" xml:space="preserve">
<value>Compatible con Game Boy Advance</value>
</data>
<data name="Game Card" xml:space="preserve">
<value>Tarjeta de juego</value>
</data>
<data name="Game Sharing" xml:space="preserve">
<value>Compartir juego</value>
</data>
<data name="Handheld mode compatible" xml:space="preserve">
<value>Compatible con modo portátil</value>
</data>
<data name="Hard Drive Installable" xml:space="preserve">
<value>Instalable en disco duro</value>
</data>
<data name="Headset Compatible" xml:space="preserve">
<value>Compatible con auriculares</value>
</data>
<data name="In-app/game/skill Purchase" xml:space="preserve">
<value>Compras dentro de la aplicación/juego/skill</value>
</data>
<data name="Keyboard" xml:space="preserve">
<value>Teclado</value>
</data>
<data name="Kempston Mouse" xml:space="preserve">
<value>Ratón Kempston</value>
</data>
<data name="Kempston Joystick" xml:space="preserve">
<value>Joystick Kempston</value>
</data>
<data name="Joystick (Analog)" xml:space="preserve">
<value>Joystick (analógico)</value>
</data>
<data name="Other Input Devices" xml:space="preserve">
<value>Otros dispositivos de entrada</value>
</data>
<data name="Leaderboards" xml:space="preserve">
<value>Tablas de clasificación</value>
</data>
<data name="Light Gun / Attachment" xml:space="preserve">
<value>Pistola óptica / accesorio</value>
</data>
<data name="Link Cable" xml:space="preserve">
<value>Cable Link</value>
</data>
<data name="Lobbies/Matchmaking" xml:space="preserve">
<value>Salas/emparejamiento</value>
</data>
<data name="Magnetic Card" xml:space="preserve">
<value>Tarjeta magnética</value>
</data>
<data name="Matchmaking" xml:space="preserve">
<value>Emparejamiento</value>
</data>
<data name="MCGA 4-color" xml:space="preserve">
<value>MCGA 4 colores</value>
</data>
<data name="Mega Cartridge" xml:space="preserve">
<value>Mega Cartucho</value>
</data>
<data name="Messaging/Friend invite in game" xml:space="preserve">
<value>Mensajería/invitación de amigos en el juego</value>
</data>
<data name="Mii Support" xml:space="preserve">
<value>Compatibilidad con Mii</value>
</data>
<data name="Modem" xml:space="preserve">
<value>Módem</value>
</data>
<data name="Monochrome Monitor Support" xml:space="preserve">
<value>Compatibilidad con monitor monocromo</value>
</data>
<data name="Motion sensor" xml:space="preserve">
<value>Sensor de movimiento</value>
</data>
<data name="Progressive Scan (480p)" xml:space="preserve">
<value>Barrido progresivo (480p)</value>
</data>
<data name="Pressure/Touch Sensitive" xml:space="preserve">
<value>Sensible a la presión/al tacto</value>
</data>
<data name="PlayStation Network compatible" xml:space="preserve">
<value>Compatible con PlayStation Network</value>
</data>
<data name="Pedals" xml:space="preserve">
<value>Pedales</value>
</data>
<data name="PC Speaker (Tweaked)" xml:space="preserve">
<value>PC Speaker (modificado)</value>
</data>
<data name="Password" xml:space="preserve">
<data name="Password" xml:space="preserve">
<value>Contraseña</value>
</data>
<data name="Online Multiplayer" xml:space="preserve">
<value>Multijugador en línea</value>
</data>
<data name="Null-modem cable" xml:space="preserve">
<value>Cable nulo de módem</value>
</data>
<data name="Network Adaptor" xml:space="preserve">
<value>Adaptador de red</value>
</data>
<data name="Multiple players required" xml:space="preserve">
<value>Se requieren varios jugadores</value>
</data>
<data name="Multiplayer" xml:space="preserve">
<value>Multijugador</value>
</data>
<data name="Multi-player" xml:space="preserve">
<value>Multijugador</value>
</data>
<data name="Multi-Link Cable" xml:space="preserve">
<value>Cable Multi-Link</value>
</data>
<data name="Racing/Steering Wheel" xml:space="preserve">
<value>Volante de carreras/dirección</value>
</data>
<data name="Raster" xml:space="preserve">
<value>Ráster</value>
</data>
<data name="Remote Play" xml:space="preserve">
<value>Juego remoto</value>
</data>
<data name="Room-Scale" xml:space="preserve">
<value>Escala de habitación</value>
</data>
<data name="Same/Split-Screen" xml:space="preserve">
<value>Misma pantalla/pantalla dividida</value>
</data>
<data name="Save Data Cloud" xml:space="preserve">
<value>Datos de guardado en la nube</value>
</data>
<data name="Scoreboard/Leaderboard" xml:space="preserve">
<value>Marcador/clasificación</value>
</data>
<data name="Sinclair Joystick" xml:space="preserve">
<value>Joystick Sinclair</value>
</data>
<data name="Single-player" xml:space="preserve">
<value>Un jugador</value>
</data>
<data name="Single-sided Disk" xml:space="preserve">
<value>Disco de una cara</value>
</data>
<data name="SNES Mouse" xml:space="preserve">
<value>Ratón SNES</value>
</data>
<data name="Standing" xml:space="preserve">
<value>De pie</value>
</data>
<data name="Steam account required" xml:space="preserve">
<value>Se requiere cuenta de Steam</value>
</data>
<data name="Tabletop mode compatible" xml:space="preserve">
<value>Compatible con modo sobremesa</value>
</data>
<data name="Team" xml:space="preserve">
<value>Equipo</value>
</data>
<data name="Teletype/Teleprinter" xml:space="preserve">
<value>Teletipo/teleimpresora</value>
</data>
<data name="Tracked motion controllers" xml:space="preserve">
<value>Mandos de movimiento con seguimiento</value>
</data>
<data name="Trading cards" xml:space="preserve">
<value>Cromos coleccionables</value>
</data>
<data name="Trophies" xml:space="preserve">
<value>Trofeos</value>
</data>
<data name="TV mode compatible" xml:space="preserve">
<value>Compatible con modo TV</value>
</data>
<data name="Valve Anti-Cheat System (VAC)" xml:space="preserve">
<value>Sistema antitrampas de Valve (VAC)</value>
</data>
<data name="VGA (Tweaked)" xml:space="preserve">
<value>VGA (modificado)</value>
</data>
<data name="Vibration" xml:space="preserve">
<value>Vibración</value>
</data>
<data name="XNA Indie Games" xml:space="preserve">
<value>Juegos indie XNA</value>
</data>
<data name="Xbox 360 Compatible" xml:space="preserve">
<value>Compatible con Xbox 360</value>
</data>
<data name="Window" xml:space="preserve">
<value>Ventana</value>
</data>
<data name="Widescreen (16 x 9)" xml:space="preserve">
<value>Panorámico (16 x 9)</value>
</data>
<data name="Wi-Fi Connection" xml:space="preserve">
<value>Conexión Wi-Fi</value>
</data>
<data name="VR Helmet/Headset" xml:space="preserve">
<value>Casco/visor de RV</value>
</data>
<data name="Voice Chat" xml:space="preserve">
<value>Chat de voz</value>
</data>
<data name="Regions" xml:space="preserve">
<data name="Regions" xml:space="preserve">
<value>Regiones</value>
</data>
<data name="Suggest description" xml:space="preserve">

View File

@@ -647,13 +647,7 @@
<data name="Ratings" xml:space="preserve">
<value>Classifications</value>
</data>
<data name="Business Model" xml:space="preserve">
<value>Modèle économique</value>
</data>
<data name="Media Type" xml:space="preserve">
<value>Type de support</value>
</data>
<data name="Video Modes" xml:space="preserve">
<data name="Video Modes" xml:space="preserve">
<value>Modes vidéo</value>
</data>
<data name="Sound" xml:space="preserve">
@@ -662,188 +656,43 @@
<data name="Players" xml:space="preserve">
<value>Joueurs</value>
</data>
<data name="Notes" xml:space="preserve">
<value>Notes</value>
</data>
<data name="Number of Players" xml:space="preserve">
<data name="Number of Players" xml:space="preserve">
<value>Nombre de joueurs</value>
</data>
<data name="Controller Support" xml:space="preserve">
<value>Support manette</value>
</data>
<data name="Input Devices Supported" xml:space="preserve">
<value>Périphériques d'entrée supportés</value>
</data>
<data name="Minimum OS Class Required" xml:space="preserve">
<value>Version minimum du SE requise</value>
</data>
<data name="Number of Offline Players" xml:space="preserve">
<value>Nombre de joueurs hors ligne</value>
</data>
<data name="Screen Orientation" xml:space="preserve">
<value>Orientation de l'écran</value>
</data>
<data name="Miscellaneous Attributes" xml:space="preserve">
<value>Attributs divers</value>
</data>
<data name="Apple Rating" xml:space="preserve">
<data name="Apple Rating" xml:space="preserve">
<value>Classification Apple</value>
</data>
<data name="Color Depth" xml:space="preserve">
<value>Profondeur de couleur</value>
</data>
<data name="ESRB Rating" xml:space="preserve">
<data name="ESRB Rating" xml:space="preserve">
<value>Classification ESRB</value>
</data>
<data name="Minimum CPU Class Required" xml:space="preserve">
<value>CPU minimum requise</value>
</data>
<data name="Minimum RAM Required" xml:space="preserve">
<value>RAM minimum requise</value>
</data>
<data name="Minimum Video Memory Required" xml:space="preserve">
<value>Mémoire vidéo minimum requise</value>
</data>
<data name="Multiplayer Game Modes" xml:space="preserve">
<value>Modes de jeu multijoueur</value>
</data>
<data name="Multiplayer Options" xml:space="preserve">
<value>Options multijoueur</value>
</data>
<data name="Number of Online Players" xml:space="preserve">
<value>Nombre de joueurs en ligne</value>
</data>
<data name="Number of Players Supported" xml:space="preserve">
<value>Nombre de joueurs supportés</value>
</data>
<data name="PEGI Rating" xml:space="preserve">
<data name="PEGI Rating" xml:space="preserve">
<value>Classification PEGI</value>
</data>
<data name="USK Rating" xml:space="preserve">
<value>Classification USK</value>
</data>
<data name="Video Modes Supported" xml:space="preserve">
<value>Modes vidéo supportés</value>
</data>
<data name="Xbox/Windows Live" xml:space="preserve">
<value>Xbox/Windows Live</value>
</data>
<data name="1 Player" xml:space="preserve">
<value>1 joueur</value>
</data>
<data name="1-2 Players" xml:space="preserve">
<value>1-2 joueurs</value>
</data>
<data name="1-4 Players" xml:space="preserve">
<value>1-4 joueurs</value>
</data>
<data name="16 Players" xml:space="preserve">
<value>16 joueurs</value>
</data>
<data name="256 MB" xml:space="preserve">
<value>256 Mo</value>
</data>
<data name="3.5&quot; Floppy Disk" xml:space="preserve">
<value>Disquette 3,5"</value>
</data>
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<value>4 bits (16 niveaux de gris), 8 bits (256 couleurs)</value>
</data>
<data name="64 MB" xml:space="preserve">
<value>64 Mo</value>
</data>
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<value>Succès, Téléchargement de contenu, Matchmaking, Multijoueur en ligne, Classement, Chat vocal</value>
</data>
<data name="Amiga 500/2000" xml:space="preserve">
<value>Amiga 500/2000</value>
</data>
<data name="Android 2.1 (Eclair)" xml:space="preserve">
<value>Android 2.1 (Eclair)</value>
</data>
<data name="Android 4.0.3" xml:space="preserve">
<value>Android 4.0.3</value>
</data>
<data name="Blu-Ray Disc" xml:space="preserve">
<value>Disque Blu-Ray</value>
</data>
<data name="CD-ROM" xml:space="preserve">
<value>CD-ROM</value>
</data>
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<value>Coopératif, Chacun pour soi / Un contre un (VS), Équipe</value>
</data>
<data name="Commercial" xml:space="preserve">
<value>Commercial</value>
</data>
<data name="Download" xml:space="preserve">
<value>Téléchargement</value>
</data>
<data name="Freeware / Free-to-play / Public Domain" xml:space="preserve">
<value>Gratuit / Free-to-play / Domaine public</value>
</data>
<data name="Game Center" xml:space="preserve">
<value>Game Center</value>
</data>
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<value>Game Center, Achat intégré</value>
</data>
<data name="Google Play game services, In-app/game/skill Purchase" xml:space="preserve">
<value>Google Play game services, Achat intégré</value>
</data>
<data name="Hot Seat" xml:space="preserve">
<value>Hot Seat</value>
</data>
<data name="Intel Pentium III" xml:space="preserve">
<value>Intel Pentium III</value>
</data>
<data name="Internet" xml:space="preserve">
<value>Internet</value>
</data>
<data name="iOS 7.0" xml:space="preserve">
<value>iOS 7.0</value>
</data>
<data name="iPhone OS 3.1.2" xml:space="preserve">
<value>iPhone OS 3.1.2</value>
</data>
<data name="iPhone OS 3.2" xml:space="preserve">
<value>iPhone OS 3.2</value>
</data>
<data name="Joystick (Digital)" xml:space="preserve">
<value>Joystick (numérique)</value>
</data>
<data name="Landscape" xml:space="preserve">
<value>Paysage</value>
</data>
<data name="Mac OS X 10.6 (Snow Leopard)" xml:space="preserve">
<value>Mac OS X 10.6 (Snow Leopard)</value>
</data>
<data name="Mouse" xml:space="preserve">
<value>Souris</value>
</data>
<data name="OCS/ECS" xml:space="preserve">
<value>OCS/ECS</value>
</data>
<data name="Palm OS 3.5" xml:space="preserve">
<value>Palm OS 3.5</value>
</data>
<data name="Portrait" xml:space="preserve">
<value>Portrait</value>
</data>
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<value>Versions séparées en noir et blanc et en couleur.</value>
</data>
<data name="Shareware" xml:space="preserve">
<value>Shareware</value>
</data>
<data name="Touch Screen" xml:space="preserve">
<value>Écran tactile</value>
</data>
<data name="Windows XP" xml:space="preserve">
<value>Windows XP</value>
</data>
<data name="Search software by genre" xml:space="preserve">
<data name="Search software by genre" xml:space="preserve">
<value>Rechercher un logiciel par genre</value>
<comment>Search software by genre</comment>
</data>
@@ -1030,460 +879,10 @@
<data name="Search software by specifications" xml:space="preserve">
<value>Rechercher des logiciels par spécifications</value>
</data>
<data name="Additional Hardware Required" xml:space="preserve">
<value>Matériel supplémentaire requis</value>
</data>
<data name="Additional Hardware Supported" xml:space="preserve">
<value>Matériel supplémentaire pris en charge</value>
</data>
<data name="Controller Types Supported" xml:space="preserve">
<value>Types de manettes pris en charge</value>
</data>
<data name="Drivers/APIs Supported" xml:space="preserve">
<value>Pilotes/API pris en charge</value>
</data>
<data name="Gamepads Supported" xml:space="preserve">
<value>Manettes prises en charge</value>
</data>
<data name="Input Device Features Supported" xml:space="preserve">
<value>Fonctionnalités des périphériques d'entrée prises en charge</value>
</data>
<data name="Input Devices Required" xml:space="preserve">
<value>Périphériques d'entrée requis</value>
</data>
<data name="Light Guns / Attachments Supported" xml:space="preserve">
<value>Pistolets optiques / accessoires pris en charge</value>
</data>
<data name="Minimum CD-ROM Drive Speed Required" xml:space="preserve">
<value>Vitesse minimale du lecteur CD-ROM requise</value>
</data>
<data name="Minimum DirectX Version Required" xml:space="preserve">
<value>Version minimale de DirectX requise</value>
</data>
<data name="Minimum DVD-ROM Drive Speed Required" xml:space="preserve">
<value>Vitesse minimale du lecteur DVD-ROM requise</value>
</data>
<data name="Minimum Supported System" xml:space="preserve">
<value>Système minimum pris en charge</value>
</data>
<data name="Mouses Supported" xml:space="preserve">
<value>Souris prises en charge</value>
</data>
<data name="Number of Local Players via System Link / LAN Cable" xml:space="preserve">
<value>Nombre de joueurs locaux via System Link / câble LAN</value>
</data>
<data name="Number of Local Players via Wi-Fi" xml:space="preserve">
<value>Nombre de joueurs locaux via Wi-Fi</value>
</data>
<data name="Number of Offline Players via Handheld Mode" xml:space="preserve">
<value>Nombre de joueurs hors ligne en mode portable</value>
</data>
<data name="Number of Offline Players via Multitap" xml:space="preserve">
<value>Nombre de joueurs hors ligne via Multitap</value>
</data>
<data name="Number of Offline Players via Tabletop Mode" xml:space="preserve">
<value>Nombre de joueurs hors ligne en mode table</value>
</data>
<data name="Number of Offline Players via TV Mode" xml:space="preserve">
<value>Nombre de joueurs hors ligne en mode TV</value>
</data>
<data name="Save Game Methods" xml:space="preserve">
<value>Méthodes de sauvegarde</value>
</data>
<data name="Sound Capabilities" xml:space="preserve">
<value>Capacités sonores</value>
</data>
<data name="Sound Devices Supported" xml:space="preserve">
<value>Périphériques audio pris en charge</value>
</data>
<data name="Supported Languages and Operating Systems" xml:space="preserve">
<value>Langues et systèmes d'exploitation pris en charge</value>
</data>
<data name="Supported Systems/Models" xml:space="preserve">
<value>Systèmes/modèles pris en charge</value>
</data>
<data name="Technology" xml:space="preserve">
<value>Technologie</value>
</data>
<data name="Text Modes Supported" xml:space="preserve">
<value>Modes texte pris en charge</value>
</data>
<data name="Video Resolutions Supported" xml:space="preserve">
<value>Résolutions vidéo prises en charge</value>
</data>
<data name="VR Helmets/Headsets Supported" xml:space="preserve">
<value>Casques de RV pris en charge</value>
</data>
<data name="VR Play Area" xml:space="preserve">
<value>Zone de jeu VR</value>
</data>
<data name="3D Accelerator" xml:space="preserve">
<value>Accélérateur 3D</value>
</data>
<data name="1-8 Players" xml:space="preserve">
<value>1-8 joueurs</value>
</data>
<data name="1-bit (monochrome)" xml:space="preserve">
<value>1 bit (monochrome)</value>
</data>
<data name="16-bit (High Color)" xml:space="preserve">
<value>16 bits (High Color)</value>
</data>
<data name="1 button" xml:space="preserve">
<value>1 bouton</value>
</data>
<data name="2-12 Players" xml:space="preserve">
<value>2-12 joueurs</value>
</data>
<data name="2-4 Players" xml:space="preserve">
<value>2-4 joueurs</value>
</data>
<data name="2-8 Players" xml:space="preserve">
<value>2-8 joueurs</value>
</data>
<data name="2-bit (4 colors)" xml:space="preserve">
<value>2 bits (4 couleurs)</value>
</data>
<data name="256 Colors Required" xml:space="preserve">
<value>256 couleurs requises</value>
</data>
<data name="2 Players" xml:space="preserve">
<value>2 joueurs</value>
</data>
<data name="5.25&quot; Floppy Disk" xml:space="preserve">
<value>Disquette 5,25"</value>
</data>
<data name="4 Players" xml:space="preserve">
<value>4 joueurs</value>
</data>
<data name="4-bit (16 greyscales)" xml:space="preserve">
<value>4 bits (16 niveaux de gris)</value>
</data>
<data name="4-bit (16 colors)" xml:space="preserve">
<value>4 bits (16 couleurs)</value>
</data>
<data name="32 Players" xml:space="preserve">
<value>32 joueurs</value>
</data>
<data name="3&quot; Floppy Disk" xml:space="preserve">
<value>Disquette 3"</value>
</data>
<data name="6 Players" xml:space="preserve">
<value>6 joueurs</value>
</data>
<data name="8+ Players" xml:space="preserve">
<value>8+ joueurs</value>
</data>
<data name="8-bit (256 colors)" xml:space="preserve">
<value>8 bits (256 couleurs)</value>
</data>
<data name="Achievements" xml:space="preserve">
<value>Succès</value>
</data>
<data name="Add-on Content (DLC)" xml:space="preserve">
<value>Contenu additionnel (DLC)</value>
</data>
<data name="Ad Hoc" xml:space="preserve">
<value>Ad hoc</value>
</data>
<data name="Analog Controller" xml:space="preserve">
<value>Manette analogique</value>
</data>
<data name="Avatars" xml:space="preserve">
<value>Avatars</value>
</data>
<data name="BASIC Cartridge" xml:space="preserve">
<value>Cartouche BASIC</value>
</data>
<data name="Built in Rumble Feature" xml:space="preserve">
<value>Vibration intégrée</value>
</data>
<data name="Cartridge" xml:space="preserve">
<value>Cartouche</value>
</data>
<data name="Cassette Tape" xml:space="preserve">
<value>Cassette</value>
</data>
<data name="Comlynx Cable" xml:space="preserve">
<value>Câble Comlynx</value>
</data>
<data name="Color Monitor Support" xml:space="preserve">
<value>Prise en charge d'un moniteur couleur</value>
</data>
<data name="Color Monitor Required" xml:space="preserve">
<value>Moniteur couleur requis</value>
</data>
<data name="Color" xml:space="preserve">
<value>Couleur</value>
</data>
<data name="Co-Op" xml:space="preserve">
<value>Coopératif</value>
</data>
<data name="Cloud" xml:space="preserve">
<value>Cloud</value>
</data>
<data name="CGA Composite (16 colors)" xml:space="preserve">
<value>CGA composite (16 couleurs)</value>
</data>
<data name="Content Download" xml:space="preserve">
<value>Téléchargement de contenu</value>
</data>
<data name="Cross-Buy" xml:space="preserve">
<value>Cross-Buy</value>
</data>
<data name="Cursor Joystick" xml:space="preserve">
<value>Joystick Cursor</value>
</data>
<data name="Custom Soundtracks" xml:space="preserve">
<value>Bandes-son personnalisées</value>
</data>
<data name="Definable Keys" xml:space="preserve">
<value>Touches configurables</value>
</data>
<data name="Floppy Disk" xml:space="preserve">
<value>Disquette</value>
</data>
<data name="External Storage" xml:space="preserve">
<value>Stockage externe</value>
</data>
<data name="Extended Basic Cartridge" xml:space="preserve">
<value>Cartouche Extended Basic</value>
</data>
<data name="Ethernet Compatible" xml:space="preserve">
<value>Compatible Ethernet</value>
</data>
<data name="Ethernet Broadband Required" xml:space="preserve">
<value>Haut débit Ethernet requis</value>
</data>
<data name="Double-sided Disk" xml:space="preserve">
<value>Disquette double face</value>
</data>
<data name="DOS 4.0 to 6.22" xml:space="preserve">
<value>DOS 4.0 à 6.22</value>
</data>
<data name="DLC available" xml:space="preserve">
<value>DLC disponible</value>
</data>
<data name="Free-for-all / One-on-one (VS)" xml:space="preserve">
<value>Chacun pour soi / un contre un (VS)</value>
</data>
<data name="Friends" xml:space="preserve">
<value>Amis</value>
</data>
<data name="Full screen" xml:space="preserve">
<value>Plein écran</value>
</data>
<data name="GameCube Compatible, Multi-Link Cable" xml:space="preserve">
<value>Compatible GameCube, câble Multi-Link</value>
</data>
<data name="Gamepad" xml:space="preserve">
<value>Manette</value>
</data>
<data name="Game Boy Advance Compatible" xml:space="preserve">
<value>Compatible Game Boy Advance</value>
</data>
<data name="Game Card" xml:space="preserve">
<value>Carte de jeu</value>
</data>
<data name="Game Sharing" xml:space="preserve">
<value>Partage de jeu</value>
</data>
<data name="Handheld mode compatible" xml:space="preserve">
<value>Compatible avec le mode portable</value>
</data>
<data name="Hard Drive Installable" xml:space="preserve">
<value>Installable sur disque dur</value>
</data>
<data name="Headset Compatible" xml:space="preserve">
<value>Compatible avec casque</value>
</data>
<data name="In-app/game/skill Purchase" xml:space="preserve">
<value>Achat in-app/jeu/skill</value>
</data>
<data name="Keyboard" xml:space="preserve">
<value>Clavier</value>
</data>
<data name="Kempston Mouse" xml:space="preserve">
<value>Souris Kempston</value>
</data>
<data name="Kempston Joystick" xml:space="preserve">
<value>Joystick Kempston</value>
</data>
<data name="Joystick (Analog)" xml:space="preserve">
<value>Joystick (analogique)</value>
</data>
<data name="Other Input Devices" xml:space="preserve">
<value>Autres périphériques d'entrée</value>
</data>
<data name="Leaderboards" xml:space="preserve">
<value>Classements</value>
</data>
<data name="Light Gun / Attachment" xml:space="preserve">
<value>Pistolet optique / accessoire</value>
</data>
<data name="Link Cable" xml:space="preserve">
<value>Câble Link</value>
</data>
<data name="Lobbies/Matchmaking" xml:space="preserve">
<value>Salons/matchmaking</value>
</data>
<data name="Magnetic Card" xml:space="preserve">
<value>Carte magnétique</value>
</data>
<data name="Matchmaking" xml:space="preserve">
<value>Matchmaking</value>
</data>
<data name="MCGA 4-color" xml:space="preserve">
<value>MCGA 4 couleurs</value>
</data>
<data name="Mega Cartridge" xml:space="preserve">
<value>Mega Cartouche</value>
</data>
<data name="Messaging/Friend invite in game" xml:space="preserve">
<value>Messagerie/invitation d'amis en jeu</value>
</data>
<data name="Mii Support" xml:space="preserve">
<value>Prise en charge des Mii</value>
</data>
<data name="Modem" xml:space="preserve">
<value>Modem</value>
</data>
<data name="Monochrome Monitor Support" xml:space="preserve">
<value>Prise en charge d'un moniteur monochrome</value>
</data>
<data name="Motion sensor" xml:space="preserve">
<value>Capteur de mouvement</value>
</data>
<data name="Progressive Scan (480p)" xml:space="preserve">
<value>Balayage progressif (480p)</value>
</data>
<data name="Pressure/Touch Sensitive" xml:space="preserve">
<value>Sensible à la pression/au toucher</value>
</data>
<data name="PlayStation Network compatible" xml:space="preserve">
<value>Compatible PlayStation Network</value>
</data>
<data name="Pedals" xml:space="preserve">
<value>Pédales</value>
</data>
<data name="PC Speaker (Tweaked)" xml:space="preserve">
<value>PC Speaker (modifié)</value>
</data>
<data name="Password" xml:space="preserve">
<data name="Password" xml:space="preserve">
<value>Mot de passe</value>
</data>
<data name="Online Multiplayer" xml:space="preserve">
<value>Multijoueur en ligne</value>
</data>
<data name="Null-modem cable" xml:space="preserve">
<value>Câble null-modem</value>
</data>
<data name="Network Adaptor" xml:space="preserve">
<value>Adaptateur réseau</value>
</data>
<data name="Multiple players required" xml:space="preserve">
<value>Plusieurs joueurs requis</value>
</data>
<data name="Multiplayer" xml:space="preserve">
<value>Multijoueur</value>
</data>
<data name="Multi-player" xml:space="preserve">
<value>Multijoueur</value>
</data>
<data name="Multi-Link Cable" xml:space="preserve">
<value>Câble Multi-Link</value>
</data>
<data name="Racing/Steering Wheel" xml:space="preserve">
<value>Volant de course/direction</value>
</data>
<data name="Raster" xml:space="preserve">
<value>Raster</value>
</data>
<data name="Remote Play" xml:space="preserve">
<value>Jeu à distance</value>
</data>
<data name="Room-Scale" xml:space="preserve">
<value>Échelle pièce</value>
</data>
<data name="Same/Split-Screen" xml:space="preserve">
<value>Même écran/écran partagé</value>
</data>
<data name="Save Data Cloud" xml:space="preserve">
<value>Données de sauvegarde dans le cloud</value>
</data>
<data name="Scoreboard/Leaderboard" xml:space="preserve">
<value>Tableau des scores/classement</value>
</data>
<data name="Sinclair Joystick" xml:space="preserve">
<value>Joystick Sinclair</value>
</data>
<data name="Single-player" xml:space="preserve">
<value>Solo</value>
</data>
<data name="Single-sided Disk" xml:space="preserve">
<value>Disquette simple face</value>
</data>
<data name="SNES Mouse" xml:space="preserve">
<value>Souris SNES</value>
</data>
<data name="Standing" xml:space="preserve">
<value>Debout</value>
</data>
<data name="Steam account required" xml:space="preserve">
<value>Compte Steam requis</value>
</data>
<data name="Tabletop mode compatible" xml:space="preserve">
<value>Compatible avec le mode table</value>
</data>
<data name="Team" xml:space="preserve">
<value>Équipe</value>
</data>
<data name="Teletype/Teleprinter" xml:space="preserve">
<value>Téléscripteur</value>
</data>
<data name="Tracked motion controllers" xml:space="preserve">
<value>Manettes de mouvement suivies</value>
</data>
<data name="Trading cards" xml:space="preserve">
<value>Cartes à collectionner</value>
</data>
<data name="Trophies" xml:space="preserve">
<value>Trophées</value>
</data>
<data name="TV mode compatible" xml:space="preserve">
<value>Compatible avec le mode TV</value>
</data>
<data name="Valve Anti-Cheat System (VAC)" xml:space="preserve">
<value>Système anti-triche Valve (VAC)</value>
</data>
<data name="VGA (Tweaked)" xml:space="preserve">
<value>VGA (modifié)</value>
</data>
<data name="Vibration" xml:space="preserve">
<value>Vibration</value>
</data>
<data name="XNA Indie Games" xml:space="preserve">
<value>Jeux indé XNA</value>
</data>
<data name="Xbox 360 Compatible" xml:space="preserve">
<value>Compatible Xbox 360</value>
</data>
<data name="Window" xml:space="preserve">
<value>Fenêtre</value>
</data>
<data name="Widescreen (16 x 9)" xml:space="preserve">
<value>Écran large (16 x 9)</value>
</data>
<data name="Wi-Fi Connection" xml:space="preserve">
<value>Connexion Wi-Fi</value>
</data>
<data name="VR Helmet/Headset" xml:space="preserve">
<value>Casque de RV</value>
</data>
<data name="Voice Chat" xml:space="preserve">
<value>Chat vocal</value>
</data>
<data name="Regions" xml:space="preserve">
<data name="Regions" xml:space="preserve">
<value>Régions</value>
</data>
<data name="Suggest description" xml:space="preserve">

View File

@@ -559,13 +559,7 @@
<data name="Ratings" xml:space="preserve">
<value>Classificazioni</value>
</data>
<data name="Business Model" xml:space="preserve">
<value>Modello di business</value>
</data>
<data name="Media Type" xml:space="preserve">
<value>Tipo di supporto</value>
</data>
<data name="Video Modes" xml:space="preserve">
<data name="Video Modes" xml:space="preserve">
<value>Modalità video</value>
</data>
<data name="Sound" xml:space="preserve">
@@ -574,188 +568,43 @@
<data name="Players" xml:space="preserve">
<value>Giocatori</value>
</data>
<data name="Notes" xml:space="preserve">
<value>Note</value>
</data>
<data name="Number of Players" xml:space="preserve">
<data name="Number of Players" xml:space="preserve">
<value>Numero di giocatori</value>
</data>
<data name="Controller Support" xml:space="preserve">
<value>Supporto controller</value>
</data>
<data name="Input Devices Supported" xml:space="preserve">
<value>Dispositivi di input supportati</value>
</data>
<data name="Minimum OS Class Required" xml:space="preserve">
<value>Versione minima del SO richiesta</value>
</data>
<data name="Number of Offline Players" xml:space="preserve">
<value>Numero di giocatori offline</value>
</data>
<data name="Screen Orientation" xml:space="preserve">
<value>Orientamento dello schermo</value>
</data>
<data name="Miscellaneous Attributes" xml:space="preserve">
<value>Attributi vari</value>
</data>
<data name="Apple Rating" xml:space="preserve">
<data name="Apple Rating" xml:space="preserve">
<value>Classificazione Apple</value>
</data>
<data name="Color Depth" xml:space="preserve">
<value>Profondità di colore</value>
</data>
<data name="ESRB Rating" xml:space="preserve">
<data name="ESRB Rating" xml:space="preserve">
<value>Classificazione ESRB</value>
</data>
<data name="Minimum CPU Class Required" xml:space="preserve">
<value>CPU minima richiesta</value>
</data>
<data name="Minimum RAM Required" xml:space="preserve">
<value>RAM minima richiesta</value>
</data>
<data name="Minimum Video Memory Required" xml:space="preserve">
<value>Memoria video minima richiesta</value>
</data>
<data name="Multiplayer Game Modes" xml:space="preserve">
<value>Modalità di gioco multigiocatore</value>
</data>
<data name="Multiplayer Options" xml:space="preserve">
<value>Opzioni multigiocatore</value>
</data>
<data name="Number of Online Players" xml:space="preserve">
<value>Numero di giocatori online</value>
</data>
<data name="Number of Players Supported" xml:space="preserve">
<value>Numero di giocatori supportati</value>
</data>
<data name="PEGI Rating" xml:space="preserve">
<data name="PEGI Rating" xml:space="preserve">
<value>Classificazione PEGI</value>
</data>
<data name="USK Rating" xml:space="preserve">
<value>Classificazione USK</value>
</data>
<data name="Video Modes Supported" xml:space="preserve">
<value>Modalità video supportate</value>
</data>
<data name="Xbox/Windows Live" xml:space="preserve">
<value>Xbox/Windows Live</value>
</data>
<data name="1 Player" xml:space="preserve">
<value>1 giocatore</value>
</data>
<data name="1-2 Players" xml:space="preserve">
<value>1-2 giocatori</value>
</data>
<data name="1-4 Players" xml:space="preserve">
<value>1-4 giocatori</value>
</data>
<data name="16 Players" xml:space="preserve">
<value>16 giocatori</value>
</data>
<data name="256 MB" xml:space="preserve">
<value>256 MB</value>
</data>
<data name="3.5&quot; Floppy Disk" xml:space="preserve">
<value>Floppy disk da 3,5"</value>
</data>
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<data name="4-bit (16 greyscales), 8-bit (256 colors)" xml:space="preserve">
<value>4 bit (16 scale di grigi), 8 bit (256 colori)</value>
</data>
<data name="64 MB" xml:space="preserve">
<value>64 MB</value>
</data>
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<data name="Achievements, Content Download, Matchmaking, Online Multiplayer, Scoreboard/Leaderboard, Voice Chat" xml:space="preserve">
<value>Obiettivi, Download di contenuti, Matchmaking, Multigiocatore online, Classifica, Chat vocale</value>
</data>
<data name="Amiga 500/2000" xml:space="preserve">
<value>Amiga 500/2000</value>
</data>
<data name="Android 2.1 (Eclair)" xml:space="preserve">
<value>Android 2.1 (Eclair)</value>
</data>
<data name="Android 4.0.3" xml:space="preserve">
<value>Android 4.0.3</value>
</data>
<data name="Blu-Ray Disc" xml:space="preserve">
<value>Disco Blu-Ray</value>
</data>
<data name="CD-ROM" xml:space="preserve">
<value>CD-ROM</value>
</data>
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<data name="Co-Op, Free-for-all / One-on-one (VS), Team" xml:space="preserve">
<value>Cooperativa, Tutti contro tutti / Uno contro uno (VS), Squadra</value>
</data>
<data name="Commercial" xml:space="preserve">
<value>Commerciale</value>
</data>
<data name="Download" xml:space="preserve">
<value>Download</value>
</data>
<data name="Freeware / Free-to-play / Public Domain" xml:space="preserve">
<value>Gratuito / Free-to-play / Pubblico dominio</value>
</data>
<data name="Game Center" xml:space="preserve">
<value>Game Center</value>
</data>
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<data name="Game Center, In-app/game/skill Purchase" xml:space="preserve">
<value>Game Center, Acquisto in-app</value>
</data>
<data name="Google Play game services, In-app/game/skill Purchase" xml:space="preserve">
<value>Google Play game services, Acquisto in-app</value>
</data>
<data name="Hot Seat" xml:space="preserve">
<value>Hot Seat</value>
</data>
<data name="Intel Pentium III" xml:space="preserve">
<value>Intel Pentium III</value>
</data>
<data name="Internet" xml:space="preserve">
<value>Internet</value>
</data>
<data name="iOS 7.0" xml:space="preserve">
<value>iOS 7.0</value>
</data>
<data name="iPhone OS 3.1.2" xml:space="preserve">
<value>iPhone OS 3.1.2</value>
</data>
<data name="iPhone OS 3.2" xml:space="preserve">
<value>iPhone OS 3.2</value>
</data>
<data name="Joystick (Digital)" xml:space="preserve">
<value>Joystick (digitale)</value>
</data>
<data name="Landscape" xml:space="preserve">
<value>Orizzontale</value>
</data>
<data name="Mac OS X 10.6 (Snow Leopard)" xml:space="preserve">
<value>Mac OS X 10.6 (Snow Leopard)</value>
</data>
<data name="Mouse" xml:space="preserve">
<value>Mouse</value>
</data>
<data name="OCS/ECS" xml:space="preserve">
<value>OCS/ECS</value>
</data>
<data name="Palm OS 3.5" xml:space="preserve">
<value>Palm OS 3.5</value>
</data>
<data name="Portrait" xml:space="preserve">
<value>Verticale</value>
</data>
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<data name="Separate black &amp; white and colour versions." xml:space="preserve">
<value>Versioni separate in bianco e nero e a colori.</value>
</data>
<data name="Shareware" xml:space="preserve">
<value>Shareware</value>
</data>
<data name="Touch Screen" xml:space="preserve">
<value>Schermo tattile</value>
</data>
<data name="Windows XP" xml:space="preserve">
<value>Windows XP</value>
</data>
<data name="Search software by genre" xml:space="preserve">
<data name="Search software by genre" xml:space="preserve">
<value>Cerca software per genere</value>
<comment>Search software by genre</comment>
</data>
@@ -942,460 +791,10 @@
<data name="Search software by specifications" xml:space="preserve">
<value>Cerca software per specifiche</value>
</data>
<data name="Additional Hardware Required" xml:space="preserve">
<value>Hardware aggiuntivo richiesto</value>
</data>
<data name="Additional Hardware Supported" xml:space="preserve">
<value>Hardware aggiuntivo supportato</value>
</data>
<data name="Controller Types Supported" xml:space="preserve">
<value>Tipi di controller supportati</value>
</data>
<data name="Drivers/APIs Supported" xml:space="preserve">
<value>Driver/API supportati</value>
</data>
<data name="Gamepads Supported" xml:space="preserve">
<value>Gamepad supportati</value>
</data>
<data name="Input Device Features Supported" xml:space="preserve">
<value>Funzionalità dei dispositivi di input supportate</value>
</data>
<data name="Input Devices Required" xml:space="preserve">
<value>Dispositivi di input richiesti</value>
</data>
<data name="Light Guns / Attachments Supported" xml:space="preserve">
<value>Pistole ottiche / accessori supportati</value>
</data>
<data name="Minimum CD-ROM Drive Speed Required" xml:space="preserve">
<value>Velocità minima dell'unità CD-ROM richiesta</value>
</data>
<data name="Minimum DirectX Version Required" xml:space="preserve">
<value>Versione minima di DirectX richiesta</value>
</data>
<data name="Minimum DVD-ROM Drive Speed Required" xml:space="preserve">
<value>Velocità minima dell'unità DVD-ROM richiesta</value>
</data>
<data name="Minimum Supported System" xml:space="preserve">
<value>Sistema minimo supportato</value>
</data>
<data name="Mouses Supported" xml:space="preserve">
<value>Mouse supportati</value>
</data>
<data name="Number of Local Players via System Link / LAN Cable" xml:space="preserve">
<value>Numero di giocatori locali tramite System Link / cavo LAN</value>
</data>
<data name="Number of Local Players via Wi-Fi" xml:space="preserve">
<value>Numero di giocatori locali tramite Wi-Fi</value>
</data>
<data name="Number of Offline Players via Handheld Mode" xml:space="preserve">
<value>Numero di giocatori offline in modalità portatile</value>
</data>
<data name="Number of Offline Players via Multitap" xml:space="preserve">
<value>Numero di giocatori offline tramite Multitap</value>
</data>
<data name="Number of Offline Players via Tabletop Mode" xml:space="preserve">
<value>Numero di giocatori offline in modalità tavolo</value>
</data>
<data name="Number of Offline Players via TV Mode" xml:space="preserve">
<value>Numero di giocatori offline in modalità TV</value>
</data>
<data name="Save Game Methods" xml:space="preserve">
<value>Metodi di salvataggio</value>
</data>
<data name="Sound Capabilities" xml:space="preserve">
<value>Capacità sonore</value>
</data>
<data name="Sound Devices Supported" xml:space="preserve">
<value>Dispositivi audio supportati</value>
</data>
<data name="Supported Languages and Operating Systems" xml:space="preserve">
<value>Lingue e sistemi operativi supportati</value>
</data>
<data name="Supported Systems/Models" xml:space="preserve">
<value>Sistemi/modelli supportati</value>
</data>
<data name="Technology" xml:space="preserve">
<value>Tecnologia</value>
</data>
<data name="Text Modes Supported" xml:space="preserve">
<value>Modalità testo supportate</value>
</data>
<data name="Video Resolutions Supported" xml:space="preserve">
<value>Risoluzioni video supportate</value>
</data>
<data name="VR Helmets/Headsets Supported" xml:space="preserve">
<value>Visori VR supportati</value>
</data>
<data name="VR Play Area" xml:space="preserve">
<value>Area di gioco VR</value>
</data>
<data name="3D Accelerator" xml:space="preserve">
<value>Acceleratore 3D</value>
</data>
<data name="1-8 Players" xml:space="preserve">
<value>1-8 giocatori</value>
</data>
<data name="1-bit (monochrome)" xml:space="preserve">
<value>1 bit (monocromatico)</value>
</data>
<data name="16-bit (High Color)" xml:space="preserve">
<value>16 bit (High Color)</value>
</data>
<data name="1 button" xml:space="preserve">
<value>1 pulsante</value>
</data>
<data name="2-12 Players" xml:space="preserve">
<value>2-12 giocatori</value>
</data>
<data name="2-4 Players" xml:space="preserve">
<value>2-4 giocatori</value>
</data>
<data name="2-8 Players" xml:space="preserve">
<value>2-8 giocatori</value>
</data>
<data name="2-bit (4 colors)" xml:space="preserve">
<value>2 bit (4 colori)</value>
</data>
<data name="256 Colors Required" xml:space="preserve">
<value>256 colori richiesti</value>
</data>
<data name="2 Players" xml:space="preserve">
<value>2 giocatori</value>
</data>
<data name="5.25&quot; Floppy Disk" xml:space="preserve">
<value>Floppy disk da 5,25"</value>
</data>
<data name="4 Players" xml:space="preserve">
<value>4 giocatori</value>
</data>
<data name="4-bit (16 greyscales)" xml:space="preserve">
<value>4 bit (16 toni di grigio)</value>
</data>
<data name="4-bit (16 colors)" xml:space="preserve">
<value>4 bit (16 colori)</value>
</data>
<data name="32 Players" xml:space="preserve">
<value>32 giocatori</value>
</data>
<data name="3&quot; Floppy Disk" xml:space="preserve">
<value>Floppy disk da 3"</value>
</data>
<data name="6 Players" xml:space="preserve">
<value>6 giocatori</value>
</data>
<data name="8+ Players" xml:space="preserve">
<value>8+ giocatori</value>
</data>
<data name="8-bit (256 colors)" xml:space="preserve">
<value>8 bit (256 colori)</value>
</data>
<data name="Achievements" xml:space="preserve">
<value>Obiettivi</value>
</data>
<data name="Add-on Content (DLC)" xml:space="preserve">
<value>Contenuto aggiuntivo (DLC)</value>
</data>
<data name="Ad Hoc" xml:space="preserve">
<value>Ad hoc</value>
</data>
<data name="Analog Controller" xml:space="preserve">
<value>Controller analogico</value>
</data>
<data name="Avatars" xml:space="preserve">
<value>Avatar</value>
</data>
<data name="BASIC Cartridge" xml:space="preserve">
<value>Cartuccia BASIC</value>
</data>
<data name="Built in Rumble Feature" xml:space="preserve">
<value>Funzione di vibrazione integrata</value>
</data>
<data name="Cartridge" xml:space="preserve">
<value>Cartuccia</value>
</data>
<data name="Cassette Tape" xml:space="preserve">
<value>Cassetta</value>
</data>
<data name="Comlynx Cable" xml:space="preserve">
<value>Cavo Comlynx</value>
</data>
<data name="Color Monitor Support" xml:space="preserve">
<value>Supporto monitor a colori</value>
</data>
<data name="Color Monitor Required" xml:space="preserve">
<value>Monitor a colori richiesto</value>
</data>
<data name="Color" xml:space="preserve">
<value>Colore</value>
</data>
<data name="Co-Op" xml:space="preserve">
<value>Cooperativo</value>
</data>
<data name="Cloud" xml:space="preserve">
<value>Cloud</value>
</data>
<data name="CGA Composite (16 colors)" xml:space="preserve">
<value>CGA composito (16 colori)</value>
</data>
<data name="Content Download" xml:space="preserve">
<value>Download di contenuti</value>
</data>
<data name="Cross-Buy" xml:space="preserve">
<value>Cross-Buy</value>
</data>
<data name="Cursor Joystick" xml:space="preserve">
<value>Joystick Cursor</value>
</data>
<data name="Custom Soundtracks" xml:space="preserve">
<value>Colonne sonore personalizzate</value>
</data>
<data name="Definable Keys" xml:space="preserve">
<value>Tasti configurabili</value>
</data>
<data name="Floppy Disk" xml:space="preserve">
<value>Floppy disk</value>
</data>
<data name="External Storage" xml:space="preserve">
<value>Memoria esterna</value>
</data>
<data name="Extended Basic Cartridge" xml:space="preserve">
<value>Cartuccia Extended Basic</value>
</data>
<data name="Ethernet Compatible" xml:space="preserve">
<value>Compatibile Ethernet</value>
</data>
<data name="Ethernet Broadband Required" xml:space="preserve">
<value>Banda larga Ethernet richiesta</value>
</data>
<data name="Double-sided Disk" xml:space="preserve">
<value>Disco a doppia faccia</value>
</data>
<data name="DOS 4.0 to 6.22" xml:space="preserve">
<value>DOS da 4.0 a 6.22</value>
</data>
<data name="DLC available" xml:space="preserve">
<value>DLC disponibili</value>
</data>
<data name="Free-for-all / One-on-one (VS)" xml:space="preserve">
<value>Tutti contro tutti / uno contro uno (VS)</value>
</data>
<data name="Friends" xml:space="preserve">
<value>Amici</value>
</data>
<data name="Full screen" xml:space="preserve">
<value>Schermo intero</value>
</data>
<data name="GameCube Compatible, Multi-Link Cable" xml:space="preserve">
<value>Compatibile GameCube, cavo Multi-Link</value>
</data>
<data name="Gamepad" xml:space="preserve">
<value>Gamepad</value>
</data>
<data name="Game Boy Advance Compatible" xml:space="preserve">
<value>Compatibile Game Boy Advance</value>
</data>
<data name="Game Card" xml:space="preserve">
<value>Scheda di gioco</value>
</data>
<data name="Game Sharing" xml:space="preserve">
<value>Condivisione di gioco</value>
</data>
<data name="Handheld mode compatible" xml:space="preserve">
<value>Compatibile con modalità portatile</value>
</data>
<data name="Hard Drive Installable" xml:space="preserve">
<value>Installabile su disco rigido</value>
</data>
<data name="Headset Compatible" xml:space="preserve">
<value>Compatibile con cuffie</value>
</data>
<data name="In-app/game/skill Purchase" xml:space="preserve">
<value>Acquisto in-app/gioco/skill</value>
</data>
<data name="Keyboard" xml:space="preserve">
<value>Tastiera</value>
</data>
<data name="Kempston Mouse" xml:space="preserve">
<value>Mouse Kempston</value>
</data>
<data name="Kempston Joystick" xml:space="preserve">
<value>Joystick Kempston</value>
</data>
<data name="Joystick (Analog)" xml:space="preserve">
<value>Joystick (analogico)</value>
</data>
<data name="Other Input Devices" xml:space="preserve">
<value>Altri dispositivi di input</value>
</data>
<data name="Leaderboards" xml:space="preserve">
<value>Classifiche</value>
</data>
<data name="Light Gun / Attachment" xml:space="preserve">
<value>Pistola ottica / accessorio</value>
</data>
<data name="Link Cable" xml:space="preserve">
<value>Cavo Link</value>
</data>
<data name="Lobbies/Matchmaking" xml:space="preserve">
<value>Lobby/matchmaking</value>
</data>
<data name="Magnetic Card" xml:space="preserve">
<value>Scheda magnetica</value>
</data>
<data name="Matchmaking" xml:space="preserve">
<value>Matchmaking</value>
</data>
<data name="MCGA 4-color" xml:space="preserve">
<value>MCGA 4 colori</value>
</data>
<data name="Mega Cartridge" xml:space="preserve">
<value>Mega Cartuccia</value>
</data>
<data name="Messaging/Friend invite in game" xml:space="preserve">
<value>Messaggistica/invito amici in gioco</value>
</data>
<data name="Mii Support" xml:space="preserve">
<value>Supporto Mii</value>
</data>
<data name="Modem" xml:space="preserve">
<value>Modem</value>
</data>
<data name="Monochrome Monitor Support" xml:space="preserve">
<value>Supporto monitor monocromatico</value>
</data>
<data name="Motion sensor" xml:space="preserve">
<value>Sensore di movimento</value>
</data>
<data name="Progressive Scan (480p)" xml:space="preserve">
<value>Scansione progressiva (480p)</value>
</data>
<data name="Pressure/Touch Sensitive" xml:space="preserve">
<value>Sensibile a pressione/tocco</value>
</data>
<data name="PlayStation Network compatible" xml:space="preserve">
<value>Compatibile con PlayStation Network</value>
</data>
<data name="Pedals" xml:space="preserve">
<value>Pedali</value>
</data>
<data name="PC Speaker (Tweaked)" xml:space="preserve">
<value>PC Speaker (modificato)</value>
</data>
<data name="Password" xml:space="preserve">
<data name="Password" xml:space="preserve">
<value>Password</value>
</data>
<data name="Online Multiplayer" xml:space="preserve">
<value>Multigiocatore online</value>
</data>
<data name="Null-modem cable" xml:space="preserve">
<value>Cavo null-modem</value>
</data>
<data name="Network Adaptor" xml:space="preserve">
<value>Adattatore di rete</value>
</data>
<data name="Multiple players required" xml:space="preserve">
<value>Sono richiesti più giocatori</value>
</data>
<data name="Multiplayer" xml:space="preserve">
<value>Multigiocatore</value>
</data>
<data name="Multi-player" xml:space="preserve">
<value>Multigiocatore</value>
</data>
<data name="Multi-Link Cable" xml:space="preserve">
<value>Cavo Multi-Link</value>
</data>
<data name="Racing/Steering Wheel" xml:space="preserve">
<value>Volante da corsa/sterzo</value>
</data>
<data name="Raster" xml:space="preserve">
<value>Raster</value>
</data>
<data name="Remote Play" xml:space="preserve">
<value>Riproduzione remota</value>
</data>
<data name="Room-Scale" xml:space="preserve">
<value>Scala stanza</value>
</data>
<data name="Same/Split-Screen" xml:space="preserve">
<value>Stesso schermo/schermo diviso</value>
</data>
<data name="Save Data Cloud" xml:space="preserve">
<value>Dati di salvataggio nel cloud</value>
</data>
<data name="Scoreboard/Leaderboard" xml:space="preserve">
<value>Tabellone/classifica</value>
</data>
<data name="Sinclair Joystick" xml:space="preserve">
<value>Joystick Sinclair</value>
</data>
<data name="Single-player" xml:space="preserve">
<value>Giocatore singolo</value>
</data>
<data name="Single-sided Disk" xml:space="preserve">
<value>Disco a faccia singola</value>
</data>
<data name="SNES Mouse" xml:space="preserve">
<value>Mouse SNES</value>
</data>
<data name="Standing" xml:space="preserve">
<value>In piedi</value>
</data>
<data name="Steam account required" xml:space="preserve">
<value>Account Steam richiesto</value>
</data>
<data name="Tabletop mode compatible" xml:space="preserve">
<value>Compatibile con modalità tavolo</value>
</data>
<data name="Team" xml:space="preserve">
<value>Squadra</value>
</data>
<data name="Teletype/Teleprinter" xml:space="preserve">
<value>Telescrivente</value>
</data>
<data name="Tracked motion controllers" xml:space="preserve">
<value>Controller di movimento tracciati</value>
</data>
<data name="Trading cards" xml:space="preserve">
<value>Carte collezionabili</value>
</data>
<data name="Trophies" xml:space="preserve">
<value>Trofei</value>
</data>
<data name="TV mode compatible" xml:space="preserve">
<value>Compatibile con modalità TV</value>
</data>
<data name="Valve Anti-Cheat System (VAC)" xml:space="preserve">
<value>Sistema antibarbatrucchi Valve (VAC)</value>
</data>
<data name="VGA (Tweaked)" xml:space="preserve">
<value>VGA (modificato)</value>
</data>
<data name="Vibration" xml:space="preserve">
<value>Vibrazione</value>
</data>
<data name="XNA Indie Games" xml:space="preserve">
<value>Giochi indie XNA</value>
</data>
<data name="Xbox 360 Compatible" xml:space="preserve">
<value>Compatibile con Xbox 360</value>
</data>
<data name="Window" xml:space="preserve">
<value>Finestra</value>
</data>
<data name="Widescreen (16 x 9)" xml:space="preserve">
<value>Schermo panoramico (16 x 9)</value>
</data>
<data name="Wi-Fi Connection" xml:space="preserve">
<value>Connessione Wi-Fi</value>
</data>
<data name="VR Helmet/Headset" xml:space="preserve">
<value>Visore VR</value>
</data>
<data name="Voice Chat" xml:space="preserve">
<value>Chat vocale</value>
</data>
<data name="Regions" xml:space="preserve">
<data name="Regions" xml:space="preserve">
<value>Regioni</value>
</data>
<data name="Suggest description" xml:space="preserve">

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Marechai.ApiClient.Models;
using Marechai.Helpers;
using Microsoft.Kiota.Abstractions;
namespace Marechai.Services;
@@ -81,9 +82,12 @@ public class SoftwareAttributesService(Marechai.ApiClient.Client client)
{
try
{
string lang = UiLanguage.GetIso639_3();
return await client.Software.Attributes.DistinctKeys.GetAsync(rc =>
{
rc.QueryParameters.Category = string.IsNullOrWhiteSpace(category) ? null : category;
rc.QueryParameters.Lang = lang;
}) ?? new List<string>();
}
catch
@@ -104,10 +108,13 @@ public class SoftwareAttributesService(Marechai.ApiClient.Client client)
{
try
{
string lang = UiLanguage.GetIso639_3();
return await client.Software.Attributes.DistinctValues.GetAsync(rc =>
{
rc.QueryParameters.Category = string.IsNullOrWhiteSpace(category) ? null : category;
rc.QueryParameters.Key = string.IsNullOrWhiteSpace(key) ? null : key;
rc.QueryParameters.Lang = lang;
}) ?? new List<string>();
}
catch

View File

@@ -667,7 +667,12 @@ public class SoftwareService(Marechai.ApiClient.Client client, IRequestAdapter r
{
try
{
List<SoftwareSpecKeyDto> specs = await client.Software.Specifications.GetAsync();
string lang = UiLanguage.GetIso639_3();
List<SoftwareSpecKeyDto> specs = await client.Software.Specifications.GetAsync(config =>
{
config.QueryParameters.Lang = lang;
});
return specs ?? [];
}
@@ -840,7 +845,12 @@ public class SoftwareService(Marechai.ApiClient.Client client, IRequestAdapter r
{
try
{
List<SoftwareAttributeDto> attributes = await client.Software[softwareId].Attributes.GetAsync();
string lang = UiLanguage.GetIso639_3();
List<SoftwareAttributeDto> attributes = await client.Software[softwareId].Attributes.GetAsync(config =>
{
config.QueryParameters.Lang = lang;
});
return attributes ?? [];
}
@@ -854,8 +864,13 @@ public class SoftwareService(Marechai.ApiClient.Client client, IRequestAdapter r
{
try
{
string lang = UiLanguage.GetIso639_3();
List<SoftwareAttributeDto> attributes =
await client.Software.Releases[releaseId].Attributes.GetAsync();
await client.Software.Releases[releaseId].Attributes.GetAsync(config =>
{
config.QueryParameters.Lang = lang;
});
return attributes ?? [];
}