Skip to content

Commit

Permalink
Examples
Browse files Browse the repository at this point in the history
  • Loading branch information
pontusab committed Jan 19, 2025
1 parent 1697ca5 commit 7da20f1
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 23 deletions.
17 changes: 17 additions & 0 deletions examples/react-i18next/locales/sv.json
Original file line number Diff line number Diff line change
@@ -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}}."
}
}
14 changes: 7 additions & 7 deletions examples/xcode-strings/Example/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
@@ -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<b>%@</b>\n<a href=\"%@\">Ver detalles</a>";
"hello" = "Hola";
"hello" = "Hola";
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>zero</key>
<string>No hay artículos disponibles</string>
<string>No hay elementos disponibles</string>
<key>one</key>
<string>Un artículo disponible</string>
<string>Un elemento disponible</string>
<key>two</key>
<string>Dos artículos disponibles</string>
<string>Dos elementos disponibles</string>
<key>few</key>
<string>%lld artículos disponibles</string>
<string>%lld elementos disponibles</string>
<key>many</key>
<string>%lld artículos disponibles</string>
<string>%lld elementos disponibles</string>
<key>other</key>
<string>%lld artículos disponibles</string>
<string>%lld elementos disponibles</string>
</dict>
</dict>
<key>countdown_days</key>
Expand All @@ -39,7 +39,7 @@
<key>zero</key>
<string>¡El evento comienza hoy!</string>
<key>one</key>
<string>El evento comienza mañana!</string>
<string>¡El evento comienza mañana!</string>
<key>two</key>
<string>El evento comienza en dos días</string>
<key>few</key>
Expand All @@ -59,10 +59,7 @@
<key>current_date</key>
<string>Fecha actual: %@</string>
<key>formatted_message</key>
<string>Aquí tienes un mensaje &lt;b&gt;formateado&lt;/b&gt; con:
• Formato HTML
• Símbolos especiales: ©®™
• Un &lt;a href="https://example.com"&gt;hipervínculo&lt;/a&gt;</string>
<string>Aquí hay un &lt;b&gt;mensaje formateado&lt;/b&gt; con:\n• Formato HTML\n• Símbolos especiales: ©®™\n• Un &lt;a href=\"https://example.com\"&gt;hipervínculo&lt;/a&gt;</string>
<key>alert_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Expand All @@ -84,7 +81,7 @@
<key>remaining_time</key>
<string>Tiempo restante: %1$@ (%2$lld segundos)</string>
<key>version_info</key>
<string>Versión %1$@ (Construcción %2$@)</string>
<string>Versión %1$@ (Compilación %2$@)</string>
<key>test</key>
<string>Prueba</string>
</dict>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,46 @@ describe("Xcode stringsdict parser", () => {
});
});

it("should parse plural rules in stringsdict plist", async () => {
const input = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@items@</string>
<key>items</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>zero</key>
<string>No items</string>
<key>one</key>
<string>One item</string>
<key>other</key>
<string>%lld items</string>
</dict>
</dict>
<key>simple</key>
<string>Simple string</string>
</dict>
</plist>`;

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 = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
Expand Down Expand Up @@ -79,6 +119,35 @@ describe("Xcode stringsdict parser", () => {
expect(result).toContain("<string>World</string>");
});

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("<key>items_count</key>");
expect(result).toContain("<key>NSStringLocalizedFormatKey</key>");
expect(result).toContain("<string>%#@items@</string>");
expect(result).toContain("<key>NSStringFormatSpecTypeKey</key>");
expect(result).toContain("<string>NSStringPluralRuleType</string>");
expect(result).toContain("<key>zero</key>");
expect(result).toContain("<string>No items</string>");
expect(result).toContain("<key>simple</key>");
expect(result).toContain("<string>Simple string</string>");
});

it("should handle empty object", async () => {
const result = await parser.serialize("en", {});
expect(result).toContain("<dict/>");
Expand All @@ -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("<key>remaining_time</key>");
expect(result).toContain(
"<string>Tiempo restante: %1$@ (%2$lld segundos)</string>",
);
expect(result).toContain("<key>version_info</key>");
expect(result).toContain(
"<string>Versión %1$@ (Compilación %2$@)</string>",
);
expect(result).toContain("<key>test</key>");
expect(result).toContain("<string>Prueba</string>");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe("Xcode xcstrings parser", () => {
},
},
version: "1.0",
sourceLanguage: "en",
});

const result = await parser.parse(input);
Expand Down Expand Up @@ -69,6 +70,7 @@ describe("Xcode xcstrings parser", () => {
},
},
version: "1.0",
sourceLanguage: "en",
});

const result = await parser.parse(input);
Expand All @@ -81,6 +83,7 @@ describe("Xcode xcstrings parser", () => {
const input = JSON.stringify({
strings: {},
version: "1.0",
sourceLanguage: "en",
});

const result = await parser.parse(input);
Expand All @@ -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",
Expand All @@ -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({});
});

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/parsers/formats/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export type XcstringsOutput = {
}
>;
version: string;
sourceLanguage: string;
};
57 changes: 55 additions & 2 deletions packages/cli/src/parsers/formats/xcode-stringsdict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
}
}

Expand All @@ -28,13 +60,34 @@ export function createXcodeStringsDictParser(): Parser {

async serialize(_, data) {
try {
// Validate that all values are strings
const result: Record<string, PlistValue> = {};

// 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}`,
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/parsers/formats/xcode-xcstrings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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,
Expand Down

0 comments on commit 7da20f1

Please sign in to comment.