From 7da20f10221ee9ce627fc24ae8ccdc3a0d213f94 Mon Sep 17 00:00:00 2001 From: Pontus Abrahamsson Date: Sun, 19 Jan 2025 19:45:11 +0100 Subject: [PATCH] Examples --- examples/react-i18next/locales/sv.json | 17 ++++ .../Example/es.lproj/Localizable.strings | 14 +-- .../Example/es.lproj/Localizable.stringsdict | 21 ++--- .../__tests__/xcode-stringsdict.test.ts | 89 +++++++++++++++++++ .../formats/__tests__/xcode-xcstrings.test.ts | 5 ++ packages/cli/src/parsers/formats/types.ts | 1 + .../src/parsers/formats/xcode-stringsdict.ts | 57 +++++++++++- .../src/parsers/formats/xcode-xcstrings.ts | 5 +- 8 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 examples/react-i18next/locales/sv.json diff --git a/examples/react-i18next/locales/sv.json b/examples/react-i18next/locales/sv.json new file mode 100644 index 00000000..586ffaae --- /dev/null +++ b/examples/react-i18next/locales/sv.json @@ -0,0 +1,17 @@ +{ + "welcome": "Välkommen till vår applikation!", + "user": { + "greeting": "Hej, {{name}}!", + "profile": { + "title": "Din profil", + "edit": "Redigera profil" + } + }, + "notifications": { + "messages": "Du har {{count}} nya meddelande(n).", + "empty": "Inga nya meddelanden." + }, + "date": { + "today": "Idag är det {{date}}." + } +} diff --git a/examples/xcode-strings/Example/es.lproj/Localizable.strings b/examples/xcode-strings/Example/es.lproj/Localizable.strings index 1e44ad41..c79d8b00 100644 --- a/examples/xcode-strings/Example/es.lproj/Localizable.strings +++ b/examples/xcode-strings/Example/es.lproj/Localizable.strings @@ -1,17 +1,17 @@ -"welcome_message" = "¡Bienvenido a Weather App!"; +"welcome_message" = "¡Bienvenido a la aplicación del clima!"; "temperature_format" = "%1$.1f°%2$@"; "wind_speed" = "Viento: %1$.1f %2$@"; "forecast_daily" = "Pronóstico diario para %@"; -"notification_body" = "Alerta de tormenta para la región de %1$@\nSe esperan velocidades de viento de hasta %2$d mph"; +"notification_body" = "Alerta de tormenta para la región %1$@\nSe esperan velocidades de viento de hasta %2$d mph"; "battery_empty" = "Batería agotada"; -"battery_low" = "Batería al %d%% - por favor carga pronto"; +"battery_low" = "Batería al %d%% - por favor, carga pronto"; "battery_full" = "Batería completamente cargada"; -"photo_count_zero" = "No hay fotos"; +"photo_count_zero" = "Sin fotos"; "photo_count_one" = "%d foto"; "photo_count_other" = "%d fotos"; "time_remaining_now" = "¡Descarga completa!"; "time_remaining_minutes" = "%d minutos restantes"; -"time_remaining_hours" = "Aproximadamente %d horas restantes"; -"share_message" = "Mira esta foto que tomé con %1$@!\nUbicación: %2$@\nHora: %3$@"; +"time_remaining_hours" = "Acerca de %d horas restantes"; +"share_message" = "¡Mira esta foto que tomé con %1$@!\nUbicación: %2$@\nHora: %3$@"; "rich_notification" = "Nuevo mensaje de %@:\n%@\nVer detalles"; -"hello" = "Hola"; \ No newline at end of file +"hello" = "Hola"; diff --git a/examples/xcode-stringsdict/Example/es.lproj/Localizable.stringsdict b/examples/xcode-stringsdict/Example/es.lproj/Localizable.stringsdict index 71897c39..7366c18b 100644 --- a/examples/xcode-stringsdict/Example/es.lproj/Localizable.stringsdict +++ b/examples/xcode-stringsdict/Example/es.lproj/Localizable.stringsdict @@ -13,17 +13,17 @@ NSStringFormatValueTypeKey lld zero - No hay artículos disponibles + No hay elementos disponibles one - Un artículo disponible + Un elemento disponible two - Dos artículos disponibles + Dos elementos disponibles few - %lld artículos disponibles + %lld elementos disponibles many - %lld artículos disponibles + %lld elementos disponibles other - %lld artículos disponibles + %lld elementos disponibles countdown_days @@ -39,7 +39,7 @@ zero ¡El evento comienza hoy! one - El evento comienza mañana! + ¡El evento comienza mañana! two El evento comienza en dos días few @@ -59,10 +59,7 @@ current_date Fecha actual: %@ formatted_message - Aquí tienes un mensaje <b>formateado</b> con: -• Formato HTML -• Símbolos especiales: ©®™ -• Un <a href="https://example.com">hipervínculo</a> + Aquí hay un <b>mensaje formateado</b> con:\n• Formato HTML\n• Símbolos especiales: ©®™\n• Un <a href=\"https://example.com\">hipervínculo</a> alert_count NSStringLocalizedFormatKey @@ -84,7 +81,7 @@ remaining_time Tiempo restante: %1$@ (%2$lld segundos) version_info - Versión %1$@ (Construcción %2$@) + Versión %1$@ (Compilación %2$@) test Prueba diff --git a/packages/cli/src/parsers/formats/__tests__/xcode-stringsdict.test.ts b/packages/cli/src/parsers/formats/__tests__/xcode-stringsdict.test.ts index 7da0f982..f4db19e3 100644 --- a/packages/cli/src/parsers/formats/__tests__/xcode-stringsdict.test.ts +++ b/packages/cli/src/parsers/formats/__tests__/xcode-stringsdict.test.ts @@ -24,6 +24,46 @@ describe("Xcode stringsdict parser", () => { }); }); + it("should parse plural rules in stringsdict plist", async () => { + const input = ` + + + + items_count + + NSStringLocalizedFormatKey + %#@items@ + items + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lld + zero + No items + one + One item + other + %lld items + + + simple + Simple string + +`; + + const result = await parser.parse(input); + expect(result.simple).toBe("Simple string"); + const itemsCount = JSON.parse(result.items_count); + expect(itemsCount.NSStringLocalizedFormatKey).toBe("%#@items@"); + expect(itemsCount.items.NSStringFormatSpecTypeKey).toBe( + "NSStringPluralRuleType", + ); + expect(itemsCount.items.zero).toBe("No items"); + expect(itemsCount.items.one).toBe("One item"); + expect(itemsCount.items.other).toBe("%lld items"); + }); + it("should handle empty plist", async () => { const input = ` @@ -79,6 +119,35 @@ describe("Xcode stringsdict parser", () => { expect(result).toContain("World"); }); + it("should serialize object with plural rules to stringsdict plist format", async () => { + const pluralRule = { + NSStringLocalizedFormatKey: "%#@items@", + items: { + NSStringFormatSpecTypeKey: "NSStringPluralRuleType", + NSStringFormatValueTypeKey: "lld", + zero: "No items", + one: "One item", + other: "%lld items", + }, + }; + + const input = { + items_count: JSON.stringify(pluralRule), + simple: "Simple string", + }; + + const result = await parser.serialize("en", input); + expect(result).toContain("items_count"); + expect(result).toContain("NSStringLocalizedFormatKey"); + expect(result).toContain("%#@items@"); + expect(result).toContain("NSStringFormatSpecTypeKey"); + expect(result).toContain("NSStringPluralRuleType"); + expect(result).toContain("zero"); + expect(result).toContain("No items"); + expect(result).toContain("simple"); + expect(result).toContain("Simple string"); + }); + it("should handle empty object", async () => { const result = await parser.serialize("en", {}); expect(result).toContain(""); @@ -94,5 +163,25 @@ describe("Xcode stringsdict parser", () => { 'Failed to serialize Xcode stringsdict translations: Value for key "key" must be a string', ); }); + + it("should serialize remaining time format", async () => { + const input = { + remaining_time: "Tiempo restante: %1$@ (%2$lld segundos)", + version_info: "Versión %1$@ (Compilación %2$@)", + test: "Prueba", + }; + + const result = await parser.serialize("es", input); + expect(result).toContain("remaining_time"); + expect(result).toContain( + "Tiempo restante: %1$@ (%2$lld segundos)", + ); + expect(result).toContain("version_info"); + expect(result).toContain( + "Versión %1$@ (Compilación %2$@)", + ); + expect(result).toContain("test"); + expect(result).toContain("Prueba"); + }); }); }); diff --git a/packages/cli/src/parsers/formats/__tests__/xcode-xcstrings.test.ts b/packages/cli/src/parsers/formats/__tests__/xcode-xcstrings.test.ts index 24b42523..a63407cd 100644 --- a/packages/cli/src/parsers/formats/__tests__/xcode-xcstrings.test.ts +++ b/packages/cli/src/parsers/formats/__tests__/xcode-xcstrings.test.ts @@ -32,6 +32,7 @@ describe("Xcode xcstrings parser", () => { }, }, version: "1.0", + sourceLanguage: "en", }); const result = await parser.parse(input); @@ -69,6 +70,7 @@ describe("Xcode xcstrings parser", () => { }, }, version: "1.0", + sourceLanguage: "en", }); const result = await parser.parse(input); @@ -81,6 +83,7 @@ describe("Xcode xcstrings parser", () => { const input = JSON.stringify({ strings: {}, version: "1.0", + sourceLanguage: "en", }); const result = await parser.parse(input); @@ -106,6 +109,7 @@ describe("Xcode xcstrings parser", () => { const parsed = JSON.parse(result); expect(parsed.version).toBe("1.0"); + expect(parsed.sourceLanguage).toBe("en"); expect(parsed.strings.greeting.extractionState).toBe("manual"); expect(parsed.strings.greeting.localizations.en.stringUnit.value).toBe( "Hello", @@ -120,6 +124,7 @@ describe("Xcode xcstrings parser", () => { const parsed = JSON.parse(result); expect(parsed.version).toBe("1.0"); + expect(parsed.sourceLanguage).toBe("en"); expect(parsed.strings).toEqual({}); }); diff --git a/packages/cli/src/parsers/formats/types.ts b/packages/cli/src/parsers/formats/types.ts index 05e6912a..72c835cc 100644 --- a/packages/cli/src/parsers/formats/types.ts +++ b/packages/cli/src/parsers/formats/types.ts @@ -27,4 +27,5 @@ export type XcstringsOutput = { } >; version: string; + sourceLanguage: string; }; diff --git a/packages/cli/src/parsers/formats/xcode-stringsdict.ts b/packages/cli/src/parsers/formats/xcode-stringsdict.ts index 041a69e3..0157786a 100644 --- a/packages/cli/src/parsers/formats/xcode-stringsdict.ts +++ b/packages/cli/src/parsers/formats/xcode-stringsdict.ts @@ -2,6 +2,31 @@ import plist from "plist"; import { createFormatParser } from "../core/format.ts"; import type { Parser } from "../core/types.ts"; +type PlistValue = + | string + | number + | boolean + | Date + | Buffer + | PlistValue[] + | { [key: string]: PlistValue }; + +interface PluralDict { + NSStringLocalizedFormatKey: string; + [key: string]: + | { + NSStringFormatSpecTypeKey: string; + NSStringFormatValueTypeKey: string; + zero?: string; + one?: string; + two?: string; + few?: string; + many?: string; + other?: string; + } + | string; +} + export function createXcodeStringsDictParser(): Parser { return createFormatParser({ async parse(input: string) { @@ -15,6 +40,13 @@ export function createXcodeStringsDictParser(): Parser { for (const [key, value] of Object.entries(parsed)) { if (typeof value === "string") { result[key] = value; + } else if (typeof value === "object" && value !== null) { + // Handle plural rules + const pluralDict = value as PluralDict; + if (pluralDict.NSStringLocalizedFormatKey) { + // Store the plural forms as is - they will be handled by the iOS system + result[key] = JSON.stringify(value); + } } } @@ -28,13 +60,34 @@ export function createXcodeStringsDictParser(): Parser { async serialize(_, data) { try { - // Validate that all values are strings + const result: Record = {}; + + // Process each translation for (const [key, value] of Object.entries(data)) { if (typeof value !== "string") { throw new Error(`Value for key "${key}" must be a string`); } + + // Try to parse as JSON to see if it's a plural rule + try { + const parsed = JSON.parse(value); + if ( + typeof parsed === "object" && + parsed.NSStringLocalizedFormatKey + ) { + result[key] = parsed as PlistValue; + continue; + } + } catch { + // Not JSON, treat as regular string + } + + // Strip surrounding quotes if present + const cleanValue = value.replace(/^"(.*)"$/, "$1"); + result[key] = cleanValue; } - return plist.build(data); + + return plist.build(result); } catch (error) { throw new Error( `Failed to serialize Xcode stringsdict translations: ${(error as Error).message}`, diff --git a/packages/cli/src/parsers/formats/xcode-xcstrings.ts b/packages/cli/src/parsers/formats/xcode-xcstrings.ts index eef34332..0212eac9 100644 --- a/packages/cli/src/parsers/formats/xcode-xcstrings.ts +++ b/packages/cli/src/parsers/formats/xcode-xcstrings.ts @@ -42,7 +42,7 @@ export function createXcodeXcstringsParser(): Parser { } }, - async serialize(_, data) { + async serialize(locale, data) { try { // Validate input data for (const [key, value] of Object.entries(data)) { @@ -54,13 +54,14 @@ export function createXcodeXcstringsParser(): Parser { const result: XcstringsOutput = { strings: {}, version: "1.0", + sourceLanguage: locale, }; for (const [key, value] of Object.entries(data)) { result.strings[key] = { extractionState: "manual", localizations: { - en: { + [locale]: { stringUnit: { state: "translated", value,